Текст
                    СЕРИЯ
УЧЕБНОЕ / ПОСОБИЕ
^ППТЕР'


УЧЕБНОЕ / ПОСОБИЕ В. В. Лаптев, А. В. Морозов, А. В. Бокова C++ ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ Задачи и упражнения Допущено Министерством образования и науки Российской Федерации в качестве учебного пособия для студентов высших учебных заведений, обучающихся по направлению подготовки «Информатика и вычислительная техника» Издательская программа 300 лучших учебников для высшей школы осуществляется при поддержке Министерства образования и науки РФ [^ППТЕР Москва • Санкт-Петербург • Нижний Новгород • Воронеж Ростов-на-Дону - Екатеринбург - Самара Новосибирск Киев Харьков • Минск 2007
ББК 32.973-018.1я7 УДК 004.43(075) Л24 Рецензенты: кафедра информатики и прикладной математики Санкт-Петербургского государственного университета информационных технологий, механики и оптики, заведующий кафедрой О. Ф, Немолочнов, доктор технических наук, профессор; Т. А. Гаврилова, доктор технических наук, профессор кафедры «Компьютерные интеллектуальные технологии» Санкт-Петербургского государственного политехнического университета Лаптев В. В., Морозов А. В., Бокова А. В. Л24 C++. Объектно-ориентированное программирование. Задачи и упражнения. — СПб.: Питер, 2007. — 288 с.: ил. ISBN 978-5-469-01437-9 Книга предназначена для изучения возможностей объектно-ориентированного стиля про- граммирования на языке C++. Приведены сведения о синтаксисе и семантике объектно-ориентированных конструкций C++, стандартной библиотеке шаблонов STL. Книга содержит необходимые теоретические сведения, упражнения и задачи для самостоятельной работы, справочную информацию по наиболее популярным средам программирования: C++ Builder 6 и Visual C++.NET 2003. Для пре- подавателей и студентов, начинающих программистов. Допущено Министерством образования и науки Российской Федерации в качестве учебного пособия для студентов высших учебных заведений, обучающихся по направлению подготовки «Информатика и вычислительная техника». ББК 32.973-018.1я7 УДК 004.43(075) Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев авторских прав. Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как надежные. Гем не менее, имея в виду возможные человеческие или технические ошибки, издательство не может гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за возможные ошибки, связанные с использованием книги. ISBN 978-5-469-01437-9 © ООО «Питер Пресс», 2007
Краткое содержание Предисловие ........................................... 9 Глава 1. Классы и объекты.............................11 Глава 2. Конструкторы и перегрузка операций...........28 Глава 3. Наследование.................................49 Глава 4. Исключения...................................69 Глава 5. Контейнеры...................................79 Глава 6. Шаблоны.....................................107 Глава 7. Многомодульные программы....................129 Глава 8. Ввод-вывод..................................144 Глава 9. Строки......................................174 Глава 10. Стандартная библиотека шаблонов.............187 Приложение А. Функции для работы с символьными массивами . . . 223 Приложение Б. Обобщенные алгоритмы....................228 Приложение В. Рекомендуемые системы программирования..252 Список литературы.....................................280 Алфавитный указатель..................................281
Содержание Предисловие ................................................................. 9 Благодарности.............................................................10 От издательства..............................................................10 Глава 1. Классы и объекты....................................................11 Краткие сведения по теме ....................................................11 Определение класса........................................................11 Использование класса......................................................13 Определение методов класса................................................15 Вложенные классы .........................................................18 Упражнения...................................................................19 Структура-пара............................................................20 Структуры и классы........................................................22 Композиция классов и объектов ............................................25 Вложенные классы .........................................................27 Глава 2. Конструкторы и перегрузка операций..................................28 Краткие сведения по теме ................................................... 28 Перегрузка операций.......................................................28 Конструкторы и деструктор.................................................34 Константы в классе........................................................37 Поля-массивы в классе.....................................................38 Статические элементы класса...............................................39 Упражнения...................................................................42 Конструкторы и перегрузка операций........................................42 Массивы и константы в классе..............................................43 Глава 3. Наследование........................................................49 Краткие сведения по теме ....................................................49 Простое открытое наследование.............................................49 Конструкторы и деструкторы при наследовании ..............................50 Поля и методы при наследовании............................................52 Статические элементы класса при наследовании..............................53 Вложенные классы и наследование...........................................53 Операция присваивания и принцип подстановки...............................53 Функции-операции преобразования ..........................................55 Закрытое наследование.....................................................56 Виртуальные функции.......................................................57 Упражнения...................................................................61 Открытое наследование.....................................................61 Наследование вместо композиции............................................63 Массивы и наследование....................................................64 Виртуальные функции.......................................................65 Абстрактные классы........................................................66
Содержание 7 Глава 4. Исключения.......................................................69 Краткие сведения по теме .................................................69 Механизм обработки исключений..........................................69 Спецификация исключений................................................73 Подмена стандартных функций завершения ................................74 Стандартные исключения.................................................75 Создание собственной иерархии исключений...............................76 Упражнения................................................................76 Функции, генерирующие исключения.......................................77 Классы с обработкой исключений.........................................78 Глава 5. Контейнеры.......................................................79 Краткие сведения по теме .................................................79 Определение контейнера.................................................79 Операции контейнера....................................................80 Реализация контейнеров.................................................82 Упражнения................................................................96 Контейнеры как параметры...............................................96 Контейнеры-массивы.....................................................97 Контейнеры-списки ....................................................106 Глава 6. Шаблоны.........................................................107 Краткие сведения по теме.................................................107 Шаблоны классов ......................................................107 Шаблоны классов с шаблонами...........................................114 Шаблоны функций.......................................................119 Обобщенные алгоритмы и функторы.......................................121 Упражнения ..............................................................123 Шаблоны классов ......................................................124 Шаблоны функций, алгоритмы и функторы ................................126 Глава 7. Многомодульные программы........................................129 Краткие сведения по теме.................................................129 Сборка исходных текстов ..............................................130 Шаблоны и модульность.................................................132 Разделение определения и реализации. Делегирование ...................135 Пространства имен.....................................................137 Межмодульные переменные и функции.....................................140 Упражнения ..............................................................142 Глава 8. Ввод-вывод......................................................144 Краткие сведения по теме.................................................144 Классификация потоков.................................................144 Подключение потоков................................................. 145 Операции ввода/вывода.................................................147 Состояния потока .....................................................152 Форматирование ваода/вывода...........................................154 Файловые потоки.......................................................159 Буферизация...........................................................163 Строковые потоки......................................................164 Позиционирование в потоке.............................................165 Широкие потоки........................................................166 Упражнения ..............................................................169
8 Содержание Глава 9. Строки........................................................174 Краткие сведения по теме.............................................. 174 Символьные массивы .................................................174 Строки в стиле C++ .................................................176 Упражнения ............................................................183 Г лава 10. Стандартная библиотека шаблонов.............................187 Краткие сведения по теме...............................................187 Контейнеры .........................................................187 Итераторы...........................................................189 Последовательные контейнеры ........................................193 Адаптеры последовательных контейнеров...............................196 Ассоциативные контейнеры............................................198 Стандартные функторы................................................202 Стандартные обобщенные алгоритмы....................................204 Примеры.............................................................208 Упражнения ............................................................215 Последовательные контейнеры ........................................215 Множества...........................................................219 Контейнер-отображение...............................................222 Приложение А. Функции для работы с символьными массивами .... 223 Приложение Б. Обобщенные алгоритмы.....................................228 Приложение В. Рекомендуемые системы программирования ..................252 Список литературы.................................................... 280 Алфавитный указатель...................................................281
Предисловие Многолетний опыт обучения студентов показывает, что освоение любого языка программирования обычно происходит постепенно: сначала осваивается элемен- тарная техника программирования, затем изучаются более сложные элементы и приемы. Язык C++ не является исключением, однако современный C++ слиш- ком обширен и многогранен [1, 2]. В соответствии с известными парадигмами (структурное, объектно-ориентированное и обобщенное программирование) C++ вполне можно разделить на несколько частей и отдельно изучать процедур- ную и объектно-ориентированную части, шаблоны и стандартную библиотеку. Студенту для приобретения технических навыков объектно-ориентированного программирования требуется выполнить в течение семестра несколько десятков (если не пару сотен) разнообразнейших упражнений. Невооруженным глазом видно, что упражнения для освоения процедурного программирования [4, 6] и упражнения по объектно-ориентированному программированию должны быть совершенно разными. Однако в настоящее время в России издан только один практикум по объектно-ориентированной части C++ [5]. Кроме того, в своем учебнике [3] Т. А. Павловская уделила практике объектно-ориентированного программирования значительную часть. Очевидно, что этих двух книг совершенно недостаточно. Они, хотя и «закрыва- ют амбразуру», не свободны от некоторых недостатков. Опыт обучения студен- тов показал, что тему «Классы» лучше разделить на ряд менее обширных: клас- сы без перегрузки операций, классы с конструкторами и перегрузкой операций, использование механизма исключений. Для понимания принципов организации стандартной библиотеки студенту необходимо самостоятельно запрограммиро- вать несколько вариантов динамических контейнеров и итераторов. Разделение программ на модули тоже имеет много тонкостей и нюансов, которые требуют отдельного изучения. В данном практикуме отражен опыт обучения студентов основам объектно-ори- ентированного программирования на C++ в Астраханском государственном тех- ническом университете. В книге десять глав, каждая из которых посвящена от- дельной теме объектно-ориентированной части C++. В начале каждой главы сформулирована цель изучения данной темы и представлен соответствующий справочный материал. Каждая глава содержит достаточно большой набор уп- ражнений. которые студент должен выполнить, чтобы усвоить теоретические основы. Для лучшего усвоения большая часть упражнений неоднократно повто- ряется во многих темах в различных вариантах и при их выполнении требуется
10 Предисловие прибегать к различным механизмам C++. Ссылки на задачи внутри главы пред- ставляют собой просто номер, например «задание 35». Ссылки на задачи из дру- гой главы представлены в виде «номер главы, номер задания». Например, зада- ние 2.15 представляет собой ссылку на задание 15 из второй главы. К сожалению, в небольшой книге невозможно охватить все аспекты обширной объектно-ориентированной части C++. С другой стороны, некоторые темы слишком сложны для начинающего программиста. Именно поэтому в книгу не попали метериалы о механизме RTTI, о множественном наследовании и о пере- грузке операций new и del ete. На наш взгляд, достаточно сложно придумать про- стые примеры для изучения всех особенностей виртуальных функций, хотя эта тема в практикуме присутствует. А уж для освоения шаблонов и стандартной библиотеки вообще не помешало бы написать отдельный сборник заданий. Для выполнения упражнений студент должен в достаточной степени владеть процедурным программированием на C++. Во всяком случае, не должно вызы- вать проблем программирование функций. Средства C++, рассматриваемые в данной книге, соответствуют International Standart ISO/IEC 14882:2003(Е). Этот стандарт в достаточной мере поддерживают две сис- темы программирования: Borland C++ Builder 6 и Microsoft Visual C++.NET 2003. Благодарности В первую очередь авторы хотят поблагодарить сотрудников издательства «Пи- тер», без самоотверженного труда которых эта книга просто не могла появиться на свет. Особенную благодарность хочется выразить руководителю проекта, кан- дидату технических наук Анатолию Николаевичу Адаменко за помощь при под- готовке книги к изданию. Большое спасибо хочется сказать профессору Татьяне Александровне Павлов- ской, чьи книги побудили нас начать писать свои собственные. Переписка и бе- седы с этой очень умной и милой женщиной значили для одного из авторов зна- чительно больше, чем заочное общение с мэтрами программирования. Спасибо нашим студентам, работа с которыми помогла этой книге стать именно такой, какая она есть. Два автора — А. В. Морозов и А. В. Бокова — побывали «по обе стороны баррикады»: будучи студентами, они выполнили большую часть из представленных в этой книге заданий, а став преподавателями, внесли свой вклад в разработку концепции практикума. От издательства Ваши замечания, предложения, вопросы отправляйте по адресу электронной почты comp@piter.com (издательство «Питер», компьютерная редакция). Мы будем рады узнать ваше мнение! На веб-сайте издательства http://www.piter.com вы найдете подробную информа- цию о наших книгах.
ГЛАВА 1 Классы и объекты В рамках данной темы изучаются техника программирования нового типа дан- ных (определение класса), использование объектов определяемого типа. Рассмат- риваются различные варианты сочетаний классов с другими классами и объекта- ми, в том числе механизм композиции и механизм вложенных классов. Краткие сведения по теме Помимо определения новых операций с помощью конструкции функции, в C++ можно определить и новый тип данных посредством конструкции класса. Класс в C++ — это структурированный тип, образованный на основе уже существую- щих типов. В этом смысле класс является расширением понятия структуры. Определение класса Класс можно определить с помощью конструкции тип_класса имя_класса { компоненты класса }; Точка с запятой в конце ставится обязательно. В этом определении: □ тип класса — одно из служебных слов class, struct, union1; □ имя класса — идентификатор; □ компоненты класса — определения и объявления данных и принадлежащих классу функций. Имя класса является по умолчанию именем типа объектов. Данные — это поля объекта, образующие его структуру. Значения полей определяют состояние объ- екта. Функция, являющаяся компонентом класса, называется методом класса. Методами класса определяются операции над объектами класса. Разрешается определять класс: □ с полями и методами; □ только с полями; 1 В данной главе рассматриваются только типы class и struct.
12 Глава 1. Классы и объекты □ только с методами; □ без полей и без методов. Класс без полей и без методов называется пустым классом. Элементы класса типа struct по умолчанию открыты и доступны в других частях программы. Члены класса cl ass по умолчанию закрыты и недоступны вне данно- го класса. Доступ к содержимому класса задается спецификаторами доступа, ко- торые обозначаются ключевыми словами public и private. Ключевое слово public объявляет элемент класса доступным вне класса, а ключевое слово private за- крывает доступ снаружи. После ключевого слова требуется поставить знак «:» (двоеточие). И в классе, и в структуре можно написать столько спецификаторов public и private, сколько необходимо, и в том порядке, в каком требуется. Очередной специфика- тор действует до следующего. Допускается индивидуально объявлять каждый элемент класса либо открытым, либо закрытым. Открытая часть класса называ- ется интерфейсом. Сокрытие информации о внутренней структуре — это один из принципов объектно-ориентированного программирования, так называемая ин- капсуляция. Примеры объявлений классов приведены в листинге 1.1. Листинг 1.1. Примеры объявлений классов class NullType {}; struct Empty {}: struct Pair { int first: double second; }: struct date { Int month,day,year: void setdnt d.int m,1nt y): void get(int &d,int &m,int &y): void nextO: void printO: }; struct Money { void Add(const long &r): void DisplayO: private: long RublS; class Fraction { int num; int denum: public: void reduced: }; class XXX { int yyy(); public: int fff(int t): int RRR: class Library // пустой класс // пустой класс // только поля - открыты // и поля и методы - открыты // поля: месяц, день, год // метод - установить дату // метод - получить дату // метод - установить следующую дату // метод - показать дату И методы - открыты // поле - закрыто // поля - закрыты // метод - открыт // метод - закрыт // метод - открыт // поле - открыто // только открытые методы
Краткие сведения по теме 13 { public: void furictionO; public: int Fnction(lnt): } Использование класса Имея определение класса, можно объявлять переменные типа «имякласса». Пе- ременная класса называется объектом, или экземпляром класса. Класс объявля- ется один раз, а вот объектов можно создать столько, сколько необходимо. Объ- екты объявляются аналогично переменным встроенного типа: имя_класса имя_объекта; // скалярный объект имя_класса *имя_объекта: // указатель имя_класса имя_объекта[количество]; // массив При объявлении переменных можно задавать ключевые слова cl ass и struct: class имя_класса имя_объекта: // скалярный объект class имя_класса *имя_объекта: // указатель struct имя_класса имя_объекта[количество]: // массив Разрешается совмещать определение класса и объявление переменной: class имя_класса { члены_класса } имя_объекта; // объект class имя_класса { члены_класса } *имя_объекта; // указатель struct имя_класса { члены_класса } имя_объекта[количество]; // массив Эти переменные получают тип «имя класса». Тогда в дальнейшем можно объяв- лять переменные без записи определения класса и без служебных слов cl ass или struct, как показано выше. Объявленные переменные подчиняются правилам ви- димости, как и переменные встроенных типов, и время их жизни зависит от мес- та объявления. Следующие переменные могут быть объявлены для приведенных в листинге 1.1 классов: Null Type a, *pb: Empty d[10]: // скалярная переменная и указатель // массив date *pd: // указатель class Money t: // скалярная переменная class Money *p; // указатель class Money m[100J: // массив XXX YYY; // скалярная переменная Library L[10]. *pl: // массив и указатель class Class { /*... */ } // определение класса vl. v2[20]: // скалярная переменная и массив Переменным vl и v2 присваивается тип Cl ass, после чего для объявления других переменных достаточно указания типа Cl ass, например Class v3. v4[10]: Переменную можно инициализировать другой переменной того же класса. Раз- решается объявлять и ссылки на объекты с обязательной инициализацией. Money х = t: // инициализация переменной date y(d); // инициализация переменной date &u = у: // обязательная инициализация ссылки
14 Глава 1. Классы и объекты Если поля открыты, разрешается обычная инициализация полей инициализато- ром структуры, например class Person { public: string Flo: double Summa; }; Person Kupaev - { "Купаев M.", 10000.00 }; Указатели могут быть инициализированы так же, как и указатели на перемен- ные встроенных типов: Money *pt = new Money.*рр = pt,*ps; // объявление указателей ps = new TMoney; // и создание объектов date *pd = new dateO. *pr; // объявление указателей pr = new dateO; // и создание объектов Форма со скобками после типа выполняет обнуление полей динамического объ- екта. Объекты класса разрешается определять в качестве полей другого класса: class Count { Money Summa; // сумма на счете unsigned long NumberCount; // номер счета date D; // дата открытия счета public: void DisplayO; // вывод на экран }; Использование объекта в качестве поля другого класса называется композицией. Объекты класса можно передавать в качестве аргументов любыми способами (по значению, по ссылке, по указателю) и получать как результат из функции: Money AddMoney(const Money &а, const Money &b); // по константной ссылке Money Init(const long double &r); // возврат значения void fl(date t); // по значению void f2(date &t); //по ссылке void f3(date *t): // по указателю Функция, возвращающая объект класса, может быть использована для инициа- лизации переменной класса: Money t = Init(200.56); Money r = AddMoney(x.y); Можно объявлять объекты-константы класса с обязательной инициализацией: const Money k = р: const date d(t); const Money t = In1t(200.50); Доступ к открытым полям выполняется обычными для структуры способами: объект.поле указатель->поле Вызов открытых методов осуществляется аналогично объект.метод(параметры) указатель->метод(параметры)
Краткие сведения по теме 15 Для массива объектов обращение к открытому полю выполняется так: имя_массива[индекс].поле Аналогично выполняется и вызов метода для элемента массива имя_массива[индекс].метод(аргументы) Примеры: date d; d.day = 21: d.month = 11; d.year = 2006: d.printO: d.set(22,2.1953): date DD[3]: DD[0].day = 12: DD[0].next(): Library t: int a = t.function(5): Library *pt = new Library; int b = pt->function(5): Count A[5]; // объявление массива fordnt i = 0; 1 < 5; i++) . A[1].Display(); // вывод элементов массива Определение размера выделяемой объектам памяти (в байтах) выполняется опе- рацией sizeof(MMH Knacca). Методы не занимают место в классе, а фактический размер класса зависит от режима выравнивания. Даже пустой класс занимает некоторое количество памяти. По умолчанию в каждой интегрированной среде установлен собственный режим выравнивания, влияющий на размер класса. Вы- равнивание по границе байта можно установить с помощью директивы препро- цессора #pragma pack(l) или #pragma pack(push, 1) В этом случае классу требуется наименьший объем памяти. Конструкция class имя-класса; называется объявлением класса. Обычно такое объявление используется в тех случаях, когда один класс зависит от другого, но определение второго класса не- доступно. Объекты такого класса объявлять нельзя — сам класс не определен. Разрешается объявлять указатели на объекты такого класса. Определение методов класса Методы класса имеют неограниченный доступ ко всем элементам класса незави- симо ни от порядка объявления элементов класса, ни от спецификаторов досту- па. Методы могут определяться как внутри класса, так и вне его. Определение метода внутри класса не отличается от определения обычной функции. Метод, определенный внутри класса, считается по умолчанию встроенной функцией (inline).
16 Глава 1. Классы и объекты Если метод определяется вне класса, то принадлежность метода классу указыва- ется префиксом-именем класса. В классе присутствует только прототип. Метод, определенный вне класса, по умолчанию не считается inline-функцией. Методы могут быть перегружены и могут принимать аргументы по умолчанию. Аргумен- том и/или возвращаемым значением метода может быть объект того же класса, и такие объекты разрешено объявлять внутри метода. Фактически левым аргументом метода является объект, для которого этот метод вызывается, например time t; t.settime(12,54): Метод неявно получает1 в качестве аргумента указатель на объект, для которого он вызван. Этот указатель обозначается зарезервированным словом thi s и может использоваться в теле метода. Через него же разумно осуществлять доступ к по- лям и методам класса в случае неоднозначности (совпадение имен). this->Summa Запись *thi s представляет собой значение текущего объекта. Часто это значение используется для возврата значения определяемого класса. Методы могут быть перегружены. Перегрузка методов — это одно из проявлений принципа полиморфизма в C++. Методы могут быть константными. Константный метод не изменяет значения полей класса. Константный метод объявляется при указании слова const после списка аргументов метода. Константные и неконстантные методы не являются эквивалентными, даже если у них полностью совпадают прототипы (кроме слова const после списка аргументов). Для объекта-константы может быть вызван только константный метод. Для объекта-переменной можно вызывать как кон- стантные, так и неконстантные методы. Для ввода значений нового типа обычно реализуется метод ReadO. В простейшем виде выполняется заполнение полей нового класса из стандартного потока ввода cin с клавиатуры. В методе перед вводом каждого значения можно вывести под- сказку-приглашение, объясняющую очередность набора. Передаваемые значения обычно проверяются на допустимость. Для вывода значений нового типа реализуется метод DisplayO. В самом простом случае он представляет собой вывод из полей нового класса на экран — в стан- дартный поток cout. Метод вывода обычно реализуется как константный (см. лис- тинг 1.2). Листинг 1.2. Определение методов class time { public: void settime(int h. int m, int s = 0) // определение внутри класса { hours = h; minutes = m; seconds = s: } void settime(const time &t): // метод перегружен 1 Из этого общего правила есть исключения — статические методы.
Краткие сведения по теме 17 time nexthourO; // возврат определяемого типа time addhours(const time &t): // параметр определяемого типа void DisplayO const: void time::Read() private: int hours, minutes, seconds: i. // вывод - константный метод // ввод полей // закрытые поля ) • void time::DisplayO const // определение вне класса // константного метода { cout « hours « "« minutes « < void time:-.ReadO < seconds: } // ввод полей { h: hourscoit « "Hours : cin » hours: if (hours > 23) goto h: coit « "Minutes: cin » minutes; // простая проверка // без проверки seconds = 0: // не вводится void time::settime(const time &t); // определение вне класса { hours = t.hours; minutes = t.minutes: seconds = t.seconds: } time time::addhours(const time &t) { time r = *this: r.hours += t.hours: r.hours^=24; // параметр определяемого типа // использование this return r; i // возврат определяемого типа J time time: -.nexthourO; { hours++; hours$=24; return *this: И возврат текущего объекта } Полезным является метод преобразования полей класса в строку toStringO1 для последующего использования этой строки в других методах, например для выво- да. Для работы со строками необходимо подключить стандартный заголовочный файл #include <string> Для показанного выше класса time самая скромная реализация может выглядеть следующим образом: std::string time::toString() { std::string s = std::string Digits = "0123456789”: unsigned char dl,d2; // формируем часы в строку dl = hours/10; d2 = hoursHO: // строка-результат // цифры для результата // выделенная цифра // получаем цифры часов s=s+Digits[dl]; s=s+Digits[d2]: s+="."; // прицепляем старшую цифру // прицепляем младшую цифру // завершение подстроки // формируем минуты в строку dl = minutes/10; d2 = minutesHO; // вычисляем цифры минут s=s+Digits[dl]; s=s+Digits[d2]; s+=".": 1 В языке программирования Java этот метод является стандартным и включен в корневой базовый класс Object.
18 Глава 1. Классы и объекты // формируем секунды в строку dl = seconds/10; d2 = secondsHO; // получаем цифры секунд s=s+Digits[dl]; s=s+Digits[d2]; return s; } Метод возвращает текущее значение полей класса time в виде строки «чч.мм.сс». Вложенные классы Класс, объявленный внутри другого класса, называется вложенным. Он является членом объемлющего класса, и его определение может быть открыто (public) или закрыто (private). Уровень вложенности стандартом [1] не ограничивается. Имя вложенного класса должно быть уникально в объемлющем классе, но мо- жет совпадать с другими именами вне класса. Доступа по умолчанию к приватным компонентам объемлющего класса вложен- ный класс не имеет, как и объемлющий класс — к приватным компонентам вло- женного. Обойти запрет помогает механизм дружественных отношений, например class External { //... friend class Inner; class Inner { friend class External; //... }: }: // Inner доступна приватная // часть External // вложенный класс // External доступна приватная // часть Inner Ни вложенный класс, ни объемлющий не могут обращаться к методам друг дру- га непосредственно; как и в случае обычных невложенных классов, необходимо объявить объект (или указатель), для которого уже вызвать нужный метод. Объ- ект объемлющего класса может передаваться методу вложенного класса как аргумент. void External::Inner::MethodInner(const External &t) { //... memlnner = t.MethodExternalO; // вызов метода объемлющего класса //... } Метод вложенного класса Methodinner О получает ссылку на объект внешнего класса и обычным способом вызывает метод MethodExternal (). Если вложенный класс объявлен как public, то его можно использовать в качест- ве типа по всей программе. Его имя следует писать с префиксом — именем объ- емлющего класса: External:;Inner *pointer; Если вложенный класс объявлен в закрытой части объемлющего класса, то он доступен только членам объемлющего класса и его друзьям. В этом случае
Упражнения 19 компоненты вложенного класса обычно делают открытыми — тогда нет нужды и объявлять другом объемлющий класс, например class External { //... friend class Inner; // Inner доступна приватная // часть External structure Inner { /* все элементы доступны в External */ }: //... }: Внутри методов вложенного класса ключевое слово thi s является указателем на текущий объект вложенного класса. Методы вложенного класса можно реализовать непосредственно внутри класса. Если же методы вложенного класса определяются вне класса, определение необ- ходимо ставить вне самого внешнего из объемлющих классов — в области гло- бальной видимости. Имя метода в таком случае должно иметь префиксы; коли- чество префиксов равно уровню вложенности классов. В области глобальной видимости вне объемлющего класса можно определить и сам вложенный класс. C++ разрешает это делать, если в объемлющем классе задать объявление класса, например class А { //... class В; // объявление вложенного класса //... }: class А::В // внешнее определение вложенного класса { //... }: Доступность определенного таким образом класса зависит от того, в какой части объемлющего класса находится объявление — если оно приватное, то и опреде- ление является приватным в объемлющем классе. Определение вложенного класса внешним образом позволит написать определение в отдельном файле1 и тем самым обеспечит повышенный уровень инкапсуляции. Упражнения В этой теме в каждом упражнении требуется реализовать в том или ином виде определение нового класса. В каждом разделе приводятся конкретные требова- ния к программированию классов раздела. Для демонстрации работы с объекта- ми нового типа во всех заданиях нужно написать главную функцию. В програм- ме обязательно должны быть продемонстрированы различные способы создания объектов и массивов объектов. Программа должна демонстрировать использова- ние всех функций и методов. Программа должна также показывать на экране размер класса — в режиме #pragma pack(l) и без него. 1 О раздельной трансляции см. главу 7, «Многомодульные программы».
20 Глава 1. Классы и объекты Структура-пара Структурой-парой называется структура с двумя полями, которые обычно име- ют имена first и second. Требуется реализовать тип данных с помощью такой структуры. Во всех заданиях обязательно должны присутствовать: □ метод инициализации Init; метод должен контролировать значения аргумен- тов на корректность; □ ввод с клавиатуры Read; □ вывод на экран Display. Реализовать внешнюю функцию с именем шаке /пмиО, где тип — тип реализуе- мой структуры. Функция должна получать в качестве аргументов значения для полей структуры и возвращать структуру требуемого типа. При передаче оши- бочных параметров следует выводить сообщение и заканчивать работу. Вариан- ты следующие. 1. Поле first — дробное число; поле second — целое число, показатель степени. Реализовать метод power() — возведение числа first в степень second. Метод должен правильно работать при любых допустимых значениях fi rst и second. 2. Поле fi rst — дробное число; поле second — дробное число, показатель степени. Реализовать метод power() — возведение числа first в степень second. Метод должен правильно работать при любых допустимых значениях fi rst и second. 3. Поле fi rst — целое положительное число, числитель; поле second — целое по- ложительное число, знаменатель. Реализовать метод i part О — выделение це- лой части дроби fi rst/second. Метод должен проверять неравенство знамена- теля нулю. 4. Поле fi rst — целое положительное число, номинал купюры; номинал может принимать значения 1, 2, 5, 10, 50, 100, 500, 1000, 5000. Поле second — целое положительное число, количество купюр данного достоинства. Реализовать метод summa () — вычисление денежной суммы. 5. Поле fi rst — дробное положительное число, цена товара; поле second — целое положительное число, количество единиц товара. Реализовать метод cost О — вычисление стоимости товара. 6. Поле first — целое положительное число, калорийность 100 г продукта; поле second — дробное положительное число, масса продукта в килограммах. Реа- лизовать метод power!) — вычисление общей калорийности продукта. 7. Поле fi rst — дробное число, левая граница диапазона; поле second — дробное число, правая граница диапазона. Реализовать метод rangecheck О — проверку заданного числа на принадлежность диапазону. 8. Поле fi rst — целое число, левая граница диапазона, включается в диапазон; поле second — целое число, правая граница диапазона, не включается в диапазон. Пара чисел представляет полуоткрытый интервал [first, second). Реализовать метод rangecheck () — проверку заданного целого числа на принадлежность диапазону.
Упражнения 21 9. Поле first — целое положительное число, часы; поле second — целое поло- жительное число, минуты. Реализовать метод minutesO— приведение времени в минуты. 10. Линейное уравнение у = Аг + В. Поле first — дробное число, коэффициент А; поле second — дробное число, коэффициент В. Реализовать метод functionO— вычисление для заданного х значения функции у. 11. Линейное уравнение у = Аг + В. Поле first — дробное число, коэффициент А; поле second — дробное число, коэффициент В. Реализовать метод rootO— вычисление корня линейного уравнения. Метод должен проверять неравенст- во коэффициента В нулю. 12. Поле first — дробное число, координата х точки на плоскости; поле second — дробное число, координата у точки на плоскости. Реализовать метод distanceO — расстояние точки от начала координат. 13. Поле first — дробное положительное число, катет а прямоугольного треуголь- ника; поле second — дробное положительное число, катет b прямоугольного треугольника. Реализовать метод hypotenuseO— вычисление гипотенузы. 14. Поле first — дробное положительное число, оклад; поле second — целое чис- ло, количество отработанных дней в месяце. Реализовать метод summa О — вычисление начисленной суммы за данное количество дней для заданного ме- сяца: оклад / дни месяца * отработанные дни. 15. Поле first — целое положительное число, продолжительность телефонного разговора в минутах; поле second — дробное положительное число, стоимость одной минуты в рублях. Реализовать метод cost О — вычисление общей сто- имости разговора. 16. Поле first — дробное число, целая часть числа; поле second — положительное дробное число, дробная часть числа. Реализовать метод multiplyO — умноже- ние на произвольное дробное число типа doubl е. Метод должен правильно ра- ботать при любых допустимых значениях first и second. 17. Поле first — целое положительное число, координата курсора/указателя по горизонтали; поле second — целое положительное число, координата курсора по вертикали. Реализовать метод changexO — изменение горизонтальной координаты курсора; реализовать метод changey() — изменение вертикальной координаты курсора. Методы должны проверять выход за границу экрана. 18. Поле first — целое число, целая часть числа; поле second — положительное целое число, дробная часть числа. Реализовать метод multiplyO — умножение на произвольное целое число типа int. Метод должен правильно работать при любых допустимых значениях fi rst и second. 19. Число сочетаний по к объектов из п объектов (k < п) вычисляется по формуле С(и, k) = п\ / {{п - k)\ х £!) Поле first — целое положительное число, к; поле second — положительное целое число, п. Реализовать метод combi nation О — вычисление С(и, k).
22 Глава 1. Классы и объекты 20. Элемент а7 геометрической прогрессии вычисляется по формуле: Uj = aorj,j = 0, 1, 2,... Поле fi rst — дробное число, первый элемент прогрессии а<>; поле second — по- стоянное отношение г. Определить метод elementjO для вычисления заданно- го элемента прогрессии. Структуры и классы Во всех заданиях, помимо указанных в задании операций, обязательно должны быть реализованы следующие методы: □ метод инициализации Ini t; □ ввод с клавиатуры Read; □ вывод на экран Di spl ay; □ преобразование в строку toString. Все задания должны быть реализованы тремя способами: □ тип данных представляется структурой с необходимыми полями, а операции реализуются как внешние функции, которые получают объекты данного типа в качестве аргументов; □ как класс с закрытыми полями, где операции реализуются как методы класса; □ инкапсулировать поля класса в независимой структуре и в ней реализовать методы InitO, ReadO, Display!), toStringO; в основном классе должно быть одно поле данных, представленное объектом-структурой. 21. Комплексное число представляются парой действительных чисел (а, Ь), где а — действительная часть, b — мнимая часть. Реализовать класс Complex для работы с комплексными числами. Обязательно должны присутствовать опе- рации: О сложения add, (a, b) + (с, d) = {а + b, с + d); О вычитания sub, (a, b) - (с, d) = {а - b, с - d); О умножения mul, (а, b) х (с, d) = (ас - bd, ad + be); О деления div, (а, b) / (с, d) = (ас + bd, be - ad) / (<? + d2); О сравнение equ, (а, b) = (с, d), если (а = с) и (b = d); О сопряженное число conj, conj(a, b) = (a, -b). 22. Создать класс vectbr3D, задаваемый тройкой координат. Обязательно должны быть реализованы: сложение и вычитание векторов, скалярное произведение векторов, умножение на скаляр, сравнение векторов, вычисление длины век- тора, сравнение длины векторов. 23. Создать класс Model Window для работы с моделями экранных окон. В качестве полей задаются: заголовок окна, координаты левого верхнего угла, размер по горизонтали, размер по вертикали, цвет окна, состояние «видимое/невиди- мое», состояние «с рамкой/без рамки». Координаты и размеры указываются в целых числах. Реализовать операции: передвижение окна по горизонтали,
Упражнения 23 по вертикали; изменение высоты и/или ширины окна изменение цвета; изме- нение состояния, опрос состояния. Операции передвижения и изменения раз- мера должны осуществлять проверку на пересечение границ экрана. Функция вывода на экран должна индуцировать состояние полей объекта. 24. Создать класс Money для работы с денежными суммами. Число должно быть представлено двумя полями: типа long для рублей и типа unsigned char — для копеек. Дробная часть (копейки) при выводе на экран должна быть отделена от целой части запятой. Реализовать сложение, вычитание, деление сумм, де- ление суммы на дробное число, умножение на дробное число и операции сравнения. 25. Создать класс Triangle для представления треугольника. Поля данных долж- ны включать углы и стороны. Требуется реализовать операции: получения и изменения полей данных, вычисления площади, вычисления периметра, вычисления высот, а также определения вида треугольника (равносторонний, равнобедренный или прямоугольный). 26. Создать класс Angle для работы с углами на плоскости, задаваемыми величи- ной в градусах и минутах. Обязательно должны быть реализованы: перевод в радианы, приведение к диапазону 0-360, увеличение и уменьшение угла на заданную величину, получение синуса, сравнение углов. 27. Создать класс Poi nt для работы с точками на плоскости. Координаты точки — декартовы. Обязательно должны быть реализованы: перемещение точки по оси X, перемещение по оси Y, определение расстояния до начала координат, расстояния между двумя точками, преобразование в полярные координаты, сравнение на совпадение и несовпадение. 28. Рациональная (несократимая) дробь представляется парой целых чисел (а, Ь), где а — числитель, b — знаменатель. Создать класс Rational для работы с ра- циональными дробями. Обязательно должны быть реализованы операции: О сложения add, (a, b) + (с, d) = (ad + be, bd); О вычитания sub, (a, b) - (с, d) = (ad - be, bd); О умножения mul, (a, b) x (c, d) = (ac, bd); О деления div, (a, b) / (c, d) = (ad, be); О сравнения equal, greate, less. Должна быть реализована приватная функция сокращения дроби reduce, ко- торая обязательно вызывается при выполнении арифметических операций. 29. Создать класс Date для работы с датами в формате «год.месяц.день». Дата представляется структурой с тремя полями типа unsigned int: для года, месяца и дня. Класс должен включать не менее трех функций инициализации: числа- ми, строкой вида «год.месяц.день» (например, «2004.08.31») и датой. Обя- зательными операциями являются: вычисление даты через заданное коли- чество дней, вычитание заданного количества дней из даты, определение високосности года, присвоение и получение отдельных частей (год, месяц, день), сравнение дат (равно, до, после), вычисление количества дней между датами.
24 Г лава 1. Классы и объекты 30. Создать класс Time для работы со временем в формате «час:минута:секунда». Класс должен включать в себя не менее четырех функций инициализации: числами, строкой (например, «23:59:59»), секундами и временем. Обязатель- ными операциями являются: вычисление разницы между двумя моментами времени в секундах, сложение времени и заданного количества секунд, вычи- тание из времени заданного количества секунд, сравнение моментов времени, перевод в секунды, перевод в минуты (с округлением до целой минуты). 31. Реализовать класс FazzyNumber для работы с нечеткими числами, которые представляются тройками чисел (г - ei, х, х + е?). Для чисел А = (А - сц, А, А + аг) и В = (В - bi, В, В + Ьг) арифметические операции выполняются по следующим формулам: О сложение А + В = (А + В - at - Ьц А + В, А + В + ar + Ьг)\ О вычитание А - В = (А - В - сц - Ьц А - В, А - В + ar + Ьг); О умножение А В = (А хВ - Вхсц - Ах bi + сцхЬ^ Ах В, АхВ + Вхсц + + А х bi + сц х bi)', О обратное число А = (1 / (А + ar), 1 / А, 1 / (А - а/)), А > 0; О деление А / В = ((А - сц) / (В + Ьг), А / В, (А + аг) / (В - bi)), В > О', 32. Реализовать класс Account, представляющий собой банковский счет. В классе должны быть четыре поля: фамилия владельца, номер счета, процент начис- ления и сумма в рублях. Открытие нового счета выполняется операцией ини- циализации. Необходимо выполнять следующие операции: сменить владель- ца счета, снять некоторую сумму денег со счета, положить деньги на счет, начислить проценты, перевести сумму в доллары, перевести сумму в евро, по- лучить сумму прописью (преобразовать в числительное). 33. Номиналы российских рублей могут принимать значения 1, 2, 5, 10, 50, 100, 500, 1000, 5000. Копейки представить как 0.01 (1 копейка), 0.05 (5 копеек), 0.1 (10 копеек), 0.5 (50 копеек). Создать класс Money для работы с денежны- ми суммами. Сумма должна быть представлена полями-номиналами, значе- ниями которых должно быть количество купюр данного достоинства. Реали- зовать сложение сумм, вычитание сумм, деление сумм, деление суммы на дробное число, умножение на дробное число и операции сравнения. Дробная часть (копейки) при выводе на экран должны быть отделена от целой части запятой. 34. Реализовать класс Bankomat, моделирующий работу банкомата. В классе долж- ны содержаться поля для хранения идентификационного номера банкомата, информации о текущей сумме денег, оставшейся в банкомате, минимальной и максимальной суммах, которые позволяется снять клиенту в один день. Сум- ма денег представляется полями-номиналами 10-1000 (см. задание 13). Реа- лизовать метод инициализации банкомата, метод загрузки купюр в банкомат и метод снятия определенной суммы денег. Метод снятия денег должен вы- полнять проверку на корректность снимаемой суммы: она не должна быть меньше минимального значения и не должна превышать максимальное зна- чение. Метод toStringO должен преобразовать в строку сумму денег, остав- шуюся в банкомате.
Упражнения 25 35. Создать класс Fraction для работы с дробными числами. Число должно быть представлено двумя полями: целая часть — длинное целое со знаком, дробная часть — беззнаковое короткое целое. Реализовать арифметические операции сложения, вычитания, умножения и операции сравнения. 36. Создать класс Goods (товар). В классе должны быть представлены поля: на- именование товара, дата оформления, цена товара, количество единиц товара, номер накладной, по которой товар поступил на склад. Реализовать методы изменения цены товара, изменения количества товара (увеличения и умень- шения), вычисления стоимости товара. Метод toStringO должен выдавать в виде строки стоимость товара. 37. Создать класс Bitstring для работы с 64-битовыми строками. Битовая строка должна быть представлена двумя полями типа unsigned long. Должны быть реализованы все традиционные операции для работы с битами: and, or, xor, not. Реализовать сдвиг влево shiftLeft и сдвиг вправо shiftRight на заданное количество битов. 38. Создать класс LongLong для работы с целыми числами из 64 бит. Число долж- но быть представлено двумя полями: long — старшая часть, unsigned long — младшая часть. Должны быть реализованы арифметические операции, при- сутствующие в C++ (без присваивания), и сравнения. 39. Создать класс Payment (зарплата). В классе должны быть представлены поля: фамилия-имя-отчество, оклад, год поступления на работу, процент надбав- ки, подоходный налог, количество отработанных дней в месяце, количество рабочих дней в месяце, начисленная и удержанная суммы. Реализовать мето- ды: вычисления начисленной суммы, вычисления удержанной суммы, вычис- ления суммы, выдаваемой на руки, вычисления стажа. Стаж вычисляется как полное количество лет, прошедших от года поступления на работу, до теку- щего года. Начисления представляют собой сумму, начисленную за отрабо- танные дни, и надбавки, то есть доли от первой суммы. Удержания представ- ляют собой отчисления в пенсионный фонд (1 % от начисленной суммы) и подоходный налог. Подоходный налог составляет 13 % от начисленной сум- мы без отчислений в пенсионный фонд. 40. Реализовать класс Cursor. Полями являются координаты курсора по горизон- тали и вертикали — целые положительные числа, вид курсора — горизонталь- ный или вертикальный, размер курсора — целое от 1 до 15. Реализовать мето- ды изменения координат курсора, изменения вида курсора, изменения размера курсора, метод гашения и восстановления курсора. Композиция классов и объектов Во всех задачах требуется реализовать по два-три класса. Один класс является основным, остальные — вспомогательные. Вспомогательные классы должны быть определены как независимые. Объекты вспомогательных классов должны использоваться в качестве полей основного класса. 41. Реализовать класс Account (задание 32), используя для представления суммы класс Money из задания 24.
26 Глава 1. Классы и объекты 42. Реализовать класс Account (задание 32), используя для представления суммы класс Money из задания 33. 43. Реализовать класс Account (задание 32). Добавить поле-дату открытия счета, используя класс Date из задания 29. Добавить метод, вычисляющий количест- во дней, прошедших с начала открытия счета, и добавляющий по 0.01 % к проценту начисления за каждый день. 44. Реализовать класс Calculator с полным набором арифметических операций, используя класс Fraction из задания 35. 45. Реализовать класс Bankomat (задание 34), используя для представления суммы класс Money из задания 33. 46. Реализовать класс Fraction (задание 35), используя для представления целой части класс LongLong из задания 38, а для представления дробной части поло- жительное дробное число типа doubl е. 47. Реализовать класс Calculator с полным набором арифметических операций, на основе класса Fraction из предыдущего задания. 48. Реализовать класс Triangle (задание 25), опираясь на класс Angle из зада- ния 26 для представления углов. 49. Реализовать класс Goods (задание 36), добавив поле-дату поступления товара на склад (использовать класс Date из задания 29). Реализовать метод, вычис- ляющий срок хранения товара. 50. Реализовать класс Goods (задание 36), используя для представления цены класс Money из задания 24. Реализовать метод уценки товара, уменьшая цену на 1% за каждый день просрочки срока годности. 51. Реализовать класс Triangle (задание 25) с полями-координатами вершин. Для представления координат вершин используйте класс Point из задания 27. 52. Реализовать класс Payment (задание 39), используя вместо поля-года поле-да- ту класса Date из задания 29. Стаж следует вычислять, используя методы класса Date. 53. Реализовать класс Payment (задание 39), используя для представления полей начислений и удержаний класс Money из задания 24. 54. Реализовать класс Money (задание 24), используя класс Fraction из задания 35. 55. Реализовать класс Model Window (задание 23), добавив поле для курсора. Ис- пользуйте для представления поля курсора класс Cursor из задания 40. 56. Реализовать класс Bill, представляющий собой разовый платеж за телефонный разговор. Класс должен включать поля: фамилия плательщика, номер телефона, тариф за минуту разговора, скидка (в процентах), время начала разговора, время окончания разговора, сумма к оплате. Для представления времени используйте класс Time из задания 30. Реализовать методы извлечения и изменения полей. Время разговора, подлежащее оплате, вычисляется в минутах; неполная минута считается за полную. Метод toStringO должен выдавать сумму в рублях. 57. Реализовать класс Set (множество) не более чем из 64 элементов целых чи- сел, используя класс Bitstring из задания 37. Множество должно обеспечи-
Упражнения 27 вать включение элемента в множество, исключение элемента из множества, объединение, пересечение и разность множеств, вычисление количества эле- ментов в множестве. 58. Реализовать класс Rational (задание 28), используя для представления числи- теля и знаменателя класс LongLong из задания 38. 59. Реализовать класс Money (задание 24), используя для представления рублей класс LongLong из задания 38. 60. Реализовать класс Cursor (задание 40), используя для представления коорди- нат класс LongLong из задания 38. Вложенные классы 61. Реализовать задания 21-40, определив соответствующую структуру как вло- женную. 62. Реализовать задания 41-60, используя конструкцию вложенного класса.
ГЛАВА 2 Конструкторы и перегрузка операций Данная глава посвящена изучению техники программирования перегруженных операций как внешними функции, так и методами класса. Рассматриваются функции-друзья класса. Изучается техника программирования конструкторов. Дополнительно изучаются статические элементы класса, поля-массивы и кон- стантные поля. Краткие сведения по теме В C++ разрешено перегружать встроенные операции — это одно из проявлений полиморфизма. Перегрузка операций не является обязательной в объектно-ори- ентированном программировании — в языках Java и C# она отсутствует. Однако наличие перегрузки операций в C++ обеспечивает дополнительный уровень удобства при использовании новых типов данных. При программировании нового типа данных желательно объявлять объекты но- вого типа аналогично встроенным — формы инициализации не должны отли- чаться от форм инициализации объектов встроенного типа. Эти возможности обеспечивают конструкторы. Перегрузка операций При перегрузке операции нужно учитывать следующие ограничения. 1. Запрещена перегрузка следующих операций: О sizeofO — определение размера аргумента; О . (точка) — селектор компонента объекта; О ?: — условная операция; О :: — указание области видимости; О — выбор компонента класса через указатель; О # и # — операции препроцессора.
Краткие сведения по теме 29 2. Операции можно перегружать только для нового типа данных — нельзя пере- грузить операцию для встроенного типа. В C++ новый тип данных можно об- разовать с помощью конструкций enum, union, struct и class. 3. Нельзя изменить приоритет перегружаемой операции и количество операн- дов. Унарная операция обязана иметь один операнд1, бинарная — два операн- да; не разрешается использовать параметры по умолчанию. Единственная операция, которая не имеет фиксированного количества операндов, — это операция вызова функции (). Операции «+», «-», «*», <<&>> допускается пере- гружать и как унарные, и как бинарные. 4. Операции можно перегружать либо как независимые внешние функции (только такой способ перегрузки допустим для enum), либо как методы класса. Четыре операции: О присваивания =; О вызова функции О; О индексирования []; О доступа по указателю ->; допускается перегружать только как методы класса. Эти операции в принци- пе нельзя перегрузить для конструкции enum. 5. Если операция перегружается как метод класса, то левым (или единственным) аргументом обязательно будет объект класса, для которого перегружается операция. Прототип функции-операции выглядит следующим образом: тип operator@(cnncoK параметров): где @ — символ операции. Слово operator является зарезервированным словом и может использоваться только в определении или в функциональной форме вызова операции. Перегрузка операций внешними функциями Прототип бинарной операции, перегружаемой как внешняя независимая функ- ция, выглядит так: тип operatorsпараметр_1, параметр_2); Для обеспечения коммутативности бинарной операции с параметрами разного типа, как правило, требуется реализовать две функции-операции: тип operatorsпараметр_1, параметр_2); тип орегаРог@(параметр_2. параметр_1): Хотя бы один из параметров должен быть нового типа. Параметры можно пере- давать любым допустимым способом. Обращение к операции выполняется дву- мя способами: □ инфиксная форма параметр_1 @ параметр 2; □ функциональная форма operator@(napaMemp_1, параметр_2). 1 Перегрузка операций инкремента (++) и декремента (—) является исключением из этого правила.
30 Глава 2. Конструкторы и перегрузка операций Прототип унарной операции, перегружаемой независимой внешней функцией, отличается только количеством параметров: тип operator^параметр); Параметр должен быть нового типа. Обращаться к перегруженной операции сле- дует так: □ инфиксная форма ©параметр-, □ функциональная форма operator© (параметр). Операции инкремента ++ и декремента - - имеют две формы: префиксную и пост- фиксную. В определении постфиксной формы операции должен быть объявлен дополнительный параметр типа i nt. тип operators параметр, int): Этот параметр не должен использоваться в теле функции. Инфиксная форма обращения к такой операции — параметр©', при функциональной форме обра- щения необходимо задавать второй фиктивный аргумент, например operatorsпараметр.О); В листинге 2.1 приведен пример перегрузки операций для перечислимого типа. Листинг 2.1. Перегрузка операций для перечислимого типа // перечислимый тип enum Week { mon = 1, tue, wed, thu, fri, sat. sun = 0 }; Week operator+(const Week &m, const int &b) { Week t = Week(b + m): return (t = Week(W)); } // вторая функция для коммутативности сложения Week operatorsconst int &b. const Week &m) { return (m+b); } Week operator++(Week &m) // префиксная форма { return (m = Week(m+D); } Week operator++(Week &m, int) // постфиксная форма { Week t = m; m = Week(m+1); return t; } void print(const Week &d) // вывод на экран названий дней недели { string Days[7] = {"Sunday", "Monday", "Tuesday". "Wednesday", "Thursday”, "Friday". "Saturday" }: cout « Days[d] « endl; Week m = sat: // m = суббота print(m+l); // выводит 'Sunday' print(2+m); // выводит 'Monday' print(operator+(m,l)); // выводит 'Sunday' print(operator+(2,m)): // выводит 'Monday' m++: // вызов постфиксной формы, m == sun print Cm); // выводит 'Sunday' print(++m): // выводит 'Monday' print(operator++(m)); // префиксная форма, выводит 'Tuesday' print(operator++(m,0)); // постфиксная форма, выводит 'Tuesday print(m): // выводит 'Wednesday'
Краткие сведения по теме 31 При перегрузке операции внешней для класса функцией поля должны быть от- крыты или класс должен предоставлять методы get О и set О для получения и изменения значений полей. Пример перегрузки операцйи внешней функцией для класса показан в листин- ге 2.2. Листинг 2.2. Перегрузка операции сложения для класса независимой функцией struct Fraction { int num; int denum; 11 поля - открыты void reduceO; 11 метод - открыт }; Fraction operator+(const Fraction &1, const Fraction &r) { Fraction t; // (a.b)+(c,d)=(ad+bc.bd) t.denum = 1,denum*r.denum; 11 знаменатель результата t.num = 1.num*r.denum+r.num*].denum; 11 числитель результата t. reduceO: //сокращение return t: } Fraction a. b, c; // ... a = b + с; // инфиксная форма a = operator+(b,c); 11 функциональная форма Перегрузка операций методами класса У методов один параметр — текущий объект — определен по умолчанию. Унар- ные операции выполняются для текущего объекта, который и является единст- венным аргументом. Поэтому унарные операции, перегружаемые как методы класса, не имеют параметров. Прототип унарной операции, реализованной как метод класса, выглядит так: тип operator©]); где @ — символ операции. Возвращаемое значение может быть любого типа, в том числе и определяемого класса. Операция может возвращать значение, ссылку или указатель на объект. Разрешается указывать void на месте типа воз- вращаемого значения. Постфиксные операции инкремента и декремента являются исключением из этого правила. Чтобы отличить постфиксную операцию от префиксной, в пост- фиксной форме задается фиктивный параметр типа int, который реально не ис- пользуется. Префиксная форма инкремента (и декремента) должна иметь прототип тип& operator©]); Постфиксная операция инкремента (и декремента) должна иметь прототип тип operator©]1 nt); Тип — это «имя класса», в котором определяется операция.
32 Глава 2. Конструкторы и перегрузка операций Прототип бинарной операции имеет один аргумент и выглядит таким образом: тип орега1ог@(параметр): Параметры разрешается передавать любым удобным нам способом: по значению, по ссылке или по указателю. Возвращать метод-операция тоже может значение, указатель или ссылку на объект, в том числе и своего класса. Разрешается указы- вать void на месте типа возвращаемого значения. При реализации метода-операции вне класса требуется в заголовке обычным об- разом указывать префикс-имя класса: тип класс::operator@() // унарная операция тип класс::operator^параметр) // бинарная операция Вызов унарной операции для объекта имеет форму @объект объект® в зависимости от того, какой вид операции используется — префиксный или постфиксный. Функциональный вызов для префиксной операции имеет форму объект.operator^) Для постфиксной операции в функциональной форме вызова нужно задавать фиктивный аргумент: объект.operator@(0) Функциональная форма вызова для бинарной операции, определенной как ме- тод класса, выглядит так: объект.operator®(аргумент) Инфиксная форма объект @ аргумент является сокращением функциональной. Операции присваивания являются бинарными операциями, поэтому должны иметь один аргумент. Если операция присваивания operator» не реализуется яв- ным образом, она создается автоматически по умолчанию. Для любого класса ав- томатически создаваемая операция присваивания имеет прототип класс& operator=(const класс &г) Допускается реализовать операцию присваивания с параметром и возвращае- мым результатом не своего класса; разрешается передавать параметр любым спо- собом; возвращать можно не ссылку, а значение или указатель — все зависит от потребностей задачи. Другие операции с присваиванием автоматически не создаются. Функции-опе- рации с присваиванием обычно должны возвращать ссылку на текущий объект. Любой метод, возвращающий неконстантную ссылку, можно использовать мно- гократно. Money& Money::operator+=(const Money Sb)
Краткие сведения по теме 33 Повторное использование можно явным образом запретить, если возвращать константную ссылку, например const Moneys Money::operator+=(const Money Sb) Модификатор const запрещает дальнейшую модификацию результата. Функции-друзья класса Любая внешняя функция, определенная как «друг» класса, имеет неограничен- ный доступ к любым элементам класса, в том числе и к закрытым. Для того что- бы внешняя функция стала другом класса, необходимо в интерфейсе класса за- дать ее прототип, добавив впереди слово friend, например friend Money operator*(const long double Sa, const Money Sb): При реализации функции слово friend писать запрещается. Не указывается так- же и префикс класса. Рекомендуемые формы перегрузки операций для класса приведены в табл. 2.1. В частности, операции ввода/вывода operator» и operator« практически всегда реализуются как внешние дружественные функции. Например, для класса Two с двумя полями х и у целого типа эти функции могут выглядеть так: ostreamS operator«(ostreamS t. const Two Sr) { return (t « '(' « r.x « « r.y « ')’): } IstreamS operator»(1 streams t. Two Sr) { t » r.x » r.y: return t: } Пример перегрузки операций методами класса и дружественными функциями показан в листинге 2.3. Листинг 2.3. Перегрузка операций методами класса class F { Int f: Int m; public: F(int t, int r = 0) // конструктор { f = t: m = r; } FS operator++() 11 префиксный инкремент { f++; return *this: } F operator++(int) // постфиксный инкремент { F t = *this: f++; return t: } FS operator+=(const FS r) 11 сложение с присваиванием { f+=r.f: m+=r.m: return *this: } /7 дружественные функции friend F operator+(const F SI, const F Sr): friend ostreamS operator«(ostreamS t. const F Sr) friend istreamS operator»(istreamS t. F Sr) // реализация дружественных функций F operator+Cconst F SI. const F Sr) { F t = 1: t+=r: return t: } // дружественные функции ввода/вывода ostreamS operator«(ostreamS t. const F Sr) { return (t « r.f « '/' « r.m): } istreamS operator»(istreamS t. F Sr) { t » r.f » r.m: return t; } \
34 Глава 2. Конструкторы и перегрузка операций Таблица 2.1. Форма перегрузки операций Операция Рекомендуемая форма перегрузки Все унарные операции Метод класса = [] о -> ->* Обязательно метод класса + = — *= /= %= &= |- А= <<= » = Метод класса Остальные бинарные операции Внешняя функция-друг Конструкторы и деструктор Конструктор — это особый метод, имеющий имя, совпадающее с именем класса. Назначением конструктора является создание и инициализация объектов. Количество и типы аргументов конструктора могут быть любые; конструктор может не иметь аргументов; аргументам разрешается присваивать значения по умолча- нию; аргументы конструктора обычно используются для инициализации полей объекта. Конструктор не возвращает результата — нельзя писать даже void. Заголовок конструктора, реализуемого вне класса, как и для обычного метода, должен сопровождаться префиксом класс::класс(параметры) Конструкторы, так же как и обычные методы, можно перегружать. Обычно раз- личают три вида конструкторов: □ конструктор без аргументов (конструктор по умолчанию); □ конструктор инициализации; □ конструктор копирования (с одним параметром определяемого класса). При отсутствии в классе явно определенных конструкторов автоматически соз- даются конструктор без аргументов (конструктор по умолчанию) и конструктор копирования. Создаваемый автоматически конструктор без аргументов имеет вид класс::класс(){} Конструктор копирования автоматически создается в таком виде: класс::класс (const класс &г) { *this = г; } Аргументом конструктора копирования всегда является объект своего класса. Эти конструкторы обеспечивают объявление объектов без инициализации и с ини- циализацией другим объектом того же типа: date d; date t(d); date r = d: // // // конструктор без аргументов конструктор копирования конструктор копирования При наличии хотя бы одного явно определенного конструктора конструктор без аргументов не создается. Конструктор копирования создается всегда, если не оп- ределен явно.
Краткие сведения по теме 35 При явном определении конструктора копирования аргумент должен переда- ваться по ссылке — передавать его по значению нельзя. В классе может быть объявлено несколько конструкторов копирования и разрешается определять не- сколько аргументов, если им присваиваются значения по умолчанию. Любой конструктор с параметрами не своего класса является конструктором инициализации. Конструктор инициализации предназначен для инициализации полей класса. class time { public: timed; // конструктор без аргументов timednt h. int m = 0, int s = 0) // конструктор инициализации { hours = h; minutes = m; seconds = s; } private: int hours, minutes, seconds: 11 закрытые поля }: time: .-timed // реализация вне класса { hours = minutes = seconds = 0: } Конструктор инициализации обеспечивает объявление и инициализацию пере- менных: time t(12,59.59), s(13,45), u = 14: Конструктор инициализации может вызываться явно: time R = time(lO): 11 явный вызов конструктора Конструктор инициализации со всеми аргументами по умолчанию играет роль конструктора без аргументов (конструктора по умолчанию). Конструкторы вызываются и при создании динамических переменных. time *а = new time: // конструктор без аргументов time *b = new timed: // конструктор без аргументов time *d = new time(12,23); И конструктор инициализации time *f = new time(*d): // конструктор копирования Конструкторы обеспечивают привычную форму объявления констант: const time d2(100,67): // конструктор инициализации const time d0 = 100: 11 конструктор инициализации const time d5 = time(lOO): // явный вызов Конструктор инициализации обеспечивает инициализацию массива: time Т[3] = { 1,2,3 }: Для каждого целого числа, представленного в списке, вызывается конструктор инициализации, поэтому инициализация массива представляет собой следующее: time T[0]=time(l): time T[l]=time(2): time T[2]=time(3): Если массив не инициализируется, то для каждого элемента массива вызывается конструктор без аргументов, если он определен явно или неявно (или вызывает- ся конструктор инициализации с аргументами по умолчанию).
36 Глава 2. Конструкторы и перегрузка операций Деструктор — это метод, имеющий имя класса, с тем отличием, что его первым символом должен быть символ - (тильда). Деструктор не возвращает результата и не имеет аргументов. Деструктор в классе может быть только один — перегруз- ка деструкторов запрещена. Деструктор вызывается автоматически при каждом уничтожении объекта. При отсутствии в классе явно определенного деструктора он создается автоматически. Вид автоматически создаваемого деструктора сле- дующий: класс::—класс(){} Если конструкторы класса не запрашивают ресурсов, которые нужно возвращать (например, динамическую память), или не выполняют инициализирующих дей- ствий, которые при уничтожении объекта требуют завершающих действий (на- пример, если файл открывается, его нужно закрывать), то деструктор можно не писать. Конструкторы и параметры Конструктор копирования вызывается неявным образом при передаче в функ- цию параметра-объекта по значению и при возврате значения из функции. При других способах передачи параметра конструктор копирования не вызывается (листинг 2.4). Листинг 2.4. Конструкторы и параметры class Money { public: 11 конструктор инициализации без аргументов Money(const double &г = 0.0) { Summa = d; } friend bool operator==(const TMoney &a, const TMoney &b); friend bool operator!=(const TMoney &a, const TMoney &b) private: double Summa: }•• // реализация дружественных функций bool operator==(const TMoney &a. const TMoney &b) { return (a.Summa == b.Summa): } bool operator!“(const TMoney &a, const TMoney &b) { return !(а == Ь): void fKMoney t); } // по значению void f2(Money *t): // по адресу void f3(const Money &t): // по константной ссылке void f4(Money &t): И по ссылке Money d2(100.67); // инициализация fl(d2): // по значению - И вызов конструктора копирования f2(&d2); //по адресу - // конструкторы не вызываются f3(d2): // по константной ссылке - не // вызываются f4(d2): // по ссылке - не вызываются Конструктор копирования вызывается для создания скрытой «внутренней» ко- пии объекта. При передаче в качестве параметра выражения может вызываться конструктор инициализации.
Краткие сведения по теме 37 fl(120): // по значению - вызывается конструктор инициализации f3(120): // по константной ссылке - вызываеся конструктор инициализации Конструкторы позволяют более естественным образом задавать значения пара- метров по умолчанию. void fKMoney t = 100); void f3(const Money &t = 200); void f2(Money *t = new Money): void f2(Money *t = new TMoney(300)); Вызов функций без аргумента во всех случаях сопровождается вызовом конст- руктора инициализации. Константы в классе В классе разрешается определять целочисленные константы; к целочисленным типам относятся char, short, int и long в знаковом и беззнаковом виде, bool, а так- же перечислимый тип enum. Константу в классе можно определить одним из двух способов: □ как перечислимый тип enum; □ как статическую константу целочисленного типа. class Constant { public: enum Week { mon = 1. tue, wed, thu, fri. sat. sun = 0 }; static const int cl = 1; static const char c2 = 'x': static const Day c3 = sun; } Обращение к перечислимым константам осуществляется либо по имени объекта, либо по имени класса: cout « Constant::sat « endl: Constant dd: cout « dd.sat « endl; // квалификатор-класс И квалификатор-объект Обращение к статическим константам обычно делается по имени класса: cout « Constant::cl « endl; 11 выводит 1 cout « Constant::c2 « endl; 11 выводит 'x' cout « Constant::c3 « endl; // выводит 0 Константы других типов объявляются как константные поля (без явной инициа- лизации), и их должен инициализировать конструктор в списке инициализации конструктора. Список инициализации задается сразу после списка параметров; в начале списка ставится двоеточие, отделяющее его от параметров; инициализа- торы полей пишутся через запятую. Константными могут быть и целочисленные поля. class Constants { const long cl: const double c2: public: // целочисленное константное поле // нецелочисленное константное поле
38 Глава 2. Конструкторы и перегрузка операций Constants(const double &r = 0) :с2(г), cl(l) { } II список инициализации // тело конструктора Один инициализатор представляет собой имя поля-константы, вслед за которым в скобках указывается инициализирующее выражение. При задании списка ини- циализации надо помнить о нескольких правилах. 1. Инициализировать в списке инициализации можно и константные поля, и обыч- ные поля-переменные. 2. Независимо от порядка перечисления полей в списке инициализации, поля получают значения в порядке объявления в классе. 3. В качестве инициализирующего выражения можно задавать любое вычисляе- мое выражение (не обязательно константное), результатом которого является значение того же (или приводимого) типа, что и тип инициализируемого поля; для полей типа класс выполняется вызов конструктора. 4. Для инициализации поля встроенного типа нулем используется специальная форма инициализатора имя поляО — выражение не задается; для поля-объекта некоторого класса вызывается конструктор без аргументов (по умолчанию). 5. На месте инициализирующего выражения в скобках можно задавать список выражений через запятую; последнее выражение должно иметь тип или при- водиться к типу поля. 6. Список инициализации выполняется до начала выполнения тела конструктора. Поля-массивы в классе В классе можно объявить поле-массив. Размер массива должен задаваться кон- стантным выражением, причем константные выражения должны быть определе- ны раньше массива. Задавать количество элементов поля-массива следует обяза- тельно (листинг 2.5). Листинг 2.5. Поля-массивы в классе class Arrays { 1ntm0[10J: static const unsigned Int к = 10: enum { n = 10 }: int ml[k]: Int m2[n]; public: Arrays() { for (int 1 = 0: 1 < к; ++1) m0[i]=ml[i]=m2[i]=0: } Массив инициализируется либо в теле конструктора (только неконстантный), либо в списке инициализации, причем в списке инициализации разрешается только инициализация нулем. Arrays!): mOO, ml(), m2() {}
Краткие сведения по теме 39 Для массива объектов некоторого типа такая запись означает вызов конструкто- ра по умолчанию (без аргументов); конструктор вызывается для каждого эле- мента массива. Операция индексирования При наличии поля-массива в классе часто бывает необходимо перегрузить опе- рацию индексирования []. Операция перегружается только как метод класса; она является бинарной: левый аргумент— это текущий объект, а правый — аргу- мент функции-операции. Выражение объект [индекс] трактуется как объект.operator! ](индекс) Операция [] (как и операция присваивания) обязана возвращать ссылку, по- скольку выражение «имя[индекс]» может находиться как справа, так и слева от операции присваивания. Тип индекса может не совпадать с типом возвращаемой ссылки; тип возвращае- мой ссылки не обязательно должен быть типом определяемого класса; тип ин- декса не обязан быть целочисленным. Обычно реализуют два метода: константный и неконстантный. тип_1& operator[](const тип_2 &1ndex) const тип_1& operator[](const тип_2 Sindex) const Неконстантный метод работает, когда выражение «имя[индекс]» стоит слева от знака операции присваивания. Константный метод вызывается, например, для параметров, передаваемых по константной ссылке. Статические элементы класса Помимо целочисленных констант, в классе разрешается объявлять константные и неконстантные статические поля любых типов (в том числе и статические мас- сивы), а также статические методы. Статическое поле «является частью класса, но не является частью объекта этого класса» [2]. Статические поля класса создаются в единственном экземпляре не- зависимо от количества определяемых в программе объектов. Все объекты (даже созданные динамически) разделяют единственную копию статических полей. При этом в класс включается только объявление, а определение статического поля записывается за пределами класса. Статические поля нельзя инициализи- ровать в конструкторе, так как они создаются до создания любого объекта. Для объявления поля статическим нужно помимо типа и «константности» указать слово static: static тип имя; static const тип имя: Аналогично объявляется и статический массив: static тип имя[количество]: static const тип имя[количество]; static тип иия[]: static const тип имя[];
40 Глава 2. Конструкторы и перегрузка операций Определение статических полей выполняется вне класса: тип класс::имя; тип класс::имя = инициализатор: const тип класс::имя = инициализатор: тип класс::имя[количество]: тип класс::имя[количество] = инициализатор: const тип класс::имя[количество] = инициализатор: тип класс::имя[] = инициализатор: const тип класс::имя[] = инициализатор: Слово static в определении писать нельзя. При определении поля выполняется и его инициализация. Если статическое поле является константным, инициали- зация обязательна. Для неконстантных полей элементарных типов при отсут- ствии явной инициализации выполняется обнуление. Для статических полей неэлементарных типов вызываются конструкторы: либо явным образом конст- руктор инициализации, либо, при отсутствии явной инициализации, конструк- тор по умолчанию. Для константного статического массива или для статическо- го массива без заданного количества элементов инициализация обязательна (листинг 2.6). Определение должно быть единственным в программе, иначе компоновщик (не компилятор) сообщит о повторном определении. Если определение отсутствует, тоже возникает ошибка компоновки. Принцип инкапсуляции соблюдается — доступ к таким полям имеют только методы и «друзья» класса. Статический метод тоже является методом класса; он не может быть ни кон- стантным, ни виртуальным1; в статическом методе нельзя вызывать виртуальные методы. Ни конструкторы, ни деструктор, ни операция присваивания не могут быть статическими. Объявление статического метода отличается от объявления обычного метода словом static перед заголовком: static тип имя(параметры): // статический метод Определение вне класса записывается обычным образом без слова static: тип класс::имя(параметры) { /* тело статического метода */ } Статические методы вправе обращаться только к статическим полям класса. Статический метод разрешено вызывать для конкретного объекта, как и любой другой метод класса. Статический метод можно вызывать как метод класса неза- висимо от определения объектов — в этом случае вызов пишется с префиксом класса. Так как статический метод не «приписан» к объекту, он не получает ука- зателя thi s в качестве параметра. Любые статические элементы могут быть объявлены во вложенных классах. Несмотря на то что поля объявлены приватными, значения им присваиваются «в открытую». Такая инициализация разрешена только в определении, которое обязан предоставить создатель класса, иначе программа не пройдет компоновку. 1 См. главу 3, «Наследование».
Краткие сведения по теме 41 Определение единственное, поэтому механизм защиты данных работает по- прежнему — присвоить значение приватной статической переменной «в откры- тую» в другом месте невозможно. Листинг 2.6. Статические поля в классе include <string> class Programmer // демонстрационный класс { std::string Fio; int year; public: // конструктор по умолчанию ProgrammerO: FioC’Liskov”), year(1948) {} И неэлементарный тип Programmer(std::string fio. int y) { Flo = fio: year = y; } }; class StaticField // конструктор инициализации { static const int sOl = 1; ,И инициализированная константа static const int s02: И неинициализированное константное И поле static const double s03; И константное поле не целого типа static int s04: // неконстантное поле целого типа static double s05: // неконстантное поле не целого типа static const Programmer p; // константное поле неэлементарного // типа static Programmer t: /7 неконстантное поле неэлем-го типа static int m01[]; // статический неконстантный массив static const double m02[]; // статический константный массив static void printO; }: // определение статического метода void StaticField::print() { /* вывод статических полей класса */ } // определение статических полей // статический метод const int StaticField::s02 = 2; И обязательная инициализация const double StaticField: ;s03 = 3.1: И обязательная инициализация int StaticField:;s04; И инициализация по умолчанию (ноль) double StaticField::s05; const // инициализация по умолчанию (ноль) Programmer StaticField::p("Kupaev", 1978): И конструктор инициализации Programmer StaticField:it; // конструктор по умолчанию int StaticField::m01[] = {1,2,3}; const double StaticField::m02[] = {2.718281828,3.141592653}; Несмотря на то что поля объявлены приватными, значения им присваиваются «в открытую». Такая инициализация разрешена только в определении, которое обязан предоставить создатель класса, иначе программа не пройдет компонов- ку. Определение единственное, поэтому механизм защиты данных работает по- прежнему — присвоить значение приватной статической переменной «в откры- тую» в другом месте невозможно.
42 Глава 2. Конструкторы и перегрузка операций Подсчет объектов класса Классическое применение статических полей — подсчет объектов. Для этого в клас- се объявляется статическое поле целого типа, которое увеличивается в конструк- торе, а уменьшается в деструкторе (листинг 2.7). Листинг 2.7. Подсчет объектов класса class Object { static unsigned int count: public: Object 0: -ObjectO; static unsigned int CountO; }; unsigned int Object::count = 0: Object::0bject(){ ++count: } Object::~Object(){ --count: } unsigned int Object::CountO { return count; } // статическое поле - счетчик 11 выдача счетчика // определение и инициализация счетчика И увеличение счетчика при создании И уменьшение счетчика при разрушении // получение значения Упражнения В этой теме в каждом упражнении требуется реализовать в том или ином виде определение нового класса. Во всех заданиях необходимо реализовать конструк- тор инициализации (один или несколько) и конструктор без аргументов. Ука- занные в задании операции реализуются посредством перегрузки подходящих операций. Во всех заданиях обязательно должны быть реализованы соответ- ствующие операции с присваиванием, ввод с клавиатуры, вывод на экран, преоб- разования в строку toString. Также надо реализовать операции инкремента и де- кремента в обеих формах, если они имеют смысл для реализуемого типа. Перегрузка операций выполняется двумя способами: □ все операции реализуются как внешние дружественные функции; □ подходящие операции реализуются как методы класса, а остальные — как внешние дружественные функции. Для демонстрации работы с объектами нового типа во всех заданиях требуется написать главную функцию. В программе обязательно должны быть продемон- стрированы различные способы создания объектов и массивов объектов. Про- грамма должна демонстрировать использование всех функций и методов. Она должна выводить на экран размер класса в режиме #pragma pack(l) и без него. Конструкторы и перегрузка операций Выполнить задания 1.1-1.20, реализовав для каждой структуры три конструк- тора: без аргументов, копирования и инициализации. Функции ввода/вывода оформить как дружественные.
Упражнения 43 Выполнить задания 1.21-1.40 как независимые классы с конструкторами и пере- грузкой операций. Все операции определить как внешние дружественные функ- ции. Выполнить задания 1.21-1.40 с конструкторами и перегрузкой операций. Поля класса инкапсулировать в подходящей структуре, а в классе должно быть одно поле данных, представленное объектом-структурой. Используемую в задании структуру нужно снабдить конструктором инициализации, перегрузить для нее операции ввода/вывода как внешние дружественные функции. Выполнить задания 1.41-1.60 с конструкторами и перегрузкой операций. Выполнить задания 1.41-1.60 с конструкторами и перегрузкой операций, ис- пользуя конструкцию вложенного класса вместо композиции. Выполнить задания 1.41-1.60 с конструкторами и перегрузкой операций, ис- пользуя конструкцию вложенного класса вместо композиции; добавить подсчет объектов для внутренних классов. Массивы и константы в классе Дополнительно к требуемым в заданиях операциям перегрузить операцию ин- дексирования []. Максимально возможный размер массива задать константой. В отдельном поле size должно храниться максимальное для данного объекта ко- личество элементов массива; реализовать метод size(), возвращающий установ- ленную длину. Если количество элементов массива изменяется во время работы, определить в классе поле count. Первоначальные значения size и count устанав- ливаются конструктором. В тех задачах, где возможно, реализовать конструктор инициализации строкой. 1. Создать класс Bitstring для работы с битовыми строками не более чем из 100 бит. Битовая строка должна быть представлена массивом типа unsigned char, каждый элемент которого принимает значение 0 или 1. Реальный размер массива задается как аргумент конструктора инициализации. Должны быть реализованы все традиционные операции для работы с битовыми строками: and, or, xor, not. Реализовать сдвиг влево и сдвиг вправо на заданное количест- во битов. 2. Создать класс Decimal для работы с беззнаковыми целыми десятичными чис- лами, используя для представления числа массив из 100 элементов типа unsigned char, каждый из которых является десятичной цифрой. Младшая цифра имеет меньший индекс (единицы — в нулевом элементе массива). Ре- альный размер массива задается как аргумент конструктора инициализации. Реализовать арифметические операции, аналогичные встроенным для целых в C++, и операции сравнения. 3. Создать класс Hex для работы с беззнаковыми целыми шестнадцатеричными числами, используя для представления числа массив из 100 элементов типа unsigned char, каждый из которых является шестнадцатеричной цифрой. Младшая цифра имеет меньший индекс. Реальный размер массива задается как аргумент конструктора инициализации. Реализовать арифметические операции, аналогичные встроенным для целых в C++, и операции сравнения.
44 Глава 2. Конструкторы и перегрузка операций 4. Создать класс Money для работы с денежными суммами (задание 1.24). Сумма должна быть представлена массивом, каждый элемент которого — десятичная цифра. Максимальная длина массива — 100 цифр, реальная длина задается конструктором. Младший индекс соответствует младшей цифре денежной суммы. Младшие две цифры — копейки. 5. Создать класс Poll пот для работы с многочленами до 100-й степени. Коэффи- циенты должны быть представлены массивом из 100 элементов-коэффициен- тов. Младшая степень имеет меньший индекс (нулевая степень — нулевой индекс). Размер массива задается как аргумент конструктора инициализации. Реализовать арифметические операции и операции сравнения, вычисление значения полинома для заданного значения х, дифференцирование, интегри- рование. 6. Создать класс Fraction для работы с беззнаковыми дробными десятичными числами. Число должно быть представлено двумя массивами типа unsigned char: целая и дробная часть, каждый элемент — десятичная цифра. Для целой части младшая цифра имеет меньший индекс, для дробной части старшая цифра имеет меньший индекс (десятые — в нулевом элементе, сотые — в пер- вом, и т. д.). Реальный размер массивов задается как аргумент конструктора инициализации. Реализовать арифметические операции сложения, вычита- ния и умножения, и операции сравнения. 7. Реализовать класс Rational (задание 1.28), используя два массива из 100 эле- ментов типа unsigned char для представления числителя и знаменателя. Каж- дый элемент является десятичной цифрой. Младшая цифра имеет меньший индекс (единицы — в нулевом элементе массива). Реальный размер массива задается как аргумент конструктора инициализации. 8. Реализовать класс Money (задание 1.33), используя для представления суммы денег массив структур. Структура имеет два поля: номинал купюры и количе- ство купюр данного достоинства. Номиналы представить как перечислимый тип nomi nal. Элемент массива структур с меньшим индексом содержит мень- ший номинал. 9. Реализовать класс Data (задание 1.29), используя для представления месяцев массив структур. Структура имеет два поля: название месяца (строка), коли- чество дней в месяце. Индексом в массиве месяцев является перечислимый тип month. Реализовать два варианта класса: с обычным массивом и статиче- ским массивом месяцев. 10. Карточка иностранного слова представляет собой структуру, содержащую иностранное слово и его перевод. Для моделирования электронного словаря иностранных слов реализовать класс Dictionary. Данный класс имеет поле-на- звание словаря и содержит массив структур WordCard, представляющих собой карточки иностранного слова. Название словаря задается при создании ново- го словаря, но должна быть предоставлена возможность его изменения во время работы. Карточки добавляются в словарь и удаляются из него. Реали- зовать поиск определенного слова как отдельный метод. Аргументом опе- рации индексирования должно быть иностранное слово. В словаре не долж- но быть карточек-дублей. Реализовать операции объединения, пересечения
Упражнения 45 и вычитания словарей. При реализации должен создаваться новый словарь, а исходные словари не должны изменяться. При объединении новый словарь должен содержать без повторений все слова, содержащиеся в обоих словарях- операндах. При пересечении новый словарь должен состоять только из тех слов, которые имеются в обоих словарях-операндах. При вычитании новый словарь должен содержать слова первого словаря-операнда, отсутствующие во втором. И. Один тестовый вопрос представляет собой структуру Task со следующими по- лями: вопрос, пять вариантов ответа, номер правильного ответа, начисляемые баллы за правильный ответ. Для моделирования набора тестовых вопросов реализовать класс TestContent, содержащий массив тестовых вопросов. Реали- зовать методы добавления и удаления тестовых вопросов, а также метод до- ступа к тестовому заданию по его порядковому номеру в списке. В массиве не должно быть повторяющихся вопросов. Реализовать операцию слияния двух тестовых наборов, операцию пересечения и вычисления разности (см. зада- ние 10). Дополнительно реализовать операцию генерации конкретного объек- та Test объемом не более К вопросов из объекта типа TestContent. 12. Карточка персоны содержит фамилию и дату рождения. Реализовать класс Li stPerson для работы с картотекой персоналий. Класс должен содержать мас- сив карточек персон. Реализовать методы добавления и удаления карточек персон, а также метод доступа к карточке по фамилии. Фамилии в массиве должны быть уникальны. Реализовать операции объединения двух картотек, операцию пересечения и вычисления разности (см. задание 10). Реализовать метод, выдающий по фамилии знак зодиака. Для этого в классе должен быть объявлен массив структур Zodiac с полями: название знака зодиака, дата нача- ла и дата окончания периода. Индексом в массиве должен быть перечисли- мый тип zodi ас. Реализовать два варианта класса: с обычным массивом и ста- тическим массивом Zodiac. 13. Товарный чек содержит список товаров, купленных покупателем в магазине. Один элемент списка представляет собой пару: товар-сумма. Товар — это класс Goods с полями кода и наименования товара, цены за единицу товара, количества покупаемых единиц товара. В классе должны быть реализованы методы доступа к полям для получения и изменения информации, а также метод вычисления суммы оплаты за товар. Для моделирования товарного чека реализовать класс Receipt, полями которого являются номер товарного чека, дата и время его создания, список покупаемых товаров. В классе Receipt реа- лизовать методы добавления, изменения и удаления записи о покупаемом то- варе, метод поиска информации об определенном виде товара по его коду, а также метод подсчета общей суммы, на которую были осуществлены покупки. Методы добавления и изменения принимают в качестве аргумента объект клас- са Goods. Метод поиска возвращает объект класса Goods в качестве результата. 14. Информационная запись о книге в библиотеке содержит следующие поля: ав- тор, название, год издания, издательство, цена. Для моделирования учетной карточки абонента реализовать класс Subscriber, содержащий фамилию або- нента, его библиотечный номер и список взятых в библиотеке книг. Один
46 Глава 2. Конструкторы и перегрузка операций элемент списка состоит из информационной записи о книге, даты выдачи, требуемой даты возврата и признака возврата. Реализовать методы добавле- ния книг в список и удаления книг из него; метод поиска книг, подлежащих возврату; методы поиска по автору, издательству и году издания; метод вы- числения стоимости всех подлежащих возврату книг. Реализовать операцию слияния двух учетных карточек, операцию пересечения и вычисления разности (см. задание 10). Реализовать операцию генерации конкретного объекта Debt (долг), содержащего список книг, подлежащих возврату из объекта типа Subscriber. 15. Информационная запись о файле в каталоге содержит поля: имя файла, рас- ширение, дата и время создания, атрибуты «только чтение», «скрытый», «сис- темный», размер файла на диске. Для моделирования каталога реализовать класс Directory, содержащий название родительского каталога, количество файлов в каталоге, список файлов в каталоге. Один элемент списка включает в себя информационную запись о файле, дату последнего изменения, признак выделения и признак удаления. Реализовать методы добавления файлов в ка- талог и удаления файлов из него; метод поиска файла по имени, по расширению, по дате создания; метод вычисления полного объема каталога. Реализовать операцию объединения и операцию пересечения каталогов (см. задание 10). Реализовать операцию генерации конкретного объекта Group (группа), содер- жащего список файлов, из объекта типа Di rectory. Должна быть возможность выбирать группу файлов по признаку удаления, по атрибутам, по дате созда- ния (до. или после), по объему (меньше или больше). 16. Используя класс Bill (задание 1.56), реализовать класс ListPayer. Класс со- держит список плательщиков за телефонные услуги, дату создания списка, номер списка. Поля одного элемента списка — это: плательщик (класс Bill), признак оплаты, дата платежа, сумма платежа. Реализовать методы добавле- ния плательщиков в список и удаления их из него; метод поиска плательщика по номеру телефона и по фамилии, по дате платежа. Метод вычисления пол- ной стоимости платежей всего списка. Реализовать операцию объединения и . операцию пересечения списков (см. задание 10). Реализовать операцию гене- рации конкретного объекта Group (группа), содержащего список плательщи- ков, из объекта типа ListPayer. Должна быть возможность выбирать группу плательщиков по признаку оплаты, по атрибутам, по. дате платежа, по номеру телефона. 17. Учебный план специальности является списком дисциплин, которые студент должен изучить за время обучения. Одна дисциплина представляет собой струк- туру с полями: номер дисциплины в плане, тип дисциплины (федеральная, региональная, по выбору), название дисциплины, семестр, в котором дисцип- лина изучается, вид итогового контроля (зачет или экзамен), общее количе- ство часов, необходимое для изучения дисциплины, количество аудиторных часов, которые состоят из лекционных часов и часов практики. Реализовать класс PlanEducation для моделирования учебного плана специальности. Класс должен содержать код и название специальности, дату утверждения, общее количество часов специальности по стандарту и список дисциплин. Один эле- мент списка дисциплин должен содержать запись о дисциплине, количество
Упражнения 47 часов для самостоятельной работы (разность между общим количеством ча- сов и аудиторными часами), признак наличия курсовой работы, выполняемой по данной дисциплине. Реализовать методы добавления и удаления дисцип- лин; метод поиска дисциплины по семестру, по типу дисциплины, по виду итогового контроля; метод вычисления суммарного количества часов всех дисциплин; метод вычисления количества экзаменов и зачетов по семестрам. Реализовать операцию объединения, операцию вычитания учебных планов и операцию пересечения учебных планов (см. задание 10). Реализовать опера- цию генерации конкретного объекта Group (группа дисциплин), содержащего список дисциплин, из объекта типа PlanEducation. Должна быть возможность выбирать группу дисциплин по типу, по семестру, по виду итогового контро- ля, по наличию курсовой работы. Должен осуществляться контроль за сум- марным количеством часов, за количеством экзаменов в семестре (не более пяти и не менее трех). 18. Нагрузка преподавателя за учебный год представляет собой список дисцип- лин, преподаваемых им в течение года. Одна дисциплина представляется ин- формационной структурой с полями: название дисциплины, семестр проведе- ния, количество студентов, количество часов аудиторных лекций, количество аудиторных часов практики, вид контроля (зачет или экзамен). Реализовать класс WorkTeacher, моделирующий бланк назначенной преподавателю нагруз- ки. Класс содержит фамилию преподавателя, дату утверждения, список пре- подаваемых дисциплин, объем полной нагрузки в часах и в ставках. Дисцип- лины в списке не должны повторяться. Объем в ставках вычисляется как частное от деления объема в часах на среднюю годовую ставку, одинаковую для всех преподавателей кафедры. Элемент списка преподаваемых дисцип- лин содержит дисциплину, количество часов, выделяемых на зачет (0,35 ч на одного студента) или экзамен (0,5 ч на студента), сумму часов по дисципли- не. Реализовать добавление и удаление дисциплин; вычисление суммарной нагрузки в часах и ставках. Должен осуществляться контроль за превышени- ем нагрузки более полутора ставок. 19. Создать класс Octal для работы с беззнаковыми целыми восьмеричными чис- лами, используя для представления числа массив из 100 элементов типа unsigned char, каждый элемент которого является восьмеричной цифрой. Млад- шая цифра имеет меньший индекс (единицы — в нулевом элементе массива). Реальный размер массива задается как аргумент конструктора инициали- зации. Реализовать арифметические операции, аналогичные встроенным для целых в C++, и операции сравнения. 20. Создать класс String для работы со строками, аналогичными строкам Turbo Pascal (строка представляется как массив 255 байт, длина — в первом байте). Максимальный размер строки должен задаваться. Обязательно должны быть реализованы: определение длины строки, поиск подстроки в строке, удаление подстроки из строки, вставка подстроки в строку, сцепление двух строк. 21. Одна запись в списке запланированных дел представляет собой структуру Dailyitem, которая содержит время начала и окончания работы, описание и признак выполнения. Реализовать класс DailySchedul е, представляющий
48 Глава 2. Конструкторы и перегрузка операций собой план работ на день. Реализовать методы добавления, удаления и изме- нения планируемой работы. При добавлении проверять корректность вре- менных рамок (они не должны пересекаться с уже запланированными меро- приятиями). Реализовать метод поиска свободного промежутка времени. Ус- ловие поиска задает размер искомого интервала, а также временные рамки, в которые он должен попадать. Метод поиска возвращает структуру Dailyltem с пустым описанием вида работ. Реализовать операцию генерации объекта Redo (еще раз), содержащего список дел, не выполненных в течение дня, из объекта типа DailySchedule. 22. Прайс-лист компьютерной фирмы включает в себя список моделей продавае- мых компьютеров. Одна позиция списка (Model) содержит марку компьютера, тип процессора, частоту работы процессора, объем памяти, объем жесткого диска, объем памяти видеокарты, цену компьютера в условных единицах и количество экземпляров, имеющихся в наличии. Реализовать класс PriceList, полями которого являются дата его создания, номинал условной единицы в рублях и список продаваемых моделей компьютеров. В списке не должно быть двух моделей одинаковой марки. В классе PriceList реализовать методы добавления, изменения и удаления записи о модели, метод поиска информа- ции о модели по марке компьютера, по объему памяти, диска и видеокарты (равно или не меньше заданного), а также метод подсчета общей суммы. Реа- лизовать методы объединения и пересечения прайс-листов (см. задание 10). Методы добавления и изменения принимают в качестве входного параметра объект класса Model. Метод поиска возвращает объект класса Model в качестве результата. 23. Реализовать класс Set (множество) не более чем из 256 элементов-символов. Решение должно обеспечивать включение элемента в множество, исключение элемента из множества, объединение, пересечение и разность множеств, вычис- ление количества элементов в множестве, проверку присутствия элемента в множестве, проверку включения одного множества в другое.
ГЛАВА 3 Наследование В данной теме вам предстоит изучить технику программирования простого на- следования, как открытого, так и закрытого. Рассматривается наследование по- лей, конструкторов, деструкторов и методов. Определяется и изучается принцип подстановки. Рассматриваются виртуальные функции и абстрактные классы. Краткие сведения по теме Наследование — это третий «кит» (наряду с инкапсуляцией и полиморфизмом), на котором стоит объектно-ориентированное программирование. При наследовании обязательно имеются класс-предок (родитель, порождающий класс) и класс-на- следник (потомок, порожденный класс). В C++ порождающий класс называется базовым, а порожденный класс — производным. Все термины эквивалентны. Отношения между родительским классом и его потомками называются иерархи- ей наследования. Глубина наследования стандартом [1] не ограничена. Порядок наследования в иерархии не регламентируется: все зависит от потребностей за- дачи и опыта программиста. Простое открытое наследование Простым (или одиночным) называется наследование, при котором производный класс имеет только одного родителя. Формально наследование одного класса от другого задается следующей конструкций: class имя_класса_потомка: [модификатор_доступа] имя_базового_класса { тело_класса } Модификатор доступа определяет доступность элементов базового класса в клас- се-наследнике. Квадратные скобки не являются элементом синтаксиса, а показы- вают, что модификатор может отсутствовать. Этот модификатор называется модификатором наследования. При разработке отдельных классов используются два модификатора доступа к элементам класса: public (общий) и private (личный). При наследовании при- меняется еще один: protected (защищенный). Защищенные элементы класса до- ступны только прямым наследникам и никому другому.
50 Глава 3. Наследование Возможны четыре варианта наследования. 1. Класс от класса. 2. Класс от структуры. 3. Структура от структуры. 4. Структура от класса. Доступность элементов базового класса из классов-наследников изменяется в за- висимости от модификаторов доступа в базовом классе и модификатора насле- дования. Каким бы ни был модификатор наследования, приватные элементы ба- зового класса недоступны в его потомках. В табл. 3.1 приведены все варианты доступности элементов базового класса в производном классе при любых вари- антах модификатора наследования. Если модификатор наследования — public, наследование называется открытым. Модификатор protected определяет защищенное наследование, а слово private означает закрытое наследование. Таблица 3.1. Доступ к элементам базового класса в классе-наследнике Модификатор доступа в базовом классе Модификатор наследования Доступ в производном классе struct class public отсутствует public private protected отсутствует public private private отсутствует недоступны недоступны public public public public protected public protected protected private public недоступны недоступны public protected protected protected protected protected protected protected private protected недоступны недоступны public private private private protected private private private private private недоступны недоступны Конструкторы и деструкторы при наследовании Конструкторы не наследуются — они создаются в производном классе (если не определены программистом явно). Система поступает с конструкторами следую- щим образом (листинг 3.1): □ если в базовом классе нет конструкторов или есть конструктор без аргумен- тов (или аргументы присваиваются по умолчанию), то в производном классе
Краткие сведения по теме 51 конструктор можно не писать — будут созданы конструктор копирования и конструктор без аргументов; □ если в базовом классе все конструкторы с аргументами, производный класс обязан иметь конструктор, в котором явно должен быть Вызван конструктор базового класса; □ при создании объекта производного класса сначала вызывается конструктор базового класса, затем — производного. Листинг 3.1. Конструкторы в наследнике // Базовый класс - с конструкторами по умолчанию class Money { public: // конструктор инициализации и без аргументов Money(const double &r = 0.0) { Summa = d; } private: double Summa: }: 11 класс-наследник без конструкторов class Backs: public Money { }: class Basis { int a.b; public: // только явные конструкторы с аргументами Basis(int х. int у) { а=х: Ь=у; } }: class Inherit: public Basis { int sum: public: Inherit(int x, int у, int s) :Basis(x,y) // явный вызов конструктора { sum=s:} }; В классе Money определен конструктор инициализации, который играет роль и конструктора без аргументов, так как аргумент задан по умолчанию. В классе- наследнике Backs конструкторы можно не писать. Во втором случае в классе Basis определен только конструктор инициализации (конструктор копирования создается автоматически, а конструктор без аргументов — нет). В классе-наслед- нике Inherit конструктор базового класса явно вызывается в списке инициализа- ции. Деструктор класса, как и конструкторы, не наследуется, а создается. С деструк- торами система поступает следующим образом: □ при отсутствии деструктора в производном классе система создает деструк- тор по умолчанию; □ деструктор базового класса вызывается в деструкторе производного класса автоматически независимо от того, определен он явно или создан системой; □ деструкторы вызываются (для уничтожения объектов) в порядке, обратном вызову конструкторов. Создание и уничтожение объектов выполняется по принципу LIFO: последним создан — первым уничтожен.
52 Глава 3. Наследование Поля и методы при наследовании Класс-потомок наследует структуру (все элементы данных) и поведение (все ме- тоды) базового класса. Класс-наследник получает в наследство все поля базового класса (хотя, если они были приватные, доступа к ним не имеет). Если новые поля не добавляются, размер класса-наследника совпадает с размером базового класса. Порожденный класс может добавить собственные поля: class Point2 { int х; int у; public: И ... }: class Point3: public Point2 { int z: public: II ... }: Добавляемые поля должен инициализировать конструктор наследника. Дополнительные поля производного класса могут совпадать и по имени, и по типу с полями базового класса — в этом случае новое поле скрывает поле базово- го класса, поэтому для доступа к последнему полю в классе-наследнике необхо- димо использовать префикс-квалификатор базового класса. Класс-потомок наследует все методы базового класса (листинг 3.2), кроме опе- рации присваивания — она создается для нового класса автоматически, если не определена явно. В классе-наследнике можно определять новые методы. В но- вых методах разрешается вызывать любые доступные методы базового класса. Если в классе-наследнике имя метода и его прототип совпадают с именем метода базового класса, то говорят, что метод производного класса скрывает метод базо- вого класса. Чтобы вызвать метод родительского класса, нужно указывать его с квалификатором класса. Листинг 3.2. Наследование методов class Base { int f; int m; publi c: Based:f().m(){} // конструктор без аргументов Base (int t, int г = 0) // конструктор инициализации { f = t; m = r; } Base& operator++() 11 префиксный инкремент { f++; return *this: } Base operator++(int) // постфиксный инкремент { F t = *this: f++; return t: } Base& operator+=(const FS r) // сложение с присваиванием { f+=r.f; m+=r.m; return *this: } class Derive: public Base { public: Derives operator--!) // новый метод: префиксный декремент { f--; return *this; } }:
Краткие сведения по теме 53 Объекты класса-наследника могут использовать операции базового класса, на- пример Derive d: ++d: d++; d+=4; Статические элементы класса при наследовании Статические поля нормально наследуются. Однако все потомки разделяют един- ственную копию статических полей. Нормально наследуются и статические ме- тоды; при вызове можно писать либо префикс наследника, либо префикс базово- го класса, например class Base { public: static void f(void); }: class Derived: public Base { }: Base::f(); 11 префикс - базовый класс Derived::f(); // префикс - наследник Вложенные классы и наследование Ограничений в наследовании вложенных классов нет: внешний класс может на- следовать от вложенного и наоборот; вложенный класс может наследовать от вложенного. class А {}; class В { public: class С: public А {}: }: class D: public В::C {}; class E { class F: public B::C {}: }; // внешний класс // вложенный класс наследует от внешнего // внешний класс наследует от вложенного // вложенный класс наследует от вложенного Нужно следить только за видимостью базового класса в точке наследования. На- пример, от вложенного класса Е:: F «снаружи» наследовать нельзя, так как класс находится в приватной части класса Е и вне его — невидим. Однако допустимо, чтобы еще один вложенный класс внутри Е наследовал от вложенного класса Е:: F: class Е { class F: public В::C {}: class G: public F {}; }; Операция присваивания и принцип подстановки Открытое наследование устанавливает между классами отношение «является»: класс-наследник является разновидностью класса-родителя. Это означает, что везде, где может быть использован объект базового класса (при присваивании, при передаче параметров и возврате результата), вместо него разрешается под-
54 Глава 3. Наследование ставлять объект производного класса. Данное положение называется принципом подстановки и поддерживается компилятором. Этот принцип работает и для ссылок, и для указателей: вместо ссылки (указателя) на базовый класс может быть подставлена ссылка (указатель) на класс-наследник. Обратное — неверно! Например, спортсмен является человеком, но не всякий человек — спортсмен. Здесь человек — базовый класс, а спортсмен — производный. Помимо конструкторов, не наследуются два вида функций: операция присваива- ния и дружественные функции. Операция присваивания, как и конструкторы с деструктором, для любого класса создается автоматически и имеет прототип: класс& класс::operator=(const класс St); Операция бинарная, левым аргументом является текущий объект. Эта стандарт- ная операция обеспечивает копирование значений полей объекта t в поля теку- щего объекта. Для базового класса Base и его наследника Derive соответствующие операции присваивания имеют вид: Base& Base::operator=(const Base St): Derives Derive::operator=(const Derive St): Наличие этих функций позволяет нам выполнять следующие присваивания: Base bl, b2: // Derive dl. d2; // bl = b2: 11 dl = d2; // bl = d2; // переменные базового класса переменные производного класса операция базового класса операция производного класса базовый = производный: операция базового класса В последнем случае работает принцип подстановки: справа от знака присваива- ния задан объект производного класса, который подставляется на место аргу- мента базового класса в операции присваивания базового класса. Преобразова- ние типа при этом указывать не нужно — это происходит автоматически. Однако нельзя выполнить присваивание dl = Ы: // производный = базовый Чтобы оно работало, требуется в производном классе определить операцию при- сваивания с прототипом Derives Derive::operator=(const Base St); Определение функции должно быть таким: Derives Derive::operator=(const Base St) { this->Base::operator=(t): // вызов родительской операции return *this; } В теле дочерней операции выполняется вызов родительской операции в функ- циональной форме. Эта операция похожа на операцию преобразования объектов базового класса в производный. В любом классе операцию присваивания можно перегрузить неоднократно. При этом разрешается, чтобы ни аргумент, ни результат не являлись определяемым классом. Единственным ограничением является видимость определения нужных классов в точке определения операции присваивания.
Краткие сведения по теме 55 Дружественные функции не наследуются, поскольку не являются методами ба- зового класса, хотя и имеют доступ к внутренней структуре класса. При откры- том наследовании можно не дублировать дружественные функции для про- изводного класса, так как принцип подстановки обеспечивает помещение аргументов производного класса на место параметров базового класса. Напри- мер, можно не дублировать функцию вывода, если вывод объектов-наследников не изменяется: ostream& operator«(ostream &t, const Base &r) { t « r: return t; } Если в производном классе определены дополнительные поля, то при подста- новке объектов происходит срезка (расщепление): объекту базового класса при- сваиваются только унаследованные производным классом поля. Point3 b; Points а: а = Ь; // подстановка в присваивании - срезка При передаче параметра по значению может произойти срезка при копировании. При передаче параметра по ссылке (или по указателю) срезки не происходит. Функции-операции преобразования Функция преобразования выполняет преобразование по умолчанию из типа клас- са в задаваемый в ней тип. Общий вид функции преобразования следующий: operator typed; где type может быть встроенным типом, типом класса или именем typedef. На месте type не может находиться тип массива или функции. Функция преобразо- вания обязательно должна быть методом. В объявлении не должны задаваться ни тип возвращаемого значения, ни список аргументов — это будет ошибкой. Конструктор инициализации класса выполняет преобразование типов по умол- чанию из типа аргумента в тип класса. Функция преобразования осуществляет приведение типов по умолчанию из типа класса в указанный тип. Например, для класса Money можно задать преобразования по умолчанию double -> Money Money -> double. определив конструктор инициализации и функцию преобразования. class Money { public: // конструктор инициализации и без аргументов Money(const double &r = 0.0) { Summa = d; } operator doubled { return Summa: } private: double Summa; } Преобразование по умолчанию double -> Money
56 Глава 3. Наследование можно запретить, определив expli ci t-конструктор explicit Moneyiconst double &r = 0.0) { Summa = d: } В этом случае конструктор инициализации неявно вызываться не будет. Закрытое наследование Закрытое наследование — это наследование реализации (в отличие от наследова- ния интерфейса — см. далее): класс реализован посредством класса. Оно прин- ципиально отличается от открытого: принцип подстановки не соблюдается. Это означает, что нельзя присвоить (во всяком случае, без явного преобразования типа) объект производного класса базовому. Поэтому закрытое наследование хо- рошо применять в тех случаях, когда требуется иметь функциональность базово- го класса, но не нужны ни копирование, ни присваивание. При закрытом наследовании все элементы класса-наследника становятся при- ватными и недоступными программе-клиенту. class Base { public: void methodlO; void method2(); }: class Derive: private Base // наследуемые методы недоступны клиенту { }: Программа, использующая класс Derive, не может вызвать ни methodi О, ни method2(). В наследнике нужно заново реализовать нужные методы, вызвав в методах на- следника методы родителя. class Derive: private Base // наследуемые методы недоступны клиенту { public: void methodic { Base: :methodl(): } void method2() { Base::method2(): }; }; Префикс при вызове задавать обязательно, иначе возникает рекурсивное обра- щение к методу-наследнику. Можно открыть методы базового класса с помощью using-объявления, которое имеет следующий синтаксис: using <имя базового класса>::<имя в базовом классе>: В наследуемом классе это делается так: class Derive: private Base // наследуемые методы недоступны клиенту { public: using Base: :methodlO; }: Таким образом, закрытое наследование позволяет ограничить предоставляемую производным классам функциональность.
Краткие сведения по теме 57 Виртуальные функции Связывание — это сопоставление вызова функции с телом. Для обычных методов связывание выполняется на этапе трансляции до запуска программы. Такое свя- зывание называют «ранним», или статическим. При наследовании обычного ме- тода его поведение не меняется в наследнике. Однако бывает необходимо, чтобы поведение некоторых методов базового класса и классов-наследников отлича- лись. Чтобы добиться разного поведения в зависимости от типа, необходимо объявить функцию-метод виртуальной; в C++ это делается с помощью ключево- го слова vi rtual. Виртуальные функции в совокупности с принципом подстанов- ки обеспечивают механизм «позднего» (отложенного) или динамического связы- вания, которое работает во время выполнения программы (листинг 3.3). Листинг 3.3. Виртуальные методы class Base { public: virtual void print!) const { cout « "Base!" « endl: } class Derive: public Base { public: virtual void print!) const { cout « "Derive!" « endl: }; void F(Base &d) { d.print!): } Base W; F(W): Derive U; F(U): Base *cl = &W: cl->print(): cl = &U: cl->print(): // базовый класс // базовый метод // производный класс И переопределенный метод // предполагается вызов метода базового класса // объект базового класса И работает метод базового класса // объект производного класса // работает метод производного класса // адрес объекта базового класса И вызов базового метода // адрес объекта производного типа вместо базового // вызывается производный метод Класс, в котором определены виртуальные функции (хотя бы одна), называется полиморфным классом. Ключевое слово virtual можно писать только в базовом классе — это достаточно сделать в объявлении функции. Даже если определение писать без слова vi rtual, функция все равно будет считаться виртуальной. Правила описания и использо- вания виртуальных функций-методов следующие. , 1. Виртуальная функция может быть только методом класса. 2. Любую перегружаемую операцию-метод класса можно сделать виртуальной, например операцию присваивания или операцию преобразования типа. 3. Виртуальная функция наследуется. 4. Виртуальная функция может быть константной. 5. Если в базовом классе определена виртуальная функция, то метод производ- ного класса с таким же именем и прототипом (включая и тип возвращаемого
58 Глава 3. Наследование значения, и константность метода) автоматически является виртуальным (слово vi rtual указывать необязательно) и замещает функцию-метод базово- го класса. 6. Статические методы не могут быть виртуальными. 7. Конструкторы не могут быть виртуальными. 8. Деструкторы могут (чаще — должны) быть виртуальными — это гарантирует корректное освобождение памяти через указатель базового класса. Внутри конструкторов и деструкторов динамическое связывание не работает, хотя вызов виртуальных функций не запрещен. В конструкторах и деструкторах всегда вызывается «родная» функция класса. Виртуальные функции-методы можно перегружать и переопределять (в наслед- никах) с другим списком аргументов. Если виртуальная функция переопределе- на с другим списком аргументов, она замещает (скрывает) родительские методы. Константный метод считается отличным от неконстантного метода с таким же прототипом (листинг 3.4). Листинг 3.4. Перегрузка и переопределение виртуальных методов class Base { public: // перегрузка виртуальных методов virtual int f() const { cout « "Base::f()"« endl; return 0; } virtual void f(const string &s) const { cout « "Base: :f(string)”« endl: } }: class Derive: public Base { public: virtual int f(int) const // переопределение виртуальной функции f cout « "Derive: :f(int)"« endl; return 0: } Base b. *pb; Derive d. *pd = &d: pb = &d: pb->f(); pb->f("name"); pb->f(l): pd->f(l); // объекты базового типа // объекты производного типа // здесь нужна виртуальность // вызывается базовый метод // вызывается базовый метод // ошибка! И нормально - вызов метода наследника Через указатель базового класса нельзя вызвать новые виртуальные методы, определенные только в производном классе. Если это необходимо, требуется явное приведение типа: ((Derive *)pb)->f(l); Родительские методы можно сделать доступными в классе-наследнике при по- мощи using-объявления. class Derive: public Base { public: virtual int f(1nt) const { cout « "Derived: :f(int)"« endl: return 0: } using Base::f: // разрешение использовать скрытые базовые методы };
Краткие сведения по теме 59 Разрешается при переопределении виртуальной функции изменить только тип возвращаемого значения, если это указатель или ссылка. Виртуальную функцию можно вызвать невиртуально, если указать квалифика- тор класса: Base *cl = new Derive!); И адресуется объект производного класса cl->print(): // вызов метода-наследника cl->Base::print(): // явно вызывается базовый метод Такой статический вызов бывает нужен, когда в базовом классе реализуются об- щие действия, которые должны выполняться во всех классах-наследниках. При наличии хотя бы одной виртуальной функции размер класса без полей ра- вен 4 — это размер указателя1. Количество виртуальных функций роли не игра- ет — размер класса остается равен 4. class One { virtual void f(void) {} }: class Two { virtual void f(void) {} virtual void g(void) {} }: // ... cout « sizeof(One) « endl: // размер = 4 cout « sizeof(Two) « endl: 11 размер = 4 Чистые виртуальные функции и абстрактные классы Виртуальная функция, не имеющая определения тела, называется чистой (риге) и объявляется следующим образом: virtual тип имя(параметры) = 0: Предполагается, что данная функция будет реализована в классах-наследниках. Класс, в котором есть хоть одна чистая виртуальная функция, называется абст- рактным классом. Объекты абстрактного класса создавать запрещено — даже операцией new. И при передаче параметра в функцию невозможно передать объ- ект абстрактного класса по значению. Однако указатели (и ссылки) определять можно (вспомните объявление класса). При наследовании абстрактность сохраняется: если класс-наследник не реали- зует унаследованную чистую виртуальную функцию, то он тоже является абст- рактным. В C++ абстрактный класс определяет понятие интерфейса. Наследова- ние от абстрактного класса — это наследование интерфейса. Виртуальные деструкторы Деструктор класса может быть объявлен виртуальным (листинг 3.5). Когда де- структор базового класса виртуальный, то и деструкторы всех наследников такие же. Это необходимо, если доступ.к динамическому объекту производного класса выполняется через указатель базового класса. В этом случае при уничто- 1 На платформе Intel.
60 Глава 3. Наследование жении объекта через указатель базового класса вызывается деструктор произ- водного класса, а он вызывает деструктор базового класса. Если деструктор базо- вого класса не виртуальный, то при уничтожении объекта производного класса через указатель базового класса вызывается деструктор только базового класса. Листинг 3.5. Виртуальные деструкторы class Base { public: -BaseO И деструктор не виртуальный { cout « "Base::Not virtual destructor!"« endl; } }: class Derived: public Base // наследник { public: -DerivedO 11 деструктор не виртуальный { cout « "Derived: :Not virtual destructor!"« endl; } }; class VBase { public: virtual -VBaseO // деструктор виртуальный { cout « "Base: ivirtual destructor!"« endl; } }: class VDerived: public VBase { public: // деструктор виртуальный -VDerivedO // virtual можно не писать { cout « "Derived::virtual destructor!"« endl; } }: // ... Base *bp = new DerivedO: // подстановка - повышающее приведение типа delete bp: // вызывается только базовый деструктор VBase *vbp = new VDerivedO: // подстановка delete vbp; // вызывается производный, а потом базовый // деструктор Деструктор может быть объявлен как чистый: virtual -VBaseO - 0: Класс, в котором определен чистый виртуальный деструктор, является абстракт- ным, и создавать объекты этого класса запрещено. Однако класс-наследник не является абстрактным классом, поскольку деструктор не наследуется, и в на- следнике при отсутствии явно определенного деструктора он создается автома- тически. При объявлении чисто виртуального деструктора нужно написать и его определение. class Abstract { public: virtual -Abstract0=0: }; Abstract::~Abstract() { cout « "Abstract"« endl; } // наследник - не абстрактный класс class NotAbstract: public Abstract { // абстрактный класс // чистый виртуальный деструктор И определение чистого деструктора Определять можно не только чистые деструкторы, но и чистые виртуальные методы. Класс, для которого это определение написано, по-прежнему является
Упражнения 61 абстрактным классом. В отличие от чистых виртуальных деструкторов, даже определенная в базовом классе чистая виртуальная функция наследуется как чистая, поэтому производный класс тоже будет абстрактным при отсутствии собственного определения. class Base { public: virtual void f() = 0: } void Base::f() { cout « "Base: :f()"« endl; } class Derive: public Base { }: // абстрактный класс // чистый виртуальный метод // определение чистого метода // тоже абстрактный класс В определении чистого виртуального метода базового класса задается общий код, который могут использовать классы-наследники в своих реализациях этой функ- ции. При этом чистый метод базового класса вызывается статически с префик- сом класса. Упражнения Во всех заданиях реализовать вывод на экран, методы получения значений по- лей и методы установки значений полей, а также необходимые конструкторы (если это не указано в задании явно). Конструкторы и методы обязательно должны проверять параметры на допустимость; в случае неправильных данных — выво- дить сообщение об ошибке и заканчивать работу. Преобразование в строку реа- лизовать в виде функции преобразования string. Для демонстрации работы с объектами нового типа во всех заданиях требуется написать главную функцию. В программе должны присутствовать различные способы создания объектов и массивов объектов. Программа должна демонстри- ровать использование всех функций и методов. Открытое наследование 1. Во всех заданиях реализовать функцию, получающую и возвращающую объ- екты базового класса. Продемонстрировать принцип подстановки. 2. Создать базовый класс Саг (машина), характеризуемый торговой маркой (строка), числом цилиндров, мощностью. Определить методы переназначения и изме- нения мощности. Создать производный класс Lorry (грузовик), характеризуе- мый также грузоподъемностью кузова. Определить функции переназначения марки и изменения грузоподъемности. 3. Создать класс Pair (пара чисел); определить методы изменения полей и срав- нения пар: пара pl больше пары р2, если (first.pl > first.р2) или (first.pl = first.р2) и (second.pl > second.р2). Определить класс-наследник Fraction с по- лями: целая часть числа и дробная часть числа. Определить полный набор ме- тодов сравнения.
62 Глава 3. Наследование 4. Создать класс Liquid (жидкость), имеющий поля названия и плотности. Определить методы переназначения и изменения плотности. Создать произ- водный класс Al cohol (спирт), имеющий крепость. Определить методы пере- назначения и изменения крепости. 5. Создать класс Pair (пара чисел); определить методы изменения полей и вы- числения произведения чисел. Определить производный класс Rectangle (прямоугольник) с полями-сторонами. Определить методы вычисления пе- риметра и площади прямоугольника. 6. Создать класс Man (человек), с полями: имя, возраст, пол и вес. Определить методы переназначения имени, изменения возраста и изменения веса. Соз- дать производный класс Student, имеющий поле года обучения. Определить методы переназначения и увеличения года обучения. 7. Создать класс Triad (тройка чисел); определить методы изменения полей и вычисления суммы чисел. Определить производный класс Triangle с поля- ми-сторонами. Определить методы вычисления углов и площади треугольника. 8. Создать класс Triangle с полями-сторонами. Определить методы изменения сторон, вычисления углов, вычисления периметра. Создать производный класс Equilateral (равносторонний), имеющий поле площади. Определить метод вычисления площади. 9. Создать класс Triangle с полями-сторонами. Определить методы изменения сторон, вычисления углов, вычисления периметра. Создать производный класс RightAngled (прямоугольный), имеющий поле площади. Определить метод вычисления площади. 10. Создать класс Pair (пара чисел); определить методы изменения полей и вы- числения произведения чисел. Определить производный класс RightAngled с полями-катетами. Определить методы вычисления гипотенузы и площади треугольника. И. Создать класс Triad (тройка чисел); определить метод сравнения триад (см. задание 2). Определить производный класс Date с полями: год, месяц и день. Определить полный набор методов сравнения дат. 12. Создать класс Triad (тройка чисел); определить метод сравнения триад (см. задание 2). Определить производный класс Time с полями: час, минута и секунда. Определить полный набор методов сравнения моментов времени. 13. Реализовать класс-оболочку Number для числового типа float. Реализовать ме- тоды сложения и деления. Создать производный класс Real, в котором реали- зовать метод возведения в произвольную степень, и метод для вычисления логарифма числа. 14. Создать класс Triad (тройка чисел); определить методы увеличения полей на 1. Определить производный класс Date с полями: год, месяц и день. Переопреде- лить методы увеличения полей на 1 и определить метод увеличения даты на п дней. 15. Реализовать класс-оболочку Number для числового типа double. Реализовать ме- тоды умножения и вычитания. Создать производный класс Real, в котором
Упражнения 63 реализовать метод, вычисляющий корень произвольной степени, и метод для вычисления числа л в данной степени. 16. Создать класс Triad (тройка чисел); определить методы увеличения полей на 1. Определить класс-наследник Time с полями: час, минута, секунда. Пере- определить методы увеличения полей на 1 и определить методы увеличения на п секунд и минут. 17. Создать базовый класс Pair (пара целых чисел) с операциями проверки на ра- венство и перемножения полей. Реализовать операцию вычитания пар по формуле (а, Ь) - (с, d) = (а - b, с - d). Создать производный класс Rational; определить новые операции сложения {a, b) + (с, d) = (ad + be, bd) и деления (a, b) / (с, d) = (ad, be)-, переопределить операцию вычитания (a, b) - (с, d) = = (ad - be, bd). 18. Создать класс Pair (пара чисел); определить метод перемножения полей и операцию сложения пар (a, b) + (с, d) = (а + b, с + d). Определить производ- ный класс Complex с полями: действительная и мнимая части числа. Опреде- лить методы умножения (a, b) x(c,d) = (ас - bd, ad + be) и вычитания (а, Ь) - - (с, d) = (а - Ь, с - d). 19. Создать класс Pair (пара целых чисел); определить методы изменения полей и операцию сложения пар (a, b) + (с, d) = (а + b, с + d). Определить класс-на- следник Long с полями: старшая часть числа и младшая часть числа. Пере- определить операцию сложения и определить методы умножения и вычитания. 20. Создать базовый класс Triad (тройка чисел) с операциями сложения с чис- лом, умножения на число, проверки на равенство. Создать производный класс vector3D, задаваемый тройкой координат. Должны быть реализованы: опера- ция сложения векторов, скалярное произведение векторов. 21. Создать класс Pair (пара целых чисел); определить метод умножения на чис- ло и операцию сложения пар (a, b) + (с, d) = (а + b, с + d). Определить класс- наследник Money с полями: рубли и копейки. Переопределить операцию сло- жения и определить методы вычитания и деления денежных сумм. Наследование вместо композиции Реализовать классы заданий 1.21-1.40 с конструкторами, но без перегрузки опе- раций, в качестве базовых классов. Реализовать для базовых классов функции ввода/вывода как дружественные. Реализовать классы-наследники, в которых методы базовых классов используются для реализации перегруженных операций. Реализовать производные классы в двух вариантах: как открытые и как закры- тые классы-наследники. При открытом наследовании не определять функции ввода/вывода объектов для классов-наследников. Реализовать классы заданий 1.21-1.40 как классы-наследники; использовать от- крытое наследование. В качестве базового класса реализовать подходящий класс-структуру: пару (Pair) или тройку (Triad) чисел с конструкторами, с пол- ным набором операций сравнения. Реализовать для структуры функции вво- да/вывода в виде дружественных. Не определять функции ввода/вывода объек- тов для основных классов.
64 Глава 3. Наследование Реализовать классы заданий 1.21-1.40 как классы-наследники с перегрузкой операций и конструкторами. В качестве базового класса реализовать класс Object со счетчиком объектов. Реализовать основные классы заданий 1.41-1.60 с конструкторами и перегруз- кой операций как классы-наследники от вспомогательных; задействовать два вида наследования — открытое и закрытое. Реализовать задания 1.41-1.60 с конструкторами и перегрузкой операций. Вспо- могательный класс определить как вложенный открытый класс-наследник от ба- зового класса Object с подсчетом объектов. Массивы и наследование Создать базовый класс Array, в котором определите поле-массив подходящего типа и поле для хранения количества элементов у текущего объекта-массива. Максимально возможный размер массива задается статической константой. Реа- лизуйте конструктор инициализации, задающий количество элементов и началь- ное значение (по умолчанию 0). Реализуйте методы доступа к отдельному эле- менту, перегрузив операцию индексирования []. При этом должна выполняться проверка индекса на допустимость (см. задание 1.8). Реализовать один из классов заданий главы 2 как производный класс от класса Array; использовать открытое наследование. Во всех заданиях необходимо реали- зовать конструкторы инициализации и конструктор без аргументов. Указанные в задании операции реализуются посредством перегрузки подходящих операций. Во всех заданиях должны быть поддержаны соответствующие операции с при- сваиванием, ввод с клавиатуры, вывод на экран. 22. Создать класс Polinom (см. задание 2.5). 23. Создать класс Bitstring (см. задание 2.1). 24. Создать класс Decimal (см. задание 2.2). 25. Создать класс Money для работы с денежными суммами (см. задание 2.4). 26. Создать класс Decimal (см. задание 2.2) для работы со знаковыми целыми. Знак представить отдельным полем sign. 27. Создать класс Hex (см. задание 2.3). 28. Создать класс Octal (см. задание 2.19). 29. Создать класс PriceList (см. задание 2.21). 30. Создать класс Fraction для работы с дробными десятичными числами. Коли- чество цифр в дробной части должно задаваться в отдельном поле и инициа- лизироваться конструктором. Знак представить отдельным полем sign. 31. Создать класс Dictionary (см. задание 2.10). 32. Создать класс TestContent (см. задание 2.11). 33. Создать класс ListPerson (см. задание 2.12). 34. Создать класс Goods (см. задание 2.13). 35. Создать класс Subscriber (см. задание 2.14).
Упражнения 65 36. Создать класс Directory (см. задание 2.15). 37. Создать класс ListPaier (см. задание 2.16). 38. Создать класс WorkTeacher (см. задание 2.18). 39. Создать класс String (см. задание 2.20). 40. Создать класс PlanEducation (см. задание 2.17). 41. Создать класс DailySchedule (см. задание 2.21). Виртуальные функции Создать базовый класс Array с полями: массив типа unsigned char и поле для хра- нения количества элементов у текущего объекта-массива. Максимально возмож- ный размер массива задается статической константой. Реализовать конструктор инициализации, задающий количество элементов и начальное значение (по умолчанию 0). Реализовать метод доступа к элементу, перегрузив операцию ин- дексирования []. При этом должна выполняться проверка индекса на допусти- мость (задание 1.8). Реализовать в классе Array виртуальную функцию поэлементного сложения мас- сивов. Реализовать два класса, переопределив виртуальную функцию сложения. Вызывающая программа должна продемонстрировать все варианты вызова вир- туальных функций. 42. Создать класс Fraction (задание 29) и класс Bitstring (задание 2.1). 43. Создать класс Bitstring (задание 2.1) и класс Hex (задание 2.3). 44. Создать класс Decimal (задание 2.2) и класс Bitstring (задание 2.1). 45. Создать класс Money (задание 2.4) и класс Bitstring (задание 2.1). 46. Создать класс Decimal (задание 25) и класс Bitstring (задание 2.1). 47. Создать класс Hex (задание 2.3) и класс Bitstring (задание 2.1). 48. Создать класс Octal (задание 2.19) и класс Bitstring (задание 2.1). 49. Создать класс Hex (задание 2.3) и класс Octal (задание 2.19). 50. Создать класс Hex (задание 2.3) и класЬ Money (задание 2.4). 51. Создать класс Hex (задание 2.3) и класс Fraction (задание 29). 52. Создать класс Decimal (задание 25) и класс Hex (задание 2.3). 53. Создать класс Octal (задание 2.19) и класс Hex (задание 2.3). 54. Создать класс Decimal (задание 25) и класс Octal (задание 2.19). 55. Создать класс Decimal (задание 25) и класс String (задание 2.20). 56. Создать класс Bitstring (задание 2.1) и класс String (задание 2.20). 57. Создать класс Fraction (задание 29) и класс String (задание 2.20). 58. Создать класс Hex (задание 2.3) и класс String (задание 2.20). 59. Создать класс Octal (задание 2.19) и класс String (задание 2.20). 60. Создать класс Money (задание 2.4) и класс String (задание 2.20). 61. Создать класс Money (задание 2.4) и класс Hex (задание 2.3).
66 Глава 3. Наследование Абстрактные классы В следующих заданиях требуется реализовать абстрактный базовый класс, опре- делив в нем чистые виртуальные функции. Эти функции определяются в произ- водных классах. В базовых классах должны быть объявлены чистые виртуаль- ные функции ввода/вывода, которые реализуются в производных классах. Вызывающая программа должна продемонстрировать все варианты вызова вир- туальных функций с помощью указателей на базовый класс. Написать функцию вывода, получающую параметры базового класса по ссылке и демонстрирующую виртуальный вызов. 62. Создать абстрактный базовый класс Figure с виртуальными методами вычис- ления площади и периметра. Создать производные классы: Rectangl е (прямо- угольник), Circle (круг), Trapezium (трапеция) со своими функциями площади и периметра. Самостоятельно определить, какие поля необходимы, какие из них можно задать в базовом классе, а какие — в производных. Площадь тра- пеции: 5 = (а + Ь) х h / 2. 63. Создать абстрактный базовый класс Number с виртуальными методами — ариф- метическими операциями. Создать производные классы Integer (целое) и Real (действительное). 64. Создать абстрактный базовый класс Body (тело) с виртуальными функциями вычисления площади поверхности и объема. Создать производные классы: Parallelepiped (параллелепипед) и Ball (шар) со своими функциями площади поверхности и объема. 65. Создать абстрактный класс Currency (валюта) для работы с денежными сум- мами. Определить виртуальные функции перевода в рубли и вывода на эк- ран. Реализовать производные классы Doi 1 аг (доллар) и Euro (евро) со своими функциями перевода и вывода на экран. 66. Создать абстрактный базовый класс Triangle для представления треугольника с виртуальными функциями вычисления площади и периметра. Поля данных должны включать две стороны и угол между ними. Определить классы-на- следники: прямоугольный треугольник, равнобедренный треугольник, равно- сторонний треугольник со своими функциями вычисления площади и пери- метра. 67. Создать абстрактный базовый класс Pair с виртуальными арифметическими операциями. Создать производные классы FazzyNumber (задание 1.31) и Complex (задание 1.21). 68. Создать абстрактный базовый класс Root (корень) с виртуальными методами вычисления корней и вывода результата на экран. Определить производные классы Linear (линейное уравнение) и Square (квадратное уравнение) с собст- венными методами вычисления корней и вывода на экран. ' 69. Создать абстрактный базовый класс Function (функция) с виртуальными ме- тодами вычисления значения функции у = f(r) в заданной точке х и вывода результата на экран. Определить производные классы Ellipse (эллипс), Hyperbola (гипербола) с собственными функциями вычисления у в зависимости
Упражнения 67 от входного параметра х. Уравнение эллипса х2 / я2 + у2 / b2 = 1; гиперболы: х2 / я2 - у2 / b2 = 1. 70. Создать абстрактный базовый класс Pair с виртуальными арифметическими операциями. Реализовать производные классы Complex (задание 1.21) и Rational (задание 1.28). 71. Создать абстрактный базовый класс Tri ad с виртуальными методами увеличе- ния на 1. Создать производные классы Date (задание 1.29) и Time (зада- ние 1.30). 72. Создать абстрактный базовый класс Pair с виртуальными арифметическими операциями. Создать производные классы Money (задание 1.24) и Fraction (за- дание 1.35). 73. Создать абстрактный базовый класс Pair с виртуальными арифметическими операциями. Реализовать производные классы Money (задание 1.24) и Money (задание 1.33). 74. Создать абстрактный базовый класс Integer (целое) с виртуальными ариф- метическими операциями и функцией вывода на экран. Определить про- изводные классы Decimal (десятичное) и Binary (двоичное), реализующие соб- ственные арифметические операции и функцию вывода на экран. Число представляется массивом, каждый элемент которого — цифра. 75. Создать абстрактный базовый класс Pair с виртуальными арифметическими операциями. Создать производные классы Money (задание 1.24) и Complex (за- дание 1.21). 76. Создать абстрактный базовый класс Series (прогрессия) с виртуальными функциями вычисления у-го элемента прогрессии и суммы прогрессии. Опре- делить производные классы: Linear (арифметическая) и Exponential (геомет- рическая). (Арифметическая прогрессия я^ = Яо + jd,j = 0, 1, 2,... Сумма ариф- метической прогрессии: sn = (п + 1) (яо + а») / 2. Геометрическая прогрессия: Oj = яоЛу = 0, 1, 2,... Сумма геометрической прогрессии: = (яо - ОпГ) / (1 - г).) 77. Создать абстрактный класс Norm с виртуальной функцией вычисления нормы и модуля. Определить производные классы Complex, Vector3D с собственными функциями вычисления нормы и модуля. (Модуль для комплексного числа вычисляется как корень из суммы квадратов действительной и мнимой частей; норма для комплексных чисел вычисляется как модуль в квадрате. Модуль вектора вычисляется как корень квадратный из суммы квадратов ко- ординат; норма вектора вычисляется как максимальное из абсолютных значе- ний координат.) 78. Создать абстрактный базовый класс Pair с виртуальными арифметическими операциями. Создать производные классы FazzyNumber (задание 1.31) и Fraction (задание 1.35). 79. Создать абстрактный базовый класс Container с виртуальными методами sortO и поэлементной обработки контейнера foreachO. Разработать производные классы Bubble (пузырек) и Choice (выбор). В первом классе сортировка реали- зуется методом пузырька, а поэлементная обработка состоит в извлечении
68 Глава 3. Наследование квадратного корня. Во втором классе сортировка реализуется методом выбо- ра, а поэлементная обработка — вычисление логарифма. 80. Создать абстрактный базовый класс Array с виртуальными методами сложе- ния и поэлементной обработки массива foreachO. Разработать производные классы SortArray и ХогАггау. В первом классе операция сложения реализуется как объединение множеств, а поэлементная обработка — сортировка. Во вто- ром классе операция сложения реализуется как исключающее ИЛИ, а поэле- ментная обработка — вычисление корня. 81. Создать абстрактный базовый класс Array с виртуальными методами сложе- ния и поэлементной обработки массива foreachO. Разработать производные классы AndArray и ОгАггау (выбор). В первом классе операция сложения реали- зуется как пересечение множеств, а поэлементная обработка представляет собой извлечение квадратного корня. Во втором классе операция сложения реализуется как объединение, а поэлементная обработка — вычисление лога- рифма.
ГЛАВА 4 Исключения В данной теме представлен механизм обработки исключений и техника его ис- пользования. Рассматриваются иерархия стандартных исключений и организа- ция собственной иерархии. Изучается техника подмены стандартных функций завершения. Дополнительно рассматривается спецификация исключений. Краткие сведения по теме Перегруженные операции и конструкторы, в отличие от обычных функций и ме- тодов, не могут ни иметь лишние параметры для сигнализации об ошибках, ни возвращать код ошибки. В C++ встроен механизм обработки исключений, с по- мощью которого методы-операции и конструкторы класса получают возмож- ность сообщать программе-клиенту об аварийной ситуации. Механизм обработ- ки исключений — это объектно-ориентированные средства обработки ошибок, возникающих при неправильной работе программ. Механизм обработки исключений В C++ исключение — это объект; при возникновении исключительной ситуации программа должна генерировать объект-исключение. Генерация объекта-исклю- чения и создает исключительную ситуацию. Общая схема обработки исключе- ний такова: в одной части программы, где обнаружена аварийная ситуация, ис- ключение порождается; другая часть программы контролирует возникновение исключения, перехватывает и обрабатывает его. Генерируется исключение оператором throw, который имеет следующий синтаксис throw выражение_генерации_исключения: «Выражение генерации исключения» обычно означает либо константу, либо переменную некоторого типа; допускается задавать и выражение. Тип объекта- исключения может быть как встроенным, так и определяемым программистом. Для представления вида исключений часто используют пустой класс: class Negat1veArgument{}: class ZeroDevide {};
70 Глава 4. Исключения Тогда генерировать исключение можно так: throw ZeroDevideO; Здесь в качестве выражения генерации исключения использован явный вызов конструктора без аргументов — это наиболее часто встречающаяся форма гене- рации исключения. Объект-исключение может быть и динамическим, например throw new ZeroDevideO: Исключение надо перехватить и обработать. Проверка возникновения исключе- ния делается с помощью оператора try, с которым связаны одна или несколько секций-ловушек catch. Оператор try объявляет контролируемый блок, записывае- мый в следующем виде try { /* контролируемый блок */ } Контролируемый блок, помимо функции контроля, обладает функциями обыч- ного блока: все переменные, объявленные внутри него, являются локальными в этом блоке и не видны вне его. После try-блока обязательно следует один или несколько catch-блоков, которые называются обработчиками исключений. Форма записи секции-ловушки следую- щая: catch (спецификация_исключения) { /* блок обработки */} Спецификация исключения может иметь следующие три формы: (тип имя) (тип) (...) Тип должен быть одним из допустимых типов исключений — либо встроенным, либо определяемым программистом. Первые две формы из приведенных спецификаций исключения предназначены для обработки конкретного типа исключений. Если же на месте спецификации исключения написано многоточие, то такой обработчик перехватывает все ис- ключения. Секцию-ловушку с параметром-многоточием следует указывать в списке catch-блоков последней. Блоки try-catch могут быть вложенными, причем как в try-блок, так и в catch- блок: try { // блок, который может инициировать исключения try { И вложенный блок catch(...){ } catch (исключение) { try { // // обработка исключения вложенный блок catch(...){ }
Краткие сведения по теме 71 Передача параметра в блок обработки Первый вариант спецификации исключений означает, что объект-исключение передается в блок обработки, чтобы там его каким-либо образом использовать, например для вывода информации в сообщении об ошибке. При этом объект-ис- ключение может передаваться в секцию-ловушку любым способом: по значению, по ссылке или по указателю, например catch (exception е) catch (exceptions е) catch (const exceptions e) catch (exception *e) 11 по значению // по ссылке // no константной ссылке // no указателю Предпочтительней выглядит передача параметра по ссылке, так как в этом слу- чае не создается временный объект-исключение. Если тип exception встроенный, никаких преобразований по умолчанию не про- изводится. Это означает, что если в программе написан заголовок обработчика catch (float е) И по значению то попадают в эту секцию только те исключения, тип выражения исключений которых совпадает с fl oat. Оператор throw 1.2 генерирует исключение типа double (константы по умолчанию имеют такой тип), поэтому будет обрабатываться секцией-ловушкой с заголовком, с которой свя- зан тип исключения doubl е. В качестве собственных типов исключений можно реализовать развитый класс, в котором конструкторы имеют аргументы. Это позволит при обработке исклю- чений получать и анализировать нужную информацию. Например, функция вы- числяет площадь треугольника по трем сторонам и при этом должна контроли- ровать параметры. Если параметры ошибочны, нужно генерировать исключение. Стороны треугольника могут быть определены и инициализированы как поля класса-исключения ErrorTri angle. struct ErrorTriangle { double a, b. с; // параметры функции string message: // сообщение // конструктор ErrorTriangle(double x, double у. double z, const strings s) : a(x), b(y). c(z), message(s) {} } Тогда генерация исключения об ошибке параметров может осуществляться так: throw ErrorTrianglе(х.у.z,"Сторона нулевой длины”); или throw ErrorTrianglе(х.у.z.”Не треугольник”).; В секции-ловушке по переданным значениям сторон уточняется причина ава- рийной ситуации.
72 Глава 4. Исключения Порядок обработки исключения Когда в try-блоке возникает исключение, начинается сравнение типа исключе- ния и типов параметров в секциях-ловушках. Выполняется тот catch-блок, тип параметра которого совпал с типом исключения. Если такой тип не найден, но есть catch с многоточием, выполняется его блок. Если же такой блок в текущем списке обработчиков не обнаруживается, ищется другой список обработчиков в вызывающей функции. Этот поиск продолжается вплоть до функции main(). Если же и там не найдется нужного catch-блока, будет вызвана стандартная функция завершения terminateO, в свою очередь, вызывающая функцию abortO. Если catch-блок найден и выполнен, то после выполнения операторов catch-бло- ка при отсутствии явных операторов перехода или оператора throw выполняются операторы, расположенные после всей конструкции try-catch. Если во время ра- боты в try-блоке не обнаружено исключительной ситуации, все catch-блоки про- пускаются, и программа продолжает выполнение с первого оператора после по- следнего catch. Если управление попало в catch-блок, выход из секции-ловушки выполняется одним из следующих способов. 1. Выполнились все операторы обработчика — происходит неявный переход к оператору, расположенному после конструкции try-catch. 2. В обработчике выполняется оператор goto; разрешается переход на любой оператор вне конструкции try-catch; внутрь контролируемого блока и в дру- гую секцию-ловушку переход запрещен. 3. В обработчике выполняется оператор return; происходит нормальный выход из функции. 4. В секции ловушке срабатывает оператор throw; такая форма допустима только внутри секции-ловушки; выполнение этого оператора вне ее приведет к не- медленному аварийному завершению программы. 5. В обработчике генерируется другое исключение. Оператор throw без выражения генерации исключения генерирует повторное ис- ключение того же типа. Ищется другой обработчик выше по иерархии вложен- ности. При генерации в секции-ловушке исключения другого типа ищется его обработчик выше по иерархии вложенности. Если нужного catch-блока не обна- руживается, программа аварийно завершается. Выход из обработчика по исключению может привести к выходу из функции. Гарантируется вызов деструкторов для уничтожения локальных объектов. Про- цесс уничтожения локальных объектов при выходе по исключению называется «раскруткой» (unwinding) стека. Раскрутка стека выполняется только для ло- кальных объектов — для динамических объектов, созданных операцией new; де- структоры автоматически не вызываются (они вызываются при выполнении операции delete). Исключения в списке инициализации конструктора Инициализировать поля класса можно в списке инициализации конструктора, причем в качестве инициализирующего выражения может выступать вызов
Краткие сведения по теме 73 функции или явный вызов конструктора. При этом вероятно возникновение ис- ключения. Ловить и обрабатывать такие исключения можно с использованием контролируемого блока уровня функции специального вида (function-try-block). Int f(int): class C { int 1; double d; public: C(int, double): } C::C(int ii. double id) try :i(f(ii)), d(id) { // ... } catch(...) { // ... } // функция, определенная в другом месте // поля // конструктор инициализации // реализация конструктора // контролируем // список инициализации // тело конструктора // обработчик исключений Слово try находится перед списком инициализации конструктора — список ини- циализации контролируется. Любое исключение, которое сгенерирует функ- ция f (), будет перехвачено и обработано в секции-ловушке. При такой организа- ции конструктора отсутствуют операторы после блока try/catch. Выход из дан- ной ситуации только один — осуществить те действия, которые возможно осу- ществить, и перенаправить исключение дальше, выполнив один из операторов: □ throw без аргумента — транслируется перехваченное исключение; □ throw с аргументом — генерируется новое исключение. Не все компиляторы поддерживают контролируемый блок уровня функции. На- пример, в среде Borland C++ Builder 6 эта форма try-блока не реализована. Спецификация исключений Для каждой функции, метода, конструктора или деструктора можно в заголовке указать спецификацию исключений. Если в заголовке функции не указана специ- фикация исключений, считается, что функция может порождать любое исключе- ние. Однако можно специфицировать в заголовке явный список исключений, ко- торые функция вправе генерировать, например void fl(void) throw(int. double): void f2(void) throw(Nul1 Argument): Если в заголовке между скобками спецификации исключений пусто: void f(void) throw!): считается, что функция исключений не генерирует. Наличие спецификации исключений, тем не менее, не является ограничением при реальной генерации исключений — функция может возбудить исключение любого типа. Однако если функция генерирует неспецифицированное исключе- ние, запускается стандартная функция unexpected!), которая вызывает функцию terminate!). Это инициирует аварийное завершение программы.
74 Глава 4. Исключения Не все компиляторы поддерживают спецификацию исключений в полном объ- еме в соответствии со стандартом. Например, Visual C++.NET 2003 реализует ее только в следующих формах: □ throw () — функция не должна генерировать исключения; □ throw!...) — функция может генерировать исключения; □ throw(Twn) — эквивалентна throw(.„). Если спецификация исключений объявлена в заголовке, то она должна быть указана и во всех прототипах. Несмотря на это, спецификация исключений не входит в прототип функции, поэтому функции с различными спецификациями исключений не считаются разными при перегрузке. Подмена стандартных функций завершения При отсутствии подходящей секции-ловушки осуществляется вызов стандарт- ной функции завершения terminate!). Эта же функция вызывается из функции unexpected!) при нарушении спецификации исключения. Обе функции можно подменить собственными реализациями. Необходимо подключить заголовок #1ncTude <exception> Для подмены стандартной функции terminate!) нужно определить собственную функцию с прототипом void FO: и указать ее имя в вызове функции setterminate!) set_terminate(F); Можно сохранить адрес прежнего обработчика void (*old_terminate)() = set_terminate(F); После этого вместо terminate!) при обработке неперехваченных исключений бу- дет вызываться наша функция F1). Функция-«терминатор» не должна возвращать управление оператором return, не должна генерировать исключение оператором throw — она может лишь завершить программу функцией exit!) или abort!). Аналогично реализуется подмена стандартной функции unexpected!): set_unexpected(f); // установка нашей функции Функция обработки неперехваченного исключения, как и терминальная функ- ция, не должна возвращать управление оператором return и завершает програм- му функцией exit!) или abort!). Однако помимо этого она может сгенерировать исключение, указанное в спецификации исключений. Произойдет подмена непе- рехваченного исключения «легальным», и далее обработка исключений пойдет стандартным образом. Функция может сгенерировать другое исключение, не специфицированное, или просто «отправить» незаявленное исключение «дальше» оператором throw. В этом случае, если в спецификации исключений не указано исключение bad exception,
Краткие сведения по теме 75 вызывается функция terminateO. А вот когда спецификация исключений содер- жит badexception, сгенерированное исключение подменяется на badexception, и начинается поиск его обработчика. Стандартные исключения В языке C++ в составе стандартной библиотеки реализован ряд стандартных исключений, которые организованы в иерархию классов. Листинг 4.1. Иерархия стандартных исключений class exception {//...}; class logic_error : public exception {// ... }; class domain_error : public logic_error {// ... }; class invalid_argument : public logic_error {// ... }: class length_error : public logic_error {// ... }; class out_of_range : public logic_error {//... }; class runtime_error : public exception {//... }; class range_error : public runtime_error {// ... }: class overflow_error : public runtime_error { class underflow_error : public runtime_error {// ... }: class bad_alloc : public exception {// ... }: class bad_cast : public exception {// ... }: class bad tipeid : public exception {// ... }: class bad_exception : public exception {// ... }: class ios_base:: failure : public exception {// ... .}; Эта иерархия служит основой для создания собственных исключений и иерар- хий исключений. Мы вправе определять свои собственные исключения, унасле- довав их от класса exception. Класс exception определен в стандартной библиотеке следующим образом: class exception { public: exception () throwO; exception (const exceptions) throwO; exceptions operator- (const exceptions) throwO: virtual -exception () throwO; virtual const char* what О const throwO: }: Все конструкторы и методы имеют спецификацию, запрещающую генерацию ис- ключений. Функция-метод what О выдает строку-сообщение об ошибке. Предполагается, что исключения типа logic error сигнализируют об ошибках в логике программы, например о невыполнении некоторого условия. Категория runtime error — это ошибки, которые возникают в результате непредвиденных обстоятельств при выполнении программы, как, скажем, переполнение при опе- рациях с дробными числами. Такие исключения программа должна возбуждать самостоятельно оператором throw. Пять стандартных исключений порождают различные механизмы C++. Эти ис- ключения тоже можно использовать в операторе throw явным образом, однако
76 Глава 4. Исключения делать это не рекомендуется. Исключение bad alloc генерирует операция new (или new[]), если запрос на память не может быть удовлетворен. Исключения badcast и badtypeid возбуждаются механизмом RTTI (Run-Time Type Identifi- cation — динамическая идентификация типов). Системой ввода/вывода генери- руется iosbase:: fail иге. Исключение badexception описано ранее. Для работы со стандартными исключениями в программе надо прописать оператор #include <stdexcept> Так как обработка исключений сопровождается накладными расходами времени и памяти во время выполнения программы, в интегрированной среде необхо- димо включить соответствующий режим, разрешающий обработку исключений (см. приложение В). Создание собственной иерархии исключений Для создания своей иерархии исключений необходимо объявить базовый класс- исключение, например class BaseException {}; Остальные классы объявляются наследниками от него, аналогично тому, как это сделано в иерархии стандартных исключений, например class Chi 1d_lexception: public BaseException {}; class Child_2exception: public BaseException {}; Класс BaseException можно унаследовать от стандартного класса exception class BaseException: public exception {}; Наследование от стандартных классов позволяет использовать метод what О для вывода сообщения об ошибке. Для наследования от стандартных исключений необходимо включить в программу заголовочный файл #include <exception> Иерархия классов-исключений позволяет вместо нескольких разных блоков-ло- вушек написать единственный блок с типом аргумента базового класса. В этом случае перехватываются и все исключения-наследники. Такую функциональ- ность обеспечивает принцип подстановки', объект класса-наследника по умолча- нию считается объектом базового класса. Это единственное неявное преобразо- вание типов, которое выполняется при передаче параметров в блок обработки. Не запрещается использовать и специализированные исключения. Если требу- ется несколько секций-ловушек для обработки исключений, то блок с базовым типом исключения должен быть последним как исключение более общего вида. Упражнения Во всех заданиях реализуемые функции или методы должны генерировать подхо- дящие исключения. Обработку исключений нужно выполнять главной функцией, которая должна демонстрировать обработку всех перехватываемых исключений.
Упражнения 77 Функции, генерирующие исключения Функции, реализуемые в заданиях, обязаны выполнять проверку передаваемых параметров и генерировать исключение в случае ошибочных. Все функции реа- лизуются в четырех вариантах: □ без спецификации исключений; □ со спецификацией throwO; □ с конкретной спецификацией с подходящим стандартным исключением; □ спецификация с собственным реализованным исключением. Собственное исключение должно быть реализовано в трех вариантах: как пустой класс, как независимый класс с полями-параметрами функции, как наследник от стандартного исключения с полями. Перехват и обработку исключений должна выполнять главная функция. 1. Функция вычисляет площадь треугольника по трем сторонам = у1р(Р ~ а)(Р ~ Ь)(р - с), где р = (а + b + с) / 2. 2. Функция вычисляет корень линейного уравнения ах + b = 0. 3. Функция вычисляет периметр треугольника. 4. Функция переводит часы и минуты в секунды. 5. Функция вычисляет корень квадратного уравнения аг2 + Ьх + с = 0. 6. Функция вычисляет сумму геометрической прогрессии Sn = (ао - anr) / (1 - г). 7. Функция выполняет деление нечетких чисел А и В. Числа представлены структурой с двумя полями (см. задание 1.31). 8. Функция вычисляет нечеткое число, обратное заданному. Числа представле- ны структурой с двумя полями (см. задание 1.31). 9. Функция выполняет деление комплексных чисел А и В. Комплексные числа представлены структурой-парой (см. задание 1.21). 10. Функция вычисляет целую часть неправильной дроби, представленной чис- лителем и знаменателем — целыми числами. 11. Функция переводит комплексное число z = х + i у из алгебраической формы в тригонометрическую z = radius(cos(angle) + i sin(angle)). Комплексное чис- ло z представлено структурой-парой (см. задание 1.21). Преобразованное чис- ло тоже представляется структурой-парой (radius, angle): radius = yjx2 + y2; angle = arcsin— x 12. Функция вычисляет разность между двумя датами в днях. Даты представле- ны структурой с тремя полями: год, месяц, день. 13. Функция вычисляет продолжительность телефонного разговора в минутах, принимая время начала и окончания. Время представлено структурой с тре- мя полями: час, минута, секунда. Неполная минута считается за полную.
78 Глава 4. Исключения 14. Функция вычисляет день недели по дате. Даты представлены структурой с тремя полями: год, месяц, день. Первое января считается понедельником. 15. Функция вычисляет углы прямоугольного треугольника. В качестве парамет- ров передаются катеты А и В. (Синус угла А1, противолежащего катету А, вы- числяется по формуле sin (Al) = а / с, где с — гипотенуза треугольника.) 16. Функция проверяет, является ли передаваемая строка палиндромом. 17. Функция определяет, существуют ли прямые Aix + Bi г/ + Ci = 0 и Агх + + Вг г/ + Сг = 0, если выражение d = А1В2 - А2В1 не равно нулю. Прямые зада- ются структурой с тремя полями. 18. Функция вычисляет расстояние между двумя точками Pt(xi, г/i) и Рг(хг, г/2) по формуле £> = 7(^1 “x2>2 +G/1 -У 2? Исключение генерируется, когда Pj и Рг — одна и та же точка. 19. Функция вычисляет расстояние от точки P(xi, z/i) до прямой Аг + Вг/ + С = О по формуле „_|Ах1+Ву1-ьС| 7л2 + в2 20. Функция выясняет, является ли год високосный. Високосность определяется следующим образом: если номер года не делится на 100, то високосным счи- тается тот, который делится на 4 без остатка; если номер года делится на 100, то номер високосного года делится на 400 без остатка. Выполнить задания 1-20, реализовав подмену стандартной функции unexpectedO. Пользовательская функция должна выводить сообщение об отсутствии обработ- чика исключения и заканчивать работу. Классы с обработкой исключений Реализовать обработку исключений в трех вариантах: □ использовать подходящие стандартные исключения; □ использовать исключения-наследники от стандартных исключений; □ определить собственные исключения; Передачу объекта-исключения выполнить разными способами: по значению, по ссылке, по указателю. Конструкторы и методы, генерирующие исключения, должны быть объявлены со спецификацией исключений. Выполнить задания 1.1-1.20 с конструкторами и обработкой исключений. Про- демонстрировать обработку стандартного исключения bad exception. Выполнить задания 1.21-1.40 с конструкторами, перегрузкой операций и обра- боткой исключений. Выполнить задания 2.21-2.40 с обработкой исключений. Решить все задания раздела «Наследование вместб композиции» главы 3 с кон- структором инициализации строкой и обработкой исключений.
ГЛАВА 5 Контейнеры В данной теме представлена техника программирования динамических контей- неров [9, 10]: массивов и списков. Рассматриваются операции доступа к элементам контейнера, изучается техника программирования итераторов [13]. Разбираются различные варианты операций добавления и удаления элементов. Рассматрива- ется реализация очередей и стеков. Краткие сведения по теме В реальных задачах требуется обрабатывать группы данных большого объема, поэтому в любом языке программирования существуют средства, позволяющие объединять данные в группы. В первую очередь это — массивы. Однако в C++ встроенные массивы — конструкции очень простые. Они не обеспечивают всех потребностей по обработке группы однородных данных. В объектно-ориентиро- ванном C++ массивов в качестве средств объединения однородных данных не- достаточно. В процессе развития языков программирования, и C++ в частности, программистское сообщество выработало более общую концепцию объединения однородных данных в группу — контейнер. Определение контейнера Контейнер — это набор однотипных элементов. Встроенные массивы в C++ — частный случай контейнера. Строка символов — другой пример контейнера. Имя контейнера — это имя переменной в программе, которое подчиняется пра- вилам видимости C++. Будучи объектом, контейнер обладает временем жизни в зависимости от места и времени создания; время жизни контейнера в общем случае не зависит от продолжительности существования его элементов. Тип контейнера складывается из типа самого контейнера и типа входящих в него элементов. Тип контейнера — это не тип его элементов. Тип элементов может быть либо встроенным, либо реализованным; элементами контейнера могут быть контейнеры. Контейнеры могут быть фиксированного размера и переменного размера. В кон- тейнере фиксированного размера количество элементов постоянное, оно обычно
80 Глава 5. Контейнеры задается при создании контейнера. Примером контейнера фиксированной дли- ны является массив. Для контейнера переменного размера количество элементов при объявлении обычно не задается. Элементы в таком контейнере добавляются и удаляются во время работы программы. Если элементы контейнера никак не упорядочены, то добавление и удаление элементов обычно выполняется в начале или в конце контейнера. Способ встав- ки и удаления определяет вид контейнера. Если вставка и удаление выполняют- ся только на одном конце, такой контейнер называется стеком. Говорят, что стек работает в соответствии с дисциплиной LIFO (Last In First Out — последним пришел, первым ушел). Если элементы добавляются на одном конце контейнера, а удаляются с другого конца, такой контейнер называется очередью. Очередь ра- ботает по принципу FIFO (First In First Out — первым пришел, первым ушел). Можно выполнять и вставку, и удаление на обоих концах контейнера — такой контейнер называется двусторонней очередью (от английского термина deque, аббревиатуры от «double ended queue»). Если контейнер каким-либо образом упорядочен, операция вставки работает в соответствии, с порядком элементов в контейнере. Операция удаления при этом может выполняться по-разному: в начале, в конце контейнера или заданного эле- мента. Операции контейнера Среди всех операций с контейнерами можно выделить несколько типовых групп: □ операции доступа к элементам, которые обеспечивают и операцию замены зна- чений элементов; □ операции добавления и удаления отдельных элементов или групп элементов; □ операции поиска элементов и групп элементов; □ операции объединения контейнеров; □ прочие (специальные) операции, зависящие от вида контейнера. Важнейшими являются операции доступа и операции объединения контейнеров. Доступ к элементам Доступ к элементам контейнера бывает: последовательный, прямой, ассоциатив- ный. Прямой доступ к элементу — это доступ по номеру (по индексу) элемента. Выражение v[ll] означает, что требуется элемент контейнера v, имеющий номер (индекс) 11. Ну- мерация элементов может начинаться с любого числа, однако в C++ принято ну- мерацию начинать с нуля, аналогично массивам. Ассоциативный доступ тоже выполняется по индексу, однако индексом является не номер элемента, а его содержимое. Например, в контейнере — словаре ино- странных слов хранятся карточки, состоящие как минимум из двух полей: ино-
Краткие сведения по теме 81 странное слово и его перевод. Индексом в словаре служит иностранное слово, например v[”word"] С иностранным словом связано слово-перевод. Поле, с содержимым которого ас- социируется элемент контейнера, называется ключом (полем доступа). Элемент контейнера, соответствующий некоторому значению ключа, называется значени- ем. Контейнер, предоставляющий ассоциативный доступ, состоит из множества пар «ключ-значение». Ассоциативный контейнер упорядочен некоторым обра- зом по ключу. В словаре упорядочение выполняется, очевидно, по алфавиту. При последовательном доступе осуществляется перемещение от элемента к эле- менту контейнера. Можно считать, что существует некий объект, который пере- бирает элементы контейнера с помощью некоторого множества операций. Тот элемент, на котором в данный момент объект позиционирован, называется теку- щим элементом. Набор операций последовательного доступа включает следую- щие: □ перейти к первому элементу; □ перейти к последнему элементу; □ перейти к следующему элементу; □ перейти к предыдущему элементу; □ перейти на п элементов вперед (от начала к концу контейнера); □ перейти на п элементов назад (от конца к началу контейнера); □ получить текущий элемент. Если контейнер — массив, то объект, который осуществляет последовательный доступ к элементам контейнера, — это указатель. Перечисленные операции — это операции с указателями. В более общем случае объект, который перебирает эле- менты контейнера, называется итератором; в книге [13] итератор представлен как один из паттернов проектирования. Итератор — это объект, обеспечиваю- щий последовательный доступ к элементам контейнера. Итератор может быть реализован как часть класса-контейнера в виде набора методов: v.firstO: П перейти к первому v.lastO: // перейти к последнему v.nextO: // перейти к следующему - v.prevO: // перейти к предыдущему v.skip(n); // перейти на п элементов вперед v.skip(-n): // перейти на п элементов назад v.current 0: // получить текущий Чаще итератор реализуется как класс, предоставляющий тот же набор операций. В практике программирования на C++ итератор реализуется с интерфейсом ука- зателя (для совместимости с массивами). Если объект-итератор имеет имя iterv, операции могут быть представлены таким образом: iterv = v.firstO; iterv = v.lastO: ++iterv: --iterv: 11 перейти к первому 11 перейти к последнему // перейти к следующему // перейти к предыдущему
82 Глава 5. Контейнеры iterv+=n; И перейти на п элементов вперед 1terv-=n; // перейти на п элементов назад *iterv // получить значение текущего элемента При наличии последовательного доступа по итератору обычными операциями являются операции вставки элемента перед текущим элементом или после него, изменение значения текущего элемента, удаление текущего элемента. Обычно в последовательном контейнере выполняется поиск элемента с заданным значе- нием. Операция поиска в случае успеха выдает итератор на найденный элемент. Операции объединения контейнеров Наиболее часто используется операция объединения двух контейнеров с полу- чением нового контейнера, которая может быть реализована в различных вари- антах: □ простое сцепление двух контейнеров: в новый контейнер попадают все эле- менты и первого, и второго контейнера; операция не коммутативна; □ объединение упорядоченных контейнеров, называемое слиянием-, в новый кон- тейнер попадают все элементы первого и второго контейнеров; объединенный контейнер упорядочен; операция коммутативна; □ объединение двух контейнеров как объединение множеств: в контейнер-ре- зультат попадают только те элементы, которые есть хотя бы в одном контей- нере; операция коммутативна; □ объединение двух контейнеров как пересечение множеств: в новый контейнер попадают только те элементы, которые есть в обоих контейнерах; операция коммутативна. Для контейнеров-множеств часто реализуется операция вычитания множеств: в контейнер-результат попадают только те элементы первого контейнера, кото- рых нет во втором; операция не коммутативна. Одной из операций с контейнером является извлечение из него части элементов и создание из них нового контейнера. Часто эту операцию выполняет конструк- тор, а часть контейнера задается двумя итераторами. Прочие операции Многие операции зависят от типа элементов контейнера. Числовые контейнеры (динамические массивы), как правило, обеспечивают набор арифметических операций, поиск максимума или минимума, вычисление сумм и произведений. Если контейнер представляет некоторую специальную структуру (например, ма- тематический вектор), то с таким контейнером могут выполняться специальные операции (например, скалярное произведение векторов). Реализация контейнеров Контейнеры реализуются с помощью динамической памяти. Выделение памяти осуществляют конструкторы класса-контейнера при создании объекта-контей- нера. Обычно в классе реализуется несколько перегруженных конструкторов. Память можно выделить посредством операции new, которая имеет две формы:
Краткие сведения по теме 83 □ выделение массива однотипных элементов new[]; □ выделение одиночного элемента new. Функции имеют прототипы void *operator new(std::size_t size) throw(std::bad_alloc); void *operator new[](std::size_t size) throw(std::bad_alloc): Выделение памяти операцией new[] обеспечивает предоставление непрерывной области памяти. Количество элементов можно задавать выражением, вычисляе- мым во время работы программы. Такая форма практически всегда используется для реализации динамических массивов. В этом случае в классе обязательно за- дается поле-указатель, которое и получает адрес динамического массива. Обыч- но задается также поле для хранения размера выделенной памяти. Если количе- ство элементов динамического массива фиксировано, то это же поле определяет количество элементов контейнера-массива. Если же количество элементов изме- няется во время работы программы, то в классе обычно присутствует еще и поле для хранения текущего количества элементов. При выделении памяти операцией new ее возврат осуществляется операцией delete. Если память выделялась массивом (операцией new[]), то и освобождать ее нужно операцией deleted. Функции возврата памяти имеют прототипы void operator delete (void *) throwO; void operator delete [](void *) throwO; Возврат памяти осуществляет деструктор, который для динамических контейне- ров реализуется обязательно. Для динамических контейнеров обязательны реализация конструктора копиро- вания и операции присваивания. Конструктор копирования, создаваемый по умолчанию, выполняет поэлементное копирование полей класса. Такое поведе- ние конструктора приводит к ошибкам вида «висячая ссылка» при уничтожении объекта. Конструктор копирования должен выполнять другие действия: запро- сить память для нового контейнера такого же размера и скопировать в новый контейнер элементы инициализирующего контейнера. Как и копирующий конструктор, стандартная операция присваивания выполня- ет поэлементное копирование полей правого операнда в поля левого (текущий объект). Для динамических контейнеров такое поведение создает как «висячие», так и «потерянные» ссылки (утечка памяти). Операция должна создать новый динамический контейнер, скопировать туда элементы присваиваемого контейне- ра и возвратить системе память прежнего контейнера. Для всех методов, запрашивающих память, обычно указывается спецификация исключения throw(bad_alloc) Кроме того, часто в контейнере задаются и собственные типы исключений, ха- рактерные для конкретной реализации. Например, извлечение из пустого стека должно генерировать исключение типа «пустой стек».
84 Глава 5. Контейнеры Числовой контейнер фиксированной длины Типичная структура класса-контейнера для реализации динамического массива фиксированной длины представлена в листинге 5.1. Для определенности тип элементов задан как int. Листинг 5.1. Числовой контейнер фиксированной длины class Array { public: typedef unsigned int UINT; 11 конструкторы ArraydlINT size, int k=0.0) throw(bad_alloc); Array(const'int *begin, const int *end)throw(bad_alloc.invalid_argument); Array(const Array Sa)throw(bad_alloc); И конструктор копирования -Array!)throw(): 11 деструктор Array& operator=(const Array Srhs); // операция присваивания // операция индексирования intS operator[](UINT index) throw!range_error); const intS operator[](UINT index) const throw!range_error): // арифметические операции с присваиванием // ... UINT sized const; // количество элементов И дружественные функции - арифметические операции // ... // дружественные функции ввода/вывода friend ostreamS operator «(ostreamS to. const Array Sa); friend 1 streams operator »(istreams to. Array Sa); private: UINT size_array; // количество элементов int *data; И динамический массив }; Конструкторы могут генерировать исключение по причине нехватки памяти (если его источник — операция new[]), поэтому в прототипе задана спецификация ис- ключения. В зависимости от аргументов могут генерироваться и другие исклю- чения. Обычно один конструктор инициализации создает контейнер-массив за- данного размера, присваивая элементам некоторые значения (по умолчанию — ноль). При отсутствии проверок параметров его реализация выглядит так: Array::Array(UINT size, int k) throw!bad_allос) { size_array = size; 11 размер массива data = new int[size_array]; // создали массив for(UINT i = 0; i<size_array; ++i) // заполняем массив data[i] = k: 11 значение по умолчанию } Важный вид конструктора — конструктор инициализации двумя указателями. Такая форма позволяет инициализировать контейнер элементами встроенного массива. Array::Array!const int *begin. const int *end) throw(bad_alloc, invalid_argument) // генерируемые исключения { if (begin < end) // защита { size_array = (end - begin): // количество элементов
Краткие сведения по теме 85 data = new 1nt[s1ze_array]; // создаем массив forCUINT 1 = 0; 1<s1ze_array: ++1) // заполняем контейнер - data[1] = *(beg1n+1); // копируем из массива } else // неправильные параметры { throw 1nval1d_argument(): } } Обычно параметры-указатели удовлетворяют условию (begin < end). При нару- шении условия генерируется подходящее исключение, собственное или стан- дартное, например Invalid argument. Деструктор обычно имеет следующий простой вид: Array::-ArrayО throwO { delete[]data: // освобождаем память data =0: И не обязательно } Обнулять указатель — не обязательно. Копирование и присваивание Копируемый массив имеет заведомо корректные значения полей, поэтому поля для нового массива можно инициализировать в списке инициализации. Реали- зация конструктора копирования для числового массива фиксированной длины должна быть такой1: Array: .-Array(const Array &t) throw (bad_alloc) : size_array(t.s1ze_array). data(new int[size_array]) { for(UINT 1 = 0; 1<size_array; ++1) data[i] = t,data[1]; } Операция присваивания должна возвращать ссылку на Array. Текущий дина- мический массив становится копией аргумента: размер принимающего массива и значения аргументов устанавливаются по массиву-аргументу. Классический текст операции присваивания выглядит так: Array& Array::operator=(const Array St) { If (this != St) // отслеживаем самоприсваивание { s1ze_array = t.s1ze_array: Int *new_data = new 1nt[size_array]; fordlINT 1 = 0: 1<size_array: ++1) И опеределяем размер new_data[i] = t.data[1J: . И копируем deleted data; // возвращаем предыдущий массив data = new_data: // вступаем во владение } return *this: } Проверка самоприсваивания this != St 1 He забывайте о порядке инициализации полей в списке инициализации конструк- тора.
86 Глава 5. Контейнеры является обязательной, так как при присваивании массива самому себе а = а; не нужно выполнять никаких действий. Второй вариант реализации операции присваивания требует реализации метода обмена массивов. Обычно в нем используется стандартная функция обмена swap О, входящая в стандартную библиотеку. void Swap(Array Sother) { std::swap(data, other.data); // стандартная функция обмена std::swap(size_array, other.size_array); } Array& Array::operator=(const Array St) { Array tmp(t); // временнный локальный объект tmp = t Swap(tmp); // обмен полями с текущим объектом return *this; И возврат текущего объекта } // tmp уничтожен В этом варианте всю работу делают конструктор копирования и деструктор. Доступ к элементам Доступ к элементам числовых массивов фиксированной длины обычно обеспе- чивается только прямой, так как нет необходимости удалять и вставлять элемен- ты. Реализуются две операции []: для константных и неконстантных аргументов. Операция обязана проверять свой аргумент-индекс и генерировать исключение в случае «вылета» за границу массива. intS Array::operatorГJ(UINT index) throw (out_of_range) { If (1ndex<s1ze_array) return data[index]: else throw out_of_range("Index out of range!"): } const IntS Array::operator!KUINT Index) const throw (out_of_range) { if (index<size_array) return data[index]: else throw out_of_range("Index out of range!"); } Для индексирования можно использовать другую операцию, которая очень хо- рошо подходит для реализации доступа к элементам — это операция вызова функции (). IntS Array::operator()(UINT Index) { if (1ndex<s1ze_array) return datatindex]; else throw out_of_range("Index out of range!"): } const intS Array::operator()(UINT index) const { if (1ndex<size_array) return datatindex]; else throw out_of_range("Index out of range!"): } Тогда обращение к элементам массива будет выглядеть так: result(i) = a(i) * b(i): Применение операции () для индексирования одномерного массива не дает ни- каких преимуществ по сравнению с обычной операцией []. Операция О имеет больше смысла при индексировании многомерных массивов.
Краткие сведения по теме 87 Реализация методов Реализацию типичных для фиксированного массива арифметических методов по- кажем на примере сложения (листинг 5.2). Обычно реализуются два метода сло- жения с присваиванием и несколько дружественных. Операция, обеспечивающая сложение массивов-аргументов, должна проверять их размер: складывать можно массивы одинаковой длины. Листинг 5.2. Методы числового контейнера фиксированной длины // Сложение с числом Array& Array::operator+=(const int Srhs) { for(UINT i = 0: i<size_array: ++i) data[i] += rhs; // складываем с текущим объектом return *this; } 11 Сложение с массивом Arrays Array::operator+=(const Array Srhs) { if (size_array == rhs.size_array) // защита { for(UINT i = 0: i<size_array: ++i) data[i] += rhs.dataCi]: // изменяем текущий объект } else throw out_of_range("Index out of range!"): return *this: } // Сложение массивов Array operator+(const Array Sa. const Array Sb) { Array temp = a: // левый аргумент if (a.sizeO == b.sizeO) И защита temp += b: 11 добавляем - наша операция else throw out_of_range("Index out of range!"): return temp: } В дополнение к этим функциям обычно реализуют еще два варианта дружест- венных функций сложения массива с числом: friend Array operator+Cconst Array Sa, const int Sb): friend Array operator+(const int Sa, const Array Sb): Два варианта необходимы, чтобы не было проблем с коммутативностью. Реализация дружественных функций ввода/вывода вполне традиционна. ostreamS operator «(ostreamS to. const Array Sa) { for(Array: :IIINT 1 = 0: i<a.size(); ++i) to « a.data[i] «' 11 выводим через пробел return to: } istreamS operator »(istreamS to. Array Sa) { for(Array::UINT 1 = 0: i<a.size(): ++1) to » a.data[i]: return to; } Выводимые элементы массива отделяются пробелом. При вводе можно разделять элементы массива пробелом или завершать каждое число нажатием клавиши Enter.
88 Глава 5. Контейнеры Числовые массивы переменного размера Типовая структура класса-контейнера динамического растущего массива пред- ставлена в листинге 5.3. Для определенности тип элементов задан как double. Листинг 5.3. Числовой растущий массив class Array { public: // типы typedef double value_type; typedef double* iterator; typedef const double* constjterator; typedef doubles reference; typedef const doubles const_reference; typedef std::size_t size_type: // конструкторы/копирование/деструктор Array(const size_typeS n = nrinsize); Arrayfconst Arrays array); Arraydterator first, iterator last); -Array!); Arrays operator=!const ArrayS); // итераторы iterator begin!) { return elems; } constjterator begin!) const { return elems: iterator end!) { return elems+Count; } } constjterator end!) const { return elems+Count : } // размеры size_type size!) const; // текущий размер массива bool empty!) const; // есть ли элементы size_type capacity!) const; // потенциальный размер void resize!size_type newsize); // доступ к элементам // изменить размер reference operator[](size_type) const_reference operator[](size_type) const; reference front!) { return elems[0]; } const reference front!) const { return elems[0]; : } reference back!) { return elems[size()-l]; } const reference back!) const { return elems[size()-l]; } // методы-модификаторы void push_back(const value_typeS v); void pop_back(); // удалить последний элемент void clear!) { Count = 0: } // очистить массив void swaptArrayS other); // обменять с другим массивом void assignlconst value_typeS v): private: // заполнить массив static const size_type minsize = 10: // минимальный размер массива size_type Size; // выделено элементов в памяти size_type Count; // количество элементов в массиве value_type * elems; // указатель на данные Массив называется растущим, так как элементы добавляются только к его кон- цу: массив «растет». В начале описаны типы, используемые в классе. Заменив double на любой другой встроенный числовой тип, получим реализацию динами-
Краткие сведения по теме 89 ческого контейнера для другого типа. Реализацию методов при этом переписы- вать не требуется, поскольку они реализованы в терминах объявленных типов. Память выделяется операцией new[], поэтому реализация конструкторов, деструк- тора и операции присваивания не отличается от предыдущего варианта. Единствен- ный нюанс — даже при отсутствии аргумента-количества выделяется некоторый минимальный объем по умолчанию, определяемый константой minsize. Прямой доступ с помощью операции индексирования тоже остается без изменений. Так как размер массива изменяется, то в класс добавляется поле count, в котором содержится текущее количество элементов массива. Обычно предоставляются три метода, определяющие количество элементов в массиве: empty() проверяет count на ноль; size() выдает count в качестве возвращаемого значения; capacityO выда- ет потенциально возможное на данный момент количество элементов, которые можно поместить в массив. Чаще всего элементы добавляются в конец массива. Удаление элемента в этом случае тривиально: память не возвращается, поле count уменьшается на 1. Добавление элемента имеет следующий вид: void Array::push_back(const value_type& v) { if (Count == Size) И места нет resize(Size *2): 11 нарастили "мощность" elems[Count++] = v; // присвоили «Вставка» элемента в конец происходит очень быстро, если не требуется увели- чивать зарезервированную память. Как и при удалении, просто корректируется счетчик элементов count. Когда будет заполнен последний элемент выделенного динамического массива, потребуется увеличить размер памяти и записать туда все элементы, добавив но- вый. Для этого обычно реализуется отдельный метод, например: void Array::resize(size_type newsize) throw(bad_alloc) { if (newsize > capacityO) { value_type *data = new value_type[newsize]: // новый массив for(size_type i = 0; i<Count: ++1) // копируем data[i] = elemstij: deleter] elems; // возвращаем память elems = data; // вступаем во владение Size = newsize: 11 увеличиваем "мощность" } } В данном случае аргумент проверяется, чтобы не выделялась память меньше ми- нимального размера по умолчанию. Массив с произвольными вставками и удалениями элементов называется «гиб- ким» (flexible) массивом. Схема вставки в середину, например на место п, долж- на быть следующей: 1. Размер массива увеличивается на один элемент; если требуется, резервирует- ся память под новый массив. 2. Элементы от n + 1 до count «сдвигаются» на один элемент «вправо». 3. На место п заносится вставляемый элемент.
90 Глава 5. Контейнеры При удалении выполняется «сдвиг» элементов от n + 1 до count «влево» на один элемент; память при этом не возвращается. Для растущих и гибких массивов часто реализуется операция объединения, на- пример Аггау& Array::operator+=(const Array Sr) throw(bad_alloc) { Array t(capacity()+r.capac1tyO); И новый большой массив for(1nt 1 = 0; 1<sizeO; 1++) t.push_back(data[i]); for(1nt 1 = 0: 1<r.sized; 1++) t.push_back(data[1]); deleted elems; // возвращаем память текущего *th1s = t; 11 заменяем текущий массив на новый return *this; } Последовательный доступ к элементам динамического массива осуществляется итератором, который является указателем соответствующего типа. Класс масси- ва обеспечивает получение начального (методы begind) и конечного (методы endd) значения итератора, получение значения первого (методы front О) и последнего (методы backd) элемента. Конечное значение итератора — за последним элемен- том массива. Перемещение по элементам и получение значения элемента выпол- няется операциями с указателями. Константные итераторы не позволяют изме- нять значения элементов контейнера. Последовательный контейнер Чисто последовательный контейцер обычно реализуется как связанный список элементов (узлов). Выделение памяти выполняется для каждого элемента в от- дельности операцией new. Прямой доступ отсутствует; последовательный доступ обеспечивается с помощью итераторов, причем как в прямом, так и в обратном направлении. Итератор реализуется либо как набор методов класса-контейнера, либо как вложенный класс. Условие begin < end обеспечивается наличием в пус- том контейнере одного лишнего невидимого элемента — это делает конструктор. Вставка выполняется в начале контейнера, в конце контейнера и после элемента, на который указывает итератор. Удаление элементов обычно реализуется в двух вариантах: по итератору, когда удаляется элемент, на который указывает итера- тор, и по значению, когда удаляется элемент, имеющий заданное значение. Часто реализуются операции поиска элементов и различные варианты объединения кон- тейнеров. Операции сравнения контейнеров и ввода/вывода, как обычно, реали- зуются внешними дружественными функциями. Один элемент контейнера имеет следующую структуру (тип элементов опреде- лим как value type, задав соответствующий typedef): typedef double value_type; struct Node { Node(const value_type Sa):ltem(a){ } Node(){} value_type item; // информационная часть элемента Node *next; // следующий элемент Node *prev: // предыдущий' элемент
Краткие сведения по теме 91 Эта структура помещается в приватную часть класса-контейнера; там же задают- ся три поля: □ указатель на первый элемент списка («голова» списка); □ указатель на невидимый элемент («хвост» списка); □ количество элементов в контейнере (без учета невидимого элемента). Примерная структура класса последовательного контейнера показана в листин- ге 5.4. Листинг 5.4. Последовательный контейнер-список class List { public: typedef double value_type: typedef std::size_t size; class iterator: // Конструкторы/копирование/присваивание ListO; List (const value_typeS a, size n = 1); List (iterator, iterator): List(const List& x): -ListO: Lists operator^const Lists); // Итераторы iterator begin 0 { return head: } iterator end 0 { return tail; } iterator beginO const { return head: } iterator end() const { return tail; } // Размеры bool empty () const { return (Head “Tail): }; size length () const { return count: }; // Доступ к элементам value_typeS frontO { return *begin(): }; value_typeS backO { iterator it = end(); value_typeS find(const value_typeSa); // Модификаторы контейнера void push_front (const value_typeS): void pop_front 0: void push_back (const value_typeS); void pop_back (); void insert (iterator, const value_typeS); void erase (iterator): void erase (iterator, iterator): void remove (const value_typeS); void swap (List S): void clear (); void splice (List S): void splice (iterator. List S): void sort 0; void merge (list S): private: struct Node { Node(const value_typeS a):item(a){ } Node(){} --it: return *it; }; // поиск // добавить в начало // удалить первый // добавить в конец // удалить последний // вставить после // удалить указанный (в позиции) // удалить группу указанных // удалить заданный (значением) // обменять с заданным списком // удалить все // прицепить список в конец // прицепить список после заданного // сортировка // слияние сортированных
92 Глава 5. Контейнеры value_type Item; Node *next: Node *prev: }; long count; Node *Head; Node *Ta11; iterator head, tail: }: // информационная часть элемента // следующий элемент // предыдущий элемент // количество элементов // "голова” списка // "хвост" списка // для итератора Счетчик увеличивается при каждом добавлении элемента и уменьшается при ка- ждом удалении элемента. Поля-указатели никогда не равны 0, так как даже в пус- том контейнере присутствует невидимый фиктивный элемент. Итератор скрывает реальные указатели, обеспечивая тип iterator, поэтому нуж- ны поля для начального и конечного значения итератора. Такой список является основой для реализации очередей и стеков. Итератор для последовательного контейнера Вложенный класс-итератор помещается в открытую часть класса-контейнера (листинг 5.5). Листинг 5.5. Итератор для последовательного контейнера typedef double value_type; class iterator { friend class List; // класс-контейнер iterator(Node *el):elem(el){} public: // Конструкторы iterator():elem(0){} iterator(const iterator &it):elem(it.elem){} // Сравнение итераторов bool operator==(const iterator Sit) const: }: bool operator!=(const iterator &it) const: // Перемещение итератора iterators operator++(): // вперед iterators operator--!); // назад value_typeS operator*!) const: // разыменование private: Node * elem; // указатель на элемент Все методы удобнее реализовать внутри класса-итератора, так как при реализа- ции вне класса придется писать слишком длинные префиксы, например: List;: iterators List:; iterator: .-operator»!const List: iterator Sit) { List: iterator: :elem = It.elem; return *this: } Реализация внутри класса значительно короче, например bool operator»»!const iterator Sit) const { return (elem == it.elem): } iterators operator++() { if (elem!=0) elem = elem->next; return *this; }
Краткие сведения по теме 93 value_type& operator”^) const { if (elem != 0) return elem->item; else throw NulliteratorO; // нулевой итератор } Операции перемещения на следующий и предыдущий элемент можно реали- зовать и в постфиксном виде. Кроме того, можно реализовать и перемещение на п элементов вперед и назад, перегрузив операции operator*» и operator •=. Конструкторы, деструктор и присваивание Конструктор по умолчанию и конструктор, создающий список из п элементов: // Пустой список - с невидимым элементом List::Li St() :Head(new ElemO), Tail (Head), count(O) { Tail->next=Tail->prev=O: head = iterator(Head); // инициализация для итератора tail = 1terator(Tai 1); } // Создаем список из n элементов и заполняем его значением List::List(const value_type& a, size n = 1) { List tmp: // пустой список с фиктивным элементом for(int i = 0: i < n; i++) // присоединяем элементы tnip. push_front (a): *this = tmp: // сделали текущим Конструктор, создающий новый список по итераторам другого списка, отличает- ся только организацией цикла: List::List(Iterator first, iterator last) { List tmp: for(iterator ip = first: ip!= last: ip++) tmp.push_front(*ip); *this = tmp: } И совсем просто реализуется конструктор копирования: List::Li st(const List& r) { List tmp(r.begin(), r.endO); // работает конструктор с итераторами *this = tmp: } // деструктор List::—Li st() Node *delete_Node = Head: for(Node *p = Head; p!=Tail:) { p = p->next: delete delete_Node: --count: delete_Node = p: // удаляемый элемент // пока не уперлись в невидимый // подготовили следующий // удалили элемент // подготовили для удаления 1 delete delete_Node: // удалили невидимый } Операция присваивания реализуется как обмен с временным объектом-контей- нером, чтобы не писать явный возврат памяти текущего объекта-списка (должна быть реализована функция обмена swapO).
94 Глава 5. Контейнеры List& List::operatorsconst List &t) { List tmp(t): Swap(tmp): return *this: } // временнный локальный объект tmp = t // обмен полями с текущим объектом // возврат текущего объекта // tmp уничтожен Реализация методов Основными являются методы вставки и удаления. Для примера покажем встав ку и удаление по итератору. // вставить после void List::insert (iterator it, const value_type Sr) { Node *el = it.elem: if (ft'== --endO) push_back(r): // если последний else { Node *nextel = 1t.elem->next: // следующий элемент Node *p = new Node(r): // образовали новый элемент p->next = nextel: p->prev = el; // связи в новом nextel->prev = p: el->next = p; // привязали новый } ++count: } // удалить указанный void List::erase (iterator it) { Node *el = it.elem; // текущий элемент if (it == beginO) pop_front(): // если первый else if (it == --end(l) pop_back(): // если последний else { Node *prevel = 1t.elem->prev: // предыдущий элемент Node *nextel = it.elem->next; // следующий элемент prevel->next = nextel; // корректируем nextel->prev = prevel; // указатели delete el: // возвращаем память } --count: } Функции удаления группы элементов, очистки контейнера, поиска элемента, слия- ния списков используют эти методы в цикле по итераторам аналогично тому, как это показано в конструкторах. Стек В программировании очень часто используются стеки (например, в транслято- рах). Стек должен обеспечивать следующий состав операций: □ добавить элемент в стек — push(); □ удалить элемент из стека — рор(); ; □ получить значение элемента с вершины стека — top(); □ проверить, не пустой ли стек — emptyO. Реализация такой дисциплины доступа к элементам может быть выполнена либо с помощью растущего массива (см. листинг 5.3), либо с помощью списка. Для этого достаточно односвязного списка. Структура элемента односвязного списка
Краткие сведения по теме 95 не отличается от структуры элемента двухсвязного, только указатель для свя- зи — единственный. struct Node { value_type data; Node *next: Node (const value_typeS d. Node *p) :data(d). next(p) { } Перебирать элементы стека не требуется, поэтому не требуется и итератор. Кон- структор инициализации для стека тоже не нужен, так как изначально стек пуст. Присваивать и копировать стеки нет необходимости; чтобы конструктор не соз- давал эти функции по умолчанию, нужно задать их прототипы в приватной час- ти класса. Таким образом, достаточно одного конструктора без аргументов, функцией которого является обнуление указателя на вершину стека. Нулевое значение указателя является признаком отсутствия элементов в стеке. При по- пытке извлечь элемент из пустого стека генерируется исключение. Кроме того, как обычно, может быть сгенерировано исключение badalloc. Реализация стека с учетом всех этих соображений представлена в листинге 5.6. Листинг 5.6. Реализация простого стека class Stack { public: typedef long value_type: class EmptyStack {}; // исключение Stack»: Head(O) {} // пустой стек -Stack» { while ((empty») pop»: } void push(const value_typeS d) // положили элемент в стек { Head = new Node(d, Head): } // получить элемент с вершины стека value_type top» const throw(EmptyStack) { return !empty()?Head->data :throw EmptyStack»: } // удалить элемент из стека void pop» throw(EmptyStack) { if (empty»)throw EmptyStack»; // если стек пуст Node *oldHead = Head; // запомнили указатель Head = Head->next: // передвинули вершину delete oldHead; // вернули память } bool empty» const // стек пустой, если указатель = 0 { return Head==0; } private: struct Node { value_type data; Node *next: Node (const value_typeS d. Node *p):data(d), next(p) { } }: Node * Head; // вершина стека Stack(const Stack S): // закрыли копирование Stack& operator=(const Stack &); }; // закрыли присваивание
96 Глава 5. Контейнеры Обратите внимание на простоту реализации — так всегда бывает, если не увле- каться универсальностью. Упражнения В этой теме в каждом упражнении требуется реализовать в том или ином виде динамические контейнеры. Для демонстрации работы с динамическим контей- нером во всех заданиях нужно написать главную функцию. В программе долж- ны присутствовать различные способы создания объектов и массивов объектов. Программа должна демонстрировать использование всех функций и методов. Контейнеры как параметры Во всех заданиях написать функцию, принимающую обычный массив в качестве аргумента и возвращающую растущий динамический массив (см. листинг 5.3) как результат. Размер обычного массива передается в качестве аргумента. Ис- ходный массив заполнить случайными числами в диапазоне от -50 до +50. Доба- вить в массив-результат сумму и среднее арифметическое по абсолютной вели- чине. 1. Добавить к каждому числу корень квадратный из произведения максимума и последнего числа. 2. Разделить каждое число на полусумму первого отрицательного и 50-го числа. 3. Вычесть из каждого числа набольшее число (далее «максимум»). 4. Умножить каждое число на минимальное. 5. Поделить все нечетные по абсолютной величине числа на среднее арифмети- ческое. 6. Вычесть из каждого числа сумму чисел. 7. Умножить каждое третье число на удвоенную сумму первого и последнего от- рицательных чисел. 8. Добавить к каждому числу первое нечетное по абсолютной величине число. 9. Умножить каждое четное число на первое отрицательное число. 10. Добавить к каждому числу половину последнего отрицательного числа. И. Поделить на половину максимума. 12. Заменить все нули средним арифметическим. 13. Заменить все положительные числа квадратом минимума. 14. Добавить к каждому числу полусумму всех отрицательных чисел. 15. Добавить к каждому числу среднее наименьшего и наибольшего по абсолют- ной величине. 16. Поделить на минимум и добавить максимум. 17. Заменить все положительные на максимум.
Упражнения 97 18. Заменить каждое четное по абсолютной величине на разность максимума и минимума. 19. Заменить каждое второе отрицательное число половиной максимума. 20. Заменить каждое третье положительное число средним арифметическим. Выполнить задания 1-20, передавая в функцию аргументы-указатели. Выполнить задания 1-20, используя в качестве аргумента растущий массив (см. листинг 5.3). Создать контейнер-список, реализовав добавление и удаление элементов только в конце списка. Вместо операции индексирования реализовать итератор как на- бор методов класса. Выполнить задания 1-20, передавая список в качестве аргу- мента и получая итератор в качестве результата. Выполнить задания 1-20 со списком, реализовав итератор как вложенный класс. Выполнить задания 1 -20, передавая в функцию аргументы-итераторы. Контейнеры-массивы Во всех заданиях обязательно должны быть реализованы безаргументные и ини- циализирующие конструкторы, в том числе конструктор с двумя аргументами- итераторами, конструктор копирования, деструктор, операция присваивания, ввод-вывод. Подходящие операции реализуются как методы класса, а осталь- ные — как внешние дружественные функции. Должна быть поддержана обработ- ка исключений по нехватке памяти: все конструкторы обязаны иметь специфи- кацию исключений badalloc. В следующих заданиях использовать в качестве образца числовой массив фикси- рованного размера (см. листинг 5.1). Размеры массива нужно задавать в конст- рукторе. Обязательно должны быть реализованы операция присваивания и соот- ветствующие задаче операции с присваиванием; операция индексирования [] должна проверять индекс на допустимость и генерировать исключение в случае ошибки; операции с двумя массивами должны проверять совпадение размеров. 21. Создать класс Vector для работы с векторами задаваемой размерности. Долж- ны быть реализованы: операции сложения и вычитания векторов, скалярное произведение векторов, умножения и деление на скаляр, вычисление евкли- довой нормы, сравнение векторов по норме. (Евклидова норма вычисляется как квадратный корень из суммы квадратов координат.) 22. Массив (г/о, У\> •••, Уп) представляет собой значения некоторой функции на от- резке [а, 6], причем а = уо, b = уп. Создать класс Integral, в котором реализо- вано вычисление определенного интеграла методом прямоугольников, мето- дом трапеций и методом Симпсона [15]. 23. Используя листинг 5.1 в качестве образца, реализовать класс Array — массив действительных чисел с полным набором арифметических операций. Реали- зовать операции проверки на равенство и неравенство. 24. Массив пар действительных чисел (xi, /ц), ..., (х„, р„) представляет собой на- бор значений дискретной случайной величины. Создать класс Rand, в котором
98 Глава 5. Контейнеры реализовать методы вычисления математического ожидания и дисперсии. Реализовать метод вычисления интеграла для заданной функции f(x) мето- дом Монте-Карло [15]. 25. Создать класс Array — одномерный массив действительных чисел с задавае- мыми границами индексов с возможностью задания отрицательных индексов. Обязательно должны быть реализованы: отслеживание количества элемен- тов, умножение и деление на скаляр, максимум и минимум, сумма элементов и среднее арифметическое, поэлементное сложение и вычитание массивов. 26. Площадь плоского многоугольника с вершинами в точках (xt, г/t), ..., (хп, уп), где хп+1 = Xi, yn+i = yi, вычисляется по формуле 5= -*мУ(Ум +У{) Реализовать класс Poli gon с методами вычисления периметра и площади мно- гоугольника. Реализовать метод, проверяющий пересечение многоугольников. 27. Создать класс Array — одномерный массив целых чисел с задаваемыми грани- цами индексов с возможностью задания отрицательных индексов. Обязатель- но должны быть реализованы: отслеживание количества элементов, все опе- рации с массивом и целым числом, поиск заданного элемента, все поэлементные операции, поддержанные в C++ для целых. 28. Создать класс Bitstring для работы с битовыми строками. Битовая строка должна быть представлена массивом типа unsigned char, каждый элемент ко- торого принимает значение 0 или 1. Младший бит имеет младший индекс. Должны быть поддержаны все операции для работы с битовыми строками: and, or, xor, not. Реализовать сдвиг влево и сдвиг вправо на заданное количест- во битов; операцию циклического сдвига влево и вправо на заданное число битов. 29. Создать класс Array — одномерный массив целых чисел с задаваемыми грани- цами индексов с возможностью задания отрицательных индексов. Должны быть реализованы: отслеживание количества элементов, сложение со скаля- ром, вычитание скаляра, сортировка, поиск заданного элемента, случайная перестановка элементов, поэлементные сложение и деление. 30. Создать класс Binary для работы с двоичными числами фиксированной дли- ны. Число должно быть представлено массивом типа unsigned char, каждый элемент которого принимает значение 0 или 1. Младший бит имеет младший индекс. Отрицательные числа представляются в дополнительном коде. ПРИМЕЧАНИЕ----------------------------------------------------------- Дополнительный код получается инверсией всех битов и прибавлением 1 к младшему раз- ряду. Например, +1 представляется как 0001, тогда -1 получается как 1110 + 0001 = 1111. 31. Реализовать все арифметические операции и операции сравнения. При пере- полнении генерировать исключение out of range.
Упражнения 99 В следующих заданиях использовать выделение памяти массивом. Массив — пе- ременного размера; первоначальный размер задавать в конструкторе. Из опера- ций, изменяющих размер массива, реализовать только добавление и удаление элемента в конце массива. Реализовать операцию индексирования operator[]. Обязательно должны быть поддержаны операция присваивания и соответствую- щие задаче операции с присваиванием. Подходящие операции реализовать как дружественные функции. Реализовать конструктор инициализации строкой, операцию преобразования в строку и операции ввода/вывода. В тех задачах, где это целесообразно, ввести функцию преобразования в double. 32. Создать класс Decimal для работы с беззнаковыми целыми десятичными чис- лами, используя массив элементов типа unsigned char, каждый элемент кото- рого является десятичной цифрой. Младшая цифра имеет меньший индекс (единицы — в нулевом элементе массива). Реализовать арифметические опе- рации, аналогичные имеющимся для целых в C++, и операции сравнения. 33. Создать класс Poli пот для работы с многочленами. Коэффициенты должны быть представлены массивом, каждый элемент которого — коэффициент. Млад- шая степень имеет меньший индекс (нулевая степень — нулевой индекс). Реализовать арифметические операции, вычисление для заданного х, интег- рирование, получение производной. 34. Создать класс Octal для работы с беззнаковыми целыми восьмеричными чис- лами, используя массив элементов типа unsigned char, каждый элемент кото- рого является шестнадцатеричной цифрой. Младшая цифра имеет меньший индекс. Реализовать все арифметические операции для целых в C++ и опера- ции сравнения. 35. Создать класс Decimal для работы со знаковыми целыми десятичными числа- ми, используя массив элементов типа unsigned char, каждый элемент которого является десятичной цифрой. Младшая цифра имеет меньший индекс (еди- ницы — в нулевом элементе массива). Знак представить отдельным полем sign. Реализовать арифметические операции, встроенные для целых в C++, и опе- рации сравнения. 36. Создать класс Fraction для работы с дробными десятичными числами. Коли- чество цифр в дробной части должно задаваться в отдельном поле и инициа- лизироваться конструктором. Знак представить отдельным полем sign. 37. Создать класс Long для работы с беззнаковыми целыми двоичными числами, используя массив элементов типа unsigned char, каждый элемент которого яв- ляется десятичной цифрой. Младшая цифра имеет меньший индекс (едини- цы — в нулевом элементе массива). Реализовать арифметические операции, встроенные для целых в C++, и операции сравнения. 38. Создать класс Hex для работы с беззнаковыми целыми шестнадцатеричными числами, используя массив элементов типа unsigned char, каждый элемент ко- торого является шестнадцатеричной цифрой. Младшая цифра имеет мень- ший индекс. Реализовать арифметические операции, поддержанные для це- лых в C++, и операции сравнения.
100 Глава 5. Контейнеры 39. Создать класс Money для работы с денежными суммами (см. задание 2.4). Сум- ма должна быть представлена массивом, каждый элемент которого — деся- тичная цифра. Младший индекс соответствует младшей цифре денежной суммы. Младшие две цифры — копейки. 40. Создать класс Long для работы со знаковыми целыми двоичными числами, ис- пользуя массив элементов типа unsigned char, каждый элемент которого явля- ется десятичной цифрой. Младшая цифра имеет меньший индекс (едини- цы — в нулевом элементе массива). Знак представить отдельным полем sign. Реализовать арифметические операции, поддержанные для целых в C++, и операции сравнения. 41. Создать класс Hex для работы со знаковыми целыми шестнадцатеричными числами, используя массив элементов типа unsigned char, каждый элемент ко- торого является шестнадцатеричной цифрой. Младшая цифра имеет мень- ший индекс. Знак представить отдельным полем sign. Реализовать арифмети- ческие операции, поддержанные для целых в C++, и операции сравнения. 42. Создать класс Octal для работы со знаковыми целыми восьмеричными числа- ми, используя массив элементов типа unsigned char, каждый элемент которого является шестнадцатеричной цифрой. Младшая цифра имеет меньший ин- декс. Знак представить отдельным полем sign. Реализовать арифметические операции, поддержанные для целых в C++, и операции сравнения. 43. Создать класс Fraction для работы с беззнаковыми дробными десятичными числами. Число должно быть представлено двумя массивами типа unsigned char: целая и дробная часть, каждый элемент — десятичная цифра. Для целой части младшая цифра имеет меньший индекс, для дробной части — старшая цифра имеет меньший индекс (десятые — в нулевом элементе, сотые — в пер- вом, и т. д.). Реализовать арифметические операции сложения, вычитания и умножения и операции сравнения. 44. Реализовать класс Rational (см. задание 1.28), используя два массива типа unsigned char для представления числителя и знаменателя. Каждый элемент является десятичной цифрой. Младшая цифра имеет меньший индекс (еди- ницы — в нулевом элементе массива). 45. Написать функцию реверсирования строки символов с использованием сте- ка. Реализовать стек символов как растущий массив. 46. Написать функцию, проверяющую соответствие скобок в строке, с помощью стека. Реализовать стек символов как растущий массив. В следующих заданиях использовать выделение памяти массивом, размер зара- нее не известен. Элементы могут вставляться в любое место массива; может быть удален любой элемент или группа элементов (см. листинг 5.3). 47. Карточка иностранного слова представляет собой структуру WordCard, содер- жащую иностранное слово и его перевод. Для моделирования электронного словаря иностранных слов реализовать класс Dictionary. Данный класс содер- жит массив карточек иностранных слов. Карточки добавляются в словарь и удаляются из него. В словаре не должно быть дублей. Реализовать поиск слова как отдельный метод. Реализовать операции слияния; пересечения и
Упражнения 101 вычитания словарей. При объединении новый словарь должен содержать без повторений все слова, содержащиеся в обоих словарях-операндах. При пере- сечении новый словарь должен состоять только из тех слов, которые имеются в обоих словарях-операндах. При вычитании новый словарь должен содер- жать слова первого словаря-операнда, которые не содержатся во втором. Ар- гументом операции индексирования должно быть иностранное слово. 48. Создать класс ListBox — одномерный массив строк. Обязательно должны быть реализованы: отслеживание текущего количества элементов, получение эле- мента по индексу, получение индекса по элементу, присвоение элемента, до- бавление и удаление элемента, присвоение массивов, удаление подмассива, вставка подмассива, замена подмассива. Реализовать метод слияния двух массивов. 49. Товарный чек содержит список товаров, купленных покупателем в магазине. Один элемент списка представляет собой пару: товар-сумма. Товар реализо- вать как класс Goods с полями кода и наименования, цены за единицу товара, количества приобретаемых единиц. В классе должны быть методы доступа к полям с целью получения и изменения информации, а также метод вычис- ления суммы оплаты товара. Реализовать класс Receipt, полями которого яв- ляются номер товарного чека, дата и время его создания, список покупаемых товаров. В классе Receipt реализовать методы добавления, изменения и удале- ния записи о покупаемом виде товара, метод поиска информации об опреде- ленном виде товара по его коду и названию, а также метод подсчета общей суммы, на которую были осуществлены покупки. 50. Один тестовый вопрос представляет собой структуру Task со следующими по- лями: вопрос, пять вариантов ответа, номер правильного ответа, начисляемые баллы за правильный ответ. Для моделирования набора тестовых вопросов реализовать класс TestContent, содержащий массив тестовых вопросов. Реали- зовать методы добавления и удаления тестовых вопросов, а также метод до- ступа к тестовому заданию по его порядковому номеру в списке. В массиве не должно быть повторяющихся вопросов. Реализовать операцию слияния двух тестовых наборов, операции пересечения и вычисления разности (см. зада- ние 46). Реализовать операцию генерации конкретного объекта Test объемом не более k вопросов из объекта типа TestContent. 51. Карточка персоны содержит фамилию и дату рождения. Реализовать класс Li stPerson для работы с картотекой персоналий. Класс должен содержать мас- сив карточек персон. Реализовать методы добавления и удаления карточек, а также метод доступа к карточке по фамилии. Фамилии в массиве должны быть уникальны. Реализовать операции объединения двух картотек, опера- ции пересечения и вычисления разности (см. задание 46). Реализовать метод, выдающий по фамилии знак зодиака. Для этого в классе должен быть объяв- лен статический массив структур Zodiac с полями: название знака зодиака, дата начала и дата окончания периода. Индексом в массиве должен быть пе- речислимый тип zodiac. Для представления дат использовать упрощенный ва- риант класса Date (см. задание 1.29).
102 Глава 5. Контейнеры 52. Информационная запись о книге в библиотеке содержит следующие поля: ав- тор, название, год издания, издательство, цена. Для моделирования учетной карточки абонента реализовать класс Subscriber, содержащий фамилию або- нента, его библиотечный номер и список взятых в библиотеке книг. Один элемент списка содержит информационную запись о книге, дату выдачи, тре- буемую дату возврата и признак возврата. Реализовать методы добавления книг в список и удаления книг из него; метод поиска книг, подлежащих воз- врату; методы поиска по автору, издательству и году издания; метод вычисле- ния стоимости всех подлежащих возврату книг. Реализовать операцию слия- ния двух учетных карточек, операцию пересечения и вычисления разности (см. задание 46). Реализовать операцию генерации конкретного объекта Debt (долг), содержащего список книг, подлежащих возврату из объекта типа Subscriber. Для представления дат использовать упрощенный вариант класса Date (см. задание 1.29). 53. Информационная запись о файле в каталоге содержит поля: имя файла, рас- ширение, дата и время создания, атрибуты «только чтение», «скрытый», «сис- темный», размер файла на диске. Реализовать класс Directory, содержащий название родительского каталога, количество файлов в каталоге, список фай- лов в каталоге. Один элемент списка содержит информационную запись о файле, дату последнего изменения, признак выделения и признак удаления. Реализовать методы добавления файлов в каталог и удаления файлов из него; метод поиска файла по имени, по расширению, по дате создания; метод вы- числения полного объема каталога. Реализовать операцию объединения и операцию пересечения каталогов (см. задание 46). Реализовать операцию ге- нерации конкретного объекта Group (группа), содержащего список файлов, из объекта типа Directory. Должна быть возможность выбирать группу файлов по признаку удаления, по атрибутам, по дате создания (до или после), по объ- ему (меньше или больше). Для представления дат использовать упрощенный вариант класса Date (см. задание 1.29). 54. Используя класс Bill (см. задание 1.56), реализовать класс ListPayer. Класс содержит список плательщиков за телефонные услуги, дату создания списка, номер списка. Один элемент списка включает информацию о плательщике (класс Bill), статус оплаты, дату платежа, сумму платежа. Реализовать мето- ды добавления плательщиков в список и удаления их из него; метод поиска плательщика по номеру телефона и по фамилии, по дате платежа. Метод вы- числения полной стоимости платежей всего списка. Реализовать операцию объединения и операцию пересечения списков (см. задание 46). Реализовать операцию генерации конкретного объекта Group (группа), содержащего спи- сок плательщиков, из объекта типа Li stPayer. Должна быть возможность вы- бирать группу плательщиков по признаку оплаты, по дате платежа, по номеру телефона. Для представления дат использовать упрощенный вариант класса Date (см. задание 1.29). 55. Одна запись в списке запланированных дел представляет собой структуру Dailyltem, которая содержит время начала и окончания работы, описание и признак выполнения. Реализовать класс DailySchedule — план работ на день. Реализовать методы добавления, удаления и изменения планируемой работы.
Упражнения 103 При добавлении проверять корректность временных рамок (они не должны пересекаться с уже запланированными мероприятиями). Реализовать метод поиска свободного промежутка времени. Условие поиска задает размер искомо- го интервала, а также временные рамки, в которые он должен попадать. Ме- тод поиска возвращает структуру Dai lyltern с пустым описанием вида работ. Реализовать операцию генерации объекта Redo (еще раз), содержащего список дел, не выполненных в течение дня, из объекта типа DailySchedule. Реализо- вать операцию объединения двух объектов типа DailySchedule; в объединен- ном объекте не должно быть одинаковых работ, выполняемых в разное время, а также разных работ, выполняемых в одно время. Для представления време- ни использовать упрощенный вариант класса Time (см. задание 1.30). 56. Прайс-лист компьютерной фирмы представляет собой список моделей прода- ваемых компьютеров. Один элемент списка (Model) содержит информацию о марке компьютера, типе процессора, частоте работы процессора, объеме памя- ти, объеме жесткого диска, объеме памяти видеокарты, цене компьютера в ус- ловных единицах и количестве экземпляров, имеющихся в наличии. Реализо- вать класс PriceList, полями которого являются дата его создания, номинал условной единицы в рублях и список продаваемых моделей компьютеров. В списке не должно быть двух моделей одинаковой марки. В классе PriceList реализовать методы добавления, изменения и удаления записи о модели, метод поиска информации о модели по марке компьютера, по объему памя- ти, диска и видеокарты (равно или не меньше заданного), а также метод под- счета общей суммы. Реализовать методы объединения и пересечения прайс- листов (см. задание 46). Метод поиска возвращает объект класса Model в каче- стве результата. Реализовать операцию генерации конкретного объекта Group (группа), содержащего список моделей, из объекта типа Model. Должна быть обеспечена возможность выбирать группу по любому из полей класса Model. Для представления дат использовать упрощенный вариант класса Date (см. задание 1.29). 57. Реализовать класс Library (библиотека модулей). Каждый модуль имеет имя (не более 30 символов) и объем в байтах. Библиотека включает в себя поле количества модулей. Реализовать методы добавления, удаления и замены модуля в библиотеке; метод объединения библиотек (см. задание 46); метод вычисления общего объема модулей в библиотеке; метод поиска модуля по имени. 58. Реализовать очередь Queue нуждающихся в улучшении жилищных условий. Элементом очереди является структура Lodger с полями: фамилия, количест- во членов семьи, дата постановки в очередь, занимаемая жилая площадь, не- обходимые жилищные условия (количество комнат и требуемая площадь). Для реализации поля даты использовать упрощенную версию класса Date (см. зада- ние 1.29). Реализовать операцию поиска элемента по фамилии, по дате (после заданной), по требуемой площади (не меньше заданной); реализовать методы постановки в очередь и удаления из очереди. Реализовать операции объеди- нения и пересечения двух очередей (если очередник есть в обеих, включать только один раз). Реализовать операцию генерации конкретного объекта Group (группа), содержащего список очередников, из объекта типа Queue. Должна
104 Глава 5. Контейнеры быть обеспечена возможность выбирать группу по любому из полей класса Queue. 59. Реализовать очередь Unemployed на бирже труда. Один элемент списка безра- ботных имеет поля: фамилия, возраст, пол, образование, профессия, должность, стаж работы в последней должности и общий стаж, дата постановки на учет, желаемая заработная плата, желаемая должность. Для реализации поля даты использовать упрощенную версию класса Date (см. задание 1.29). Образова- ние, профессия и должность должны быть представлены в классе статически- ми массивами строк, а в элементе списка безработных — как индексы в этих массивах. База вакансий должна быть представлена классом Vacancy, в кото- ром содержится список фирм с полями: название фирмы, вакантная долж- ность, заработная плата, требования к кандидату: образование, возраст, пол, общий стаж и стаж работы в аналогичной должности. Для обоих списков реализовать обычные операции добавления и удаления. Реализовать опера- цию генерации конкретного объекта Group (группа), содержащего список очередников, из объекта типа Unemployed. Должна быть обеспечена возмож- ность выбирать группу по любому из полей класса Unemployed. Реализовать методы поиска в очереди и в базе вакансий для осуществления снятия с уче- та на бирже. 60. Реализовать класс Realty, моделирующий риэлторскую контору (купля-про- дажа жилья). Список квартир на продажу состоит из элементов со струк- турой: район, адрес, количество комнат, общая площадь, жилая площадь, год постройки дома, запрашиваемая стоимость, телефон продавца. Список потреб- ностей состоит из элементов со структурой: требуемое количество комнат, требуемая площадь, предлагаемая цена и телефон покупателя. Реализовать методы поиска вариантов в списках спроса и предложения для удовлетворе- ния спроса. Для обоих списков реализовать обычные операции добавления и удаления. Реализовать операцию генерации конкретного объекта Group (груп- па), содержащего список предложений, из объекта типа Realty. Должна быть обеспечена возможность выбирать группу по любому из полей класса Real ty. 61. Реализовать модель станции техобслуживания автомобилей. Один элемент очереди — автомобиль — представляет собой структуру с полями: марка авто- мобиля, требуемая марка бензина, объем бака, остаток бензина, объем масла, необходимость мойки. Марка автомобиля и марка бензина представляются статическими массивами строк, а в элементе списка эти поля представлены индексами. Станция техобслуживания предоставляет следующий набор услуг: заправка бензином, заливка масла, мойка. Каждая услуга имеет цену: заливка масла и мойка фиксированную, бензин — цену за литр. Время заливки бензи- на — 2 мин, заливки масла — 1 мин, мойки — 3 мин. Автомобили прибывают на станцию в случайные моменты времени, в среднем — раз в 5 мин. Требуе- мые услуги также генерируются случайным образом. Промоделировать рабо- ту станции за сутки. 62. Список абонентов кабельного телевидения состоит из элементов следующей структуры: фамилия, район, адрес, телефон, номер договора, дата заключения договора, оплата установки, абонентская плата помесячно, дата последнего
Упражнения 105 платежа. Реализовать класс Abonent, в котором предусмотреть методы добав- ления абонентов в список и удаления их из него; методы поиска абонента по номеру договора, номеру телефона и по фамилии, по дате заключения догово- ра; метод вычисления стоимости платежей одного абонента и всего списка. Реализовать операцию объединения и операцию пересечения списков (см. за- дание 46). Реализовать операцию генерации конкретного объекта Group (груп- па), содержащего список абонентов одного района, из объекта типа Abonent. 63. Нагрузка преподавателя за учебный год представляет собой список дисциплин, которые он должен прочитать в течение года. Одна дисциплина представля- ется информационной структурой с полями: название дисциплины, семестр проведения, количество студентов, количество часов аудиторных лекций, ко- личество аудиторных часов практики, вид контроля (зачет или экзамен). Реа- лизовать класс WorkTeacher, моделирующий бланк назначенной преподавате- лю нагрузки. Класс содержит фамилию преподавателя, дату утверждения, список преподаваемых дисциплин, объем полной нагрузки в часах и в став- ках. Дисциплины в списке не должны повторяться. Объем в ставках вычисля- ется как частное от деления объема в часах на среднюю годовую ставку, оди- наковую для всех преподавателей кафедры. Элемент списка преподаваемых дисциплин содержит поля: дисциплина, количество часов, выделяемых на за- чет (0,35 ч на одного студента) или экзамен (0,5 ч на студента), сумму часов по дисциплине. Реализовать добавление и удаление дисциплин; вычисление суммарной нагрузки в часах и ставках. Должен осуществляться контроль пре- вышения нагрузки (допустимый максимум — полуторы ставки). 64. Создать класс Li stPayment (зарплата). В классе содержится список сотрудни- ков, для которых рассчитывается заработная плата. Сотрудник представлен классом Person с полями: табельный номер, фамилия-имя-отчество, оклад, год поступления на работу, процент надбавки, подоходный налог, количество от- работанных дней в месяце, количество рабочих дней в месяце, начисления, удержания. Реализовать методы вычисления класса Person: начисленной сум- мы, удержанной суммы, суммы, выдаваемой на руки, и стажа. Стаж вычисля- ется как полное количество лет, прошедших от года поступления на работу, до текущего года. Начисления представляют собой сумму, начисленную за от- работанные дни и надбавки — доли от суммы, начисленной за отработанные дни. Удержания представляют собой отчисления в пенсионный фонд (1 % от начисленной суммы) и подоходный налог. Подоходный налог составляет 13 % от начисленной суммы без отчислений в пенсионный фонд. Реализовать ме- тоды добавления сотрудника в список и удаления из него; методы объедине- ния списков (см. задание 46); методы поиска по полям класса Person. Реализо- вать методы вычисления полных сумм по всему списку: начислено, удержано, на руки, подоходный налог, пенсионный фонд. Реализовать операцию генера- ции объекта Group (группа), содержащего список сотрудников с одинаковым стажем работы, из объекта типа Li stPayment. 65. Реализовать класс Set (множество) типа double. Множество должно обеспечи- вать включение элемента в множество, исключение элемента из множества, объединение, пересечение и вычитание множеств, отслеживание количества
106 Глава 5. Контейнеры элементов в множестве, проверку присутствия элемента в множестве, провер- ку включения одного множества в другое. 66. Создать класс String — одномерный массив символов. Обязательно должны быть реализованы: определение длины строки, поиск подстроки в Строке слева и справа, замена подстроки в строке, удаление подстроки, вставка подстроки в строку, сравнение строк, сцепление строк, перевод в строчные и прописные. 67. Реализовать базовый класс Array — динамический массив переменного разме- ра — с подходящим типом элементов (см. листинг 5.3). Деструктор объявить чистым виртуальным. Реализовать задания предыдущего раздела как приват- ные наследники от класса Array. Контейнеры-списки Реализовать задачи 31-45, используя вместо массива двухсвязный список (см. лис- тинг 5.4). Вместо операции индексирования реализовать итератор как набор ме- тодов списка. Выполнить задачи 46-65, используя вместо массива двухсвязный список (см. лис- тинг 5.4). Вместо операции индексирования реализовать итератор как набор ме- тодов списка. Реализовать базовый класс Li st (см. листинг 5.4) с итератором в виде вложенно- го класса. Реализовать задания 46-65 как приватные наследники от класса Li st. В классе Li st реализовать чистый виртуальный деструктор. Выполнить задачи 46-65, используя вместо массива двухсвязный список (см. лис- тинг 5.4). Вместо операции индексирования реализовать итератор как набор ме- тодов списка. Во всех заданиях, кроме 65, дополнительно реализовать стек в виде односвязного списка с подходящим типом элементов, и использовать его для инверсии списков, представленных в задании. Выполнить задания 46-65, реализовав стек как приватный наследник базового класса List (см. листинг 5.4) и используя его для инверсии списков, представ- ленных в задании. В классе Li st реализовать чистый виртуальный деструктор.
ГЛАВА 6 Шаблоны В данной теме изучается техника использования шаблонов как классов, так и функций. Для шаблонов классов рассматриваются виды параметров, в част- ности параметры по умолчанию и параметры-шаблоны; изучаются вопросы вза- имодействия шаблонов и традиционных средств C++, в частности: шаблоны и на- следование, статические элементы в шаблонах. Предполагается, что шаблоны функций послужат основой для написания обобщенных алгоритмов и функторов. Краткие сведения по теме Такие контейнеры, как стеки и очереди (см. главу 5), характеризуются дисцип- линой доступа к элементам и фактически не зависят от типа элементов. Анало- гично, контейнер-массив предоставляет независимый от типа доступ к элемен- там с помощью операции индексирования. Создать универсальный контейнер, безразличный к типу элементов, можно двумя способами: □ используя в качестве элемента контейнера указатель void *; □ на основе шаблона класса, в котором тип элементов задается параметром шаблона. Первый способ — наследие от С, поскольку в этом языке не было других средств. В C++ обычно используется другой способ — так называемые шаблоны [11]. Шаблоны классов Шаблон класса является заготовкой, из которой создаются конкретные классы в процессе инстанцирования (то есть создания сущности, инстанции). Делается это путем подстановки конкретного типа в качестве аргумента. Шаблон класса еще называют параметризованным типом. Термин «параметризованный тип» эквивалентен терминам «шаблон класса» и «шаблон». Синтаксис класса-шаблона выглядит так: template <параметры> // объявление шаблона class имя_класса { // определение класса };
108 Глава 6. Шаблоны Префикс tempi ate «параметры» показывает компилятору, что следующее определе- ние класса является шаблоном, а не обычным классом. Шаблоном может быть и структура. Как и имя обычного класса, имя класса-шаблона не должно совпа- дать с другими именами в одной области видимости. Параметры шаблона могут быть следующих видов: □ параметр-тип; □ параметр целочисленного или перечислимого типа; □ параметр-указатель на объект или указатель на функцию; □ параметр-ссылка на объект или ссылка на функцию; □ параметр-указатель на член класса; К целочисленным типам относятся все целые, символьные и булевский (bool) типы. Нельзя использовать в качестве параметра ни один из встроенных дроб- ных типов — ни fl oat, ни doubl е, ни 1 ong doubl е. Параметры шаблона, если их не- сколько, записываются,через запятую. Все виды параметров, кроме параметра- типа, пишутся обычным образом (как в функциях), например: long р. int (*f)(double), int *t, const char *s Имена параметров видимы в пределах всего шаблона, поэтому внутренние для шаблона имена должны быть разными. Параметр-тип является наиболее часто используемым и объявляется как class имя или typename имя В качестве имени параметра разрешается применять любой допустимый иденти- фикатор. Внутри класса такой параметр может появляться на тех местах, где по- зволено писать конкретный тип. Шаблон структуры-пары с двумя полями разного типа можно определить раз- ными способами: template «class Tl. class T2> struct Pair { Tl first: T2 second: }: template «typename Tl. typename T2> struct Pair { Tl first: T2 second: }; template «class Tl, typename T> struct Pair { Tl first; T second: }: Шаблон, как и обычный класс, можно объявить. Объявление состоит из заголов- ка шаблона и не включает в себя тело класса, например: template «typename Т> class Stack: template «typename Tl. typename T2> struct Pair: template «class T> class Array: Определение объектов для некоторого класса-шаблона в общем виде выглядит так: имя_класса_шаблона«аргументы> имя_объекта; // одиночный объект имя_класса_шаблона«аргументы> имя_объекта[количество]; // массив имя_класса_шаблона«аргументы> *имя_объекта; // указатель
Краткие сведения по теме 109 Или для случая константной ссылки в качестве параметра: const имя_класса_шаболона<аргументы> &имя_объекта; В угловых скобках на месте параметров при объявлении объектов задают кон- кретные аргументы. Параметры шаблона являются позиционными (как и аргу- менты функции), поэтому подставляемое фактическое значение должно соответ- ствовать виду параметра шаблона: если на данном месте в определении класса- шаблона находился параметр-тип, то и аргумент должен быть типом или клас- сом. Например, определение конкретных объектов-пар может быть таким: Разr<int, int> х; Pa1r<1nt, double> у; Pair<string, date> d: В качестве аргумента-типа можно использовать и класс-шаблон, например: Pair<Pair<int, long>. float> разг; Пример класса-шаблона Самое простое применение класса-шаблона — это реализация более «умного» массива, чем встроенный. В шаблоне из листинга 6.1 поддержана операция ин- дексирования, но уже с учетом допустимости индекса. Тип элементов массива и размер массива являются параметрами шаблона. Листинг 6.1. Простой массив — шаблон #1nclude <stdexcept> tempiate<class T. std::s1ze_t N> // параметры шаблона class Array { public: // типы typedef T value_type; typedef T& reference: typedef const T& const_reference; typedef std::size_t s1ze_type; static const size_type stat1c_s1ze = N; // размер массива Array(const T &t = TO); // конструктор size_type sizeО const // получение размера { return static_size; } reference operator[](const s1ze_type& i) // доступ к элементам { rangecheck(1); return elem[1J; } const_reference operatorf](const s1ze_type& D const { rangecheck(1); return elem[i]; }; private: void rangecheck (const s1ze_type& 1) const // проверка индекса { if (1 >= sizeO) { throw std::range_error("Array - range!"); } T elem[N]; // поле-массив }:
110 Глава 6. Шаблоны tempiate<typename Т, std::s1ze_t N> // реализация конструктора Array<T,N>::Array(const T &t) { for (int 1 = 0: i<N; i++) elem[i] = t; } Функциональность класса — минимальная. В начале класса-шаблона определе- но несколько типов-синонимов. Проверка индекса в операциях индексирования [] выполняется приватной функцией rangecheckO, которая генерирует стандарт- ное исключение при «вылете» за границу массива. Конструктор инициализации является и конструктором по умолчанию. Конст- руктор класса-шаблона задан внешним образом: определение метода должно на- чинаться со слова tempi ate вместе с параметрами шаблона: tempiate<typename Т, std::size_t N> Тип класса-шаблона Array<T,N>:: задается в качестве префикса. В теле метода для всех имен класса префикс может отсутствовать. Для методов, определенных внутри шаблона, префиксы не указываются. Параметру присвоено значение по умолчанию const Т &t = ТО При инстанцировании шаблона (подстановка конкретного типа) эта конструк- ция означает инициализацию нулем для элементарных встроенных типов; а для классов влечет вызов конструктора по умолчанию (то есть класс, используемый в качестве подставляемого аргумента шаблона, должен иметь конструктор по умолчанию). Данную конструкцию можно использовать не только в списке параметров, но в любом месте шаблона, где допускается объявить переменную и присвоить ей значение. Например, в цикле инициализации массива можно было бы написать: elem[i] = ТО: что превращается в корректную конструкцию при подстановке любого типа, как встроенного, так и реализованного. В классе-шаблоне разрешено использовать инициализацию нулем и в списке ини- циализации конструктора. Если отказаться от присвоения значения по умолча- нию и использовать явное обнуление, конструктор класса-шаблона примет вид: tempiate<typename Т, std::size_t N> // реализация конструктора Array<T,N>::Array():elem(){ } Массив будет проинициализирован нулем, если вместо Т подставить встроенный тип. Для классов тогда будет вызываться конструктор без аргументов. Использовать такой шаблон — просто. Array<int, 10> W; Array<int, 10> t(0): Array<int, 10> r(t); Array<double. 50> p(2); for (int i = 0: i < p.sized; i++) cout « p[i] « ' '; // инициализируем нулем // инициализируем нулем // конструктор копирования // все элементы = 2 // доступ справа
Краткие сведения по теме 111 for (Int 1 = 0; p[i] = i: < p.sizeO: i++) И доступ слева В качестве типа-аргумента могут подставляться любые возможные типы, опре- деление которых видимо в точке объявления переменной-массива. Array<void*,100> рр; Array<Pair<int,1nt>, 100> тр; Array<Paiг<1ong,1nt>*,10> *трр; Array<date. 10> dd; И массив указателей И массив объектов-пар // указатель на массив указателей на И пары И массив дат Класс date должен иметь конструктор без аргументов, вызываемый в конструк- торе массива. Действительно также и присваивание: и = г: // присваиваем - типы одинаковы Копирование и присваивание работают только для массивов с одинаковым типом элементов и размером массива. Изменяя тип элементов и (или) размер, получа- ем массивы разных типов. При попытках использовать оператор присваивания или конструктор копирования с такими разнотипными массивами компилятор выдает сообщение о невозможности преобразования типов. Например, нельзя написать присвоение р = t: Слева массив с типом элементов double, а справа — int. Преобразование типов по умолчанию не выполняется. Объекты такого класса-шаблона можно передавать как параметры и получать как результат из функций. Необходимо помнить, что при передаче (и при воз- врате) массива по значению вызывается конструктор копирования, поэтому луч- ше такой массив передавать по ссылке. Array<double, 10> F(const Array<int, 10> &t); Эта функция получает ссылку на массив из 10 элементов типа int, а возвращает массив из 10 элементов типа double. Нужно также помнить, что преобразование типов по умолчанию не работает. Параметры шаблона, задаваемые по умолчанию Параметрам класса-шаблона можно присваивать значения по умолчанию, в том числе и для параметров-типов. Для целочисленных параметров допускается за- давать в качестве значения константное выражение, которое компилятор спосо- бен вычислить на этапе трансляции. Для параметров-типов можно указывать либо встроенный тип, либо любое видимое в точке определения шаблона имя типа. Например, для шаблона «умного» массива Array (листинг 6.1) можно присвоить по умолчанию тип double и ограничить массив десятью элементами. Заголовок шаблона пишется привычным способом tempiate<typename Т = double. std::size_t N = 10>
112 Глава 6. Шаблоны В реализации методов ничего изменять не требуется. Тогда допускаются объяв- ления массивов такого вида: Array<int. 20> t; // полное объявление Array<date> d; // количество по умолчанию • Аггауо р; // тип и количество по умолчанию Пустые угловые скобки указывать обязательно, иначе возникает ошибка време- ни компиляции, поскольку компилятор будет искать обычный класс Array. Во всех объявлениях инициализация выполняется неявно. Класс date должен иметь конструктор без аргументов или конструктор инициализации с аргументом по умолчанию. Как и для функций с аргументами по умолчанию, присваивать значения нужно правым параметрам. Можно написать заголовок шаблона Array так: tempiate<typename Т. std::size_t N = 100> и не разрешается — таким способом: tempiate<typename Т = double, std::size_t N> Как и для функций с аргументами по умолчанию, при определении объекта нельзя пропускать левые параметры, например: Array<10> t; // ожидается ошибка трансляции! Подобное объявление приведет к ошибке трансляции — компилятор не обнару- жит определения шаблона с одним целочисленным параметром. Специализация шаблона-класса При программировании шаблонов классов часто бывает нужно наряду с общим шаблоном иметь специализированные версии. Для этого в C++ имеется меха- низм специализации. Специализация заключается в том, что на основе исходного первичного шаблона создается его специализированная версия, в которой на ме- сто параметров подставлены аргументы (и по-другому реализованы некоторые методы — в соответствии с аргументами). Специализация — это не присвоение параметров по умолчанию. Шаблон с параметрами по умолчанию является пер- вичным шаблоном, который тоже можно специализировать. Специализация шаб- лона называется полной, если конкретизированы все аргументы. Если задана только часть аргументов, такая специализация называется частичной. При определении шаблона и его специализированных версий должен соблю- даться порядок следования: сначала должен быть определен первичный шаблон, и только после него разрешается определять специализированные версии. tempiate<class Т> // первичный шаблон class Class { // определения полей и методов класса . }: tempiate<> // полная специализация class Class<void *> { // определения полей и методов специализированной версии класса }:
Краткие сведения по теме 113 Аргументы шаблона, подлежащие специализации, указаны в угловых скобках после имени класса. В данном случае шаблон Cl ass специализирован для бести- повых указателей. Объект такого класса нужно объявлять как объект класса- шаблона с аргументом void *, например: Class<vo1d *> ddd; При частичной специализации конкретизируется только часть параметров пер- вичного шаблона. Для указателей возможна частичная специализация, даже если параметр шаблона всего один, например: tempiate<class Т> // частичная специализация class Class<T*> { // определения полей и методов специализированной версии класса }; Обозначение <Т*> после имени подразумевает, что эта специализация должна ис- пользоваться всегда, когда аргументом шаблона является указатель любого типа, отличного от void *, для которого «реализована более специализированная пол- ная специализация». Определения объектов выглядят так: Class<date*> ml: - // <Т*> - это <date*> -> Т = date Class<int*> m2: // <Т*> - это <int*> -> Т = Int Class<double**> m3: // <Т*> - это <double**> -> Т = double* Чтобы специализировать шаблон, не обязательно иметь полное определение пер- вичного шаблона — достаточно объявления, например: template <typename Т> class Class: // объявление первичного шаблона template <typename Т> class Class<T*> // частичная специализация { // определения полей и методов специализированной версии класса }: template <> class Class<void *> // полная специализация { // определения полей и методов специализированной версии класса } Все будет корректно работать, если не пытаться инстанцировать первичный шаблон. Статические элементы в шаблонах В классе-шаблоне разрешено объявлять и статические методы, и статические поля. В этом случае при различных вариантах параметров шаблона получаются фак- тически разные конкретные классы, и каждый из них обладает собственной ко- пией статических членов, как показано в листинге 6.2. Листинг 6.2. Класс-шаблон со статическими элементами template <typename Т> class StaticClass { static T t; // зависимое имя static int count; // независимое имя public: StaticClass () {++count; } -StaticClass () {--count: } // конструктор считает объекты // деструктор
114 Глава 6. Шаблоны static void print(T x = t); // статический метод }: tempiate<typename T> int StaticClass<T>::count = 0; // счетчик объектов tempiate<typename T> T StaticClass<T>: :t = TO; // определение t template <class T> // реализация метода void StaticClass<T>::print(T x) { std::cout « x «" "« count; t = x; } В классе определен счетчик объектов — статическое поле count. Это поле не за- висит от параметра шаблона, однако при определении поля нужно соблюдать синтаксис для элементов класса-шаблона. Определение статического поля t, тип которого зависит от шаблона, можно задать без явного вызова безаргументного конструктора, так как он вызывается по умолчанию: tempiate<typename Т> Т StaticClass<T>::t; // вызов конструктора по умочанию Реализация статического метода, как обычно, должна сопровождаться префик- сом шаблона tempi ate<cl ass Т>. Разрешается специализировать статические поля: template^» int StaticClass<int>;:t = 22; // специализация t template<> double StaticClass<double>::t = 15; // специализация t Специализация статического поля вызывает инстанцирование шаблона с пара- метром заданного типа. В данном случае создаются классы Stati cClass<int> и StatIcCI ass<double>. Для каждого из них заводятся статические поля-счетчики count, статические поля t, которым присваиваются значения, и создаются стати- ческие методы printO. Шаблоны классов с шаблонами К шаблонам применимо, как в экономике, отношение «шаблон-класс-шаблон». Нет ограничений на вложенность классов-шаблонов: класс может быть объявлен внутри шаблона и шаблон — внутри как класса, так и шаблона. Единственное ограничение — класс-шаблон нельзя объявлять внутри функции (а обычный класс можно). Шаблон может наследовать от обычного класса; класс может на- следовать от шаблона; шаблон может наследовать от шаблона. В классе-шаблоне допускаются вложенные конструкции: □ инстанцированный класс-шаблон в качестве поля в классе и в шаблоне; □ класс-шаблон в качестве параметра-типа по умолчанию; □ метод-шаблон, в том числе конструктор. Поле-шаблон В классе допускается объявить поле, инстанцировав некоторый шаблон. Объяв- ление поля-шаблона ничем не отличается от объявления обычного поля. Напри- мер, на основе класса-шаблона Array (см. листинг 6.1) можно реализовать стек ограниченного размера.
Краткие сведения по теме 115 class Stack { Array<double, 100> t; // поле - инстанцированный шаблон public: // методы }: Объявить объект-стек можно обычным образом: Stack t; Поле-шаблон в шаблоне выглядит так: template <typename Т, std::size_t N - 100> // параметры стека class Stack { Array<T, N> st: // зависимое имя public: // методы }: Так как количество элементов стека явно не объявлено, определять переменные Stack разрешено так: Stack<double> st: Или с явным указанием количества элементов Stack<double, 500> stack: Параметр-шаблон Разрешается использовать в качестве параметра шаблон. Параметру-шаблону можно присвоить значение по умолчанию, например Array (листинг 6.1). template <typename Т, // тип элементов std::size_t N = 100, // количество template <class, std::size_t> // параметр-шаблон class Container = Array // конец списка class Stack { Containers, N> s: public: // поле-контейнер // методы }; Параметр-шаблон объявлен последним: template <class, std::size_t> class Container = Array> Объявление переменных-стеков в программе может выглядеть так: Stack <double> si: // размер и контейнер по умолчанию Stack <int, 500> s2: // контейнер по умолчанию Stack <long, 1000, Array> s3; // все задано явно В третьем случае задается только имя контейнера без аргументов. Вместо шабло- на Array разрешается указывать любой контейнер, имеющий два параметра: тип элементов и беззнаковое целое.
116 Глава 6. Шаблоны Метод-шаблон В любом классе, как в обычном, так и в шаблоне, можно объявить метод-шаблон. Класс-шаблон Array (листинг 6.1) не позволяет присваивать массивы разной длины и (или) массивы с разными типами элементов. Этот недостаток можно устранить, если определить в классе Array шаблон операции присваивания, как показано в листинге 6.3. Листинг 6.3. Метод-шаблон в классе-шаблоне #include <cstdlib> • // для size_t #include <stdexcept> tempiate<typename T = double, std::size_t N = 10> // параметры шаблона class Array { public: // типы и методы (см. листинг 6.1) tempiate<typename TI, std::size_t NI> Array<T.N>& operator=(const Array<TI, NI>& r); private: // ... T elem[N]; // поле-массив }: // реализация конструкторов и методов // Определение метода-шаблона tempi ate<cl ass Т, std;-.slze_t N> // внешний шаблон tempiate<class TI. std::size_t NI> // внутренний шаблон Array<T,N>& Array<T,N>::operator=(const Array<TI, NI>& rhs) { if ((void *)this!=(vold *)&rhs) // проверка самоприсваивания { if (N>=NI) // проверка размера fordnt i = 0: 1<NI: ++i) elem[i]=static_cast<T>(rhs[i]): // преобразование типа } return *this: } Прототип метода выглядит следующим образом: tempiate<typename TI, std::size_t NI> Array<T,N>& operator=(const Array<TI, NI>& rhs): Определение метода-шаблона начинается с заголовка template шаблона-класса, а затем задается его собственный заголовок template. Методы-шаблоны не мо- гут быть виртуальными; для обычных методов класса-шаблона такого ограниче- ния нет. Шаблон операции присваивания не замещает оператор присваивания, генери- руемый по умолчанию. Для присвоения массивов одного типа будет вызываться стандартный оператор присваивания. Аналогично, шаблон конструктора копи- рования никогда не замещает собой генерируемый конструктор. С помощью шаблона-присваивания выполняется присвоение «меньшего» масси- ва «большему» — как по размеру, так и по типу.
Краткие сведения по теме 117 Array<1nt, 10> mn: Array<int, 10; > mm; // тип и размер mm = m mm = mn: // встроенная операция Array<int, 5> х: // размер х < размер mn mn = х: Array<double. 15> у: // операция-шаблон // тип и размер отличаются У = х: // операция-шаблон Шаблоны и наследование Шаблон может наследовать от простого класса — такой базовый класс называет- ся независимым базовым классом. class Base {}: template <class T> class Template: public Base {//... }: Это может понадобиться, например, для того, чтобы сделать статические поля общими для любых классов, инстанцированных из шаблона-наследника. class CommonVarlable { public: static const double Exp: static const double Pi ; }: const double CommonVarlable::Exp = 2.718281828: const double CommonVarlable::Pi = 3.141592653; template <typename T> class DeriveTemplate: public CommonVarlable {//... Здесь статические поля Exp и Pi будут общими для всех классов, инстанцирован- ных из шаблона Deri veTempl ate. Шаблон может наследовать от шаблона. template <typename Tl. typename T2> // базовый класс-шаблон struct Pair { Tl first: T2 second; }; template <class T> // шаблон-наследник class Derive: public Pair<T,T> {}: Простой класс также может наследовать от шаблона. Простой класс не предпо- лагает параметров шаблона, наследование выполняется фактически не от шабло- на, а от конкретного класса, инстанцированного из шаблона. template <class Т> // базовый класс-шаблон class Base { }; class Derave: public Base<date> // наследник -.простой класс { }: Последнее используется для отслеживания количества объектов класса, как это сделано в листинге 6.4.
118 Глава 6. Шаблоны Листинг 6.4. Подсчет объектов класса template<class Т> // шаблон класса со счетчиком class Count { static unsigned int counter; // счетчик public: // функциональность подсчета объектов CountO { ++counter: } Count(const Count<T>& t) { ++counter; } -CountO { --counter; } static unsigned int getCounterO { return counter; } }: template <class T> // инициализация счетчика unsigned int CountedObject<T>::counter = 0; // наследование от шаблона class ObjectOl: public Count<0bject01> { // дополнительная функциональность }; class 0bject02: public Count<0bject02> { // дополнительная функциональность }; В таком варианте каждый из классов-наследников будет иметь собственную ко- пию счетчика объектов. Шаблоны и дружественные отношения У шаблона могут быть друзья, и шаблоны тоже могут дружить. Дружественная функция вывода для класса-шаблона должна быть такой: template <typename Т> class Templateclass { Т х; public: TemplateClassiconst Т &t):x(t) {} // конструктор инициализации friend std::ostream& operator« <>(std::ostream &os, const TempiateClass<T> &t); }: // внешнее определение tempiate<class T> std: :ostream& operator«(std: :ostream &os, const TempiateClass<T>&t) { return os « « t.x « ')': } Пустые скобки <>, указанные в прототипе после имени функции, свидетельствуют, что дружественная функция является шаблоном. Для дружественных функций- шаблонов не выполняются по умолчанию преобразования типов аргументов. При определении дружественной функции внутри класса-шаблона ее аргументы обязаны зависеть от параметров шаблона. Определенные внутри класса-шаблона функции-друзья не являются функциями-шаблонами — это обычные функции, несмотря на то что аргументы у них зависят от параметра шаблона. template <class Т> class Template { Т х; public:
Краткие сведения по теме 119 Tempi ate(const Т &t):x(t) {} friend std: :ostream& operator«(std: :ostream &os, const Template<T> &t) { return os « '(' « t.x « } }; Шаблоны функций Помимо определения шаблонов классов, C++ разрешает писать шаблоны функ- ций. Синтаксис шаблона функции следующий: tempiа!е<параметры> заголовок { тело } Параметры шаблона функции могут быть такими же, как и параметры шаблона- класса. В шаблонах функций тоже разрешено использовать параметры, не яв- ляющиеся типами; параметр-тип можно задавать как в качестве типа аргументов функции, так и в качестве типа возвращаемого значения. Например, шаблон функции поиска минимального значения в массиве может быть таким: template <typename Т> Т M1n(T const *beg1n, Т const *end) { Т min = *begin; while (begin != end) If (min > *(++beg1n)) min = *begin; return mln; } Вызов этой функции-шаблона выглядит как вызов обычной функции: int а[10]= {1.2.3.4.5.6.7.8.9.10}; cout « Min(a, а+10) « endl; // выбор минимума из массива int double b[10]- {1.1.2.3.4,5,6.7,8.9,10.1}; cout « Min(b, b+10) « endl; // выбор минимума из массива double При вызове шаблона-функции не заданы аргументы шаблона в угловых скобках о. Компилятор самостоятельно подставляет тип на основании информации о типе фактического аргумента при вызове. Для шаблонов классов аргументы нужно задавать всегда явно. Явное указание аргументов можно делать и при вызове функ- ции-шаблона, например: cout « Min<1nt>(a, а+10) « endl; // выбор минимума из массива int cout « Min<double>(b, b+10) « endl; Вывод типов не работает для типа возвращаемого значения — такой аргумент при вызове функции-шаблона всегда нужно указывать явно. Поэтому, если тре- буется, чтобы тип возвращаемого значения тоже был параметром шаблона, сле- дует ставить его на первую позицию списка. template <dass RT, class T> 1 RT MultiplyCT const *begin, T const *end) { RT total = 1; while (begin != end) { total *= *begin; ++begi n; } return total;
120 Глава 6. Шаблоны Теперь вызовы нужно писать с явным указанием аргумента для типа возвращае- мого значения, например: Int а[10]= {1.2,3.4,5,6.7.8.9.10},- cout « Multiply<long>(a, а+10) « endl; double b[10]= {1.1,2,3,4,5,6.7,8.9.10.1}: cout « Muitiply<double>(b, b+10) « endl: Типы параметров функции компилятор выясняет самостоятельно. Попытка вы- звать функцию-шаблон без аргумента, наподобие cout « Multiply(b, b+10) « endl: приводит к ошибке трансляции. Параметры, не являющиеся типами, компилятор обычно не может идентифици- ровать сам, поэтому их, как правило, надо указывать явно. Например, шаблон функции инициализации числового массива может быть таким: template <std::size_t N, class T, T t> void In1t(T array[N]) // передача массива { for(std::size_t 1 = 0; 1<N: ++1) array[i] = t; } Вызов этой функции требует задания всех аргументов шаблона: int а[100]; Init<100, int, 3>(а): Перегрузка и специализация шаблонов функций Допускается перегрузка функций-шаблонов шаблонами и обычными функция- ми. Как и при перегрузке обычных функций, шаблоны (и функции) должны от- личаться списками параметров. Компилятор при обработке конкретного вызова старается подобрать наиболее подходящий вариант. Помимо перегрузки, для шаблонов функций реализован механизм полной спе- циализации. Частичная специализация для функций-шаблонов запрещена. На- пример, функция тах() с параметрами-указателями на символы, шаблон и спе- циализация шаблона с параметрами-указателями выглядят так: char *max(const char *x. const char *y) { return (strcmp(x.y) > 0)?x:y: } // функция tempiate<typename T> // шаблон T const& max(T const& x. T const& y) { return (x > y)?x:y: } tempiate<> // специализация шаблона char* const& max(char* const& x, char* const& y) { return (strcmp(x.y) > 0)?x:y; } Специализация начинается с ключевого слова tempi ate с пустыми угловыми скоб- ками. Вообще говоря, для полной специализации требуется задавать специали- зирующие аргументы после имени шаблона: tempiate<> // специализация шаблона char* const& max<char*>(char* const& x, char* const& y) { return (strcmp(x.y) > 0)?x:y; }
Краткие сведения по теме 121 Но для шаблонов функций это практически всегда необязательно, поскольку ком- пилятор способен выяснить тип самостоятельно. Параметры по умолчанию В шаблонах функций нельзя задавать параметры по умолчанию. Однако это ограничение легко обойти, используя класс-обертпку. В листинге 6.5 такая функ- ция является статическим методом класса. Листинг 6.5. Класс-обертка шаблона функции template <class RT= double, class T = float> class Function { public: static RT Multiply!!- const *begin. T const *end) { RT total = 1; while (begin != end) { total *= *begin; ++begin: } return total; } }; Вызов функции должен сопровождаться префиксом класса-шаблона. float f[10]= {1.1,2,3,4.5.6.7.8.9.10.1}; cout « Function»: :Multiply(f, f+10) « endl; int L[10]= {1,2,3,4,5,6,7,8,9.10}; cout « Functional oat, int>: ;Multiply(L, L+10) « end Обобщенные алгоритмы и функторы Обобщенным алгоритмом называется шаблон функции, который способен выпол- нять заданную операцию с последовательным контейнером любого вида. Пара- метрами обобщенного алгоритма, как правило, являются итераторы входного (и выходного) контейнера. Примером обобщенного алгоритма, обрабатывающего входной контейнер «по месту», может служить функция сортировки, которая может иметь следующий прототип tempiate<class Iterator> void sortdterator first. Iterator last): Примером алгоритма, перебирающего входной контейнер и записывающего ре- зультаты в выходной, может служить алгоритм копирования. Прототип алгорит- ма примерно следующий: tempiate<class Inputiterator, class Outputlterator> Outputiterator copy! Inputiterator first. Inputiterator last, Outputiterator result); Итераторы с типом Inputiterator определяют интервал обрабатываемых элементов входного контейнера, а итератор типа Outputiterator является начальным итера-
122 Глава 6. Шаблоны тором выходного контейнера. Как правило, выходной контейнер должен сущест- вовать к моменту вызова функции и иметь достаточный размер, чтобы вместить все элементы входного контейнера. Часто параметром алгоритма является функтор. Функтор представляет собой объект, ведущий себя как функция. Класс, в котором перегружена операция operator О вызова функции, называется классом-функтором. Объект такого клас- са называют объектом-функцией, или функтором. Например, функтор, определяющий нечетность своего аргумента, пишется так: class Odd { public: Int operatorO (const int& d) { return (d£2): } }: Вызов данного функтора выглядит как вызов обычной функции: Odd f; f(5) Функтор, возвращающий булевский результат, называется функтором-предика- том. Функтор с одним аргументом называется унарным, функтором', функтор с двумя аргументами — бинарным. Класс-функтор является обычным классом, поэтому в нем могут быть опреде- лены любые поля, конструкторы, методы. Любые методы, в том числе и пере- груженные операции вызова функции (их может быть несколько), могут быть объявлены виртуальными. Класс-функтор вправе участвовать в иерархии насле- дования, вправе запрашивать динамическую память, может быть абстрактным. Пример класса-функтора с полями и конструктором: class GreaterEqual { Int value: public: GreaterEqual(const int &v): value(v) {} bool operatorO (const int& d) { return (d >= value); } }: При наличии конструктора инициализации вызов функтора можно представить как вызов конструктора, например: Greater(3) Обычно функтор оформляют в виде шаблона класса, например: template <class Т> class GreaterEqual { Т value: public: GreaterEqual(const T &v): value(v) {} bool operatorO(const T& d) { return (d >= value): } }:
Упражнения 123 Шаблон класса-функтора — это еще один способ реализации шаблона функции, к которой разрешается применять все «прелести» шаблонов классов. Примером обобщенного алгоритма, использующего функтор, может служить ал- горитм for eachO со следующим прототипом: tempiate<class Iterator, class Function> void for_each(Iterator first, Iterator last. Function f); Алгоритм применяет функтор f к каждому элементу контейнера из заданного интервала. Реализация обобщенного алгоритма с функтором показана в листин- ге 6.6. Листинг 6.6. Обобщенный алгоритм copy_if() с функтором template < class Inputiterator, class Outputiterator, class Predicate void copy_if( Inputiterator first. Inputiterator last. Outputiterator result. Predicate Functor ) { for ( ; first != last; ++first) if (Functor(*first)) // вызов функтора { *result = *first; ++result; } return; } Данный алгоритм копирует элементы, удовлетворяющие предикату Functor, из заданного интервала входного контейнера в выходной. Этот алгоритм с приве- денными выше функторами может пригодиться для следующих задач: int а[10] = { 1,2,3.4.5,б,7,8,9,0}: int b[10] ={0}: copy_if(a, а+10, b, OddO); Odd f; copy_1f(a, a+10, b. f); GreaterEqual five(5); copy_if(a, a+10. b, five); copy_if(a. a+10, b, GreaterEqual(6)); copy_if(a, a+10. b, GreaterEqual<1nt>(4)); Упражнения Далее предлагается реализовать шаблоны классов и шаблоны функций. Для де- монстрации работы с шаблонами во всех заданиях требуется написать главную функцию.
124 Глава 6. Шаблоны Шаблоны классов В следующих задачах нужно реализовать дружественные операции ввода/выво- да двумя способами: как внешние функции-шаблоны и как «нешаблонные», оп- ределенные внутри класса. Во всех заданиях задействовать итератор как набор методов класса. В главной функции продемонстрировать не менее двух инстан- цирований шаблона с разными аргументами и вызов всех методов. 1. Взяв за образец шаблон Array (см. листинг 6.1), реализовать шаблон массива с задаваемыми пределами индексов. 2. Взяв за образец шаблон Array (см. листинг 6.1), реализовать шаблон числово- го массива фиксированной длины (см. задание 5.23). 3. Реализовать шаблон Array как класс-шаблон числового массива с задаваемы- ми пределами индексов. 4. Реализовать динамический числовой массив фиксированной длины как класс- шаблон (см. задание 5.27). 5. Реализовать растущий массив (см. листинг 5.3) в виде класса-шаблона. 6. Разработать шаблон числового растущего массива (см. листинг 5.3), добавив все арифметические операции и операции проверки на равенство и неравен- ство. 7. Реализовать шаблон стека в виде растущего массива (см. листинг 5.3). 8. Разработать шаблон «гибкого» массива, взяв за образец динамический расту- щий массив (см. листинг 5.3). Добавление и удаление элементов выполнять только по одному в произвольном месте. 9. Разработать шаблон числового «гибкого» массива, взяв за образец динамиче- ский растущий массив (см. листинг 5.3). Реализовать все арифметические операции и операции проверки на равенство и неравенство. 10. Реализовать шаблон очереди в виде «гибкого» массива, взяв за образец дина- мический растущий массив (см. листинг 5.3). Добавление и удаление элемен- тов выполнять только по одному; добавление — в конце массива, удаление — в начале. И. Реализовать шаблон двусторонней очереди в виде «гибкого» массива, взяв за образец динамический растущий массив (см. листинг 5.3). Добавление и уда- ление элементов выполнять только по одному в начале и в конце. 12. Разработать шаблон «гибкого» массива, реализовав групповые добавление, удаление и замену элементов. Группа представляется диапазоном итераторов. Реализовать методы поиска элементов и групп элементов. 13. Реализовать шаблон растущего массива (см. листинг 5.3) как двусвязный спи- сок. В шаблоне должны быть лишь методы, представленные в листинге 5.3. Операции доступа по индексу заменить итератором. 14. Реализовать шаблон числового растущего массива (см. задание 6) как дву- связный список. Операции доступа по индексу заменить итератором. 15. Разработать шаблон списка. Добавление и удаление элементов выполняется только по одному в произвольном месте списка — по итератору.
Упражнения 125 16. Разработать шаблон списка, поддерживающий групповые добавление, удале- ние и замену элементов. Группа представляется диапазоном итераторов. Реа- лизовать методы поиска элементов и групп элементов. 17. Реализовать стек в виде шаблона односвязного списка (см. листинг 5.6). 18. Реализовать очередь в виде шаблона двусвязного списка (см. листинг 5.4). 19. Реализовать двустороннюю очередь в виде шаблона двусвязного списка (см. листинг 5.4). 20. Реализовать шаблон самоорганизующегося списка. Основной операцией яв- ляется операция поиска. При нахождении элемента он открепляется со сво- его места и вставляется в начало списка. 21. Реализовать класс Set (см. задание 5.64) в виде шаблона. 22. Разработать шаблон списка. Добавление и удаление элементов выполнять только по одному в произвольном месте списка — по итератору. Поддержать методы простого слияния и слияния упорядоченных списков. 23. Разработать шаблон списка, реализовав групповые добавление, удаление и за- мену элементов. Группа представляется диапазоном итераторов. Реализовать методы поиска элементов и групп элементов. Добавить методы комбинирова- ния списков как объединение и пересечение множеств. 24. Реализовать шаблон стека, определив параметр-шаблон. В качестве аргумен- та использовать шаблон растущего массива (задание 5) и шаблон двусторон- ней очереди (задание 11). 25. 'Реализовать шаблон очереди, определив параметр-шаблон. В качестве аргу- мента использовать шаблон двусторонней очереди (задание 19) и шаблон списка (задание 15). 26. Реализовать шаблон стека, определив параметр-шаблон. В качестве аргумен- та использовать шаблон очереди (задание 10) и шаблон двусторонней очере- ди (задание 19). 27. Реализовать шаблон множества, определив параметр-шаблон. В качестве ар- гумента использовать шаблон «гибкого» массива (задание 12) и шаблон спи- ска (задание 16). 28. Реализовать шаблон очереди, определив параметр-шаблон. В качестве аргу- мента использовать шаблон «гибкого» массива (задание 8) и шаблон списка (задание 15). 29. Реализовать шаблон двусторонней очереди, определив параметр-шаблон. В ка- честве аргумента использовать шаблон «гибкого» массива (задание 8) и шаб- лон списка (задание 15). 30. Реализовать шаблон очереди, определив параметр-шаблон. Аргумент — шаб- лон «гибкого» массива (задание 12) и шаблон списка (задание 16). Выполнить задания 1-30, определив итератор как вложенный класс. Выполнить задания 1-30, добавив конструктор копирования и операцию при- сваивания в виде шаблонов.
126 Глава 6. Шаблоны Выполнить задания 5.21-5.30, объявив в реализуемом классе массив как поле- шаблон Array (см. листинг 6.1). Реализовать классы в заданиях 5.21-5.30 как специализации шаблона Array (см. листинг 6.1). Выполнить задания 5.21-5.30, определив классы как специализации шаблона числового массива фиксированной длины (см. задание 4). Выполнить задания 5.21-5.30, объявив классы как наследники шаблона Array (см. листинг 6.1). Реализовать классы в заданиях 5.21-5.30 как наследники шаблона числового массива фиксированной длины (см. задание 4). Оформить классы в заданиях 5.31-5.45 как специализации шаблона растущего массива, реализованного в виде списка (см. задание 14). Оформить классы в заданиях 5.31-5.45 как шаблоны-наследники, опираясь на шаблон растущего массива, реализованного в виде списка (см. задание 14). Выполнить задания 5.31-5.45, представив классы как шаблоны-наследники. В качестве базового требуется использовать шаблон растущего массива (см. за- дание 5). Реализовать динамический числовой массив фиксированной длины (см. зада- ние 5.27) как абстрактный класс-шаблон, объявив арифметические операции чистыми виртуальными функциями. Реализовать задачи 5.31-5.43 как шабло- ны-наследники. Используя шаблон «гибкого» массива в качестве базового, выполнить задания 5.46-5.65, представив классы как наследники шаблона. Методы объединения реализовать в наследниках. Разработать шаблон «гибкого» массива, реализовав групповые добавление, уда- ление и замену элементов. Группа представляется диапазоном итераторов. Реа- лизовать методы объединения массива как для множества. Выполнить задания 5.46-5.65. Выполнить задания 5.46-5.65, добавив наследование от шаблона с подсчетом объектов (см. листинг 6.4). Используя шаблон множества, выполнить задания 5.46-5.63 с помощью компо- зиции, объявив в классе поле-множество нужного типа. Используя шаблон множества в качестве базового, реализовать задания 5.46-5.63 как классы-наследники. Шаблоны функций, алгоритмы и функторы Выполнить упражнения 5.1-5.20, оформив функции как шаблоны. Тип и размер массива-параметра передаются как параметры шаблона «class Т, int п>. Выполнить задания 5.1-5.20, оформив функцию в виде класса-обертки (см. лис- тинг 6.5). Определить третий параметр шаблона Т к со значением по умолчанию. Параметр к прибавлять к каждому элементу массива.
Упражнения 127 Выполнить задания 5.1-5.20, оформив функцию в виде класса-обертки (см. лис- тинг 6.5). Определить третий параметр шаблона Т к со значением по умолчанию. Четвертым параметром шаблона задать функтор, выполняющий одну из следую- щих операций с элементом массива и значением к: П1. Прибавление к и деление на к. П2. Умножение на к и вычитание к. ПЗ. Деление на к и сложение с к. П4. Сложение и деление пополам. П5. Корень квадратный из абсолютного значения суммы элемента массива и к. П6. Логарифм абсолютного значения суммы элемента массива и к. П7. Синус суммы элемента массива и к. П8. Экспонента частного при делении к на элемент массива. П9. Логарифм абсолютного значения произведения элемента массива и к. П10. Экспонента разности к и элемента массива. ПИ. Корень квадратный из абсолютного значения разности элемента массива и к. 1112. Элемент массива в степени к. П13. Возведение к в степень «значение элемента массива». П14. Корень квадратный из абсолютного значения степенной функции с осно- ванием «значение элемента массива» и степенью к. П15. Косинус произведения значения элемента массива и к. П16. Экспонента произведения к и элемента массива. П17. Прибавление (1 - к) и деление на это выражение. П18. Деление на к и вычитание (1 - к). П19. Логарифм абсолютного значения «элемент массива в степени к». П20. Умножение на (1 - к) и взятие синуса. Выполнить задания 5.1-5.20, оформив функции как обобщенные алгоритмы, по- лучающие в качестве параметров итераторы и функторы (см. задания 1-20). Для демонстрации работы алгоритма использовать шаблоны динамических массивов и шаблоны списков. Реализовать не менее двух разных функторов. В следующих заданиях разработать обобщенные алгоритмы для оперирования контейнерами по образцу алгоритма copy ! f () (листинг 6.6). Параметрами долж- ны являться итераторы и функторы. Для демонстрации работы алгоритма ис- пользовать шаблоны динамических массивов и шаблоны списков. Должно быть не менее двух разных функторов. В тех алгоритмах, которые не изменяют коли- чество элементов контейнера, использовать в качестве аргументов массивы. 31. Реализовать алгоритмы mi n_el ement О и mi n_el ement i f (). 32. Реализовать алгоритмы count!) и count !fО. 33. Реализовать алгоритм копирования copy!) входного контейнера в выходной и алгоритм копирования в обратном порядке copyback(). 34. Реализовать алгоритмы удаления erase!) и erase !fО.
128 Глава 6. Шаблоны 35. Реализовать алгоритмы поиска searchO и search ifO первого вхождения по- следовательности элементов в другую последовательность. 36. Реализовать алгоритмы поиска search_end() и search end i f (-) последнего вхо- ждения последовательности элементов в другую. 37. Реализовать алгоритмы сравнения последовательностей equal О и equal_if(). 38. Реализовать алгоритмы удаления erase copyO и erase copy_if(). Удаляемые последовательности копируются в выходной контейнер. 39. Реализовать алгоритмы сортировки sortO и sort_if(). 40. Реализовать алгоритмы mergeO и mergeifO слияния сортированных последо- вательностей. 41. Реализовать алгоритмы set orO и set or ifO объединения последовательно- стей как множеств. 42. Реализовать алгоритмы set andO и set_and_if() пересечения последователь- ностей как множеств. 43. Реализовать алгоритмы set xorO и set_xor_if() симметричного вычитания последовательностей как множеств. 44. Реализовать алгоритмы replасе_сору() и repl ace copy i f () замены одной по- следовательности на другую. Результат записывается в выходную последова- тельность. 45. Разработать алгоритм for eachO, применяющий заданный функтор к каждо- му элементу последовательности. Реализовать алгоритм generate О, приме- няющий заданный функтор для заполнения элементов контейнера. 46. Реализовать алгоритмы accumulate О и accumulate if О, выполняющие накоп- ление элементов последовательности. Способ накопления определяется функ- тором, передаваемым в качестве аргумента. 47. Реализовать алгоритмы binoperateO и binoperate_if(), выполняющие задан- ную функтором операцию с двумя интервалами и записывающие результат в выходную последовательность. 48. Реализовать алгоритмы unique copyO и unique copy ifO удаления повторяю- щихся элементов из последовательности. Результат записывается в выход- ную последовательность. 49. Реализовать алгоритмы reverse_copy() и reverse copy ifO обращения элемен- тов последовательности. Результат записывается в выходную последователь- ность. 50. Реализовать алгоритмы сортировки sort copyO и sort copy ifO. Результат за- писывается в выходную последовательность.
ГЛАВА 7 Многомодульные программы Целью изучения данной темы является ознакомление с техникой организации многомодульных программ. Рассматривается разделение класса на определение и реализацию, в том числе особенности организации многомодульных программ с использованием шаблонов. Изучаются некоторые приемы повышения незави- симости модулей — делегирование. Рассматриваются пространства имен. Краткие сведения по теме Большие программы требуется разбивать на части. Отдельная часть большой программы называется модулем. В C++ отсутствуют конструкции для обозначе- ния модуля. В стандарте [1] определено понятие единицы трансляции. Единица трансляции — это отдельный файл с исходным текстом на C++, который получа- ется после обработки препроцессором. Разбиение программы на отдельные модули требует последующей сборки. В C++ отсутствуют и конструкции для сборки модулей в исполняемую программу — это делается средствами интегрированной среды. В интегрированной среде созда- ется проект (см. приложение В), в составе которого перечисляются все модули. Сборка осуществляется одним из двух способов. 1. Объединяются исходные тексты. 2. Программа собирается из объектных модулей; объектный модуль — это ре- зультат компиляции одной единицы трансляции исходного текста. В C++ объединение исходных текстов делается с помощью препроцессора. Не- достаток — компиляция программы может занимать много времени. Второй способ был придуман для того, чтобы избежать компиляции всей про- граммы целиком. Такой способ называется раздельной трансляцией. Процесс сборки полной программы из объектных модулей называется компоновкой, и вы- полняет эту работу программа-компоновщик, которая входит в состав системы программирования. Эта программа часто называется «линкером» (от английско- го слова linker), что, собственно, и подразумевает «сборщика».
130 Глава 7. Многомодульные программы При компоновке в программу собираются не только разработанные модули, но и стандартные. Стандартные модули не транслируются вместе с программой — они поставляются вместе с системой в виде объектных модулей и объектных библиотек. Разделение программ на модули требует согласования определений и объявле- ний в разных единицах трансляции. Во всех единицах трансляции должны быть согласованы объявления и определения классов, функций, переменных и кон- стант, перечислений, шаблонов и пространств имен. Сборка исходных текстов Разработанный класс сохраняется в отдельном файле имя_файла.срр. Например, реализацию стека (см. листинг 5.6) можно назвать именем Stack, срр. Другой мо- дуль-файл с именем main.срр может содержать код программы-клиента (лис- тинг 7.1), использующей стек. При создании проекта средствами интегрирован- ной среды файл main.срр должен быть единственным в проекте1. Файл Stack.cpp должен включать в себя следующую конструкцию: #1fndef _STACK #define _STACK class Stack { }: #endif /* STACK */ 11 "страж" определен? // определение "стража" 11 определение класса // конец #1fndef Но первая строка может быть записана по-другому, #if !defined(_STACK) // "страж" определен? что эквивалентно предыдущему варианту. Такая последовательность команд препроцессора называется «стражем* включения и предназначена для предот- вращения повторного включения содержимого файла в программу. Листинг 7.1. Содержимое файла main.срр #include "Stack.cpp" #include <1ostream> using namespace std: int mainO { Stack st: st.push(7): st.push(6): st.push(34): while (Ist.emptyO) { cout « st.topO « endl; st.popO; } } // файл с реализацией стека // системный заголовок // стандартное пространство имен И кладем в стек числа И пока стек не пустой И выводим число с вершины И удаляем элемент из стека 1 Visual Studio.NET 2003.
Краткие сведения по теме 131 Объединение модулей в единую программу осуществляется препроцессором по директиве включения: #include имя_файла Имя файла задается либо в виде "файл", либо в виде <файл>. В первом варианте предполагается, что модуль-файл должен находиться в текущем каталоге — там же, где находится файл main.cpp. Можно поместить подключаемый файл в лю- бой каталог, но тогда нужно указывать полное имя, например "с: WP1 terWchOlWStack. срр" или "с:/Piter/chOl/Stack.срр" Вторая форма <файл> применяется для включения стандартных библиотечных файлов. Файл ищется препроцессором в стандартном каталоге интегрированной среды \include. В этот каталог можно помещать и собственные файлы-модули. Имя файла нужно указывать абсолютно точно. Пробелы внутри угловых скобок или внутри кавычек — значимые, поэтому без необходимости дополнительные пробелы писать нельзя. Имена стандартных файлов-заголовков рекомендуется писать без расширения .h. Чтобы предупредить возможные конфликты имен, заголовки библиотек С следу- ет начинать с символа «с». В составе C++ имеется 32 системных файла-заголовка: <algorithm> <1omanlp> <1ist> <queue> <streambuf> <bitset> <ios> <locale> <set> <string> <complex> <1osfwd> <map> <sstream> <typei nfo> <deque> <iostream> <memory> <stack> <ut111ty> <exception> <1 stream> <new> <stdexcept> <valarray> <fstream> <iterator> <numeric> <strstream> <vector> <functional> <11mlts> <ostream> и 18 стандартных файлов- заголовков, унаследованных от С: <cassert> <ciso646> <csetjmp> <cstdio> <ctime> <cctype> <climi ts> <csignal> <cstdl1b> <cwchar> <cerrno> <clocale> <cstdarg> <cstr1ng> <cwctype> <cfloat> <cmath> <cstddef> Отделение интерфейса от реализации Обычно интерфейс класса (который является его определением) отделяется от его реализации. Интерфейс класса и определения статических полей помещают- ся в файл-заголовок имя_файла.й (заголовок, «шапка» — от английского header); «стража» включения нужно задать в заголовке. Реализация методов класса вы- носится в отдельный файл имя_файла.срр. К другим модулям, которые исполь- зуют данный класс, подключается только заголовок; заголовок подключается и к файлу с реализацией методов. /* TClass.h, файл с интерфейсом класса */ // необходимые системные заголовки #1fndef _STRAG • 11 "страж" определен? #define _STRAG // определение "стража"
132 Глава 7. Многомодульные программы class TClass { // поля и прототипы методов #endif /* _STRAG */ // конец ifndef // определения статических полей /* TClass.срр, файл с реализацией класса */ // необходимые системные заголовки #indude "TClass.h” // реализация методов внешним способом В проект интегрированной среды включаются файл реализации и файл с про- граммой-клиентом. Файл-заголовок в состав проекта обычно не входит. ! Разделение класса на интерфейс и реализацию не мешает наследованию. Класс- наследник тоже делится на интерфейс-заголовок и реализацию; заголовок базо- вого класса подключается к заголовку класса-наследника. /* Base.h, файл с интерфейсом базового класса */ // необходимые системные заголовки #ifndef _STRAGBASE #define _STRAGBASE class Base { // поля и прототипы методов // "страж" определен? // определение "стража" #endif /* _STRAGBASE */ // конец ifndef // определения статических полей /* Base.cpp, файл с реализацией базового класса */ // необходимые системные заголовки #include "Base.h" // реализация методов внешним способом /* Derive.h, файл с интерфейсом класса-наследника */ // необходимые системные заголовки #include "Base.h" #ifndef _STRAGDERIVE #define _STRAGDERIVE class Derive: public Base { // поля и прототипы методов // для наследования! // "страж" определен? // определение "стража" j #endif /* _STRAGDERIVE */ // конец ifndef // определения статических полей /* Derive.срр, файл с реализацией класса-наследника */ // необходимые системные заголовки #include "Derive.h" // реализация методов внешним способом В проект включаются файлы реализации базового класса, производного и код программы-клиента. Шаблоны и модульность Шаблоны нельзя делить на интерфейс и реализацию и транслировать отдельно. Шаблон представляет собой только заготовку для построения кода: пока шаблон не инстанцирован («конкретизирован конкретным типом»), объектный код из него не транслируется, и сборщику нечего делать. Модули, использующие шаблон,
Краткие сведения по теме 133 должны подключить файл с полным определением шаблона посредством #include. Такой способ организации кода с шаблонами в стандарте [1] называется моделью включения. Явное инстанцирование Второй вариант организации кодов с шаблонами — модель явного инстанцирова- ния. Этот способ работает и для шаблонов функций, и для шаблонов классов. В этом случае шаблоны инстанцируются (то есть реализуются, конкретизиру- ются) с помощью директивы явного инстанцирования. Здесь класс-шаблон, как и обычный класс, делится на интерфейс и реализацию, и в проект включаются следующие файлы: □ файл с определением интерфейса класса-шаблона; □ файл с реализацией класса-шаблона; □ файл с директивами явного инстанцирования; □ файл с программой-клиентом. В качестве примера приведем файлы для шаблона Array (см. листинг 6.1). /* Array.h, интерфейс */ tempiate<class T. std::size_t N> // параметры шаблона class Array { public: // типы typedef T value_type; typedef T& reference: typedef const T& const_reference; typedef std::si ze_t s i ze_type; static const size_type static_size = N; // размер массива Array(const T &t= TO): // конструктор size_type sized const: // получение размера reference operator[](const size_type& i): // доступ к элементам const_reference operator[](const size_type& i) const: private: void rangecheck (const size_type& 1) const: // проверка индекса T elem[NJ: // поле-массив } /* ImplArray.h, реализация */ #include <stdexcept> // для исключения #include "InstTemp.h" // подключили интерфейс tempiate<typename T, std::size_t N> // реализация конструктора Array<T,N>: .-Array(const T &t) { for (int i = 0: i<N: i++) elemfi] - t: } tempiate<typename T, std::size_t N> // получение размера typename Array<T,N>::size_type Array<T,N>::size() const { return static_size: } tempiate<typename T. std::size_t N> // доступ к элементам typename Array<T,N>::reference Array<T,N>::operator[](const size_type& i) { rangecheck(i); return elemfi]; } tempiate<typename T. std::size_t N> typename Array<T,N>::const_reference Array<T,N>::operator[](const size_type& 1) const
134 Глава 7. Многомодульные программы { rangecheck(i): return elem[i]; }; tempiate<typename Т, std::size_t N> // проверка индекса void Array<T,N>::rangecheck (const size_type& i) const { if (i >= sized) { throw std::range_error("Array - range!"): } } /* DefArray.h, явное инстанцирование */ #include "ImplArray.h" #include <string> using std::string: template class Array<double, 10>: template class Array<string, 10>: /* Client.срр. программа-клиент */ #include "InstTemp.h" int maind { Array<double, 10> A; Array<string, 10> B; return 0: } // подключается реализация! // директива инстанцирования // директива инстанцирования // подключаем интерфейс! // аргументы совпадают с директивой // явного инстанцирования Директива явного инстанцирования начинается обязательным словом template, после которого следует объявление инстанцированного класса-шаблона. В систе- ме Visual C++.NET 2003 директивы явного инстанцирования можно писать без слова cl ass, например template Array<double,10>: Это является ошибкой, так как по стандарту слово cl ass пропускать нельзя. Следует обратить внимание на такие моменты: □ файл с интерфейсом шаблона включается дважды: в файле реализации и в программе-клиенте — поэтому в нем нужно поселить «стража»; этот файл разрешается в проект не включать, так как он подключается препроцессором; □ файл с реализацией подключается в файле с директивами инстанцирования, поэтому в проект его тоже можно не включать (хотя обычно это не так); □ объявление объекта в директиве инстанцирования и объявление того же объ- екта в программе-клиенте должны быть абсолютно идентичны. Модель явного инстанцирования для шаблонов функций позволяет задавать в программе-клиенте только прототип функции-шаблона, не подключая весь шаблон. В проект включаются три файла: □ файл с шаблоном функции; □ файл с программой-клиентом, где задается прототип функции-шаблона; □ файл с набором директив явного инстанцирования. В качестве примера приведем файлы для шаблона функции Mind: /* Tempi ate.h. шаблон функции */ template <typename T> T Min(T const *begin. T const *end) { T min = *begin:
Краткие сведения по теме 135 while (begin != end) if (min > *(++begin)) min = *begin; return min; } /* Templnst.h. директивы инстанцирования */ #include "Tempi ate.h" // подключение шаблона // набор директив явного инстанцирования template double Min<double>(double const *begin, double const *end): template int M1n<int>(int const *begin, int const *end); /* Client.срр, программа-клиент */ #include <iostream> using namespace std: template <typename T> // прототип шаблона функции T Min(T const *begin. T const *end); int mainO { int a[10]= {1,2,3.4,5.6.7.8.9,10}; cout « Min(a, a+10) « endl; double b[10]= {1.1,2,3.4,5.6,7,8.9.10.1}; cout « M1n(b, b+10) « endl; return 0: } И для функций директива явного инстанцирования должна начинаться с ключе- вого слова template, за которым следует инстанцированный прототип функции- шаблона с явным заданием аргумента шаблона. В программе-клиенте два вызова функции Min(), поэтому в файле с директивами инстанцирования — две директи- вы с аргументами тех типов, которые используются при вызове функции Min(). В программе-клиенте не подключаются ни шаблон, ни файл с директивами явного инстанцирования — задается только прототип функции-шаблона. Можно сочетать модель явного инстанцирования и модель включения. Напри- мер, можно подключить файл явного инстанцирования в программе-клиенте #include "Templnst.h" В стандарте [1] также определен механизм экспорта шаблонов, который обеспе- чивает организацию файлов с шаблонами, известную как модель разделения. Од- нако на момент написания книги ни Visual C++.NET (2003), ни C++ Builder (6) не поддерживают эту модель. Разделение определения и реализации. Делегирование Для повышения степени инкапсуляции классов используется идиома конверт/ письмо [8]. Еще одно название — паттерн проектирования [13] «мост» (bridge). Обычное разделение класса выглядит так («стражи» не указаны): /* TCI ass.h, интерфейс класса */ class TClass { // открытые и защищенные члены класса private: // закрытые члены класса // при внесении изменений - клиентский код нужно перекомпилировать
136 Глава 7. Многомодульные программы // определения статических полей /* TClass.cpp. реализация */ // необходимые системные заголовки #include "TClass.h" // реализация методов внешним способом При обычном разделении внесение изменений в закрытую часть класса влечет за собой перекомпиляцию клиентского кода. Для предотвращения этого служит ука- занная идиома. Исходное определение класса заменяется определением с указа- телем. Этот класс называется классом-конвертом (классом-оболочкой). Клиенту предоставляется интерфейс класса-конверта. Класс-конверт обычным образом делится на определение и реализацию. Тип указателя определяется классом-письмом (классом-телом). Класс-письмо реализует функциональность класса-конверта. Методы класса-конверта обраща- ются к методам класса-письма, «переадресуй» выполнение работы. Этот прием называется делегированием. Класс-письмо обычным образом делится на опреде- ление и реализацию. И тогда разделение классов выглядит так: /* TClass.h, определение класса-конверта */ class Implementation: // предварительно объявленный // класс-тело class TClass { // открытые и защищенные члены класса private: Implementation *pi; // указатель на класс-тело // определения статических полей /* TClass.cpp. реализация методов класса-конверта */ // необходимые системные заголовки #1nclude "TClass.h” // реализация методов внешним способом /* Implementation.!). определение класса-письма */ class Implementation { // внесение изменений не отражается на классе-конверте } II определения статических полей /* Implementation.срр. реализация класса-письма */ // необходимые системные заголовки #include "Implementation.!)” // реализация методов внешним способом При такой структуризации классов любые изменения в классе-письме не отра- жаются на клиентском коде, если не изменяется исходный интерфейс класса- конверта. Файлы с реализацией включаются в проект. В файле-интерфейсе класса-конверта указано предварительное объявление класса-письма. Предварительное объявление класса вводит имя класса в область видимости, и это позволяет объявить указатель в приватной части интерфейса
Краткие сведения по теме 137 класса-конверта и переадресовать работу методам класса-письма. Конструктор, деструктор и методы класса-конверта выглядят так: // конструктор создает динамический объект-письмо TClass:: TCI ass (): pi (new ImplementationO) {}; // деструктор возвращает память TClass::~TClass(){ delete pi: } // методы класса-интерфейса с косвенными обращениями тип TClass::метод(параметры) { pi->Implementation::метод(параметры): } Универсальное делегирование Между классами TClass и Implementation установлено отношение: класс реализо- ван посредством класса. Делегирующий полномочия класс TCI ass использует для реализации своей функциональности методы класса-порученца Implementation. Делегирование можно реализовать в более общей форме [8], перегрузив опера- цию доступа по указателю operator-* в делегирующем классе (листинг 7.2). Листинг 7.2. Делегирование с перегрузкой операции operator-> class Implementation // класс-порученец { // ... public: Implementation!){} // конструктор viod MethodA(void): viod MethodB(void): // ... class TClass // класс-поручитель { Implementation *delegate; // для делегирования public: TClass!) { delegate = new Implementation 0: } viod MethodD(void): // собственный метод Implementation* operator-*!) { return delegate: } // ... }: Такая реализация позволяет использовать методы класса Implementation, вызы- вая их с помощью операции доступа по указателю: Derived D: D.MethodDO: // вызов собственного метода D->MethodA(); // вызов «базового» метода D->MethodB(): // вызов «базового» метода Пространства имен Деление программы на файлы-модули — это физическое разделение программы на части. Чтобы разделить большую программу на логически связанные части, в C++ предусмотрены пространства имен. В стандарте [1] определено стан- дартное пространство имен std.
138 Глава 7. Многомодульные программы Пространство имен может иметь имя. Объявление пространства имен — это на- значение имени для области, в которой будут видны компоненты пространства имен. Синтаксис именованного объявления выглядит так: namespace имя { // объявления и определения } Идентификатор namespace является зарезервированным словом. Пространство имен может содержать объявления и определения переменных, функций, клас- сов, типов, шаблонов и т. д. Эти имена считаются членами данного пространства имен. Определения должны быть в единственном числе. В пространство имен можно включать и заголовочные файлы, например: namespace SPACE { #include “Stack.h" } Доступ к элементам пространства имен (в той же единице трансляции, но вне его, или в другой единице трансляции) выполняется при помощи операции раз- решения контекста (::). Идентификатор пространства имен служит квалифика- тором для имени компонента: «пространство имен>::<имя компонента> Например, имена из стандартного пространства имен можно писать так: std::cin std::cerr Чтобы не сопровождать имена квалификаторами, можно использовать using- объявления using «пространство имен>::«имя компонентам Слово using, так же как и namespace, является зарезервированным словом. В даль- нейшем объявленные имена можно использовать в программе без квалификато- ра. Для стандартных имен из пространства std это выглядит так: using std::cout: using std:.:endl: Можно объявить доступным сразу все пространство имен с помощью using-ди- рективы using namespace «пространство имен>: В дальнейшем все имена из указанного пространства имен можно писать без префикса. Эта директива действует до конца единицы трансляции. На другие файлы ее влияние не распространяется. Для использования любого имени из стандартного пространства имен std нужно задать директиву using namespace std: Эту директиву можно писать всякий раз, когда у вас в программе встречается включение стандартного заголовка из списка, указанного в стандарте [1]. Любую форму using можно использовать внутри некоторого пространства имен с целью включения в него имен из другого пространства. Не используйте гло- бальную директиву using в заголовочных файлах!
Краткие сведения по теме 139 В определениях членов имена из того же пространства имен разрешено исполь- зовать без указания квалификатора — аналогично тому, как в методах класса по- зволено употреблять любые имена из того же класса без префикса. Стандарт C++ позволяет объявлять синонимы имен {псевдонимы), например: namespace MFC = Microsoft_Foundation_Class; Такая декларация используется для переопределения длинных имен, придуман- ных разработчиками библиотек. Пространства имен могут быть вложенными, например: namespace External { // внешнее пространство имен double а: namespace Internal { // вложенное пространство имен void FO { а=7.2; } // работает External ::а int а: void GO { а++; } // работает Internal::а: } } Для обращения к именам из вложенного пространства следует указывать двой- ной префикс-квалификатор, например: External: internal: :FO; Псевдонимы можно назначать и для вложенных пространств имен, например: namespace Borland_Builder { // внешнее /* члены внешнего namespace */ namespace Visual_Component_Library { // вложенное /* члены внутреннего namespace */ } } // Псевдонимы namespace // Внешнее пространство namespace ВВ = Borland_Bui1der; // Внутреннее пространство задается с квалификатором namespace VCL = Borland_Builder:: Visual_Component_Library; Физическое разделение пространства имен При физическом разделении программы на модули пространство имен разбива- ется на части. Но если в разных единицах трансляции объявлено одно и то же пространство имен, оно «склеивается» в единое целое. Именно таким образом определено стандартное пространство имен std. Это свойство «склеивания» пространства имен хорошо подходит для организа- ции библиотеки. Аналогично классам, исходный код разделяется на две части: интерфейс и реализацию. Интерфейсная часть помещается в отдельный заголо- вочный файл, а реализация — в другой файл. В клиентском коде подключается только заголовочный файл. Пример: /* vector.h */ namespace vector { const double pi = 3.141615926:
140 Глава 7. Многомодульные программы class vector { /* ... */ }; vector operators const vector &vl, const vector &v2); double scalar_product (const vector &vl. const vector &v2): double length(const vector &v): } /* vector.cpp */ #include "vector.h” namespace vector { // реализация методов класса vector vector operator+(const vector &vl, const vector &v2) {/*...*/} double scalar_product (const vector &vl, const vector &v2) {/*...*/} double length(const vector &v) {/*...*/} } /* user.срр, клиентский код */ #include "vector.h" // ... void F(vector::vector &v) { // ... double d = vector::length!v); // ... } Такая организация пространства имен аналогична разделению класса на интер- фейс и реализацию. Межмодульные переменные и функции Каждое имя, обозначающее объект, имеет некоторую область видимости (дейст- вия), в которой это имя объявлено и может использоваться. В стандарте C++ [1] определены следующие области видимости: оператор, прототип, блок, функция (только метки), класс, файл, пространство имен. Имена в одной области видимо- сти не должны быть одинаковы, но в разных областях они могут и совпадать. Имена, объявленные вне всех областей видимости, входят в глобальное простран- ство имен. Так, все имена Windows API входят в глобальное пространство имен. Для обращения к глобальному имени используется операция разрешения кон- текста ::, так как по умолчанию (без указания квалификаторов) всегда выбира- ется имя с «наименьшей» областью видимости. В соответствии с правилом одного определения (one-definition rule — ODR), еди- ница трансляции не должна содержать более одного определения любой пере- менной, функции, класса, перечисления и шаблона. Объявлений может быть не- сколько: объявления добавляют некоторое имя в данную область видимости и используются для согласования типов. Переменная, определенная в файле вне всех классов и функций, называется гло- бальной. В другом модуле-файле она должна быть объявлена с ключевым словом extern.
Краткие сведения по теме 141 // Модуль с определением переменной Int Global = 1; // Модуль с объявлением переменной extern int Global: Определение функции включает тело, а объявлением является прототип. Опре- деление задается в одном модуле, а в других можно использовать только прото- тип, например: // файл с определением функции void f(void) { cout « "fO" « endl: } // любой другой файл с прототипом функции void f(void): Слово extern писать не требуется, хотя и не запрещается. Прототипы void f(void): extern void f(void); являются эквивалентными. Имена глобальных переменных и функций обладают врожденным свойством внешнего связывания, то есть видны сборщику во время компоновки программы. Инициализация глобальных объектов Глобальные и статические (как локальные, так и нет) переменные компилятор размещает в статической памяти, и время жизни таких переменных совпадает со временем выполнения программы. Статические переменные инициализируются неявно (по умолчанию) до начала выполнения функции mainO. Эта инициализа- ция называется статической', явная инициализация, задаваемая программистом, называется динамической. Встроенные типы по умолчанию инициализируются нулями. Для глобальных объектов невстроенных типов вызывается конструктор по умолчанию (без аргументов). Если в классе его нет, возникает ошибка транс- ляции. Конструктор инициализации применяется для явной инициализации. В рамках одного модуля порядок инициализации переменных встроенных типов определяется порядком объявления. Конструкторы для создания и инициализа- ции глобального объекта тоже вызываются в порядке объявлений объектов. Де- структоры вызываются перед завершением программы в обратном порядке. Очередность статической инициализации глобальных объектов, размещенных в разных единицах трансляции, стандартом не определена. Локализация имен в модуле Переменные и функции, объявленные в файле с атрибутом static, подвержены внутреннему связыванию и являются локальными в модуле, где определены. Константы обладают свойством внутреннего связывания по умолчанию. В раз- ных файлах можно объявлять глобальные константы с одинаковыми именами. Чтобы сделать константу, объявленную в одном файле, видимой в другом, нуж- но использовать слово extern. // модуль с определением глобальной константы extern const int а = 2;
142 Глава 7. Многомодульные программы // модуль с объявлением той же константы extern const int а; Функции, объявленные как inline, по умолчанию связываются внутренним об- разом: функция, определенная в одном модуле, не видна в другом модуле. Так- же, как и определение класса, определение inline-функции может быть включе- но в программу несколько раз — по одному разу на модуль. Можно сделать inline-функцию глобальной, как и константу, используя в опре- делении extern: // модуль с определением глобальной inline-функции extern Inline void f(void) { cout « a « "f()\n"; } В другом модуле достаточно указать прототип: // модуль с объявлением внешней inline-функции void f(void); К прототипу можно добавить extern и inline: extern inline void f(void); Неименованные пространства имен Для локализации имени в файле вместо атрибута static в C++ разрешается зада- вать анонимные (неименованные) пространства имен: namespace // анонимное пространство имен { // члены локального пространства имен } Анонимные пространства имен являются локальными пространствами для еди- ницы трансляции. Для каждого анонимного пространства компилятор генериру- ет уникальное внутреннее имя, поэтому такие пространства не «склеиваются». Упражнения 1. Реализовать задания главы 2 с разделением на интерфейс и реализацию. 2. Реализовать задания главы 2 с разделением на интерфейс и реализацию, до- бавив подсчет объектов. 3. Реализовать задания главы 2 как многомодульные, разделив каждый класс на интерфейс и реализацию с использованием делегирования. 4. Реализовать задания главы 3 как многомодульные, разделив каждый класс на интерфейс и реализацию. 5. Реализовать задания главы 3, включив базовые классы в пространство имен Library, а наследников — в пространство имен Derive. 6. Реализовать задания главы 4, в которых организуется собственная иерархия исключений. Исключения поместить в отдельное пространство имен Exception, а основные классы — в пространство имен Library.
Упражнения 143 7. Реализовать задания главы 5, разделив каждый класс на интерфейс и реали- зацию. Классы-контейнеры поместить в пространство имен Library. 8. Реализовать задания главы 5, разделив каждый класс на интерфейс и реали- зацию с использованием универсального делегирования. 9. Реализовать задания главы 6, используя модель явного инстанцирования. Шаблоны поместить в пространство имен Library. 10. Реализовать задания главы 6, используя модель явного инстанцирования. Шаблоны поместить в пространство имен Library. Добавить подсчет объектов в «шаблонном» виде (см. листинг 6.4).
ГЛАВА 8 Ввод-вывод Здесь разъясняется техника использования объектно-ориентированной системы ввода/вывода. Рассматриваются стандартные потоки, строковые потоки и фай- ловые потоки, в том числе особенности работы с текстовыми и двоичными фай- лами. Изучаются особенности применения «широких» потоков. Краткие сведения по теме Ввод/вывод в C++ основан на концепции потоков. В стандартной библиотеке <cstdio>, доставшейся по наследству от С, потоки реализованы в процедурном стиле; библиотека < iostream > предоставляет объектно-ориентированную версию потоков. Объектно-ориентированный подход обеспечивает большую надежность и более прост в использовании1. Классификация потоков Поток — это последовательность символов. Так как в C++ реализовано два вида символов (обычные и «широкие»), то и потоки в C++ бывают «узкими» и «ши- рокими». Ввод информации из внешней среды в программу осуществляется из входного потока, вывод программа производит в выходной поток. Потоки могут быть фор- матируемыми и неформатируемыми. Форматируемость означает, что при опера- циях ввода/вывода выполняется форматное преобразование информации: при вводе — из символьного вида в двоичный, а при выводе — наоборот, из двоично- го в символьный. Обычно потоки — буферизуемые: обмен фактически осуществляется не между программой и потоком, а между программой и буфером потока. При чтении сим- волов программа извлекает символы из буфера входного потока, а при записи в выходной поток символы помещаются в буфер выходного потока. Если во время операции чтения окажется, что символы в буфере исчерпаны, в него с устройст- 1 Рассматривается только объектно-ориентированная библиотека, которая описана в стан- дарте [1] в разделе 27, с. 605.
Краткие сведения по теме 145 ва заносится новая порция символов. Если буфер окажется полон, его содержи- мое будет «сброшено» на устройство, а буфер очищен. Потоки C++ бывают трех видов: □ стандартные; □ строковые; □ файловые. Стандартные потоки — только однонаправленные: либо входные (информация читается из потока), либо выходные (информация пишется в поток). Файловые потоки могут быть и однонаправленными, и двунаправленными. Двунаправлен- ный поток является одновременно и входным, и выходным; операции чтения и записи выполняются с одним и тем же потоком. Объектно-ориентированные строковые потоки тоже могут быть как однонаправленными, так и двунаправ- ленными. Стандартные потоки не требуется объявлять в программе: они обозначаются стан- дартными именами, которые нельзя использовать в другом смысле. Эти имена «привязаны» к стандартным устройствам: клавиатуре и экрану; стандартные по- токи можно перенаправить на другие устройства (например, в файл на диске). Потоки других видов требуется объявлять в программе как переменные соответ- ствующего типа. Эти переменные, как и любые другие, имеют время жизни и об- ласть видимости. Переменная файлового потока связывается с файлом на диске. Строковые потоки ни с какими устройствами не связываются. Стандартные потоки — форматируемые. Строковые потоки тоже форматируемые. Файловые потоки могут быть как форматируемыми, так и неформатируемыми. Подключение потоков Для использования стандартных потоков достаточно задать в программе дирек- тиву #include <iostream> Заголовочный файл < iostream > содержит описания классов ввода/вывода и че- тыре стандартных системных объекта1, связанные со стандартными потоками библиотеки <cstdio> и стандартными устройствами: □ cin — объект класса istream, соответствующий стандартному вводу (stdin); по умолчанию связан с клавиатурой; □ cout — объект класса ostream, соответствующий стандартному выводу (stdout); по умолчанию связан с экраном; □ clog — объект класса ostream, соответствующий стандартному выводу для ошибок (stderr); по умолчанию связан с экраном; □ сегг — объект класса ostream, соответствующий стандартному выводу для ошибок (stderr); по умолчанию связан с экраном. 1 Речь идет об «узких» потоках.
146 Глава 8.' Ввод-вывод Объект cout предназначен для «нормального» вывода, а объекты сегг и clog — для вывода сообщений об ошибках. Используются они точно так же, как и cout. Специально объявлять в программе стандартные потоки не требуется. Чтобы использовать строковые потоки, нужно задать в программе директиву #1nclude <sstream> После этого в программе можно объявлять объекты-строковые потоки трех видов: □ входной i stringstream; □ выходной ostri ngstream; □ двунаправленный stringstream. Для использования файловых потоков необходимо включить в программу ди- рективу #include <fstream> Объекты типа fstream связываются с файлами; их можно читать, и в них можно записывать информацию. После этого в программе можно объявлять объекты-файловые потоки трех видов: □ входной i fstream; □ выходной ofstream; □ двунаправленный fstream. Если требуются лишь входные потоки-файлы, достаточно использовать #include <ifstream> Для работы только с выходными файловыми потоками используйте #1nclude <ofstream> Иерархия классов Иерарахия классов объектно-ориентированной библиотеки показана в листин- ге 8.1. Листинг 8.1. Иерархия классов объектно-ориентированной библиотеки ввода/вывода class ios_base {...}: templates. .>class basicjos: public ios_base: templates. .>class basic_istream: virtual public basic_ios<...>: tempiate<..,>class basic_ostream: virtual public basic_ios<...>: tempiate<..,>class basic_1ostream: // множественное наследование public basic_istream<.. public basic_ostream<.. tempiate<...>cl ass basic_fstream: public basic_iostream<...>; template*...>cl ass basic_stringstream: public basic_iostream<...>; template*...>class basicji fstream: public basicjstream*.. tempiate<...>class basic_ofstream: public basic_ostream<...>; template*..,>cl ass basic_istringstream: public basic_istream<...>: template*..,>class basic_ostringstream: public basic_ostream<..
Краткие сведения по теме 147 template*...>class basic_streambuf { ... }; template*..,>class basic_filembuf: public basic_streambuf*...>; template*..,>cl ass basic_stringbuf: public basic_streambuf*...>; Все классы включены в пространство стандартных имен std. Базовым классом является класс ios base. Это не класс-шаблон. В нем определены константы, поля и методы, управляющие состоянием потока и форматированием. Все классы-шаблоны имеют два обязательных параметра — тип символов и класс свойств (трактовок) символов. В пространстве std определены специализации шаблонов для двух типов символов (char — «узкие», wchar t — «широкие»), на- пример: namespace std { typedef basic_1ostream<char> iostream: // узкий поток typedef basic_iostream<wchar_t> wiostream; // широкий поток } Соответственно, специализации шаблонов определяют два вида потоков: широ- кие (wide) и узкие (narrow). Назначение этих определений типов — ввести короткие имена вместо длинных. Операции ввода/вывода Для форматируемых потоков вывод, как правило, осуществляется перегружен- ной операцией сдвига влево operator**, а ввод — перегруженной операцией сдви- га вправо operator». Операции перегружены для всех элементарных встроенных типов. При выводе выполняется преобразование (для несимвольных типов) из внутреннего двоичного формата в символьный вид, при вводе — преобразование из символьного вида во внутренний двоичный формат данных. Вывод данных элементарных типов Вывод выполняется одинаково для всех видов потоков: стандартных, строковых или файловых. Разрешается выводить константы, переменные и выражения, на- пример: stream « 3.4; stream « '\n‘; stream « 3.45/1.23+0.67; stream « '\n’; int a = 10; stream « a; stream « '\n': char ch = 'w'; stream « ch; stream « '\n'; double r = 4.123e-2; stream « r; stream « '\n'; stream « (a-t/r); Так как операция *< является операцией сдвига влево, у нее имеется приоритет. При выводе выражений иногда получаются неожиданные результаты, обуслов- ленные порядком выполнения операций. Пользуйтесь скобками для обеспечения нужного порядка вычислений. Разрешается выводить несколько значений в один объект stream, например: stream <* г « '; ’ « а « '\п';
148 Глава 8. Ввод-вывод Операция « работает и с символьными массивами, и со строками string, на- пример: char s2[] = " символьный массив\п string s3 = " строка символов\п stream « s2 « s3: При работе в системах Visual Studio.NET 2003 и Borland C++ Builder 6 в узкие потоки правильно выводятся только символьные константы английского языка. Массивы в общем случае нужно выводить поэлементно в цикле. Единственное исключение из этого правила — символьный массив. Для вывода всего символь- ного массива достаточно (как показано) задать его имя. При выводе по умолчанию существуют ограничения: □ целые числа всегда отображаются в десятичной системе счисления, даже если заданы как шестнадцатеричные или восьмеричные константы; □ значения типа bool по умолчанию выводятся как целые; true заменяется на 1, a fal se — на 0; □ значения перечислимого типа enum по умолчанию выводятся как целые; □ дробные числа (независимо от типа — float или double) усекаются до шести значащих цифр; □ указатели по умолчанию представляются в шестнадцатеричной системе, неза- висимо от типа (кроме указателей на символы). При выводе указателя на символьную константу требуется преобразование к void*, так как по умолчанию получается строка символов: char *s = "символьная константа\п"; stream « s « « (void *) s « '\n'; Символьные переменные неявно печатаются как символы. Для вывода символь- ной переменной в виде числа нужно задать явное приведение типа: char п = -76: stream « int(n) « ’\n'; Методы вывода символов Символы выводятся в поток без преобразования. Отдельный символ можно по- местить в поток также методом put О, который имеет прототип ostreamS put(char Ch); Например: char ch = 'ц': stream.put(ch); Метод writeO имеет прототип ostreamS write(const char* buffer,int size): Метод записывает в выходной поток содержимое символьного массива buffer. Символы копируются до тех пор, пока не возникнет ошибка или не будет скопиро- вано size символов. Метод writeO не отслеживает завершающий байт строки —
Краткие сведения по теме 149 нулевые байты тоже записываются. Метод writeO обычно используется для вы- вода в двоичные файлы. Метод writeO позволяет вывести один символ: char ch = 'ц': stream.write(&ch,l); Этот же метод дает возможность выполнить вывод символьного массива, напри- мер: char s2[] = "Символьный массив \п"; stream.write(s2, strlen(s2)): Метод writeO не работает с переменными типа string непосредственно, однако в классе string имеется метод c_str(), который возвращает адрес символьного массива, содержащего символы строки: string s = "символьная константа \п stream.writeCs,c_str(). stг1en(s,c_str())); stream.write(s.c_str(), s.lengthO): Методы вывода возвращают ссылку на поток. Допускается соединять их в одно выражение-оператор: stream.write(s2,strlen(s2)).put(‘\n').put('\n'); Ввод данных элементарных типов Ввод значений осуществляется из входного потока в переменные программы. Он выполняется одинаково для всех видов потоков — стандартных, строковых или файловых. stream » а; Ввод нескольких переменных можно объединять в одно выражение-оператор, например: stream » а » b: Ввод в переменную завершается, если очередной символ во входном потоке не соответствует типу вводимого значения. При вводе целых чисел допустимыми являются цифры и знаки плюс («+») и минус («-»). В случае дробных чисел к допустимым символам относятся также точка («.») и латинские буквы «е» и «Е». Числа при вводе должны выглядеть точно так же, как и написанные в программе целые и дробные константы. Стандартными символами-разделителями в потоке являются пробел, знак табу- ляции и литерал ’\п’ (клавиша Enter для стандартного входного потока cin). Не- сколько символов-разделителей подряд воспринимаются как один; начальные символы-разделители пропускаются. Булевские значения должны подаваться как 1 (true) и 0 (false), любые другие символы воспринимаются как ошибочные. Операция operator» по умолчанию не действует для перечислимых типов, по- скольку перечислимый тип определяет новое имя типа. Для ввода данных пере- числимого типа требуется перегрузить операцию ввода.
150 Глава 8. Ввод-вывод Ввод символов и строк Символы читаются из потока без преобразования. При вводе символов операци- ей » символы-разделители пропускаются. Поместить в символьную перемен- ную любой символ (в том числе и символ-разделитель) можно с помощью пере- груженного метода get(); двумя его разновидностями: int_type get(); istreamS getCcharS Ch); Ввод выполняется так: ch = stream.getO; stream.get(ch); Ввод одного символа можно выполнить методом read(), который имеет прототип istreamS read(char* buffer, int size); Первым аргументом метода read О является адрес символьного массива, вто- рым — количество вводимых символов. Ввод выполняется так: stream.read(Sch,1): Операция » позволяет вводить и символьные массивы, и строки типа string, од- нако ввод заканчивается на первом символе-разделителе (обычно пробеле). Для ввода в символьный массив строк с пробелами используются методы get О и getlineO. Метод get() имеет следующие разновидности: istreamS get(char *str. streamsize count): istreamS get(char *str, streamsize count, char delim); Метод извлекает символы из входного потока и помещает их в массив str. Ввод прекращается при возникновении состояния1 eof (комбинация клавиш Ctrl+z для стандартного входного потока), либо по достижению count символов, либо при обнаружении указанного разделителя delim. Сам разделитель остается во входном потоке. В массив str после всех символов заносится нулевой байт. Для ввода строки с пробелами в символьный массив s[50] достаточно написать в программе вызов stream.get(s. 5.0): В конце вводимой последовательности символов должен стоять литерал '\п' (клавиша Enter для стандартного входного потока cin), который остается во входном потоке. Обозначение ’ \п ’ вызывает ввод управляющего символа для за- вершения ввода строки по умолчанию; можно задать любой символ-разделитель для завершения, например точку с запятой. stream.get(s,50,':'); В символьный массив s попадут все символы входного потока до точки с запя- той. Сам символ «;» останется во входном потоке. Для окончания ввода из стан- дартного входного потока требуется нажать клавишу Enter. 1 См. далее раздел «Состояния потока».
Краткие сведения по теме 151 Метод getlineO работает при вводе строк аналогично методу get(), но удаляет из входного потока символ-разделитель (' \п’ по умолчанию). Прототипы следую- щие: istream& getline(char* str, streamsize count): istream& getline(char* str. streamsize count, char delim); Использование getlineO выглядят так: stream.getlineCs,50); stream.getlinets,50.’.'); Методы get О и getlineO не подходят для ввода строк типа string. В библиотеке <string> присутствует собственная функция-шаблон getlineO со следующим прототипом: tempiate<class CharType, class Traits, class Allocator» basic_istream<CharType, Traits>& getline ( basic_i stream<CharType. Trai ts>S, basic_string<CharType, Traits, Allocators, CharType ); Первым параметром является входной поток, вторым — переменная типа string, третьим — символ-разделитель, который будет отслеживаться во входном пото- ке. Третий параметр можно не задавать; по умолчанию используется символ пе- ревода строки ' \п' (клавиша Enter для стандартного входного потока cin). Пове- дение этой функции аналогично поведению одноименного метода из <iostream>. Вызывать функцию надо так (ввод из стандартного входного потока): string s; getline(cin, s); // ввод из стандартного потока getlineCcin. s,'.'): // ввод из стандартного потока до точки Кириллические символы, набираемые на клавиатуре, направляемые в строковые переменные, правильно отображаются в консольном окну — никаких специаль- ных действий для перекодировки выполнять не требуется. Другие методы Метод gcountO, имеющий прототип streamsize gcountO const: позволяет посчитать количество введенных символов при последней операции неформатированного ввода. Метод реекО, с прототипом 1nt_type реекО: позволяет «увидеть» следующий доступный символ во входном потоке. Символ из потока не извлекается. Метод ungetO, имеющий прототип 1 streams ungetO;
152 Глава 8. Ваод-вывод возвращает в поток последний прочитанный символ. Этот символ становится первым символом, который будет прочитан при следующей операции ввода. Метод putbackO, с прототипом 1 streams putback(char Ch); помещает в поток символ Ch. Этот символ становится первым символом, кото- рый будет прочитан при следующей операции ввода. Метод ignoreO, имеющий прототип IstreamS ignore(streamsize count = 1. int delim = EOF); позволяет «пропускать» символы входного потока. Метод извлекает символы из входного потока, ничего не занося в переменные. Первый аргумент — количество удаляемых из потока символов; по умолчанию равен 1. Второй аргумент — сим- вол-ограничитель, который тоже удаляется из потока; значение по умолчанию — символ конца файла. Метод используется в сочетании с методом get(), с целью удаления из потока вхождений ‘ \п‘, например; stream.get(s,50); stream.ignore(); Метод flushO принудительно записывает содержимое буфера на устройство. Метод имеет прототип ostreamS flushO; Состояния потока Каждый поток в каждый момент времени находится в одном из состояний: good, eof, fail, bad. Если по завершении операции ввода/вывода поток имеет статус good, это означает, что во время операции не произошло никаких непредвиден- ных событий, и следующая операции ввода/вывода может выполняться. В ос- тальных случаях следующая операция выполнена не будет. Состояния fail и bad являются показателями ошибки. Если поток находится в одном из этих состояний, операции обмена данными не производятся. Состоя- ние fail устанавливается, если операция ввода/вывода завершилась неудачно, например при ошибках форматирования в процессе чтения. Состояние bad уста- навливается при «фатальных» ошибках потока. В состоянии bad поток неработо- способен. В этом случае он одновременно находится и в состоянии fail. Состояние eof может возникнуть только при операции чтения. Для стандартного потока ввода это случается при нажатии комбинации клавиш Ctrl+z. Для файлов состояние eof устанавливается при первой попытке чтения после последнего байта файла. Тогда поток тоже переводится в состояние fail. Состояния определены в классе ios base как целые статические константы, кото- рые называют флагами {признаками) состояния потока. typedef int iostate; goodbit = 0x00 badbit = 0x01 eofbit = 0x02 fail bit = 0x04
Краткие сведения по теме 153 Значения флагов зависят от реализации; названия их определены в стандарте [1]. В программе имена признаков состояния нужно сопровождать префиксом класса: std::1 os::eofbit Методы, работающие с флагами состояния, определены в классе base! os. Метод iostate rdstateO const: позволяет прочитать значение поля битов. Методы bool goodO const: // следующая операция может выполняться bool eof() const: // обнаружен конец ввода bool failO const: // следующая операция не выполняется bool bad() const: // поток "поврежден" опрашивают значения конкретных флагов. Методов, устанавливающих новые значения флагов, всего два: void setstate(iostate state): void clear(iostate state = goodbit); Установить любой флаг можно методом setstateO; для установки нескольких флагов нужно воспользоваться битовыми операторами, например setstate(std::ios::eofbit | std::ios::failbit); Поток из состояния fail мы можем вернуть в нормальное состояние с помощью метода с!еаг(), например: if (stream.fai 1 ()) stream.clearO: После этого можно выполнять операции обмена — до очередной ошибки. Метод clear() сбрасывает все флаги в ноль; задав аргумент, можно установить нужные флаги состояния. Состояния потока целесообразно проверять в условиях оператора if и циклов (см. ниже раздел «Примеры операций с файлами»). Состояния потока и исключения По умолчанию при ошибках ввода/вывода исключения не генерируются. Режим возбуждения исключений можно установить собственноручно. В состав класса basic ios входит метод except!ons(), который позволяет указать, при установке каких флагов состояния должно произойти исключение. Прототип метода сле- дующий: void exceptions(iostate flags); Задать генерацию исключения при установке флага ios:: badbi t можно так: stream.exceptions(ios::badbit); Флаги, как обычно, можно комбинировать, например: stream.exceptions(ios::badbit|ios::faiIbit); Если аргумент равен нулю или ios::goodbit, исключения генерироваться не бу- дут.
154 Глава 8. Ввод-вывод Вызов без аргумента возвращает текущие флаги, условия генерации исключе- ния, например: 1 os:: 1 estate flags = stream, exceptions О; Если возвращается goodbit, исключения исключаются. Исключения будут генерироваться не только во время операций ввода/вывода, но и при установке флагов методами с1еаг() и setstateO. Эти исключения явля- ются объектами класса iosbase:: fail иге — наследника класса exception. Форматирование ввода/вывода К форматируемым потокам относятся: стандартные потоки, строковые потоки, файловые потоки, связанные с текстовыми файлами. Для всех этих видов пото- ков разрешено использовать стандартные средства, к которым относятся: □ флаги форматирования; □ методы форматирования; □ манипуляторы. Флаги форматирования Флаги — это битовые константы, отвечающие за то или иное действие. Флаги форматирования определены в базовом классе библиотеки ввода/вывода iosbase. left = 0x0001, // выравнивание влево right = 0x0002, // выравнивание вправо internal = 0x0004, ' // знак влево, число вправо dec = 0x0008, // вывод десятичного целого hex = 0x0010, // вывод шестнадцатеричного целого oct = 0x0020. // вывод восьмеричного целого fixed = 0x0040, // вывод дробного в виде dddd.dd scientific = 0x0080, // вывод дробного в научном виде boolalpha = 0x0100, // вывод true и false вместо 1 и 0 showbase = 0x0200, // вывод префиксов для целых oct и hex showpoint - 0x0400, // вывод незначащих нулей спереди showpos = 0x0800, ' // вывод явного + для положительных целых skipws = 0x1000, // пропускать символы-разделители // (по умолчанию) unitbuf - 0x2000, // очищать буфер после каждой операции uppercase = 0x4000 // Е и X вместо е и х Названия и назначение флагов являются стандартными; значения битов опреде- ляются реализацией. Все флаги, кроме skipws, по умолчанию сброшены (равны 0). Флаг skipws по умолчанию установлен (равен 1). Ряд флагов объединяются в группы-«переключатели». Для каждой группы опре- делена маска: adjustfield = internal | left | right basefield = dec | hex | oct
Краткие сведения по теме 155 floatfield = fixed | scientific В каждой конкретный момент времени может быть активен только один из фла- гов группы. Для манипулирования флагами предусмотрено несколько методов: typedef int fmtf1ags; fmtflags flags0 const: fmtflags flags(fmtflags flags); fmtflags setf(fmtflags flag): void unsetf(fmtflags flags); // получить флаги // получить и установить флаги // получить и установить флаги // сбросить флаги Метод fl ags О сначала сбрасывает все флаги, а метод setfO — не сбрасывает. От- дельные флаги обнуляются методом unsetfO. Для работы с группами флагов имеется перегруженный метод setfO. fmtflags setf(fmtflags flag, fmtflags mask); Этот метод сначала сбрасывает все флаги группы, а потом поднимает требуемый. Установить одновременно несколько флагов для выходного потока stream мож- но, объединив их побитовым ИЛИ (operator |): stream.fl ags(los::showpos|ios::showbase|1os;uppercase); stream.setf(1 os::showpos11 os;:showbase11 os::uppercase); Очистить несколько флагов можно так: stream.unsetfdos::showpos11 os::showbase|ios; uppercase); Методы форматирования Метод precisionO позволяет получить и установить точность — количество цифр после десятичной точки; метод widthO позволяет получить и установить ширину поля (вывода); метод fill О позволяет получить и установить символ- заполнитель. Эти методы имеют прототипы streamsize precIsionO const; streamsize prec1sion(streamsize prec); streamsize widthO const; streamsize width(streamsize newwldth); char fillO const; char fill(char fill); // получить текущую точность // установить точность // получить текущую ширину // установить ширину // получить символ-заполнитель // задать символ-заполнитель Метод preci si on () и метод fillO влияют на все операции вывода. Это значит, что один раз установленная точность не изменится до следующего вызова precisionO. Метод widthO работает для текущей операции вывода «. Если выводимое значе- ние не помещается в заданной ширине поля, выводится значение целиком. Метод copyfmtO позволяет скопировать состояние формата потока в другой по- ток, например: stream.copyfmt(cout); Манипуляторы В библиотеке C++ реализован набор функций для форматирования потока дан- ных во время операций ввода/вывода, называемых манипуляторами. В <ios>
156 Глава 8. Ввод-вывод определены манипуляторы, соответствующие флагам форматирования (табл. 8.1). Это манипуляторы без аргументов, и их названия совпадают с имена- ми флагов — фактически они тоже устанавливают и сбрасывают одноименные флаги. Одиночным флагам соответствуют по два манипулятора: один поставлен в соответствие установленному флагу, а другой — с префиксом «по» — сброшен- ному флагу. Флагам, объединенным в группы, достаточно одного манипулятора. Таблица 8.1. Манипуляторы, соответсвующие флагам форматирования Манипулятор Эквивалентный вызов метода left stream.setf(std::ios::left, std::ios::adjustfield); right stream.setf(std::ios::right, std::ios::adjustfield); internal stream.setf(std::ios::internal, std::ios::adjustfield); dec stream.setf(std::ios::dec, std::ios::basefield); hex stream.setf(std::ios::hex, std::ios::basefield); oct stream.setf(std::ios::oct, std::ios::basefield); fixed stream.setf(std::ios::fixed, std::ios::floatfield); scientific stream.setf(std::ios::scientific, std::ios::floatfield); boolalpha stream.setf(std::ios::boolalpha); noboolalpha stream.unsetf(std::ios::boolalpha); showbase stream.setf(std::ios::showbase); noshowbase stream.unsetf(std::ios::showbase); showpoint stream.setf(std::ios::showpoint); noshowpoint stream.unsetf(std::ios::showpoint); showpos stream.setf(std::ios::showpos); noshowpos stream.unsetf(std::ios::showpos); skipws stream.setf(std::ios::skipws); noskipws stream.unsetf(std::ios::skipws); unitbuf stream.setf(std::ios::unitbuf); nounitbuf stream.unsetf(std::ios::unitbuf); uppercase stream.setf(std::ios::uppercase); nouppercase stream.unsetf(std::ios::uppercase); В cistream> определен манипулятор ввода ws — ввод с игнорированием пропус- ков, в <ostream> — манипуляторы вывода endl, ends, flush. Манипулятор flush принудительно сохраняет выходной буфер на устройство. Действие манипуля- тора endl состоит в записи в выходной поток разделителя ' \п', и затем выходной
Краткие сведения по теме 157 буфер принудительно выводится на устройство. Манипулятор ends записывает в поток символ ' \0' (символ завершения строки). В <iomanip> определены шесть манипуляторов с аргументами, эквивалентные методам; пять из них представлены в табл. 8.2. Чтобы их использовать, нужно за- дать в программе директиву #1nclude <1omaniр> Таблица 8.2. Манипуляторы с аргументами Манипулятор Эквивалентный метод setprecision(streamsize n) streamsize precision(streamsize n); setw(streamsize n) streamsize width(streamsize n); setfill(char ch) char fill(char fill); setiosflags(fmtflags n) fmtflags setf( fmtflags flag); resetiosflags(fmtflags n) fmtflags unsetf(fintflags flag); Шестой манипулятор setbased nt base) может использоваться вместо манипуляторов dec, oct, hex (или вместо установки соответствующих флагов): □ setbase(8) эквивалентно oct; □ setbase (10) эквивалентно dec; □ setbase(16) эквивалентно hex. Вызов setbase(O) переключает вывод в десятичную систему счисления. Написание простейших манипуляторов Для создания собственного манипулятора без аргументов надо написать функ- цию, которая получает и возвращает ссылку на поток. Например, манипулятор, вставляющий в поток символ «точка» и знак табуляции, может выглядеть так: std::ostream& pointab(std::ostream& os) { return (os « '.'<< ’\t'): } Используется такой манипулятор так же, как и стандартный: cout « "Point and tab” « pointab « 10 « endl: Обычно манипуляторы без аргументов пишутся для объединения свойств не- скольких стандартных манипуляторов. Например, если нужно объединить за- данные ширину, символ-заполнитель и выравнивание: ostream& setfield(ostream& os) { os.width(lO): os.fill(’O'); os.setfdos: internal. ios::adjustfield); return os:
158 Глава 8. Ввод-вывод Тогда вывод в cout переменной w в этом формате выглядит так: cout « setfield « vv « endl; Для создания манипулятора с аргументом нужно разработать класс, в котором определить конструктор инициализации с требуемым аргументом. Как обычно, для класса определяется дружественная функция operator«, в которой и выпол- няется требуемое форматирование. Например, мы хотим вывести строку ведомо- сти зарплаты. Структура данных имеет вид: struct Рау { unsigned int TabN; // табельный номер string fio: // фамилия float Summa; //'сумма Тогда класс может выглядеть следующим образом: class FormatPay { Pay р: public: FormatPaylconst Pay& t):p(t){} friend ostream& operator«(ostream& os, const FormatPay& t): }: inline ostream& operator«(ostream& os, const FormatPay& t) { os « right « setw(5) « t.TabN « ' |: os « left « setw(20) « t.FIO « '|; os « right « setw(10) « setprecision(2)« t.Summa « 11': return os: } Использование такого класса просто: cout « FormatPay(v) « endl; Форматирование при вводе Большинство средств форматирования предназначены для форматирования вы- вода. Однако некоторые из них оказывают влияние на ввод. Установленный флаг skipws означает, что по умолчанию символы-разделители пропускаются. Для того чтобы они учитывались, нужно сбросить флаг (или ис- пользовать манипулятор nosklpws). На ввод влияют флаги группы basefield (манипуляторы dec, hex, oct или манипу- лятор setbaseO). Если установлен один из флагов (или использован один из ма- нипуляторов dec, hex, oct, setbase(lO), setbase(8), setbase(16)), то он задает фикси- рованное основание, в соответствии с которым выполняется контроль вводимых символов числа. Если ни один флаг не установлен (или активен манипулятор setbase(O)), основание вводимого числа определяется при вводе. Если в начале числа находится префикс Ох или ОХ, это число считается шестнадцатеричным; когда число начинается с 0, оно считается восьмеричным. Во всех остальных случаях число будет причислено к десятичным. Если установлен флаг bool al pha (или использован манипулятор), то во входном потоке булевские значения должны быть представлены как строки "true" или
Краткие сведения по теме 159 "false" (без кавычек). Если этот флаг не установлен (по умолчанию), во входном потоке должны быть заданы числа 0 (false) и 1 (true). . Установка ширины поля (метод width() или манипулятор setwO) влияет на ввод в объект типа string или в символьный массив. Если размер поля равен п, ввод выполняется либо до первого символа-разделителя, либо до п символов. При вводе других типов данных значение ширины поля не играет никакой роли. Файловые потоки Правила работы с файлами следующие: 1. Файл должен быть открыт. 2. Выполняются операции обмена между программой и файлом. 3. Файл закрывается. Файловый поток представлен в программе переменной потокового типа, которая имеет область видимости и время жизни в соответствии с объявлением. Файл на диске представлен именем файла, которое не имеет отношения к переменной по- тока. В программе имя файла представляется либо константой-строкой, либо сим- вольным массивом, куда помещается строка-имя файла. Тип string непосредст- венно применить нельзя — следует воспользоваться методом c_str() класса string. Файловый поток должен быть связан с файлом на диске. Связь между потоко- вой переменной и именем файла устанавливается при открытии файла. При за- крытии файла эта связь разрывается. Файл может быть открыт как явно, с помощью метода ореп(), так и неявно — конструктором при создании потока. Закрывается файл также либо явно — ме- тодом closeO, либо неявно — деструктором. Операции обмена между программой и файлом зависят от типа связываемого с файлом потока. По умолчанию файловый поток является форматируемым и соотносится с текстовым файлом на диске. Операции обмена с текстовым файлом сопровождаются преобразованием информации: для входного потока if stream выполняется преобразование из символьного вида в двоичный, а для выходного потока ofstream — наоборот, из двоичного представления в символьное. Можно применять любые средства форматирования, описанные ранее. В конструкторах и методе open О присутствует второй аргумент, отвечающий за режим открытия потока (файла). Режим открытия — это целая статическая кон- станта размером в один бит, поэтому режимы ассоциированы с флагами. Режи- мы открытия описаны в табл. 8.3. Режимы можно объединять битовой операцией operator |, например std::i os::in|std::1os::bi nary std::1os::in|std::i os::out|std::1 os;:ate При открытии разрешается задавать следующие комбинации флагов (файл от- крывается в соответствующем режиме): □ in — чтение (файл должен существовать); □ out — стирание и запись (файл создается, если его нет);
160 Глава 8. Ввод-вывод □ out | trunk — стирание и запись (файл создается, если его нет); □ арр — дозапись (файл создается, если его нет); □ out | арр — дозапись (файл создается, если его нет); □ 1 n | out — чтение и запись (файл должен существовать); □ in|out|trunk — стирание, чтение и запись (файл создается при его отсут- ствии). Таблица 8.3. Режимы открытия потоков Режим Описание in Открытие потока для чтения (умолчание для ifstream) out Открытие потока для записи (умолчание для ofstream) trunk Удаление старого содержимого файла (умолчание для ofstream) арр Открытие потока для записи в конец файла ate Открытие потока (чтение и/или запись) и позиционирование в конец файла binary Открывает поток в двоичном режиме (по умолчанию — текстовый) К перечисленным комбинациям можно добавить флаги binary и ate. Различие между флагами арр и ate в том, что по режиму ate позиционирование осуществ- ляется один раз при открытии, а по режиму арр позиционирование в конец дела- ется при каждой операции записи в поток. Двоичные файлы Поток открывается в двоичном режиме, если задать флаг binary, например: std::ios::out|std::ios:rbinary Вывод в двоичные файлы выполняется методом writeO, который рассматривал- ся при выводе символов и строк. Обычно с его помощью обрабатывают не сим- волы, а данные других типов. Метод имеет прототип ostream& writeCconst char *str. streamsize count); Метод записывает count символов символьного массива str в поток данных. Ни- какие символы-разделители не влияют на вывод. Он также возвращает ссылку на поток, поэтому после операции можно проверить состояние потока. Исполь- зуя преобразование указателей, можно вывести в выходной двоичный поток зна- чение переменной любого типа. Ввод из двоичных файловых потоков производится методом readO, который имеет такой же прототип istream& read(char *str, streamsize count); Метод читает count символов в символьный массив str. Размер символьного мас- сива должен быть достаточен, чтобы вместить count символов. Метод возвращает ссылку на поток, поэтому после операции можно проверить состояние потока.
Краткие сведения по теме 161 Никакие символы-разделители не влияют на ввод. Если обнаружен конец фай- ла, устанавливаются флаги eofbit и fail bit. Существует еще один метод ввода, имеющий прототип streamsize readsome(char *str. streamsize count); Метод работает аналогично методу readO, но возвращает не ссылку на поток, а количество введенных символов. Примеры операций с файлами Создаем выходной файловый поток, открываем новый текстовый файл и связы- ваем его с потоком. ofstream strm("d:/f11es/number.txt"); Создаем входной поток, открываем его методом ореп(), связывая с существую- щим текстовым файлом. После обработки — закрываем файл. ifstream strm; // входной поток-объект strm.open("c:/files/number.txt"); // открываем файл // обработка - ввод информации из файла strm.closeO; // закрываем Создаем выходной поток, открываем новый текстовый файл для дозаписи в ко- нец файла и связываем его с потоком. ofstream strm("c:/files/number.new". std; :ios; :app)•; Состояние потока после выполнения любой операции, в том числе и после от- крытия файла, можно проверять непосредственно в условиях if и циклов. if (strm.is_open()) // проверка открытия { // файл открылся нормально } if (strm) // проверка открытия { // файл открылся нормально } Операция operator! позволяет проверить поток на аварийное состояние после открытия или операции ввода. if (Istrm) // проверка открытия { // файл не открылся } if ([(stream » х)) { // ввод завершился неудачей } Ввод и проверку состояния потока можно разделить: stream » х; if (!stream) { // ввод завершился неудачей } При разделении ввода и проверки состояния лучше явно вызывать метод опроса состояния. Наиболее часто используется метод eofО. Правильная последова-
162 Глава 8. Ввод-вывод тельность операторов, с помощью которой выполняется обработка входного файлового потока, выглядит так: // операция чтения из потока from while (Ifrom.eofO) // явная проверка end-of-file { // требуемые действия с введенными данными // операция чтения из потока from Еще вариант: бесконечный цикл с проверкой «end-of-file» внутри цикла. while (true) { // чтение из потока from if(from.eofO) break; // явная проверка end-of-file // требуемые действия с введенными данными } В других вариантах последовательности действий возможно возникновение разного рода ошибок, например повтор обработки последних данных входного потока. В условиях можно задавать явный вызов методов ввода: int nl = 0: while(stream.get(ch)) // читать все символы, в том числе пробельные 1f(ch--'\n') П1++; В данном случае цикл закончится при возникновении ситуации «end-of-file». Создаем входной поток, открываем двоичный файл и связываем его с потоком. ifstream to ("c:/files/number.dat", std::ios::binary): Можно вывести в двоичный файл данные любых типов: double г = 5.2; to.write((char *)&r. sizeof(r)); date d(12, 01. 2006); to.write(reinterpret_cast<char *>(&d). sizeof(date)): В том числе и массив, например: int m[10]; to.write((char *)&m[0], sizeof(m)): Чтобы ввести из входного двоичного потока значение переменной любого типа, используйте преобразование указателей. float f: date dd; from.read((char *)&f. sizeof(f)): from.read(reinterpret_cast<char *>(&dd). sizeof(date)): int m[10J; from.read((char *)&m[0], sizeof(m)): Достаточно просто реализуется копирование файлов1. 1 Более просто и быстро копирование файлов делается с помощью переназначения бу- фера.
Краткие сведения по теме 163 Листинг 8.2. Копирование текстовых файлов #include <fstream> using namespace std: // функция копирования потока in в поток out; потоки должны быть открыты void filecopy (ifstream &in, ofstream &out) { char ch; whiIe(in.get(ch)) // читать все символы. out.put(ch); // в том числе пробельные } int main() { ifstream istreamm ("c:/text/number.txt”); ofstream ostream ("c:/text/number.new"); if (istream) filecopy(istream, ostream); // копирование файлов return EXIT_SUCCESS; } Для копирования двоичных файлов сгодится та же функция filecopyO, если от- крыть потоки как двоичные. // копирование файлов { ifstream istrm("c:/binary/number.dat". std::ios::binary): ofstream ostrm("c:/binary/number.new". std::ios::bi nary); if (istrm) filecopy(istrm. ostrm): } Объединяются файлы той же функцией: достаточно открыть выходной файл в режиме дозаписи. // дозапись нового файла в конец старого { ifstream instrm ("c:/binary/number.dat", ios::binary): ofstream outstrm("c:/binary/number.new". ios::app|ios::binary): if (instrm) filecopy(instrm, outstrm): } Буферизация Ввод/вывод, если не управлять им из программы, является буферизованным. За буферизацию отвечает флаг unitbuf. Флаг unitbuf по умолчанию сброшен, что означает накопление символов потока в буфере вывода. Если этот флаг устано- вить, вывод выполняется без буферизации — выходной буфер очищается после каждой операции вывода. В объектно-ориентированной библиотеке буферы — это объекты. Конкретный тип буфера зависит от вида потока, но базовым типом для любых буферов является streambuf (см. листинг 8.1). Каждый объект-поток содержит указатель на некото- рый объект-буфер. В потоковых классах определен метод rdbufO, который воз- вращает этот указатель: streambuf* rdbufO const; streambuf* rdbuf(streambuf *Sb): В классах basic_i stream и basicostream есть конструкторы, принимающие в каче- стве аргумента указатель на буфер. Это позволяет объявить несколько разных потоков, связанных с одним буфером.
164 Глава 8. Ввод-вывод ostream hexout( cout. rdbufO): hexout.setffios: :hex. ios::basefield): hexout.setf(ios::showbase): // hexout связан c cout // выводить в hex-форме // показывать основание С помощью этого метода можно уместить функцию копирования (см. лис- тинг 8.2) в одну строку — буфер одного потока целиком передается в другой по- ток. void filecopy (ifstream &istrm. ofstream &ostrm) { ostrm « istrm.rdbufO: } Таким же способом можно вывести текстовый файл в стандартный поток cout. istream fi1e("c:/fi1es/fi1e.txt"); cout « file. rdbufO; Посредством того же метода rdbufO делается и перенаправление потоков — бу- фер одного потока связывается непосредственно с буфером другого потока: from, rdbuf (to. rdbufO) Например, направим стандартный вывод в текстовый файл: ofstream fi1е("с:/fi1es/fi1e.txt”); streambuf *b = cout.rdbufO: cout.rdbuf(fi1e.rdbuf ()); cout « "расширенные коды ASCII!" « endl: cout.rdbuf(b): cout « "расширенные коды ASCII!" « endl: // текстовый файл // сохранили cout // перенаправление // в файл // восстановили cout //в cout Строковые потоки Строковые потоки — только форматируемые. С помощью строковых потоков обычно выполняется преобразование данных из внутреннего вида в строку сим- волов (с типом string) и обратно. Строковые потоки вывода осуществляют пре- образование из двоичного вида в строку, а строковые потоки ввода — из строки в двоичный формат. С помощью двунаправленного строкового потока можно вы- полнять оба преобразования. Для строковых потоков разрешается без ограниче- ний применять любые средства форматирования, описанные в разделах выше. Строку, являющуюся внутренним буфером строковых потоков, можно извлечь с помощью метода str(). Для входного строкового потока этот буфер можно инициализировать существующей строкой. Выходной строковый поток удобно использовать для реализации методов преобразования в строку toStringO. На- пример, для класса date реализация может выглядеть так: string date::toString() { ostringstream os; // строковый выходной поток os « year « « month «'-' « day; // переводим if (ostr) // если не было ошибок, return os.strO; // возвращается строка } Все, что касается установки и проверки состояния потока, относится и к строко- вым потокам.
Краткие сведения по теме 165 Позиционирование в потоке Позиционирование разрешено в строковых и файловых потоках (как текстовых, так и двоичных), причем позиционироваться можно и во входных, и в выходных потоках. Методы позиционирования различаются для входных и выходных потоков: в первом случае имена методов заканчиваются символом g, а методы выходных потоков заканчиваются символом р. Методы позиционирования пере- числены в табл. 8.4. Таблица 8.4. Методы позиционирования Метод Описание pos_type tellg() Получить текущую (абсолютную) позицию чтения istream& seekg (pos type p) Установить абсолютную позицию чтения istream& seekg (off_type p, ios::seekdir s) Установить относительную позицию чтения pos type tellp() . Получить текущую (абсолютную) позицию записи istream& seekp (pos_type p) Установить абсолютную позицию записи istream& seekp (off type p, ios::seekdir s) Установить относительную позицию записи Методы tellpO и tellgO возвращают абсолютное смещение от начала потока. Символы потока нумеруются, начиная с нуля. Получить текущую позицию чте- ния в потоке можно так: Ios::pos_type pos = stream.tellg(); streampos pos = stream.tellpO; Переход к позиции, сохраненной в переменной pos, делается следующим обра- зом: stream.seekp(pos): Позиционирование в начало потока можно выполнить так: stream.seekg(O); Хотя тип postype не является целым типом, тем не менее целые константы мож- но использовать в качестве аргумента в методах установки позиции — выполня- ется преобразование по умолчанию. Методы относительного позиционирования позволяют устанавливать новую те- кущую позицию в потоке относительно начала, конца или текущей позиции. Первый аргумент обычно передает целое число (выполняется преобразование по умолчанию) и является смещением (в символах) от указанной позиции. Поло- жительное значение приводит к смещению вперед — ближе к концу файла, отри- цательное — смещение назад, к началу файла. Второй аргумент задает позицию, относительно которой нужно позициониро- ваться. В классе iosbase определены константы
166 Глава 8. Ввод-вывод static const seekdir beg. // позиционирование от начала потока cur. // позиционирование от текущей позиции end; // позиционирование от конца потока Нужно следить, чтобы позиция оставалась внутри файла. Попытка позициони- роваться вне его приводит поток в состояние bad. Позиционироваться в конец потока можно так: stream.seekg(0. ios::end); Методы относительного позиционирования позволяют реализовать прямой до- ступ к записям двоичного файла. Если запись файла имеет тип Т, то позициони- рование на k-ю запись для чтения выполняется методом stream.seekg(k*sizeof(T), beg); Если необходимо сместиться вперед от текущей позиции на одну запись типа Record, можно воспользоваться любой из следующих инструкций: // эквивалентные вызовы seekg stream.seekg(stream.tellg() + sizeof(Record)); stream.seekg(sizeof(Record). ios_base::cur); Широкие потоки «Широкие» (wide) потоки работают с «широкими» символами wchart. Если зна- чение sizeof(char) по стандарту равно единице, то размер wchar t зависит от реа- лизации. Обычно размер sizeof(wchart) равен двум. Все стандартные классы объектно-ориентированной библиотеки реализованы и в «узком», и в «широ- ком» смысле. В стандартном пространстве имен std для них заданы имена typedef basic_streambuf<wchar_t> wstreambuf: typedef basic_istream<wchar_t> wistream; typedef basic_ostream<wchar_t> wostream; typedef basic_iostream<wchar_t> wlostream; typedef basic_stringbuf<wchar_t> wstringbuf; typedef basi c_i stringstream<wchar_t> wistrlngstream: typedef basic_ostringstream<wchar_t> wostringstream; typedef bas1c_stringstream<wchar_t> wstringstream; typedef bas1c_filebuf<wchar_t> wfilebuf; typedef basic_1fstream<wchar_t> wifstream: typedef basic_ofstream<wchar_t> wofstream; typedef basic_fstream<wchar_t> wfstream;
Краткие сведения по теме 167 Заголовки нужно подключать те же самые, что и для обычных узких потоков. Каждый узкий поток имеет соответствующий парный — широкий поток. Стан- дартные широкие потоки: □ wcin — объект класса wistream, соответствующий стандартному вводу (stdin); по умолчанию связан с клавиатурой; □ wcout — объект класса wostream, соответствующий стандартному выводу (stdout); по умолчанию связан с экраном; □ wclog — объект класса wostream, соответствующий стандартному выводу для ошибок (stderr); по умолчанию связан с экраном; □ wcerr — объект класса wostream, соответствующий стандартному выводу для ошибок (stderr); по умолчанию связан с экраном. Ввод/вывод для широких потоков Для широких потоков перегружены операции ввода operator» и вывода operator«. Ввод/вывод данных встроенных числовых типов для широких потоков ничем не отличается от работы с узкими потоками: все средства форматирования, все ме- тоды работают точно так же, как и для узких потоков. Вывод символов английского алфавита и строк отличается только формой объ- явления символов и строк: wchar_t ch = L'a'; // один широкий символ wchar_t s[] = L”Hello world!": // константа - символьный массив wstring ws = L"Hello, all!": // константа - широкая строка wcout « ch « s « ws « endl ; Отдельный символ можно преобразовать в широкий с помощью метода widen(). Этот метод используют для преобразования специальных символов: wcout « wcout.widen!'\n') « endl: Ввод англоязычных широких символов и строк выполняется теми же средствами, что и для узких потоков. Ввод строк операцией operator» по умолчанию тоже выполняется до пробела. Для ввода с пробелами нужно использовать либо средства форматирования (флаг и манипулятор noskipws), или функции getlineO, описан- ные ранее. * Для ввода/вывода символов русского алфавита и строк необходимо установить русскоязычный локальный контекст. Локаль (locale) — это объект, инкапсули- рующий национальные особенности внешнего представления данных и устанав- ливающий нужную кодировку. Локальный контекст устанавливается для пото- ка. Для работы с локалями следует задать в программе директиву #include <locale> Чтобы выводить русские символы на консоль, нужно установить русскую ло- каль для wcout. Это делается с помощью метода потока imbueO, который имеет прототип locale 1mbue(const locale& L):
168 Глава 8. Ввод-вывод Аргументом служит объект-локаль, но его не обязательно объявлять в програм- ме — можно использовать временный анонимный объект: wcout.1mbue(1 оса1е("rus_rus.866")); Русский контекст устанавливается для стандартного широкого потока wcout. Строка «rusrus.866» — это сокращенное имя локального контекста; вид зависит от реализации1. Полное имя локал и: Russian_Russian.866 Первое слово Russian означает язык — русский, второе Russian — страна, 866 — кодовая страница. После установки русской локали на экран в консольное окно нормально выводятся русские широкие строковые константы. Для поддержки русских букв при вводе нужно точно таким же способом задать русский контекст для входного широкого потока wcin: wc i n. i mbue (1 оса 1 е (" rus _rus. 866")): После этого вводите русские строки и символы обычными способами и сколько хотите. Широкие строковые и файловые потоки Широкие строковые потоки работают идентично узким потокам. wstring date::toString() { wostrlngstream os; os « year « « month « day: // переводим if (ostr) return os.strO: } // широкий строковый поток // если не было ошибок, // возвращается строка Для работы с русскими буквами нужно установить русскую локаль. wstring s = О'Русская строка "; wostringstream os; os.imbue(locale("rus_rus.866'')); // русская локаль - строковый поток wcout.imbue(locale(''rus_rus.866")): // русская локаль - стандартный поток os « ССтрока: " « s « endl; wcout « 1"Вывод:” « os.strO « endl; wcout « ^'Количество символов=" « os.strO.lengthO « endl; Широкие текстовые потоки связывают с текстовыми файлами. wofstream strm; strm.open("c:/textfiles/wnumber.txt"): if (strm.is_open()) { ford nt i =0; i <10; 1++) strm « randOXlO « endl; strm.closed; // открываем тот же текстовый файл для чтения wifstream strm(”c.7textfi1es/wnumber.txt"); if (strm) { int number, summa = 0; // выходной поток-объект // открываем // проверка открытия // выводим 10 чисел // закрываем выходной поток-файл // проверка открытия 1 Мы работаем в системе Visual C++.NET 2003 под Windows.
Упражнения 169 while(strm » number) summa+=number; wcout « summa « endl: strm.closeO; } } // ввод числа // суммирование // вывод результата // закрываем поток-файл Все символы выводятся в широкий файл в однобайтовом режиме. Если русская локаль не установлена, русские буквы в файл не выводятся. Широкие текстовые потоки можно читать как узкие в двоичном режиме. При этом не происходит форматного преобразования «новой строки» (' п'). Упражнения Следующие задания требуется решить с привлечением текстовых файлов. Нуж- но написать функцию, с помощью которой подготовить входной файл, записав в него 100 случайных целых чисел в диапазоне от -50 до +50 по одному на стро- ке. Сформировать выходной файл, преобразовав числа входного файла. 1. Записать выходной файл, добавить к каждому числу последнее число файла. 2. Записать выходной файл, разделить каждое число на полусумму первого от- рицательного и 50-го числа файла. 3. Записать выходной файл, вычесть из каждого числа набольшее число файла. 4. Записать выходной файл, умножить каждое число на минимальное из чисел файла. 5. Записать выходной файл, разделив все нечетные по абсолютной величине числа на среднее арифметическое. 6. Записать выходной файл, вычесть из каждого числа сумму чисел файла. 7. Записать выходной файл, умножить каждое третье число на удвоенную сум- му первого и последнего отрицательных чисел. 8. Записать выходной файл, добавить к каждому числу первое нечетное по абсо- лютной величине число файла. 9. Записать выходной файл, умножить каждое четное число на первое отрица- тельное число файла. 10. Записать выходной файл, добавить к каждому числу половину последнего от- рицательного числа файла. 11. Записать выходной файл, разделить все числа на половину максимального числа. 12. Записать выходной файл, заменив все нули средним арифметическим. 13. Записать выходной файл, заменив все положительные числа квадратом ми- нимума. 14. Записать выходной файл, добавив к каждому числу полусумму всех отрица- тельных чисел. 15. Записать выходной файл, добавить к каждому числу среднее арифметическое наименьшего по абсолютной величине и наибольшего из чисел файла.
170 Глава 8. Ввод-вывод 16. Записать выходной файл, разделив число на минимум и добавив максимум. 17. Записать выходной файл, заменив все положительные числа на максимум. 18. Записать выходной файл, заменив каждое четное число по абсолютной вели- чине на разность максимума и минимума. 19. Записать выходной файл, заменив каждое второе отрицательное число поло- виной максимума. 20. Записать выходной файл, заменив каждое третье положительное число сред- ним арифметическим отрицательных чисел. Реализовать задания 1-20 с широкими потоками. Реализовать задания 1-20 с двоичными файлами. Для проверки результата на- писать функцию вывода двоичного файла на экран. Реализовать задания 1-20 с одним исходным двоичным файлом, выполнив пере- запись по месту. Для проверки результата написать функцию вывода двоичного файла на экран. Реализовать задания 1-20, используя строковые потоки. Исходный файл явля- ется текстовым, выходной — двоичным. Входное число считывается как строка и преобразуется во внутренний формат с помощью входного строкового потока. Для проверки результата написать функцию вывода двоичного файла на экран. Реализовать задания 1-20, используя строковые потоки. Исходный файл явля- ется двоичным, выходной — текстовым. Выходное число преобразуется в строку с помощью выходного строкового потока. Реализовать задания 5.31-5.45, добавив в классы метод saveO, записывающий данные на диск, и метод loadO, обеспечивающий загрузку данных, сохраненных методом saveO. Реализовать задания 5.46-5.65, добавив в классы метод saveO, записывающий данные на диск, и метод load О, обеспечивающий загрузку данных с диска. Следующие задания реализовать с двоичными файлами. С файлом выполняются операции добавления, удаления и замены записи. Написать функцию, с по- мощью которой осуществляется первичный ввод информации с клавиатуры и дозапись в файл. Написать функццю, преобразующую данные исходного файла в текстовый файл в виде ведомости с графой общих итогов. Удаление и замену записей выполнять с помощью перезаписи исходного файла в новый. При поис- ке сохраняйте выбранные записи в новый файл. 21. Счет в банке представляет собой структуру с полями: номер счета, код счета, фамилия владельца, сумма на счете, дата открытия счета, годовой процент на- числения. Поиск по номеру счета, коду счета и владельцу. 22. Запись о товаре на складе представляет собой структуру с полями: номер скла- да, код товара, наименование товара, дата поступления на склад, срок хране- ния в днях, количество единиц товара, цена за единицу товара. Поиск по но- меру склада, коду товара, дате поступления и сроку хранения (просроченные и непросроченные товары).
Упражнения 171 23. Запись о преподаваемой дисциплине представляется структурой: код дисцип- лины в учебном плане, наименование дисциплины, фамилия преподавателя, код группы, количество студентов в группе, количество часов лекций, количе- ство часов практики, наличие курсовой работы, вид итогового контроля (зачет или экзамен). Зачет — 0,35 часа на одного студента; экзамен — 0,5 ч на сту- дента. Поиск вести по фамилии преподавателя, коду группы, наличию курсо- вой работы, виду итогового контроля. 24. Информационная запись о книге, выданной на руки абоненту, представляет собой структуру следующего вида: номер читательского билета, фамилия абонента, дата выдачи, срок возврата (количество дней), автор, название, год издания, издательство, цена. Поиск по полям: номер читательского билета, автор, издательство, дата возврата (просроченные). 25. Информационная запись о файле содержит поля: каталог, имя файла, расши- рение, дата и время создания, атрибуты «только чтение», «скрытый», «сис- темный», признак удаления, количество выделенных секторов (размер секто- ра принять равным 512 байт). Поиск выполнять по каталогу, дате создания, по признаку удаления. Обеспечить операцию реального удаления файла с по- мощью перезаписи файла-каталога. 26. Разовый платеж за телефонный разговор описвается структурой с полями: фамилия плательщика, номер телефона, дата разговора, тариф за минуту раз- говора, скидка (в процентах), время начала разговора, время окончания раз- говора. Поиск по фамилии, дате, номеру телефона. 27. Характеристики модели компьютера представлены кодом и названием торговой марки, типом и тактовой частотой работы процессора, объемом ОЗУ, объ- емом жесткого диска, объемом памяти видеокарты, ценой компьютера в ус- ловных единицах и количеством экземпляров, имеющихся в наличии. Поиск в базе: по типу процессора, объему памяти, видеокарты и объему жесткого диска. 28. Список абонентов кабельного телевидения состоит из элементов следующей структуры: фамилия, район, адрес, телефон, номер договора, дата заключения договора, оплата за установку, абонентская плата помесячно, дата последнего платежа. Поиск по району, дате заключения договора, дате последнего платежа. 29. Сотрудник представлен структурой Person с полями: табельный номер, номер отдела, фамилия-имя-отчество, оклад, дата поступления на работу, процент надбавки, подоходный налог, количество отработанных дней в месяце, коли- чество рабочих дней в месяце, начислено, удержано. Поиск по номеру отдела, полу, дате поступления. 30. Запись о багаже пассажира авиарейса содержит следующие поля: номер рей- са, дата и время вылета, пункт назначения, фамилия пассажира, количество мест багажа, суммарный вес багажа. Поиск выполнять по номеру рейса, дате вылета, пункту назначения, весу багажа (превышение максимально допусти- мого). 31. Одна учетная запись пользования спорткомплексом имеет структуру: фами- лия клиента, код и вид спортивного занятия, фамилия тренера, дата и время начала, количество минут, тариф за минуту. Поиск по фамилиям клиента и
172 Глава 8. Ввод-вывод тренера, по виду занятия, по дате начала, по количеству минут (больше или меньше). 32. Одна запись о лекарственном препарате содержит следующие поля: номер ап- теки, название лекарства, количество упаковок, имеющихся в наличии в дан- ной аптеке, стоимость одной упаковки, дата поступления в аптеку, срок хра- нения (в днях). Поиск по номеру аптеки, наименованию лекарства, дате поступления. 33. Учетная запись торговой фирмы содержит поля: код игрушки, название иг- рушки, тип игрушки, возрастные границы (например, от 10 до 15), цена за единицу, количество в наличии, дата поступления в магазин, поставщик. По- иск по дате поступления, поставщику, возрастному цензу. 34. Один элемент -- автомобиль — представляет собой структуру с полями: фа- милия владельца, код марки автомобиля, марка автомобиля, требуемая марку топлива, мощность двигателя, объем бака, остаток бензина, объем масла. Дана фиксированная цена за литр бензина и цена заливки масла. Поиск по марке автомобиля, марке бензина, мощности двигателя. 35. Одна запись в журнале зимней экзаменационной сессии представляет собой структуру с полями: курс, код группы, фамилия студента, номер зачетной книжки, дисциплина, оценка за экзамен по дисциплине. Вычисляются сред- ние баллы по дисциплине, по группе, по курсу. Поиск по курсу, по группе, по номеру зачетной книжки, по фамилии, по оценкам. 36. Структура одной записи оплаты коммунальных услуг: номер дома, номер квартиры, фамилия владельца, вид платежа (квартплата, газ, вода, электриче- ство), дата платежа, сумма платежа, процент пени, на сколько дней просрочен платеж. Поиск по номеру дома, квартиры, владельцу, виду платежа, по дате. 37. Одна запись счета за ремонтные работы содержит поля: название фирмы, вид работ, единица измерения, стоимость за выполненную единицу работ, дата исполнения, объем выполненной работы. Поиск по названию фирмы, виду работ, по дате исполнения. 38. Одна учетная запись в журнале стоянки автомобилей имеет структуру: номер автомобиля, фамилия владельца, дата и время начала, дата и время оконча- ния, тариф за час. Поиск по номеру автомобиля, по дате и времени стоянки, по фамилии владельца. 39. Структура одной записи о сельскохозяйственном продукте содержит поля: наименование района (где выращивают), наименование продукта, площадь (га), урожайность (кг/га), цена за 1 кг, потери при транспортировке (%), стои- мость продукта. Поиск по наименованию района, по наименованию продукта, по урожайности, по площади. 40. В туристической фирме учетная запись о проданном туре содержит следую- щие поля: наименование тура, фамилия клиента, цена одного дня (в р.), коли- чество дней, стоимость проезда, курс валюты, количество валюты, стоимость поездки. Поиск выполнять по наименованию тура, по фамилии клиента, по стоимости проезда, по количеству дней.
Упражнения 173 Реализовать задания 21-40 в виде классов, обеспечивающих нужные операции. Для форматирования записи выходного текстового файла написать класс-мани- пулятор. Использовать широкие потоки. Выполнить задания 21-40, используя класс «гибкого» динамического массива (см. листинг 5.3), для которого удаление и вставка выполняются в произвольном месте, с операциями saveO и loadO. Выполнить задания 21-40, реализовав помещение записи по месту старой запи- си. Операцию удаления выполнять без перезаписи файла. Для этого добавить в структуру поле — признак удаления (true — удалена). Реализовать операцию раскО, выполняющую уплотнение файла через перезапись исходного файла, с физическим изъятием помеченных на удаление записей. Реализовать задания 21-40, разработав обобщенный класс-шаблон, параметром которого является тип элементов. Для заданий 21-40 разработать класс Index, выполняющий построение простей- шего индексного массива. Индексный массив — динамический, состоящий из пар «ключ-адрес». Массив должен быть отсортирован по возрастанию ключа. Поиск нужной записи выполняется по индексу. Класс должен обеспечивать работу с любым файлом и индексацию по любому из требуемых полей класса. Класс должен включать в себя методы saveO и loadO. Выполнить задания 21-40 в виде классов, реализовав класс Index в виде шаблона.
ГЛАВА 9 Строки В данной теме представлена техника использования строк в программах на C++. Рассматриваются все виды строк C++: «узкие» и «широкие» символьные масси- вы, строки с типами string и wstring. Предметом многих упражений является разбор строк в текстовых файлах. Краткие сведения по теме В C++ в настоящее время поддерживаются два вида символьных строк: С-стро- ки и строки в стиле C++. Строки в стиле С — это символьные массивы; С++- строки — это класс string, который определен в стандарте [1]. Строковые кон- станты являются С-строками и имеют тип константного символьного массива: □ const chart] — для обычных символов; □ const wchar_t[] — для «широких» символов. Поэтому при использовании строковых констант обычно выполняются преобра- зования типов. Например, при передаче символьной константы в функцию в ка- честве аргумента происходит преобразование либо в const char*, либо в string в зависимости от типа параметра функции. Символьные массивы Обработка строк-символьных массивов выполняется функциями, прототипы ко- торых находятся в системном заголовочном файле <cstring>, подлежащий под- ключению. #include <cstring> В стандартное пространство имен в Visual C++.NET 2003 входят следующие имена 21 функции для обработки обычных символьных массивов (см. табл. 47 стандарта [1]): namespace std { using ::memchr: using ::memcpy: using ::memset: using ::size_t: using ::memcmp; using ::memmove; // имя типа, а не функции
Краткие сведения по теме 175 using ::strcat; using ;:strchr; using ::strcmp; using ::strcoll; using ::strcpy; using ::strcspn; using ::strlen; using ::strncat; using ::strncmp: using ::strncpy: using ::strpbrk: using ::strrchr: using ::strspn; using ::strstr; using ::strtok; using ::strxfrm; } Имена 21 функции для обработки широких символьных массивов, входящие в стандартное пространство имен (см. табл. 48 стандарта [1]), определены в <cwchar>: namespace std { } using ::size_t; using ::wcscat; using ::WCSChr; using ::wcscmp; using ::wcscoll; using ::wcscpy; using ::wcscspn; using : ,-wcslen; using ::wcsncat; using ; ::wcsncmp; using ::WCSncpy; using ;:wcspbrk; using ::wcsrchr; using : ::wcsspn: using : ::wcsstr; using : ::wcstok; using : ::wcsxfrm; using : ::wmemchr; using : ;:wmemcmp; using : ::wmemcpy; using : ::wmemmove; using : ::wmemset: ПРИМЕЧАНИЕ --------------------------------------------------------- В разных интегрированных средах, несмотря на наличие стандартов С и C++, реализуется много нестандартных функций для работы со строками. В системе Visual C++.NET 2003 такие функции всегда начинаются с подчеркивания. В системе Borland C++ Builder 6 име- на многих функций, не регламентированных стандартом С, никак не выделены. Поэтому рекомендуется пользоваться системой Visual C++.NET 2003. Прототипы и назначение функций описаны в приложении А. Обычная ошибка при обработке С-строк — отсутствие резервирования памяти для строки-результата: // неправильная обработка char *destination = // нет памяти для строки char*blank - " ", *с = "C++", *Borland = "Borland"; strcpy(destination, Borland); strcatidestination, blank); strcatidestination, c); // правильная обработка char *destination[100]; // есть память для строки char *blank = " ", *c = "C++", *Borland = "Borland"; strcpy(dest)nation, Borland); strcat(destination, blank); strcat(destination. c);
176 Глава 9. Строки В первом случае переменная destination — обычный указатель; для символов строки памяти не выделено. Во втором примере память выделена явно в виде массива. Чтобы исправить первый пример, нужно заказать память динамически: char *destination = new char[1001: // память выделяется динамически Строки в стиле С++ С++-строки определены в библиотеке <string>, которую требуется подключить: #include <string> Все имена, определенные в этом файле, входят в стандартное пространство имен std. Здесь определяется базовый шаблон basicstring: template <class charT, class traits = char_traits<charT>, class Allocator = allocator<charT» class basic_string; Первый параметр шаблона определяет тип символов. Так как в C++ два сим- вольных типа, то определены и две специализации базового шаблона: typedef basic_string <char> string: typedef basic_string <wchar_t> wstring; Все различия, связанные с типом символов, собраны в классе-шаблоне свойств символов char_trats<>, который является вторым параметром базового шаблона basic string. Инкапсуляция различий в классе свойств приводит к тому, что пользователь не замечает разницы при манипулировании обычными и «широки- ми» строками — все операции со строкой совершенно не зависят от типа симво- лов и те же методы применимы и к обычным и к широким строкам. Единствен- ное различие заключается в объявлении типа строки: string и wstring. Третий параметр базового шаблона задает стандартный распределитель памяти. Базовый шаблон представлен в листинге 9.1. Листинг 9.1. Базовый шаблон строк tempiate<class charT, class traits = char_traits<charT>, class Allocator = allocator<charT> > class basic_string { public: // типы typedef traits traits_type: typedef typename traits::char_type value_type; typedef Allocator allocator_type: typedef typename Allocator::size_type size_type: typedef typename Allocator::difference_type difference_type; typedef typename Allocator::reference reference; typedef typename Allocator::const_reference const_reference; typedef typename Allocator::pointer pointer; typedef typename Allocator::const_pointer constjoointer;
Краткие сведения по теме 177 typedef implementation defined iterator: typedef implementation defined const_it’erator; typedef std;;reversejterator<iterator> reverse_iterator: typedef std::reversejterator<constJterator* const_reverse_iterator: static const sizejype npos = -1; // конструкторы/копирование/деструктор explicit basic_string(const Allocators a = Allocator!)): basicjtring(const basic_stringS str); basic_string(const basic_stringS str, size_type pos, size_type n = npos, const Allocators a = Allocator!)): basic_string(const charT* s, size_type n, const Allocators a = Allocator!)); basic_string(const charT* s, const Allocators a = Allocator!)); basic_string(size_type n, charT c, const Allocators a = Allocator!)): tempiate<class Inputlterator> basic_string!lnputlterator begin, Inputiterator end, const Allocators a = Allocator!)); ?basic_string(): basic_stringS operator=(const basic_stringS str); basic_stringS operator=(const charT* s): basic_stringS operator=(charT c); // итераторы iterator begin!): const_iterator begin!) const; iterator end!); const_iterator end!) const; reversej terator rbeginO: constjeverseJ terator rbegin!) const; reversejterator rend!); constjeverseJ terator rend!) const; // размеры size_type size!) const; sizejype length!) const; sizejype maxjize!) const: void resize!sizejype n, charT c); void resize(sizejype n); sizejype capacity!) const; void reserve(sizejype res_arg = 0); bool empty!) const; // доступ по индексу constjeference operator[](sizejype pos) const; reference operatort](sizejype pos); const_reference at(sizejype n) const; reference at(sizejype n); // сцепление basic_stringS operator+=(const basicjtringS str): basicjtringS operator+=(const charT* s); basicjtringS operator+JcharT c); basicjtringS appendtconst basicjtringS str); basicjtringS append(const basicjtringS str. sizejype pos. sizejype n); basicjtringS appendtconst charT* s, sizejype n): basicjtringS appendtconst charT* s); basicjtringS appendtsizejype n, charT c); tempiate<class Inputlterator> basicjtringS append!Inputiterator first. Inputiterator last); void push_back(charT c):
178 Глава 9. Строки // присваивание basic_string& assign(const basic_string& str): bas1c_str1ng& assign(const basic_string& str, size_type pos, size_type n); basic_string& assign(const charT* s, size_type n): basic_string& assign(const charT* s): basic_str1ng& assign(size_type n, charT c); tempiate<class Inputlterator> basic_string& assign(Inputiterator first. Inputiterator last); // модификаторы basic_string& insert(size_type post, const basic_string& str): basic_string& 1nsert(size_type post, const basic_string& str, size_type pos2, size_type n): basic_string& insert(size_type pos. const charT* s, size_type n): basic_string& 1nsert(s1ze_type pos, const charT* s); basic_string& insert!size_type pos, size_type n. charT c); iterator insertdterator p, charT c): void 1nsert(iterator p, size_type n, charT c): tempiate<class Inputlterator> void insert(iterator p, Inputiterator first. Inputiterator last): basic_string& erase(size_type pos = 0, size_type n = npos): iterator erase(iterator position): iterator erasedterator first, iterator last): basic_string& replace(size_type posl,size_type nl,const basic_string& str): basic_string& replace(size_type posl.size_type nl,const basic_string& str, size_type pos2, size_type n2): basic_string& replace(size_type pos, size_type nl,const charT* s, size_type n2): basic_string& replace(size_type pos, size__type nl, const charT* s): basic_string& replace(size_type pos, size_type nl, size_type n2, charT c): basic_string& replace(iterator 11. iterator 12, const basic_string& str): basic_string& replace(iterator il, iterator 12. const charT* s.size_type n): basic_string& replacedterator 11, Iterator 12, const charT* s): basic_string& replacedterator 11, iterator 12, size_type n, charT c): tempiate<class Inputlterator> basic_string& replacedterator il, iterator 12, Inputiterator jl. Inputiterator J2): void clearO: void swap(basic_string& str): basic_string substr(size_type pos = 0, size_type n = npos) const: // получение символьного массива const charT* c_str() const: // explicit const charT* dataO const: size_type copy(charT* s. size_type n, size_type pos = 0) const: allocator_type get_allocator!) const; // поиск size_type find (const basic_string& str, size_type pos = 0) const: size_type find (const charT* s, size_type pos, size_type n) const: size_type find (const charT* s, size_type pos = 0) const; size_type find (charT c, size_type pos = 0) const: size_type rfind(const basic_string& str, size_type pos = npos) const: size_type rfind(const charT* s, size_type pos, size_type n) const; size_type rfindCconst charT* s, size_type pos = npos) const; size_type rfind(charT c, size_type pos = npos) const: size_type find_first_of(const basic_string& str, size_type pos = 0) const; size_type f1nd_f1rst_of(const charT* s. si2e_type pos, size_type n) const;
Краткие сведения по теме 179 sizejype find_first_of(const charT* s. sizejype pos = 0) const: sizejype findJifst_of(charT c, size_type pos = 0) const; sizejype findjast_of( const basic_string& str, size_type pos = npos) const: sizejype findjast_of (const charT* s, size_type pos, sizejype n) const: size_type find_last_of (const charT* s, size_type pos = npos) const: size_type find_last_of (charT c, sizejype pos - npos) const; s1ze_type f1nd_f1rst_not_of(const basic_string& str, sizejype pos=0) const: size_type find_first_not_of(const charT* s.size_type pos,size_type n) const: sizejype f1nd_f1rst_not_of(const charT* s, size_type pos = 0) const; sizejype flnd_first_not_of(charT c, size_type pos = 0) const: sizejype find_last_not_of (const basic_string& str. size_type pos = npos) const: size_type find_last_not_of (const charT* s,size_type pos,size_type n) const: sizejype findjast_not_of (const charT* s, sizejype pos = npos) const; sizejype find_last_not_of (charT c, size_type pos = npos) const; // сравнения int comparetconst basic_string& str) const; int compare(size_type posl. size_type nl. const basic_string& str) const; int compare(size_type posl. size_type nl. const basic_string& str, size_type pos2. sizejype n2) const: int compare(const charT* s) const; int compare(size_type posl, size_type nl, const charT* s) const; int compare(size_type posl,size_type nl,const charT* s,size_type n2) const: } Среди определяемых типов две строки typedef implementation defined iterator; typedef implementation defined constjterator: определяются реализацией. Например, в системе Borland C++ Builder 6 эти две строки в классе-шаблоне представлены так: typedef typename Allocator::pointer iterator: typedef typename Allocator:: const jroi nter constjterator: А в среде Visual C++. NET 2003 эти типы просто являются вложенными классами: class constjterator: friend class constjterator: class iterator: friend class iterator; Создавать объекты-строки Можно многими способами: string s: // создает пустую строку string s(cstr): // создает строку из С-строки string s(cstr. len); // создает строку из len символов С-строки string s(num. ch): string s(str); // создает строку из num символов ch // создает строку-копию из строки str string s(str, ind); // создает строку из символов строки str // начиная с индекса ind strihg s(str, Ind, len); // создает строку длиной не более len символов // строки str, начиная с индекса ind string s(beg, end): // создает строку из интервала [beg, end) // символов другой строки Строка не может инициализироваться единичной символьной константой, на- пример, '2', но один из конструкторов является конструктором преобразования
180 Глава 9. Строки const charT* -> string. Обратное преобразование можно выполнять только явным образом с помощью трех методов: □ data(). Возвращает содержимое строки в виде массива символов (в конце мас- сива отсутствует завершающий символ ' \0'); □ c_str(). Возвращает содержимое строки в виде С-строки (с завершающим сим- волом ' \0'); □ соруО. Копирует содержимое строки в символьный массив-аргумент метода (завершающий символ ' \0' отсутствует). Функции dataO и c_str() возвращают указатель на внутренний буфер строки, поэтому вызывающая сторона не должна изменять символы или освобождать память. Методы Интерфейс строк существенно приближен к интерфейсу контейнера-вектора (см. главу 10, «Стандартная библиотека шаблонов»): присутствует доступ к сим- волам по индексу и с помощью итераторов; набор методов для определения и из- менения размера фактически совпадает с аналогичным набором методов векто- ра; те же функции вставки, удаления и замены. Это позволяет рассматривать строку как контейнер символов, похожий на vector<char>. К строкам применимы очень многие стандартные алгоритмы (см. приложение Б), которые применимы к векторам. Однако для строк реализован ряд методов, применимых только к ним: операции сцепления, множество методов поиска, методы сравнения. Кроме того, можно видеть большое разнообразие перегрузок одной и той же функции — это связано с тем, что требуется выполнять операции и с С++-строками, и с С-строками. По- этому практически каждый метод реализован как минимум в двух вариантах: с аргументами типа basicstring и типа charT*. Размеры Класс строк предоставляет фактически те же методы определения и изменения размеров, что и вектор (см. главу 10, «Стандартная библиотека шаблонов»): bool empty О const: // size_type size () const: // size_type max_size 0 const: // void resize (size_type): // void resize (size_type, T); // size_type capacity 0 const: // void reserve (size_type); // пустая строка? длина строки максимально возможная длина строки изменение количества символов в строке то же с инициализацией новых символов размер выделенного массива для строки увеличение размера массива Длину строки можно узнать еще с помощью метода lengthO, который является просто переименованным методом sizeO. Присваивание Класс строк предоставляет операцию присваивания operator3. Для обеспечения присвоения части строк реализовано семейство методов assignO. Метод-шаблон tempi ate<cl ass Input Iterator* basic_string& assigndnputlterator, Inputiterator):
Краткие сведения по теме 181 позволяет присвоить строке любую часть другой строки или символьного мас- сива. Доступ Доступ к элементам строки выполняется и по индексу, и с помощью итераторов. Метод at О проверяет индекс на корректность и генерирует исключение out of range, а операция индексирования этого не делает. Предоставляется полный доступ с помощью прямых и обратных итераторов (см. главу 10, «Стандартная библиотека шаблонов»). Константные итераторы не позволяют изменять символы строки. Сцепление Класс строк предоставляет операцию сцепления с присваиванием operator*». На- бор методов сцепления append!) обеспечивает сцепление текущей строки с частя- ми строк или символьных массивов. Аргументы те же, что и у методов присваи- вания assign!). Метод push back!) присоединяет один символ. Операция сцепления operator* реализована как набор внешних дружественных функций со всеми возможными сочетаниями типов параметров. template <class charT, class traits, class Allocatoo basic_string operator* (const basic_string&. const basic_string&): template <class charT, class traits, class Allocatoo basic_string operator* (const charT*. const basic_string&): template <class charT, class traits, class Allocatoo basic_string operator* (charT, const basic_string&): template <class charT, class traits, class Allocatoo basic_string operator* (const basic_string&, const charT*): template <class charT, class traits, class Allocator» basic_string operator* (const basic_string&, charT): Сравнение Набор методов сравнения compare!) выполняет лексикографическое сравнение текущей строки и строки-аргумента. Методы возвращают отрицательное значе- ние, 0 или положительное значение как результат сравнения. В частности: □ < 0, если size!) < str.size!); □ 0, если size!) = str.size!); □ > 0, если size!) > str.size!). Как внешние дружественные функции реализованы операции сравнения; каж- дая операция имеется в трех вариантах со всеми возможными сочетаниями ар- гументов. Например, проверка на равенство выполняется следующими тремя функциями: template <class charT. class traits, class Allocator» bool operator== (const basic_string&, const basic_string&); template <class charT. class traits, class Allocatoo bool operator™ (const charT*. const basic_string&); template <class charT. class traits , class Allocatoo bool operator™ (const basic_string&, const charT*):
182 Глава 9. Строки Методы-модификаторы Методы-модификаторы представлены тремя группами: вставка символов insertO, удаление символов eraseO, замена символов replaceO. Обязательным первым аргументом всех методов-модификаторов является либо индекс символа, либо позиция итератора, начиная с которого требуется выполнять операцию. Метод clear О очищает всю строку, а метод swapO позволяет обменяться с другой строкой. Метод substr О не модифицирует строку, но позволяет получить любую часть текущей строки. Как обычно, реализована функция внешняя дружественная функция обмена swapO: template <class charT, class traits, class Allocator void swap(basic_string<charT.traits,Allocators a, basic_string<charT.traits,Al 1ocator>& b); В виде внешних дружественных функций реализованы операции ввода/вывода строк. tempiate<class charT, class traits, class Allocator basic_istream<charT, traits>& operator»(i streams, basic_string&): template <class charT, class traits, class Allocator basic_ostream<charT, traits>& operator«(ostreamS, const basic_string&): template <class Stream, class charT, class traits, class Allocator basic_istream<charT, traits>& getline(Streams, basic_stringS, charT): Поиск Методы поиска составляют самое многочисленное семейство. Поддерживаются следующие возможности: □ поиск отдельных символов; поиск подстрок; поиск строки из нескольких по- вторяющихся символов; □ поиск в прямом (слева направо) и в обратном (справа налево) направлениях; □ поиск с любой позиции. Методы поиска в случае неудачи возвращают значение npos, которое определено в классе как статическая целая константа. Методы группы f i nd() ищут в текущей строке символ или строку, представлен- ную аргументами, слева направо. Группа методов rfindO делает то же самое, но справа налево. Методы группы fi nd_fi rst of() ищут первое вхождение (слева) одного из симво- лов, входящих в строку-аргумент. Группа методов findlastofO делает то же са- мое, но справа налево: ищется последнее вхождение одного из символов, входя- щих в строку-аргумент. Методы группы fi nd_fi rst not of() ищут в текущей строке первый символ (сле- ва), не входящий в строку-аргумент. Методы группы find last not ofO делают то же самое, но справа налево: ищется последний символ текущей строки, не входящий в строку-аргумент.
Упражнения 183 Упражнения Выполнить все задания в четырех вариантах: □ с «узкими» С-строками; □ с «широкими» С-строками; □ с С++-строками типа string; □ с С++-строками типа wstring. Предполагается, что во всех заданиях текст записан в файле с именем input.txt. В программе формируется строка с результатом и выводится на экран. 1. В английском тексте заменить артикль «а» определенным артиклем «the». 2. В русском тексте заменить местоимение «мы» местоимением «они». 3. В тексте заменить все вхождения подстроки Strl подстрокой Str2, которая вводится с клавиатуры. 4. В тексте после каждого сочетания Ch вставить строку Str. 5. В заданном русском тексте заменить все точки сокращением «тчк», запятые — сокращением «зпт». 6. В заданном тексте удалить все лишние пробелы, оставив между словами только один пробел, а между предложениями — два пробела. 7. В заданном тексте выполнить следующие преобразования: после запятой, точки, двоеточия, точки с запятой должен быть один пробел. Вставить пробел перед и после тире (минуса). 8. Дана строка, представляющая собой запись выражения на C++. Перед и по- сле каждого знака бинарной операции вставить пробелы; для операции запя- тая пробел вставляется только после нее и запятая прижимается к левому вы- ражению. Знаки инкремента и декремента прижимаются к аргументу без пробела. 9. Заменить в английском тексте местоимение «we» местоимением «they». 10. В заданном тексте после каждого символа Ch удалить все символы до ближай- шего символа, не являющегося буквой. 11. В тексте перед каждым сочетанием Ch вставить строку Str. 12. В английском тексте заменить каждое вхождение строки «Гт» на строку «I ат». 13. В тексте программы на C++, где встречаются русские идентификаторы, вста- вить перед и после каждого идентификатора символ подчеркивания «_». 14. В тексте программы на C++ перед каждым ключевым словом int вставить ключевое слово signed. 15. В тексте программы на C++ перед каждым ключевым словом char вставить ключевое слово unsigned. 16. Заменить в тексте каждое вхождение константы «1» словом «один». 17. В тексте программы на паскале заменить каждое ключевое слово begin на от- крывающую скобку «{», а слово end — на скобку «}».
184 Глава 9. Строки 18. В тексте программы на паскале удалить ключевое слово then, а логическое выражение между 1 f и then заключить в скобки. 19. В тексте программы на C++ в заголовке оператора while удалить скобки во- круг логического выражения и вставить после него ключевое слово do. 20. В тексте программы на паскале преобразовать заголовки процедур в заголов- ки функций C++ (не трогая список аргументов). Во всех заданиях текст записан в файле с именем input.txt. Вывести результат в текстовый файл output.txt. 21. Дана программа на C++. Записать в выходной файл построчно все ключевые слова, встречающиеся в этой программе, указав количество появлений клю- чевого слова в ней. 22. Дана программа на C++. Записать в выходной файл построчно все идентифи- каторы, встречающиеся в этой программе. Подсчитать количество появлений идентификатора в тексте. 23. Дана программа на C++. Записать в выходной файл построчно все числовые константы, встречающиеся в этой программе, вместе с номером строки про- граммы. 24. Дана программа на паскале. Записать в выходной файл построчно все ключе- вые слова, встречающиеся в этой программе, указав количество появлений. 25. Дана программа на паскале, набранная строчными буквами. Записать в вы- ходной файл ту же программу, все ключевые слова вывести прописными бук- вами. 26. Дан текст на русском языке. Записать в выходной файл построчно все слова, встречающиеся в этом тексте, указав номер строки текста для первого вхож- дения этого слова. 27. Дан текст на русском языке. Записать в выходной файл предложения по од- ному на строке, указав номер строки, в которой начинается предложение. 28. Дан текст программы на C++. Записать в выходной файл построчно все пере- менные, определенные в программе, указав номер строки, в которой впервые встречается определение переменной. 29. Дана программа на C++. В выходном файле многострочные комментарии за- писать следующим образом /* комментарий */ 30. Дан текст программы на C++. Записать тот же текст, разместив фигурные скобки по одной на строке. Вложенные фигурные скобки должны быть сдви- нуты на две позиции вправо. 31. Дан текст программы на паскале. Записать тот же текст, разместив ключевые слова begin и end по одному на строке. Вложенные begin и end должны быть сдвинуты на две позиции вправо.
Упражнения 185 32. Дан текст программы на C++. Записать в выходной файл построчно все вы- ражения, изменяющие объявленные в программе переменные, и номер строки программы, где это происходит. 33. Дан текст, в котором пронумерованы заголовки разделов, подразделов и пунктов. Номер раздела имеет вид N.; номер подраздела имеет вид N.M.; номер пункта имеет вид N.M.K. Заголовки разделов и подразделов располагаются на отдельной строке, заголовок пункта начинается с красной строки и продол- жается до первой точки. Записать в выходной файл оглавление текста. Заго- ловок подраздела сдвинут на одну позицию вправо, заголовок пункта сдвинут на две позиции вправо. Должен быть указан номер строки, в котором встреча- ется заголовок. 34. Дан текст программы на C++. Записать в выходной файл построчно все опе- рации и частоту их использования в программе. 35. Дана программа на C++. Записать в выходной файл построчно все ключевые слова, определяющие тип переменной, и количество появлений ключевого слова в программе. 36. Дан текст на русском языке. Записать в выходной файл частоты появлений всех двухбуквенных сочетаний, имеющихся в тексте. 37. Дан текст на русском языке. Записать в выходной файл частоты появлений предлогов, имеющихся в тексте. 38. Дан текст на русском языке. Записать в выходной файл все слова в алфавит- ном порядке. 39. Дан текст на русском языке. Записать в выходной файл слова в порядке воз- растания длины слова. 40. Дан текст программы на C++. Записать в выходной файл построчно все вызо- вы функций и номер строки программы, где это происходит. В следующих заданиях написать функцию, получающую параметры числового типа и возвращающую строку типа string в качестве результата. Не использо- вать функции преобразования вроде itoa(). 41. Дано дробное число типа double, представляющее собой денежную сумму в рублях и копейках. Преобразовать число в строку, разделив тройки цифр в рублях пробелом; в конце добавить слово «рублей», «рубля» или «рубль»; по- сле копеек вставить сокращение «коп.». Например, 123 765 рублей 98 коп. 42. Дана дата в виде трех целых чисел: день, месяц, год. Получить дату в виде строки, преобразовав год в числительное. Например, для чисел 22 02 2006 вы- водится строка «22 февраля две тысячи шестого года». 43. Дано время в часах, минутах и секундах. Получить время в виде строки, доба- вив слова «час», «минута» и «секунда» в нужном падеже. Например, для чи- сел 15 56 12 получить строку «15 часов 56 минут 12 секунд». 44. Дано значение угла в градусах, минутах и секундах. Получить значение в виде строки, добавив слова «градус», «минута» и «секунда» в нужном падеже. На- пример, для чисел 315 56 12 получить строку «315 градусов 56 минут 12 се- кунд».
186 Глава 9. Строки 45. Дано целое число п < 1 000 000 000 000. Преобразовать строку, вставив слова «миллиард», миллион», «тысяча» в нужной форме. Например, 3 234 765 987 преобразуется в строку «3 миллиарда 234 миллиона 765 тысяч 987». 46. Дано дробное число п < 1 000 000 типа double, представляющее собой массу в килограммах. Преобразовать число в строку, разделив тройки цифр пробе- лом, добавив слова «тонна», «килограмм», «грамм» в нужной форме. Напри- мер, число 123 765,98 преобразуется в строку «123 тонны 765 килограммов 980 грамм». 47. Дана дата в виде трех целых чисел: день, месяц, год. Поучить дату в виде стро- ки. Например, для чисел 22 02 2006 выводится строка «двадцать второе фев- раля две тысячи шестого года». 48. Дано время в часах, минутах и секундах. Получить время в виде строки. На- пример, для чисел 15 56 12 получить строку «пятнадцать часов пятьдесят шесть минут двенадцать секунд». 49. Дано целое число п < 1 000 000 000 000. Получить строку-числительное. На- пример, 3 234 765 987 преобразуется в строку «три миллиарда двести три- дцать четыре миллиона семьсот шестьдесят пять тысяч девятьсот восемьде- сят семь». 50. Дано целое трехзначное число п < 1000. Получить строку-числительное в раз- личных падежах (именительный, родительный, дательный, винительный, творительный, предложный). Например, для числа 654 числительное в пред- ложном падеже — «о шестистах пятидесяти четырех».
ГЛАВА 10 Стандартная библиотека шаблонов В данной теме изучаются основы стандартной библиотеки шаблонов [10, 12]. Рас- сматриваются последовательные и ассоциативные контейнеры, итераторы, стан- дартные алгоритмы и функторы. Краткие сведения по теме Главными составляющими стандартной библиотеки шаблонов являются контей- неры, итераторы и алгоритмы. Основная концепция STL — разделение данных и операций. Данные хранятся в контейнерах, а операции определяются адапти- руемыми алгоритмами. Итераторы «склеивают» эти два компонента. Благодаря им любой алгоритм может работать практически с любым контейнером. Универ- сальность алгоритмов еще более увеличивается при использовании функторов, реализованных в STL. Важнейшей особенностью STL является то, что все компоненты, поскольку они оформлены в виде шаблонов, работают с произвольными типами. Контейнеры Контейнер STL — это набор однотипных элементов. Все контейнеры делятся на два класса — последовательные и ассоциативные: □ последовательные контейнеры представляют собой неупорядоченные коллек- ции однотипных элементов. Каждый элемент занимает в контейнере опреде- ленную позицию, которая зависит только от времени и места вставки, но не зависит от значения элемента. К последовательным контейнерам относятся вектор (vector), список (list) и двусторонняя очередь (deque); □ ассоциативные контейнеры представляют собой отсортированные по умолча- нию коллекции однотипных элементов, в которых позиция элемента зависит от его значения. К ассоциативным контейнерам относятся множество (set) и мультимножества (multiset — множество с повторяющимися элементами),
188 Глава 10. Стандартная библиотека шаблонов отображение (тар) и мультиотображение (multimap — отображение с повто- ряющимися элементами). Помимо основных, STL содержит ряд специальных контейнеров-адаптеров: стек (stack), очередь (queue) и очередь с приоритетами (priority queue). В библиотеке реализованы и другие структуры, которые формально не являются стандартными контейнерами элементов: булевский вектор (vector<bool>) и бито- вые поля (bitset). ПРИМЕЧАНИЕ -------------------------------------------------------------- STL, поставляемая с системой программирования, может содержать и другие, нестандарт- ные контейнеры. Например, в STLport, входящей в комплект поставки Borland С++ Builder 6, реализованы контейнеры slist или hash_map. Все контейнеры являются динамическими: один из параметров шаблона опреде- ляет по умолчанию распределитель памяти. Для использования контейнеров и адаптеров необходимо подключить стандарт- ные заголовочные файлы: #1nclude <vector> // контейнер-вектор и vector<bool> #include <deque> // контейнер-двусторонняя очередь #1nclude <llst> // контейнер-список #include <stack> // стек #1nclude <queue> // очередь и очередь с приоритетами #include <set> // множество и мультимножество #include <map> // отображение и мультиотображение #include <b1tset> // последовательности битов Требования к элементам контейнеров Элементы STL-контейнера могут быть произвольного типа Т, но этот тип должен удовлетворять определенным требованиям. 1. Все контейнеры создают внутренние копии своих элементов и возвращают эти копии. Поэтому тип Т должен иметь конструктор копирования. Копия обязана быть равна оригиналу, а ее поведение не должно отличаться от пове- дения оригинала. 2. Контейнеры используют оператор присваивания для замены старых элемен- тов новыми. Это означает, что тип (класс) Т должен поддерживать операцию присваивания. 3. Контейнеры уничтожают свои внутренние копии при удалении элементов из контейнера. Следовательно, деструктор Т не должен быть закрытым (private). Эти три операции при отсутствии явных определений автоматически генери- руются для любого класса. Следовательно, любой класс по умолчанию удовле- творяет предъявленным требованиям, если для него не определять специальные версии этих операций. Указанным требованиям удовлетворяют также все встро- енные типы. Но именно им не удовлетворяют элементы битовых полей и булевских векторов, вследствие чего ни первые, ни вторые не считаются стандартными контейнерами.
Краткие сведения по теме 189 Стандартные встроенные указатели тоже не в полной мере отвечают означенным требованиям, поэтому использовать указатели в качестве элементов контейнера нужно очень осторожно. Исключения В библиотеке STL существует только одна функция-метод, которая генерирует исключение, — это функция at(), реализующая доступ по индексу к элементу вектора или двусторонней очереди. Функция генерирует стандартное исключе- ние out_of range, если аргумент превышает size(). В остальных случаях генери- руется стандартное исключение bad alloc при нехватке памяти. Стандартная библиотека обеспечивает базовую гарантию безопасности исклю- чений: возникновение исключений в стандартной библиотеке не приводит к утеч- ке ресурсов и нарушению контейнерных инвариантов. Это означает, что если программист будет аккуратно разрабатывать свои классы, уделяя внимание по- тенциальным исключениям, то стандартная библиотека практически гарантиру- ет безопасность обработки элементов в контейнере. Итераторы Итераторы стандартной библиотеки обеспечивают доступ к элементам контей- нера. Все контейнеры предоставляют методы для присвоения начального и ко- нечного значения итераторам. Итераторы контейнеров реализованы как вложенные классы, поэтому при действиях с контейнерами можно использовать и соответ- ствующие итераторы. Например, при работе с контейнером-вектором vectored nt> L; мы можем объявить в программе и соответствующий итератор vector<int>: iterator il = L.beginO: Категории итераторов Итераторы стандартной библиотеки делятся на категории. В библиотеке опреде- лено 5 категорий итераторов в порядке повышения «способностей»: □ входной (input) — чтение элементов контейнера в прямом направлении; □ выходной (output) — запись элементов контейнера в прямом направлении; □ прямой (forward) — чтение и запись элементов контейнера в прямом направ- лении; □ двунаправленный (bidirectional) — чтение и запись элементов контейнера в прямом и обратном направлениях; □ произвольного доступа (random access) — произвольный доступ для чтения и записи элементов контейнера. На практике для любого контейнера гарантируется двунаправленный итератор. Более того, для всех контейнеров, кроме списка, обеспечивается итератор произ- вольного доступа, который реализует полную арифметику встроенных указате- лей.
190 Глава 10. Стандартная библиотека шаблонов Итераторы могут быть константными и неконстантными. Константные итерато- ры не используются для изменения элементов контейнера. Итераторы могут быть действительными (когда итератору соответствует элемент контейнера) и недействительными (при отсутствии соттветствия между итера- тором и элементами контейнера). Итератор может стать недействительным по трем причинам: □ итератор не был инициализирован; □ итератор равен end(); □ контейнер, с которым связан итератор, изменил размеры или вовсе уничто- жен. Последнее часто является источником ошибок в программах, так как програм- мисты забывают, что при удалении и вставке итераторы становятся недействи- тельными. Для всех категорий итераторов определены следующие операции (1 и J — итера- торы): □ 1++ — смещение вперед (возвращает старую позицию); □ ++1 — смещение вперед (возвращает новую позицию); □ 1 = j — присваивание итераторов; □ i = j — сравнение итераторов; □ i != j — сравнение двух итераторов на неравенство; □ *1 — обращение к элементу; □ i->member — обращение к компоненту элемента; эквивалентно операции (*i).member. Остальные операции, определенные для разных категорий итераторов, представ- лены в табл. 10.1. Таблица 10.1. Операции с итераторами разных категорий Категория итератора Операции Контейнеры Входной х = *i — чтение элемента все Выходной *1 = х — запись элемента все Прямой х = *i, *1 = х все Двунаправленный х = *i, *1 = х , —i, i— все Произвольного доступа x = *i, *i = x , —i, i—, i + n, i - n, i +- n, i -= n, i < j, i > j, i <= j, i > j все, кроме list Адаптеры итераторов К адаптерам итераторов относятся: обратные итераторы, итераторы вставки, по- токовые итераторы. Методы rbeginO и rend(), реализованные во всех контейнерах, возвращают на- чальное и конечное значения обратного итератора. Использование обратного
Краткие сведения по теме 191 итератора не отличается от использования «нормального» прямого итератора, только операция ++ определена таким образом, что перебор элементов контейне- ра осуществляется от последнего элемента к первому. Обратные итераторы по- зволяют, например, отсортировать контейнер в обратном порядке. Чтобы использовать итераторы вставки и потоковые итераторы, нужно подклю- чить заголовочный файл #include <iterator> Итераторы вставки и потоковые итераторы применяются, как правило, в сочета- нии с модифицирующими алгоритмами (см. далее). Модифицирующие алго- ритмы работают в режиме замещения: выходной контейнер к моменту вызова алгоритма должен существовать, и в нем должно быть достаточно элементов. Прежние значения элементов выходного контейнера заменяются новыми. Итераторы вставки позволяют вставлять в существующий пустой контейнер новые элементы. Библиотека предоставляет три вида итераторов вставки: □ backinsertiterator — вставка в конец последовательности; □ frontinsertiterator — вставка в начало последовательности; □ insert iterator — вставка перед заданным элементом. Итератор вставки в конец (конечный итератор) работает с любым последова- тельным контейнером. Итератор вставки в начало (начальный итератор) нельзя использовать с вектором, поскольку вектор не обеспечивает вставку элементов в начало — для него не определен метод pushfrontO. Третий вид итераторов подходи^ для любого контейнера. Обращаться к итераторам вставки непосредственно не слишком удобно, поэтому в библиотеке реализованы функции-обертки в виде шаблонов. Так, «конечный» итератор для контейнера создается функцией backinserterO: template <class Container» back_insert_iterator<Container> back_inserter(Container& x); Аналогичный вид имеет функция front inserterO: template <class Container» front_insert_iterator<Container> front_inserter(Container& x): Обе функции получают в качестве аргумента контейнер, в который будет выпол- няться вставка, и возвращают соответствующий итератор вставки. Третья функция обеспечивает работу с произвольным итератором вставки. В ка- честве аргумента, кроме контейнера, передается еще итератор, задающий эле- мент, перед которым требуется выполнить вставку. template <class Container, class Iterator» insert_iterator<Container> inserter(Container& x. Iterator i): ' Потоковые итераторы — это итераторы в виде адаптеров, воспринимающих по- ток данных в качестве источника или приемника данных. Потоковый итератор ввода читает элементы из потока данных, а потоковый итератор вывода записы- вает данные в выходной поток.
192 Глава 10. Стандартная библиотека шаблонов Потоковый итератор вывода создается одним из двух конструкторов: □ ostream_iterator<T>(stream, delim) — создается итератор, связанный с выход- ным потоком stream, в котором выводимые значения разделяются строкой delim (тип const char *); □ ostream_iterator<T>(stream) — создается итератор, связанный с выходным по- током stream, в котором выводимые значения не разделяются. Вывод в поток выполняется операцией присваивания, а «перемещение» — опе- рацией ++, как и «перемещение» итераторов вставки. Можно использовать итера- тор вывода непосредственно, однако чаще выходной итератор применяется со- вместно с алгоритмами. // вывод десяти элементов контейнера на экран сору(а, а+10. ostream_iterator<int>(cout,".")): fill_n(ostream_iterator<int>(cout,”:”),3.5); // вывод 5,-5:5; // вывод в файл Integers.out десяти элементов контейнера по одному на строке ofstream outfllefintegers.out"); сору(а, а+10, ostream_iterator<int>(outfile,"\n")): Потоковый итератор ввода позволяет алгоритмам читать исходные данные не из контейнера, а из входного потока. Для получения данных из потока необходи- мо иметь два итератора: итератор входного потока и итератор конца потока. Итераторы создаются двумя конструкторами: □ istream_iterator<T>(stream) — создается итератор, связанный с входным пото- ком stream; □ 1 stream ) terator<T>() — создается итератор конца потока. Если попытка чтения заканчивается неудачей (например, наступает ситуация end-of-file), потоковый итератор ввода превращается в «конечный» итератор. Итераторы ввода можно определять отдельными переменными, а можно исполь- зовать анонимные непосредственно в качестве аргументов при вызове. vector<int> v(10); istream_iterator<int> Readtcin): // входной итератор istream_iterator<int> end; // итератор конца потока copy(Read, end, insertertv. v.begint))); // вставка в вектор // без объявления объектов итераторов copy(1streamj terator<)nt>(cin), // входной итератор 1 streamj terator<1 nt>(), // итератор конца потока insertertv. v.begint))): // вставка в вектор // прочитать последовательность строк из файла ifstream infileC'integers.out"); // входной файл istream_iterator<int> istinfile); // входной итератор связан с файлом istream_iterator<int> end: // итератор конца потока vector<int> tt; copytis, end. inserterttt, tt.begint))); // ввод чисел в вектор Вспомогательные функции для итераторов Для большего удобства при работе с итераторами в библиотеке реализованы две вспомогательные функции:
Краткие сведения по теме 193 // передвинуть итератор template <class Inputiterator, class Distance» void advance!Inputlterator& i, Distance n): // вычислить расстояние между итераторами templates! ass Inputiterator» typename iterator_traits<lnputlterator»::difference_type distancednputlterator first. Inputiterator last): Чтобы иметь возможность использовать эти функции, нужно подключить заго- ловочный файл #include <iterator> Функция advanced позволяет выполнять операции 1 += n, 1 -= п с итератором любой категории, а не только с итератором произвольного доступа. Функция distanced вычисляет расстояние между двумя итераторами, ссылаю- щимися на элементы одного контейнера. Расстояние — это значение типа differencetype, которое и является разностью итераторов (last-first) и факти- чески определяет количество элементов в указанном диапазоне. Последовательные контейнеры Все последовательные контейнеры обладают сходным интерфейсом. Классы- шаблоны имеют идентичные параметры: template <class Т. class Allocator = allocator^» > class vector {/*...*/}: template <class T, class Allocator = allocator^» > class deque {/*...*/}; template <class T, class Allocator = allocator^» > class list {/*...*/}; Параметр T определяет тип элементов контейнера. Параметр шаблона Al 1 ocator задает распределитель памяти. Для всех контейнеров реализованы стандартные распределители по умолчанию. Помнить об этом параметре нужно в двух случа- ях: если писать собственный распределитель памяти и при использовании по- следовательного контейнера в качестве аргумента другого класса-шаблона. В начале каждого последовательного контейнера определены типы. typedef Т value_type: typedef Allocator allocator_type; typedef typename Allocator::size_type size_type: typedef typename Allocator::difference_type difference_type: typedef typename Allocator::reference reference: typedef typename Allocator::const_reference const_reference: class iterator: class constjterator: typedef typename std::reverse_iterator<iterator> reverse_iterator: typedef typename std::reversejterator<const iterator» const_reverse_iterator:
194 Глава 10. Стандартная библиотека шаблонов Конструкторы позволяют создавать последовательные контейнеры следующим образом (вместо contai пег подставьте vector, 11 st или deque): contaiпег<тип> v: contaiпег<тип> v(n); contaiпег<тип> v(n, vv): contaiпег<тип> v(beg. end); contaiпег<тип> vl(v2); // пустой контейнер, не содержащий // элементов // контейнер из п элементов // контейнер из п элементов, равных w // контейнер, инициализированный элементами // интервала итераторов [beg, end) // контейнер vl - копия контейнера v2 В первом случае память резервируется, но элементы в контейнере отсутствуют. Второй и третий варианты позволяют задать первоначальное количество элемен- тов. Количество элементов п может задаваться не только константой, но и произ- вольным выражением, вычисляемым во время работы программы. Если инициа- лизирующее значение w не задано, выполняется инициализация нулем. Конструктор с аргументами-итераторами позволяет создавать контейнеры из лю- бого другого контейнера с элементами того же типа. В качестве исходного кон- тейнера может использоваться стандартный массив — аргументами в этом слу- чае служат указатели. Для любого стандартного контейнера реализован конструктор копирования. Все последовательные контейнеры предоставляют методы для определения и из- менения размера контейнера: bool empty О const; size_type size 0 const; size_type max_size 0 const; void resize (size_type); void resize (size_type, T); // есть ли элементы в контейнере? // количество элементов в контейнере // максимально возможное количество // изменение количества элеметов // то же с инициализацией новых Вектор предоставляет еще два метода, обеспечивающие работу с памятью: size_type capacity О const; void reserve (size_type); Все последовательные контейнеры предоставляют доступ к элементам с по- мощью итераторов. Для вектора и двусторонней очереди реализован итератор произвольного доступа, а список поддерживает двунаправленный итератор (см. табл. 10.1). Все последовательные контейнеры обеспечивают следующие ме- тоды для установки значений итераторов: iterator begin О; constjterator begin О const; iterator end (); constjterator end 0 const; reverse_iterator rbegin (); const_reverse_iterator rbegin () const; reversejterator rend 0; const_reverse_iterator rend () const; Все контейнеры обеспечивают доступ к первому и последнему элементам: reference front О; const_reference front О const; reference back О; const_reference back 0 const:
Краткие сведения по теме 195 Вектор и двусторонняя очередь предоставляют прямой доступ по индексу в двух вариантах: reference operator[] (size_type); const_reference operators (size_type) const; reference at (size_type); const_reference at (size_type) const: Операция индексирования [] не проверяет корректность индекса — это делает метод at О, который в случае ошибки генерирует исключение out_of range. Следующие методы-модификаторы обеспечиваются всеми контейнерами: void push_back (const Т&); // вставка в конец void pop_back О: // удаление последнего iterator insert (iterator, const T&); // вставка после заданного void insert (iterator, size_type, const T&): template <class Inputiterator» void insert (Iterator, Input Iterator, Inputiterator): // удаление элементов iterator erase (iterator): iterator erase (iterator, iterator); void clear О; // удаление всех элементов void swap (containers. Allocators); // обмен с контейнером того же вида Двусторонняя очередь и список позволяют выполнять вставку и удаление в на- чале контейнера: void push_front'(const Т&); void pop_front О; Список имеет ряд специальных функций: void splice (iterator, lists, Allocators); void splice (iterator, lists, Allocators. iterator): void splice (iterator, lists, Allocators, iterator, iterator); void remove (const T&); template <class Predicate> void removejf (Predicate); void unique 0: template <class BinaryPredicate> void unique (BinaryPredicate): void merge (lists. Allocators); template <class Compare> void merge (lists, Allocators, Compare); void sort 0; template <class Compare> void sort (Compare); void reverseO; Эти методы являются специализированными версиями стандартных алгорит- мов, оптимизированных именно для работы со списками. Методы splice() выполняют простое слияние (сцепление) списков, не учитывая сортировку. Элементы списка-аргумента вставляются в заданную позицию теку- щего списка. Метод reverseO переставляет элементы списка в обратном порядке. Четыре метода (sortO, mergeO, uniqueO, removeO) представлены в двух видах: в обычном и в виде шаблона. Обычный метод выполняет операцию по умолча- нию, метод-шаблон получает в качестве аргумента функтор, который и исполь-
196 Глава 10. Стандартная библиотека шаблонов зуется при выполнении операции. Методы sortO сортируют список по возраста- нию. Если оба списка уже упорядочены, методы merge() присоединяют элементы списка-аргумента к текущему списку с сохранением порядка сортировки. Мето- ды unique О удаляют дубликаты элементов списка, оставляя только по одному экземпляру (список должен быть отсортирован). Метод removeO удаляет все эле- менты, имеющие указанное значение; метод-шаблон remove! f () удаляет из спи- ска все элементы, для которых предикат возвращает значение true. Для всех последовательных контейнеров реализован полный набор операций сравнения и функция обмена. Эти функции не являются членами класса-кон- тейнера и реализованы как внешние функции-шаблоны (вместо container можно подставить vector, list или deque ): template <class T> bool operator== (const containers, Al locators. const containers,Allocators); template <class T> bool operators (const containers. Al locators, const containers,Allocators); template <class T> bool operator< (const containers, Al locators, const containers, Al locators); template <class T> bool operator* (const containers, Al locators. const containers, Al locators); template <class T> bool operators (const containers, Al locators, const containers, Al locators); template <class T> bool operator» (const containers, Al locators, const containers, Al locators); template <class T, class Allocator» void swap (const containers,Allocators, const containers.Allocators); Аргументами должны быть контейнеры одного типа. Равенство одного контей- нера другому означает следующее: размеры одинаковы, все элементы одного равны элементам другого. Один контейнер не равен другому, если у них разные размеры, или разные элементы. Сравнение на меньше-больше выполняется лек- сикографически, что означает следующее: □ контейнер с меньшим количеством элементов считается меньшим контейне- ром; □ если размеры равны, выполняется попарное сравнение элементов, и резуль- тат определяется первой несовпадающей парой. Адаптеры последовательных контейнеров Стандартные адаптеры реализованы на основе двусторонней очереди, которая определена как поле класса-шаблона: template <class Т, class Container = dequeS> > class stack
Краткие сведения по теме 197 {/*...*/}; template <class Т. class Container = deque<T> > class queue {/*...*/}; Интерфейс — традиционный: // методы стека bool empty О const: // стек пуст? size_type size () const: // количество элементов value_type& top 0: // элемент на вершине const value_type& top 0 const: // элемент на вершине void push (const value_type&): // положить в стек vold pop (): // методы очереди // удалить элемент из стека bool empty () const; // очередь пуста? size_type size () const; // количество элементов value_type& front (): // первый элемент const value_type& front 0 const: // первый элемент value_type& back (); // последний элемент const value_type& back () const: // последний элемент void push (const value_type&); // поставить в очередь void pop 0; // удалить из очереди Адаптеры не поддерживают доступ к внутренним элементам контейнера ни по итератору, ни по индексу — стеки и очереди имеют определенную дисциплину доступа к элементам только на концах. Помимо методов, реализован полный набор операций сравнения, как и для стан- дартных последовательных контейнеров. Стеки и очереди объявляются аналогично другим контейнерам. Однако разре- шается объявлять стеки и очереди с использованием других контейнеров вместо двусторонней очереди. Допускается создавать контейнер на базе одного типа контейнера, а инициализировать другим. Для очереди в качестве базы нельзя ис- пользовать вектор — он не обеспечивает удаление элемента в начале. Класс-шаблон priority_queue<> реализует очередь, в которую элементы вставля- ются не в порядке поступления, а в порядке приоритета. Очередь с приоритета- ми реализована на основе вектора. template <class Т, class Container = vector<T>, class Compare = less<typename Container::value_type> > class priority_queue {/*...*/}: Вместо вектора для очереди с приоритетами можно использовать двустороннюю очередь, а список — нельзя, так как список не поддерживает итератор произволь- ного доступа. Помимо типа элемента и «базового» контейнера-аргумента, в шаблоне задан тре- тий параметр — функтор, обеспечивающий сравнение приоритетов. По умолча- нию элементы очереди с приоритетами сортируются в порядке убывания. Можно задать другой критерий сортировки, указав бинарный предикат в виде функции или функтора:
198 Глава 10. Стандартная библиотека шаблонов priority_queue«double. deque«double». greater<double> > pq; Такое объявление определяет согласованную по приоритетам очередь с элемен- тами, упорядоченными по возрастанию. Конструкторы позволяют объявлять очереди тем же образом, что и другие по- следовательнее контейнеры. Очередь с приоритетами предоставляет минимально необходимый набор методов: bool empty О const; size_type size () const: const value_type& top () const; void push (const value_type&); void pop(): // очередь пуста? // количество элементов // первый элемент очереди // поставить в очередь // удалить из очереди Ассоциативные контейнеры Элементы ассоциативных контейнеров представляют собой пары «ключ-значе- ние». Зная ассоциированный со значением ключ (key), можно получить доступ к значению, которое часто называется отображаемым значением. В библиотеке реализован шаблон pai г для представления разнородных пар значений. Ассоциа- тивные контейнеры опираются на тип pair для представления пары «ключ-зна- чение». Структура pair определена в заголовочном файле «utility». template «class TI, class T2> struct pair { typedef TI first_type; typedef T2 second_type: TI first; T2 second; pair():first(T10), second(T20) {} pair(const T1& x. const T2& y):first(x), secondly) {} template <class V, class U> pair (const pair <V, U>& p): first(P.first). second(p.second) {} -pairO; } Для пар определен полный набор попарных сравнений: template «class TI. class T2> inline bool operator==(const pair«Tl,T2>& x, const pair<Tl,T2>& y); template <class TI. class T2> Inline bool operator<(const pair«Tl,T2>& x. const pair<Tl,T2>& y); template <class TI, class T2> bool operator!’ (const pair<Tl, T2>&, const pair<Tl, T2>&); template «class TI, class T2> bool operator» (const pair«Tl, T2>&, const pair<Tl, T2>&); template «class TI, class T2> bool operator<= (const pair«Tl, T2>&, const pair«Tl, T2>&); template «class TI, class T2> bool operator»’ (const pair«Tl, T2>&, const pair«Tl, T2>&); Библиотека обеспечивает функцию-шаблон make pairO, чтобы упростить конст- руирование пар.
Краткие сведения по теме 199 template <class Tl, class T2> inline pair<Tl.T2> make_pair(const T1& x, const T2& y) { return pair<Tl,T2>(x, y); } Эту функцию можно использовать вместо конструктора пары: функция обладает тем преимуществом, что при конструировании пары нет необходимости задавать типы элементов. Интерфейс ассоциативных контейнеров Ассоциативные контейнеры тоже обладают сходным интерфейсом, который сде- лан похожим на интерфейс последовательных контейнеров настолько, насколь- ко это возможно. template <class Key, class Compare = less<Key>, class Allocator = allocator<Key> > class set {/*...*/}; template <class Key, class Compare = less<Key>, class Allocator = allocator<Key> > class multiset { /* ... */ }: template <class Key. class T. class Compare = less<Key>. class Allocator = allocator<pair<const Key, Т» > class map {/*...*/}: template <class Key. class T, class Compare = less<Key>, class Allocator = allocator<pair<const Key, Т» > class multimap {/*...*/}: Во всех контейнерах присутствует параметр-ключ Key и параметр-критерий упо- рядочения Compare, который по умолчанию является стандартным функтором lesso (см. далее). По умолчанию элементы контейнеров упорядочены по возрастанию ключа. В контейнерах-множествах сам ключ является значением, а в контейне- рах-отображениях определен параметр Т, определяющий тип значений, связан- ных с ключом. В начале контейнеров-множеств определены типы: typedef Key key_type: typedef Key value_type; typedef Compare key_compare: typedef Compare value_compare; typedef Allocator allocator_type: typedef typename Al 1ocator::size_type size_type: typedef typename Allocator::difference_type difference_type; typedef typename Allocator: .-reference reference; typedef typename Al 1ocator::const_reference const_reference; class iterator; class constjterator: typedef typename std: :reversejterator<1terator> reversejterator: typedef typename std: :reversejterator<constjterator> const_reverseJterator;
200 Глава 10. Стандартная библиотека шаблонов В начале контейнеров-отображений определены типы, которые отличаются от типов контейнеров-множеств только в части типов для значений: typedef Key key_type: typedef T mapped_type; typedef pair<const Key, T> value_type: typedef Compare key_compare; typedef Allocator allocator_type: typedef typename Allocator::size_type size_tYPe: typedef typename Allocator::difference_type difference_type: typedef typename Allocator: reference reference: typedef typename Allocator::const_reference const_reference: class iterator: class const_iterator; typedef typename std::reverse_iterator<iterator> reverse_iterator; typedef typename std::reverse_iterator<const_iterator> const_reverse_i terator; Конструкторы обеспечивают следующие способы создания (если не использо- вать критерий сортировки) ассоциативных контейнеров: contaiпег<тип> sO: // пустой контейнер contaiпег<тип> sl(sO); // копирование contaiпег<тип> s2(begin, end): // инициализация диапазоном В последнем случае допустимы итераторы любого стандартного контейнера (или указатели массива). Критерий сортировки можно задавать либо как параметр шаблона, либо как аргумент конструктора. Ассоциативные контейнеры, как и последовательные, предоставляют стандарт- ный доступ по итераторам — прямым и обратным. Методов определения размеров только три: bool empty О const; size_type size () const; size_type max_size 0 const: Методы удаления и вставки на концах отсутствуют; при вставке всегда выполня- ется поиск по ключу. Все ассоциативные контейнеры предоставляют следующие методы вставки и удаления: iterator insert (iterator, const value_type&): template <class Inputlterator> void insert (Inputiterator. Inputiterator): void erase (iterator); size_type erase (const key_type&): void erase (iterator, iterator); void clear (); Итератор в методе вставки значения используется как начальная позиция для поиска места вставки. Для каждого ассоциативного контейнера, как и для последовательных, опреде- лен метод swapO — обмен данными с контейнером того же типа. Один метод вставки отличается для контейнеров с неповторяющимися элемен- тами (set и тар) и для мультиконтейнеров (multiset и multimap):
Краткие сведения по теме 201 Iterator Insert (const value_type&); // мультиконтейнер pair<iterator. bool> insert (const valuejype&i 11 одноэлементный В мультиконтейнере элемент просто вставляется, и возвращается итератор на него. В одноэлементном контейнере просто вставить нельзя, так как элемент может уже присутствовать. Если такой элемент есть, возвращается объект-пара: □ в поле first заносится значение итератора — позиция существующего эле- мента; □ в поле second возвращается false. Во всех ассоциативных контейнерах реализованы методы, возвращающие объек- ты-функторы для сравнения ключей и значений: key_compare key_comp() const: value_compare value_comp() const: Используются эти методы одинаково. Например, для множества: set <int. less<1nt> > si; set<int. less<int> >::key_compare kcl = sl.key_comp() : bool resultl = kcl(2,3) ; Результат в данном случае равен true, так как 2 < 3. В ассоциативных контейнерах реализовано несколько методов поиска: iterator find (const key_value&): constjterator find (const key_value&) const: size_type count (const key_type&) const; iterator lower_bound (const key_type&); const_iterator lower_bound (const keyjype&) const; iterator upper_bound (const key_type&); constjterator upper_bound (const key_type&) const: palr<iterator. iterator> equal_range (const key_type&): pair<constJterator. constjterator> equal_range (const keyjype&) const; Метод countO для одноэлементных контейнеров выдает только 0 или 1. Для мультиконтейнеров вычисляется количество элементов с заданным ключом. Ме- тод find() находит первый (а для одноэлементных контейнеров — и единствен- ный) элемент с заданным ключом. Остальные методы применяются в мульти- контейнерах для определения набора одинаковых элементов. В одноэлементных контейнерах они работают как метод find(). Для контейнера-отображения тар определена операция доступа по индексу mapped_type& operator^ (const keyjype&); Аргумент — ключ, результат — отображаемое значение. Именно этот метод и реа- лизует ассоциативность контейнера. Контейнер-отображение можно предста- вить себе как вектор, у которого индекс не обязательно должен быть целым. Для каждого ассоциативного контейнера, как и для всех последовательных кон- тейнеров, определен полный набор операций сравнения, а также шаблон функ- ции обмена swapO, которые тоже реализованы в виде внешних функций.
202 Глава 10. Стандартная библиотека шаблонов Стандартные функторы Библиотека STL предоставляет программисту шесть стандартных арифметиче- ских функторов (табл. 10.2) и девять функторов-предикатов (табл. 10.3). Чтобы использовать стандартные объекты-функторы, к программе должен быть под- ключен заголовочный файл #include <functional> Стандартные функторы являются наследниками шаблона бинарной функции binary_function. Только один функтор является наследником шаблона унарной функции binary function — функтор negated. Арифметические функторы реализованы однотипно. Например, реализация функтора modul us выглядит так: template <class Т> struct modulus: binary_function<T. T, T> { Т operatorO (const Т& a, const Т& b) const { return (аЖЬ); } Таблица 10.2. Стандартные арифметические функторы Объект-функтор Реализуемая операция plus<T> x + у (сложение) minus<T> х - у (вычитание) multiplies<T> х * у (умножение) divides <Т> х / у (деление) modulus<T> х % у (остаток от деления) negate<T> -х (изменение знака) Таблица 10.3. Стандартные логические функторы Объект-функтор Реализуемая операция logical_and<T> x && у (логическое И) logical_or<T> х || у (логическое ИЛИ) logical_not<T> !х (логическое отрицание) equal_to<T> х == у (равенство) not_equal_to<T> х != у (неравенство) greater<T> х > у (больше) less<T> х < у (меньше) greater_equal<T> х >= у (больше или равно) less_equal<T> х <= у (меньше или равно)
Краткие сведения по теме 203 Стандартные функторы-предикаты являются наследниками binary function, на- пример: template <cl^ss Т> struct less: binary_function<T, T, bool> { bool operatorO (const T& a. const T& b) const { return a<b; } Функтор less<> является критерием по умолчанию при сортировке ассоциатив- ных контейнеров и при сравнении объектов. Пользователь вправе писать собственные функторы-наследники, например: template <class Arg> class times_x: public unary_function<Arg. Arg> { Arg multiplier: public: times_x(const Arg& x) : multiplier(x) { } Arg operatorO (const Arg& x) { return (x * multiplier): } }: Этот функтор просто умножает аргумент на заданный множитель. Адаптеры функторов Помимо функторов, библиотека поддерживает и адаптеры функторов. Все адап- теры устроены однотипно: реализован класс-наследник от базового шаблона unaryfunction или binary function, а затем реализована функция-шаблон, возвра- щающая значение нужного класса. В библиотеке STL реализованы два адаптера- связывателя (binders): □ bindlst(op,value) — фиксирует первый аргумент бинарного функтора; □ bind2nd(op, value) — фиксирует второй аргумент бинарного функтора. Адаптеры-«отрицатели» (negators) представляют собой реализацию в виде функ- тора операции логического отрицания ! (not): □ notl(op) выполняет логическое отрицание для унарного предиката !ор(ргш); □ not2(op) выполняет отрицание для бинарного предиката !op(prml,prm2). Библиотека предоставляет несколько адаптеров для указателей на функции и ме- тоды: □ адаптер для указателей на функцию ptrfun; □ адаптеры для методов memfun и memfunref. Хотя в библиотеке реализован единственный адаптер ptr fun для функций, он используется одинаково и для унарных функций, и для бинарных. Адаптер mem fun ref для указателя на метод применяется в случае методов без аргументов или методов с одним аргументом. Адаптер mem fun для указателя на метод приме- няется в случае контейнера, элементами которого являются указатели.
204 Глава 10. Стандартная библиотека шаблонов Стандартные обобщенные алгоритмы В стандарте [1] алгоритмы STL делятся на четыре группы: □ алгоритмы, не изменяющие элементы последовательности; □ алгоритмы, модифицирующие элементы последовательности; □ сортирующие алгоритмы и родственные им; □ вычислительные алгоритмы. Чтобы использовать алгоритмы первых трех групп, нужно задать в программе директиву #lnclude <algorithm> Для использования вычислительных алгоритмов к программе должен быть при- соединен заголовочный файл #include <numeric> Все алгоритмы реализованы как шаблоны функций. Все алгоритмы обрабатыва- ют входную последовательность элементов контейнера, задаваемую парой итера- торов [first, last). Пара итераторов всегда представляет собой полуоткрытый интервал: второй итератор служит «стражем» — элемент, на который он указы- вает, алгоритмом не обрабатывается. Последним обрабатываемым элементом по- следовательности является элемент last - 1. Для прямого итератора позиция first обязательно должна быть левее позиции last. Позиция last должна быть достижима, если начать с first и последователь- но применять оператор инкремента. Однако компилятор проверить это не в со- стоянии, поэтому если требование не будет выполнено, поведение программы не определено — обычно это заканчивается крахом. Для обратного итератора — наоборот: начальная позиция итератора должна быть правее конечной. Многие алгоритмы представлены в двух модификациях: с параметром-функто- ром и без него. В некоторых случаях наличие функтора отмечается суффиксом if в имени алгоритма, например find() и find ifO. В других случаях наличие функтора никак в имени не фиксируется. Например, алгоритм sortO в библио- теке имеет две модификации — без функтора и с функтором: template <class RandomAccessIterator void sort (RandomAccessIterator first. RandomAccessIterator last); template <class RandomAccessIterator, class Compare> void sort (RandomAccessIterator first, RandomAccessIterator last, Compare comp): Алгоритмы, модифицирующие контейнер, обычно имеют две версии: одна моди- фицирует элементы контейнера по месту, а вторая записывает преобразованные элементы в новый контейнер. Во втором случае в имени алгоритма присутствует суффикс сору. Однако не у всех алгоритмов, модифицирующих контейнер, пре-
Краткие сведения по теме 205 дусмотрена такая версия. Например, алгоритм сортировки sort() всегда сортиру- ет контейнер по месту. Большинство алгоритмов применимы только к последовательным контейнерам. Так как ассоциативные контейнеры являются упорядоченными по определению, то сортирующие и перестановочные алгоритмы к ним применять не имеет смыс- ла. Кроме того, в модифицирующих алгоритмах ассоциативные контейнеры не могут быть приемниками. Все алгоритмы для удобства изложения распределены на несколько категорий. Деление достаточно условное: некоторые алгоритмы можно отнести к несколь- ким разным группам. В приложении Б алгоритмы рассматриваются в алфавит- ном порядке (кроме алгоритмов работы с кучей — они практически не применя- ются начинающими программистами). Алгоритмы поиска Шестнадцать алгоритмов предоставляют различные способы поиска определен- ного значения в контейнере (в интервале): □ maxelementO — поиск максимального элемента в интервале; □ minelementO — поиск минимального элемента в интервале; □ count () — подсчет количества элементов в интервале; □ countifO — подсчет в интервале количества элементов, удовлетворяющих заданному предикату; □ find О — поиск заданного элемента в интервале; □ fi nd_i f () — поиск в интервале элемента, удовлетворяющего заданному преди- кату; □ binarysearchO — проверка вхождения заданного элемента в упорядоченный интервал; □ lower boundO — бинарный поиск в упорядоченном интервале первого элемен- та, большего либо равного заданному; □ upper boundO — бинарный поиск в упорядоченном интервале первого элемен- та, большего чем заданный; □ search nO — поиск интервала из п элементов, равных заданному элементу; □ equal rangeO — бинарный поиск в упорядоченном интервале интервала эле- ментов, равных заданному элементу; □ searchO — поиск в одном интервале первого вхождения другого; □ findendO — поиск в одном интервале последнего вхождения другого; □ findfirstofO — поиск в первом интервале одного из значений второго ин- тервала; □ adjacentfindO — поиск первой пары совпадающих элементов; □ mismatch О — ищет первые несовпадающие элементы в двух интервалах.
206 Глава 10. Стандартная библиотека шаблонов Алгоритмы сортировки и перестановок Алгоритмы сортировки и упорядочения предлагают различные способы упоря- дочения элементов контейнера (интервала). Устойчивый (stable) алгоритм сохра- няет относительный порядок элементов с одинаковыми значениями или удовле- творяющих одному и тому же предикату. Разбиение (partition) — это разделение элементов контейнера на две группы: удовлетворяющие и не удовлетворяющие некоторому условию. Алгоритмы сортировки нельзя применять к списку и ассо- циативным контейнерам. □ sortO — сортировка интервала; □ stablesortO — устойчивая сортировка интервала; □ parti al sort О — сортировка начальной части интервала; □ partialsortcopyO — то же, с копированием результата в другой контейнер; □ nth elementO — элементы, меньшие n-го, перемещаются левее его, большие — перемещаются правее; □ reverseO — переставляет элементы интервала в обратном порядке; □ reverse_copy() — то же с копированием результата в другой контейнер; □ rotate О — циклическая перестановка элементов интервала; □ rotate_copy() — то же с копированием результата в другой контейнер; □ partitionO — перестановка элементов, удовлетворяющих заданному предика- ту, в начало интервала; □ stablepartitionO — то же с сохранением относительного порядка элементов; □ random shuffleO — случайная перестановка элементов интервала; □ nextpermutatlonO — получить следующую перестановку; □ prevpermutationO — получить предыдущую перестановку. Алгоритмы заполнения и модификации Шесть алгоритмов генерирования и модификации либо создают и заполняют новую последовательность, либо изменяют значения в существующей. □ fill О — заполняет интервал заданным значением; □ fill nO — заполняет первые п элементов контейнера заданным значением; □ generate() — заполняет интервал значениями, вычисляемыми заданным функ- тором (функцией); □ generate nO — заполняет первые п элементов контейнера значениями, вычис- ляемыми заданным функтором (функцией); □ for eachO — применяет заданный функтор (функцию) ко всем элементам за- данного интервала; □ transform() — применяет заданный функтор к заданному интервалу (интерва- лам), получая новую последовательность элементов.
Краткие сведения по теме 207 Алгоритмы подстановки и удаления Алгоритмы удаления и подстановки предоставляют различные способы замены или удаления одного элемента или целого диапазона из заданного интервала. □ сору() — копирование исходного интервала в новый контейнер; □ copy_backwards() — копирование исходного интервала в новый контейнер в об- ратном порядке; □ remove О — удаляет из интервала все элементы, равные заданному; □ remove_copy() — то же с копированием в новый контейнер; □ remove_if() — удаляет из интервала все элементы, удовлетворяющие заданно- му предикату; □ remove i f_copy() — то же с копированием в новый контейнер; □ uniqueO — удаляет из интервала одинаковые соседние элементы, оставляя един- ственный; □ uniquecopyO — то же с копированием результата в новый контейнер; □ repl асе О — заменяет все элементы интервала, равные заданному, на новый; □ repl асе_сору() — то же с копированием результата в новый контейнер; □ repl ace i f () — заменяет все элементы интервала, удовлетворяющие заданно- му предикату, на новый; □ replace copy_if() — то же с копированием результата в новый контейнер; □ Iter swapO — обменивает значения элементов, адресованных парой итерато- ров, но не модифицирует сами итераторы; □ swapO — обменивает значения объектов; □ swap rangeO — обменивает значения из двух интервалов. Алгоритмы сравнения В библиотеке реализовано несколько разных алгоритмов сравнения одного кон- тейнера (интервала) с другим: □ шах(), ш1п() — сравнение двух элементов; □ equal () — проверяет равенство двух интервалов; □ includes() — проверяет, входит ли один упорядоченный интервал в другой; □ lexicographical_compare() — сравнивает два интервалащопарно лексикографи- чески. Алгоритмы объединения интервалов Четыре алгоритма этой категории реализуют теоретико-множественные операции с контейнерами любого типа. Исходные последовательности элементов должны быть отсортированы по возрастанию. □ set_un1on() — объединение множеств-интервалов; □ set_1ntersection() — пересечение множеств-интервалов □ set_difference() — разность множеств-интервалов; □ set_symmetric_difference() — симметричная разность множеств-интервалов.
208 Глава 10. Стандартная библиотека шаблонов Два алгоритма выполняют слияние двух упорядоченных последовательностей: все элементы обоих интервалов попадают в результирующую последовательность. □ merge О — запись объединенной последовательности в новую; □ inpl асе mergeО — слияние на месте. Численные алгоритмы Следующие четыре алгоритма реализуют численные операции с контейнером. Для их использования необходимо включить заголовочный файл <numeric>. □ accumulateO — накопление; □ parti al _sum() — частичные суммы; □ innerproductO — скалярное произведение; □ adjacent differenceO — частичные разности. Примеры // соруО и copy_backward() Int dl[4] = {1,2,3,4}: int d2[4] = {5.6.7.8}: vector<1nt> vKdl.dl + 4), v2(d2,d2 + 4). v3(d2.d2 + 4): vector<1nt> v4: copy(vl.beginO ,vl.endO,v2.beginO): copy_backward(vl.beginO, vl.endO ,v3.endO); ostream_iterator<int.char> out(cout." ”); copy(v4.begin(),v4.end().out); Первый вызов соруО скопирует все элементы vl в v2. Вызов copybackwardO ско- пирует элементы vl в v3 — обратите внимание, что третий аргумент указывает на конец целевого контейнера, то есть копирование выполняется от последних эле- ментов к первым. В последнем случае сначала определяется выходной потоко- вый итератор out, который затем используется для вывода значений вектора v4 в выходной поток. Итераторы вставки обычно применяются совместно с алгоритмом копирования. int а[10] = { 1.2.3.4,5.б,7,8.9.О }: vector<1nt> vCa, а+10): 1ist<int> L: // пустой список copy(v.beginO, v.endO. b'ack_inserter(L)): // вставка в пустой список // вставка в пустой список в обратном порядке copy(v.begin(), v.endO. front_inserter(L)): // вставка в список в обратном порядке - второй вариант сору(v.begiпС). v.endO, inserter(L. L.beginO)): Функторы и адаптеры функторов также используются в алгоритмах. // поиск в контейнере v первого элемента, меньшего 5 pos = find_if(v.beginO. v.endO, bind2nd(less<1nt>0.5)); // поиск в контейнере первого четного элемента pos = f1nd_if(v.beginO. v.endO. notl(bind2nd(modulus<tnt>(),2))); // поиск первого непростого числа в контейнере v // IsPrimeO - внешняя функция
Краткие сведения по теме 209 pos = find_if(v. begind, v.endd. notl(ptr_fun(isPrime))); // поиск первой строки, не совпадающей с "Adamenko" pos = find_1f(v.begi n(),v.end().notl(bi nd2nd(ptr_fun(strcmp)."Adamenko"))); // в классе Person есть метод printAddressO для вывода адреса персоны // контейнер v содержит элементы типа Person // вывод на экран адресов всех персон for_each(v.begin(). v.endO, mem_fun_ref(&Person::pri ntAddress)): 11 контейнер v содержит указатели на объекты типа Person for_each(v.begind, v.endO. mem_fun(&Person: :printAddress)); Многие алгоритмы просты и их действия интуитивно понятны, поэтому в при- мерах демонстрируется работа только наиболее интересных алгоритмов. И accumulated typedef vector<int>::Iterator iterator: int d[10] = {1,2.3.4.5.6.7.8.9,10}; vector<1nt> vl(dl, dl+10); Int sum = accumulate(vl. begind. vl.endO, 0); int prod = accumulated!.begind, vl.endO, 1. multi pl 1es<int>d): // сцепление строк vector<string> rgs: rgs.push_back("This rgs.push_back("is "); rgs.push_back("one "); rgs.push_back("sentence. cout « accumulate(rgs.begin().rgs.endO,string("")) « endl; Первый вызов алгоритма вычисляет сумму элементов вектора vl, равную 55, второй — произведение всех элементов, равное 10! = 3 628 800. Однако, как по- казывает последний вызов, алгоритм можно применять не только с числовыми данными, но с любым типом, для которого определены соответствующие опера- ции. Последний вызов демонстрирует применение accumulated со строками. На- чальное значение — пустая строка, а операция сложения — операция сцепления строк. // inner_product() int al[3] = {6, -3. -2}: int a2[3] = {-2. -3. -2}; list<1nt> Hal. al+3): vector<int> v(a2. a2+3); int prodl = inner_product(l .begind. l.endd, v.beginO, 0); int prod2 = inner_product(1 .begind. l.endd, v.beginO. 1, plus<int>(), minus<int>d); int bl[5]={l. 2. 3. 4. 5}; int b2[5]={l. 4. 9, 16. 25}; int ip2 = inner_product(bl. bl+5, b2, 1. multiplies<int>d,plus<int>d); Первый вызов вернет значение 0 + 6 х (-2) + (-3) х (-3) + (-2) х (-2) = 1. Во втором случае результат вычисляется как 1 + (6 - (-2) + (-3) - (-3) + (-2) - (-2)) = 9. Третий вызов вычисляет значение 1 х (1 + 1) х (2 + 4) х (3 + 9) х (4 + 16) х (5 + 25) = 86 400. // generated. generatejid template <class Т>
210 Глава 10. Стандартная библиотека шаблонов class gen_value { private: Т v; public: gen_value(const T& val): v(val) {} T& operatorO О { v += v: return v: } }: // ... int d[4] = {1.2.3.4}: gen_value<1nt> g(l): // инициализация функтора vector<int> vKdl.dl + 4), v2(dl,dl + 4): vector<1nt> v3; generate(vl.beginO, vl.endO, g): generate_n(v2.begin(), 3, g); generate_n(back_1nserter(v3), 5, g); g'enerate_n(ostream_iterator<1nt>(cout," "). 3. g): В начале определяется функтор gen value, который удваивает свой аргумент. В функторе определен конструктор инициализации. Затем объявляются контей- неры и объект-функтор д. Первый вызов generate О полностью заменит элементы вектора vl, поместив в него значения {2, 4, 8, 16}; второй вызов generatenO заме- нит в векторе v2 первые три элемента: элементы вектора станут равны {2, 4, 8, 4}; третий вызов generate nOобратится к итератору вставки в конец и добавит в пустой вектор v3 пять чисел {2, 4, 8, 16, 32}; в четвертом случае используется по- токовый итератор вывода, который выведет на экран три числа 2, 4, 8 (через про- белы). Листинг 10.1. Пример использования алгоритма for_each() template <class Туре> class MultVal { private: Type Factor: public: MultVal(const Type& V ):Factor(V) { } void operatorO(Type& elem ) const { elem *= Factor; } }: // Функтор, вычисляющий среднее арифметическое class Average { private: long num: // количество элементов long sum; // сумма элементов public: AverageO:num(0), sum(O) {} void operatorO(int elem) { num++: // увеличиваем количество sum += elem; // накапливаем сумму // функция преобразования возвращает среднее арифметическое operator doubled { return static_cast<double>(sum)/static_cast<double>(num): } }:
Краткие сведения по теме 211 // ... vector<int> vl; // заполняем вектор fordnt 1 = -4; i<=2; 1++) vl.push_back(i); // умножаем вектор на -2 for_each(vl.begind, vl.endO. MuitVal<int>(-2)); // умножаем вектор на 5 for_each(vl.begin(), vl.endO. MuitVal<1 nt>(5)); // вычисляем среднее арифметическое double av = for_each(vl.beg1n(). vl.endO, Averaged); Вначале объявлены два функтора: первый умножает аргумент на значение сво- его поля Factor, второй — вычисляет среднее арифметическое. Во втором функ- торе для этого определен метод-преобразование doubled. Исходный вектор за- полняется числами {-4, -3, -2, -1, 0, 1, 2}. Первый вызов for eachO умножает элементы вектора на -2, и вектор становится равным {8, 6, 4, 2, 0, -2, -4}. Второй вызов for eachO умножает эти значения на 5 и получается {40, 30, 20, 10, 0, -10, -20}. Третий вызов наиболее интересен и использует то, что алгоритм for eachO в качестве результата возвращает передаваемый ему как параметр функтор. Для преобразования функтора в значение типа double вызывается метод-преобразо- вание doubled. // removed и метод erased tempiate<class Arg> struct all_true : public unary_function<Arg, bool> { bool operatorO(const Arg& x){ return true; } }: int arr[10] = {1,2.3.4,5,6.7,8,9.10}; vector<int> v(arr. arr+10); // удаляем 7 vector<int>: iterator result = remove(v.begin(). v.endd, 7); v.erase(result, v.endd); // реальное удаление // удаление всех элементов после 4-го result = remove_if(v.begin()+4, v.endd, all_true<int>0); v.erase(result, v.endd): // реальное удаление copy(v.begi n(),v.end(), ostream_i terator<i nt,cha r>(cout." ")): Определен функтор-предикат all true, всегда выдающий true в качестве резуль- тата. Исходный вектор равен {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}. После вызова removed он станет равен {1, 2, 3, 4, 5, 6, 8, 9, 10, 10} — элементы сдвинулись на место числа 7. Итератор result показывает на второе число 10. Вызов метода erased реально удалит эту десятку из вектора, и вектор станет равен {1, 2, 3, 4, 5, 6, 8, 9, 10}. Сле- дующий вызов remove_1f() с элементами контейнера фактически ничего не сде- лает, но установит итератор result на число 5. Поэтому последующий вызов erased обрежет весь «хвост» вектора. Вызов соруО выведет в поток (использует- ся потоковый итератор вывода) значения 1, 2, 3, 4. // remove_if() const int VECTOR_SIZE = 8 ; vector<int > Numbers(VECTOR_SIZE); typedef IntVector; iterator Iterator; Iterator start, end. it, last; start = Numbers.begin();
212 Глава 10. Стандартная библиотека шаблонов end = Numbers.endO; NumberstO] = 10; Numbers[l] = 20; Numbers[2] = 10; Nunibers[3] = 15; Numbers[4] = 12; Numbers[5] = 7 ; Numbers[6] = 9 ; Numbers[7] = 10; last = remove_if(start. end. bind2nd(1 ess equal<int>(),10)) : for(it = start; it != end; 1t++) cout « *it « cout « endl; for(it = start: it != last: it++) cout « *it « cout « endl; В этом примере сначала объявляется и заполняется вектор. Исходное значение вектора равно {10, 20, 10, 15, 12, 7, 9, 10}. Вызов remove ifO «удаляет» числа, не большие 10. Оставляемые числа (20, 15 и 12) переносятся в начало вектора, и его значение становится таким {20, 15, 12, 15, 12, 7, 9, 10} — это показывает первый цикл вывода в поток cout. Итератор last установлен на второе число 15, поэтому второй цикл вывода в cout покажет только три числа: 20, 15, 12. // transformO vector<int> vl, v2(7), v3(7); for (int i = -4: i <= 2; i++) vl.push_back(i); // заполняем вектор // модификация vl по месту transform( vl.beginO. vl.endO, vl.beginO. MuitVal<1 nt>(2)): // умножаем каждый элемент vl на 5 и результат помещаем в v2 transfornK vl.beginO. vl.endO, v2.beg1n(), MuitVal<int>(5)); // второй вариант алгоритма // перемножаются элементы векторов vl и v2, результат помещается в v3 transform(vl.begin(). vl.endO, v2.begin(), v3.begin(), multipiies<1 nt>()); // заполнение контейнера v факториалами числового массива m // factorial О - внешняя функция int m[7] = {1.2.3,4,5,6.7}; transforms, m+7, v2.begin(), ptr_fun(factorial)); Объявляются векторы, и вектор vl заполняется числами {-4, -3, -2, -1, 0, 1, 2}. Первый вызов transformO умножает элементы вектора на 2, используя опреде- ленный выше функтор MultVal (см. листинг 10.1 — пример для for eachO), и век- тор становится равным {-8, -6, -4, -2, 0, 2,4}, Второй вызов transformO умножа- ет эти значения на 5 и помещает результат {--40, -30, -20, -10, 0, 10, 20} в вектор v2. Третий вызов алгоритма осуществляет поэлементное умножение векторов vl и v2, сохраняя результат {320, 180, 80, 20, 0, 20, 80} в v3. // uniqueO, unique_copy() bool mod_equal(int elemi, int elem2) { return abs(eleml) == abs(elem2); } // ... int a[] = {5.-5.5.-5.5,-5,5.-5.4.4.4,4,7}; vector<int> vl(a, a+sizeof(a)/sizeof(a[0]): vector<int>: iterator NewEndl, NewEnd2, NewEnd3; NewEndl = unique (vl.beginO, vl.endO): NewEnd2 = unique (vl.beginO, NewEndl. mod_equal); NewEnd3 = unique (vl.beginO. NewEnd2, greater<int>0); int al[20] - {4,5,5,9,-l.-l.-1,3.7,5,5,5,6,7,7,7,4.2.1,1}; vector<int> v(al+0, al+20), result: // создаем итератор вставки insert_iterator<vector<int> > ins(result. result.beginO): unique_copy(v.begin(). v.endO, ins); // копируем в result Сначала определяется функция mod equaK), выполняющая сравнение абсолют- ных величин аргументов. Затем определяется и инициализируется вектор vl.
Краткие сведения по теме 213 Первый вызов uniqueO преобразует исходную последовательность чисел в {5, -5, 5, -5, 5, -5, 5, -5, 4, 7, 4, 4, 7}. Итератор NewEndl устанавливается на первое число 4 после первого числа 7. Поэтому второй вызов uniqueO использует этот итератор как финишный. Получая в качестве аргумента функцию modequal О, он преобра- зует вектор в {5, 4, 7, -5, 5, -5, 5, -5,4, 7, 4, 4, 7}. Итератор NewEnd2 устанавливается на первое число -5 после первого числа 7. В третьем вызове используется стан- дартный функтор, с помощью которого из вектора «удаляются» элементы, большие предыдущих. В данном случае это первое число 7, однако с элементами вектора ни- каких перемещений не выполняется, просто итератор NewEnd3 выставляется на 7. // set_1nterection(), set unionO Int a2[6] = {2.4.6.8,10.12}: int a3[4J = {3.5.7,8}: set<int> even(a2+0, a2+6), result_u, small_u(a3+0,a3+4); // создаем итератор вставки для множества result_u 1nsert_1terator<set<int. less<int> > > res_u(result_u, result_u.begin()): set_union(small_u.begin(). small_u.end(). even.beginO, even.end(), res_u); int al[10] = {1,3.5,7.9,11}: int a3[4] = {3.5,7.8}: set<int> odd(al+0, al+6), result_i. small_i(a3+0,a3+4); insert_iterator<set<int. less<int> > > res_i(result_i, result_i.beginO); set_intersection(small_i .beginO. small_i ,end(), odd.beginO. odd.endO, res_i): Первым объявляются и заполняются множества. Затем объявляется итератор встав- ки в множество result и. Вызов set unionO разместит в множестве resultu объедине- ние множеств even и smallu {2, 3, 4, 5, 6, 7, 8, 10, 12}. Аналогично заполняется и мно- жество result i, которое равно пересечению множеств odd и small !: {3 ,5, 7}. // set_differenceO. set_symmetric_difference() int al[10] = {1,2,3.4.5.6.7.8.9.10}: int a2[6] = {2,4,6,8,10.12}: set<int> all(al+0, al+10), even(a2+0, a2+6), odd: // итератор вставки для множества odd insert_iterator<set<int. iess<int> > > odd_ins(odd. odd.beginO): set_difference(all .beginO, all.endO, even.beginO, even.endO. oddjns): В результате вызова setdifferenceO в множество odd попадут следующие эле- менты: {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} - {2, 4, 6, 8, 10, 12} = {1, 3, 5, 7, 9}. int al[] = {1,3,5,7.9.11}; int аЗ[] = {3,5,7,8}: set<1nt, less<int> > odd(al+0,al+6), result, small(a3+0.a3+4): insert_iterator<set<int> > res_ins(result, result.beginO); set_symmetric_difference(small .beginO. small ,end(), odd.beginO, odd.endO. res_ins); Вызов set symmetric differenceO запишет в множество result значения {1,8,9,11} — симметричную разность множеств {3, 5, 7, 8} и {1, 3, 5, 7, 9, И}. Алгоритмы группы set_ можно применять не только к множествам, но и к отсор- тированным последовательным контейнерам. bool mod_lesser(int elemi, int elem2) { return abs(eleml) < abs(elem2) } // ...
214 Глава 10. Стандартная библиотека шаблонов int а[5] = {-1. О, 1. 2. 3}: Int b[5] = {-3. -2. -1. О, 1}; vector<1nt> vla(a,a+5), vlb(b, b+5), vl(12); vector<int>::iterator Result2; // сортировка по возрастанию (по умолчанию) Resultl = set_union(vla.beg1n(), vla.endO. vlb.beginO, vlb.endd, vl.beginO); //результат vector<int> v2a(vla), v2b(vlb), v2(vl ); vector<int>: iterator Result2; // сортировка по убыванию sort(v2a.begin(), v2a.end(). greater<1nt>0): sort(v2b.begin(), v2b.end(), greater<int>O); Result2 = set_union(v2a.begin(), v2a.end(). v2b.begin(), v2b.end(), v2.begin(), // результат greater<int>0); 11 предикат vector<int> v3a(vla), v3b(vlb), v3(vl): vector<int>::iterator Results; // сотрировка по возрастанию абсолютных величин sort(v3a.begin(), v3a.end(), modjesser): sort(v3b.begin(). v3b.end(), modjesser): Results = set_union(v3a.begin(), v3a.end(), v3b.begin(), v3b.end(), v3.begin(), // результат modjesser); // предикат Объявляются и инициализируются векторы via, vlb массивами, в которых числа выстроены по возрастанию. Вызов set uniond занесет в вектор vl объединенный набор чисел {-3, -2, -1, 0, 1, 2, 3}. Затем объявляются и инициализируются век- торы v2a, v2b. Сортировка и объединение выполняются по убыванию, поэтому вектор-результат v2 будет содержать набор чисел в обратном порядке {3, 2, 1, О, -1, ~2, -3}. В последнем случае для сортировки применяется функция mod lesserO, которая сравнивает абсолютные величины чисел. Поэтому вектор v3 будет содержать набор чисел {0, 1, 2, 3}. // merged, inplace_merge() int dl[4] = {1.2.3.4}; int d2[8] = {11,13,15,17,12,14.16,18}: vector<int> vKdl.dl + 4), v2(dl,dl + 4); vector<int> v3(d2,d2 + 8),v4(d2,d2 + 8), v5(d2,d2 + 8).v6(d2,d2 + 8); vector<int> v7; merge(vl.begin(),vl.end().v2.begin(),v2.end(), v3.begin()); merge!vl.beginO ,vl.end() ,v2.begin() ,v2.end() ,v4.begin(), less<int>0); merge(vl.begin().vl.endO,v2.begin(),v2.end(), back_inserter(v7)); vector<1nt>::iterator mid = v5.begin(); advance(mid,4); // передвинули итератор на позицию 12 inplace_merge(v5.beginO,mid.v5.end()); // слияние v5 по месту Сначала, как обычно, объявляются и инициализируются контейнеры, причем значения упорядочены по возрастанию, как того требуют алгоритмы слияния. Первый вызов merged объединяет vl и v2, помещая результат {1, 1, 2, 2, 3, 3, 4, 4} в v3. Второй вызов делает то же самое — это просто демонстрация передачи пре- диката в качестве аргумента. Наконец, третий вызов merged показывает, как
Упражнения 215 можно использовать итератор вставки при слиянии контейнеров: результат по- мещается в пустой вектор v7. Для демонстрации работы алгоритма 1пр1асе_ merge О сначала объявляется и инициализируется итератор mid, который пере- двигается на вторую половину вектора v5. Вызов inplacemergeO преобразует по- следовательность {И, 13, 15, 17, 12, 14, 16, 18} в {И, 12, 13, 14, 15, 16, 17, 18}. Упражнения Последовательные контейнеры Написать функцию, с помощью которой подготовить текстовый файл input.txt, сохранив в него 100 случайных целых чисел в диапазоне от -50 до +50 по одно- му на строке. Файл возвращается функцией как результат. Написать функцию inputfileO, получающую файл как аргумент и возвращающую последовательный контейнер, заполненный числами из файла. Написать функцию modifyO, полу- чающую в качестве аргумента контейнер-результат функции inputfileO. Моди- фицированный контейнер возвращается в качестве результата. Добавить в кон- тейнер-результат вычисление суммы и среднего арифметического по абсолютной величине. В качестве контейнера использовать вектор, двустороннюю очередь и список. 1. Прибавить к каждому числу корень квадратный из произведения максимума и последнего числа. 2. Разделить каждое число на полусумму первого отрицательного и 50-го числа. 3. Вычесть из каждого числа набольшее число. 4. Умножить каждое число на минимальное из чисел. 5. Разделить все нечетные по абсолютной величине числа на среднее арифмети- ческое. 6. Вычесть из каждого числа сумму чисел. 7. Умножить каждое третье число на удвоенную сумму первого и последнего от- рицательных чисел. 8. Добавить к каждому числу первое.нечетное по абсолютной величине число. 9. Умножить каждое четное число на первое отрицательное число. 10. Добавить к каждому числу половину последнего отрицательного числа. И. Разделить на половину максимального значения. 12. Заменить все нули средним арифметическим. 13. Заменить все положительные числа квадратом минимума. 14. Добавить к каждому числу полусумму всех отрицательных чисел. 15. Добавить к каждому числу среднее наименьшего и наибольшего пО абсолют- ной величине. 16. Разделить на минимум и добавить максимум.
216 Глава 10. Стандартная библиотека шаблонов 17. Заменить все положительные числа на максимум. 18. Заменить каждое четное число по абсолютной величине на разность макси- мального и минимального чисел. 19. Заменить каждое второе отрицательное число половиной максимума. 20. Заменить каждое третье положительное число средним арифметическим. Выполнить задания 1-20, используя для ввода/вывода потоковые итераторы. Написать функцию outfileO, записывающую элементы контейнера в текстовый файл output.txt. Выполнить задания 1-20, реализовав функции modify О с аргументами-итерато- рами. Выполнить задания 1-20, используя для заполнения контейнера алгоритм generate!), а для обработки вместо собственной функции modify!) — алгоритм for eachO. Выполнить задания 1-20, используя для заполнения контейнера алгоритм generate!), а для обработки вместо собственной функции modify!) — алгоритм transform!). Следующие задания реализовать в виде класса-шаблона, используя в качестве контейнера последовательный контейнер. С контейнером выполняются опера- ции добавления в конец контейнера, удаления и замены элемента контейнера. Использовать методы контейнера. Реализовать ввод/вывод элементов с помощью потоковых итераторов. Основной операцией является операция поиска и выбор- ки подмножества контейнера по заданным критериям. Операцию поиска реали- зовать в двух вариантах: □ использовать алгоритмы последовательного поиска; □ сортировать исходный контейнер и использовать алгоритмы двоичного поиска. При поиске осуществлять сохранение выбранных записей в контейнер-очередь. Написать функцию, осуществляющую вывод очереди в текстовый файл в виде ведомости с подведением общих итогов. Вывести очередь в файл в обратном по- рядке, для чего применить контейнер-стек. 21. Счет в банке представляет собой структуру с полями: номер счета, код счета, фамилия владельца, сумма на счете, дата открытия счета, годовой процент на- числения. Поиск по номеру счета, дате открытия и владельцу. 22. Запись о товаре на складе представляет собой структуру с полями: номер склада, код товара, наименование товара, дата поступления на склад, срок хранения в днях, количество единиц товара, цена за единицу товара. Поиск по номеру склада, коду товара, дате поступления и сроку хранения (просро- ченные и не просроченные товары). 23. Запись о преподаваемой дисциплине представляется структурой: код дисцип- лины в учебном плане, наименование дисциплины, фамилия преподавателя, код группы, количество студентов в группе, количество часов лекций, коли- чество часов практики, наличие курсовой работы, вид итогового контроля (зачет или экзамен). Зачет — 0,35 ч на одного студента; экзамен — 0,5 ч на студента. Поиск осуществлять по фамилии преподавателя, коду группы, на- личию курсовой, виду итогового контроля.
Упражнения 217 24. Информационная запись о книге, выданной на руки абоненту, представляет собой структуру следующего вида: номер читательского билета, фамилия абонента, дата выдачи, срок возврата (количество дней), автор, название, год издания, издательство, цена. Поиск по полям: номер читательского билета, автор, издательство, дата возврата (просроченные). 25. Информационная запись о файле содержит поля: каталог, имя файла, расши- рение, дата и время создания, атрибуты «только чтение», «скрытый», «сис- темный», признак удаления, количество выделенных секторов (размер секто- ра принять равным 512 байт). Поиск выполнять по каталогу, дате создания, по признаку удаления. 26. Разовый платеж за телефонный разговор является структурой с полями: фа- милия плательщика, номер телефона, дата разговора, тариф за минуту разго- вора, скидка (в процентах), время начала разговора, время окончания разго- вора. Поиск по фамилии, дате разговора, номеру телефона. 27. Модель компьютера характеризуется кодом и названием марки компьютера, типом процессора, частотой работы процессора, объемом оперативной памя- ти, объемом жесткого диска, объемом памяти видеокарты, стоимостью компь- ютера в условных единицах и количеством экземпляров, имеющихся в нали- чии. Поиск по типу процессора, объему ОЗУ, памяти видеокарты и жесткого диска. 28. Список абонентов сети кабельного телевидения состоит из элементов сле- дующей структуры: фамилия, район, адрес, телефон, номер договора, дата за- ключения договора, оплата установки, абонентская плата помесячно, дата по- следнего платежа. Поиск по фамилии, району, дате заключения договора, дате последнего платежа. 29. Сотрудник представлен структурой Person с полями: табельный номер, номер отдела, фамилия, оклад, дата поступления на работу, процент надбавки, подо- ходный налог, количество отработанных дней в месяце, количество рабочих дней в месяце, начислено, удержано. Поиск по номеру отдела, полу, дате по- ступления, фамилии. 30. Запись о багаже пассажира авиарейса содержит следующие поля: номер рей- са, дата и время вылета, пункт назначения, фамилия пассажира, количество мест багажа, суммарный вес багажа. Поиск выполнять по номеру рейса, дате вылета, пункту назначения, весу багажа (превышение максимально допусти- мого). 31. Одна учетная запись посещения спорткомплекса имеет структуру: фамилия клиента, код и вид спортивного занятия, фамилия тренера, дата и время нача- ла, количество минут, тариф за минуту. Поиск по фамилии клиента и трене- ра, по виду занятия, по дате начала, по количеству минут (больше или мень- ше). 32. Одна запись о медикаменте содержит следующие поля: номер аптеки, назва- ние лекарства, количество упаковок, имеющееся в наличии в данной аптеке, стоймость одной упаковки, дата поступления в аптеку, срок хранения (в днях). Поиск по номеру аптеки, наименованию препарата, дате поступления.
218 Глава 10. Стандартная библиотека шаблонов 33. Одна запись журнала содержит поля: код игрушки, название игрушки, тип игрушки, возрастные границы (например, от 10 до 15), цена за единицу, коли- чество в наличии, дата поступления в магазин, поставщик. Поиск по дате по- ступления, поставщику, возрастным границам. 34. Один элемент — автомобиль — представляет собой в базе данных структуру с полями: фамилия владельца, код марки автомобиля, марка автомобиля, тре- буемая марка бензина, мощность двигателя, объем бака, остаток бензина, объ- ем масла. Дана фиксированная цена литра бензина и заливки масла. Поиск по марке автомобиля, марке бензина, мощности двигателя, фамилии вла- дельца. 35. Одна запись в журнале зимней экзаменационной сессии представляет собой структуру с полями: курс, код группы, фамилия студента, номер зачетной книжки, дисциплина, оценка за экзамен по дисциплине. Вычисляются сред- ние баллы по дисциплине, по группе, по курсу. Поиск по курсу, по группе, по номеру зачетной книжки, по фамилии, по оценкам. 36. Структура одной записи оплаты за коммунальные услуги содержит поля: но- мер дома, номер квартиры, фамилия владельца, вид платежа (квартплата, газ, вода, электричество), дата платежа, сумма платежа, процент пени, на сколько дней просрочен платеж. Поиск по номеру дома, квартиры, владельцу, виду платежа, по дате. 37. Одна запись счета за ремонтные работы содержит поля: название фирмы, вид работ, единица измерения, стоимость единицы выполненных работ, дата ис- полнения, количество выполненной работы. Поиск по названию фирмы, виду работ, по дате исполнения. 38. Одна учетная запись журнала стоянки автомобилей имеет структуру: номер автомобиля, фамилия владельца, дата и время начала, дата и время оконча- ния, тариф за час. Поиск по номеру автомобиля, по дате/времени стоянки, по фамилии владельца. 39. Структура одной записи о сельскохозяйственном продукте содержит поля: наименование района (где выращивают), наименование продукта, площадь (га), урожайность (кг/га), цена за 1 кг, потери при транспортировке (%), стоимость продукта. Поиск по наименованию района, по наименованию про- дукта, по урожайности, по площади. 40. В туристической фирме учетная запись о проданном туре содержит следую- щие поля: наименование тура, фамилия клиента, цена 1 дня (в р.), количество дней, стоимость проезда, курс валюты, количество валюты, стоимость поезд- ки. Поиск выполнять по наименованию тура, по фамилии клиента, по стои- мости проезда, по количеству дней. Выполнить задания 21-40, используя для замены и удаления элементов основ- ного контейнера соответствующие обобщенные алгоритмы. Вместо очереди вы- борку осуществлять в последовательный контейнер. Для обращения контейнера использовать обобщенные алгоритмы обращения и сортировки.
Упражнения 219 Выполнить задания 5.31-5.43 в трех вариантах, используя в качестве поля клас- са вектор, список и двустороннюю очередь. Использовать итераторы вставки и потоковые итераторы. Выполнить задания 5.31-5.43 в трех вариантах, реализовав классы как произ- водные от вектора, списка и двусторонней очереди. Использовать итераторы вставки и потоковые итераторы. Выполнить задания 5.46-5.63, используя в качестве основного контейнера дву- стороннюю очередь и список. Для операций поиска отсортировать контейнер по Соответствующему полю, написав соответствующий функтор. Поиск выполнять с помощью алгоритмов двоичного поиска. Множества В следующих заданиях использовать множества подходящего типа. Для выпол- нения операций с множествами задействовать алгоритмы работы со множествами. 41. В озере водятся рыбы нескольких видов. Три рыбака поймали экземпляры, представляющие некоторые из имеющихся видов. Определить, какие виды рыб есть у каждого рыбака; рыбу каких видов выловил хотя бы один рыбак; какие рыбы есть в озере, но не в улове любого из рыбаков. 42. Дан текст из цифр и строчных латинских букв, за которыми следует точка. Определить, каких букв — гласных или согласных — больше в этом тексте; напечатать в алфавитном порядке все согласные буквы, которые входят толь- ко в одно слово. 43. На трех участках возделывают сельскохозяйственные культуры. Известны виды культур, выращиваемых на каждом из участков. Определить виды куль- тур, произрастающих на каждом из участков; хотя бы на одном из участков; не возделываемых ни на одном участке. (Культуры; картофель, укроп, мор- ковь, горох, капуста, редис.) 44. Дан текст из строчных латинских букв, за которыми следует точка. Напеча- тать все буквы, входящие в текст не менее двух раз; все согласные буквы, вхо- дящие только в одно слово. 45. В трех магазинах продают некоторые виды товаров из имеющегося списка. Определить, какими товарами торгуют в каждом магазине; какие товары про- дают только в одном магазине; какие товары есть хотя бы в двух магазинах. 46. Дан текст на русском языке. Напечатать в алфавитном порядке все гласные буквы, присутствующие в каждом слове; все согласные, которые не входят хотя бы в одно слово. 47. Для каждого из четырех классов указаны имена девочек, обучающихся в них. Определить, какие из этих имен встречаются во всех классах; какие есть хотя бы в двух классах; какие имена встречаются только в одном классе. 48. Дан текст на русском языке. В алфавитном порядке напечатать все гласные буквы (а, е, ё, и, й, о, у, ы, э, ю, я), входящие в этот текст более двух раз; все согласные, которые входят только в одно слово.
220 Глава 10. Стандартная библиотека шаблонов 49. Задан некоторый набор товаров. Определить для каждого из товаров, какие из них имеются в каждом из п магазинов; какие товары в наличии хотя бы в одном магазине; каких товаров нет на прилавке ни одного из магазинов. 50. Дан текст из строчных латинских букв, за которыми следует точка. Напеча- тать все буквы, входящие в текст по одному разу; все согласные, которые вхо- дят в каждое слово. 51. Известны марки машин, изготовляемых в данной стране и импортируемых за рубеж. Названы некоторые п стран. Определить для каждой из марок, какие автомобили были доставлены во все страны; доставлены в некоторые из стран; остались невостребованы. 52. Дан текст на русском языке. Напечатать в алфавитном порядке все согласные буквы, не найденные ни в одном слове; все гласные буквы, которые входят в текст более двух раз. 53. В трех колхозах выращивают некоторые сельскохозяйственные культуры из имеющегося перечня. Определить культуры, возделываемые во всех колхо- зах; возделываемые хотя бы в одном колхозе; только в двух колхозах. 54. Дан текст на русском языке. Напечатать в алфавитном порядке все звонкие согласные буквы, которые входят в каждое нечетное слово и не входят ни в одно четное слово; согласные буквы, входящие только в одно слово. 55. Есть список игрушек, некоторые из которых имеются в п детских садах. Вы- яснить, каких игрушек нет ни в одном из садиков; какие есть в каждом из дет- садов; нашлись хотя бы в одном детсаде. 56. Дан текст на русском языке. Напечатать в алфавитном порядке все звонкие согласные буквы, которые найдены хотя бы в одном слове; все гласные бук- вы, которые не входят только в одно. 57. Имеется список продуктов питания, хранящихся на оптовой базе. Для каждо- го из четырех магазинов указано, какие продукты они получают с этой базы. Определить, какие продукты поступают с базы во все магазины; какие про- дукты поставляют хотя бы в два магазина; какие продукты не поступают ни в один магазин. 58. Дан текст на русском языке. Напечатать в алфавитном порядке все глухие со- гласные буквы, которые не входят хотя бы в одно слово; все гласные, которые входят в каждое слово. 59. Три дачника выращивают на своих участках цветы из имеющегося списка (ирис, роза, астра, пион, георгин, хризантема, гладиолус). Определить, цветы каких видов выращивают все дачники; хотя бы два из них; не выращивает ни один из дачников. 60. Дан текст на русском языке. Напечатать в алфавитном порядке все согласные буквы, которые входят только в одно слово; все звонкие согласные, которые не входят ни в одно слово. 61. На трех фермах разводят домашних животных из имеющегося списка. Опре- делить, каких животных выращивают хотя бы на одной ферме; на всех фер- мах; только на одной ферме.
Упражнения 221 62. Дан текст на русском языке. Напечатать в алфавитном порядке все глухие со- гласные буквы, которые не входят только в одно слово; все гласные, которые входят хотя бы в два слова. 63. Имеется список сортов хлеба, изготавливаемых на хлебозаводе. Для каждого из п магазинов указано, хлеб каких сортов они получают с этого завода. Опре- делить, какие сорта хлеба продаются во всех магазинах; только в одном мага- зине; хотя бы в одном магазине. 64. Дан текст на русском языке. Напечатать в алфавитном порядке все звонкие согласные буквы, которые входят более чем в одно слово; все гласные, кото- рые не входят ни в одно слово. 65. В три газетных киоска поступают газеты из имеющегося списка. Определить, какие газеты не заказали ни в один из киосков; какие газеты бывают в каж- дом киоске; какие можно купить хотя бы в одном киоске. 66. Дан текст на русском языке. Напечатать в алфавитном порядке все согласные буквы, которые встречаются в тексте менее трех раз; все звонкие согласные, которые входят только в одно слово. 67. На четырех садовых участках растут плодовые деревья из имеющегося спи- ска. Определить, какие деревья культивируют хотя бы на одном участке; ка- кие растут на каждом участке; какие деревья не выращивают ни на одном из участков. 68. Дан текст на русском языке, нормально оканчивающийся точкой. Вывести на экран все гласные буквы, которые встречаются хотя бы в одном слове не ме- нее двух раз; все звонкие согласные, которые входят только в одно слово. 69. На трех фабриках изготавливают некоторые виды товаров из имеющегося списка. Определить, какие товары производит только одна фабрика; какие то- вары изготавливают только две фабрики; какие товары изготавливаются на каждой фабрике. 70. Дан текст на русском языке. Напечатать в алфавитном порядке все согласные буквы, которые встречаются только один раз; все гласные буквы, которые не входят только в одно слово. 71. Известны виды культур, выращиваемых на каждом из трех участков. Опреде- лить виды культур, которые возделывают только на одном участке; возделы- вают хотя бы на одном из участков; не возделывают ни на одном участке. (Культуры: морковь, картофель, свекла, редис, горох, капуста, кабачки). 72. Дан текст на русском языке. Напечатать в алфавитном порядке все гласные буквы, которые входят только в одно слово; все звонкие согласные, которые не входят ни в одно слово. 73. Задан набор некоторых товаров. Для каждого из N магазинов известны виды товара из набора продаваемых в нем. Определить, какие товары имеются во всех магазинах; каких товаров нет ни в одном из магазинов; какие товары есть только в одном магазине.
222 Глава 10. Стандартная библиотека шаблонов 74. Дан текст на русском языке, оканчивающийся точкой. Вывести на экран все согласные, которые входят только в одно слово; все гласные буквы, которые встречаются хотя бы в одном слове не менее двух раз. 75. В четыре газетных киоска поступают газеты из имеющегося списка. Опреде- лить, какие газеты бывают в каждом киоске; какие есть хотя бы в одном киос- ке; каких нет только в одном киоске. 76. Дан текст на русском языке. Напечатать в алфавитном порядке все гласные буквы, которые не входят ни в одно слово; все гласные буквы, которые входят в текст более двух раз. 77. В трех колхозах выращивают некоторые сельскохозяйственные культуры из имеющегося перечня. Определить культуры, возделываемые только в двух колхозах; возделываемые хотя бы в одном колхозе; возделываемые во всех колхозах. 78. Дан текст из заглавных латинских букв, оканчивающийся точкой. Напечатать в обратном алфавитном порядке все буквы, входящие в текст по два раза; все согласные, которые входят только в одно слово. 79. В трех магазинах продают некоторые виды товаров из имеющегося списка. Определить, какие товары продаются в каждом магазине; какие товары про- дают только в одном магазине; какими товарами торгуют только в двух мага- зинах. 80. Дан текст на русском языке. Вывести на экран все согласные буквы, которые встречаются хотя бы в одном слове не менее двух раз; все гласные, которые входят только в одно слово. 81. Реализовать класс ListBox (см. задание 5.47), используя множество. Контейнер-отображение Выполнить задания 21-40, реализовав операции поиска через преобразование исходного контейнера в ассоциативный контейнер-отображение с подходящим ключом. Выполнить задания 5.46-5.64, используя в качестве основного контейнера ассо- циативный контейнер-отображение и выбрав подходящее поле в качестве основ- ного ключа. Выполнить задания 8.21-8.40, реализовав индексный массив в виде ассоциатив- ного контейнера-отображения.
ПРИЛОЖЕНИЕ А Функции для работы с символьными массивами Функции для работы с массивами char[ ] Пять функций оперируют произвольными областями памяти, выполняют в том числе операции с символьными массивами, не отслеживая завершающий нуле- вой байт. const void* memchr(const void* buf, int ch, sizet count); void* memchr(const void* buf, int ch, size t count); Возвращает указатель на первое вхождение младшего байта аргумента ch в мас- сиве buf длиной count байтов. int memcmpCconst void *bufl, const void *buf2, size t count); Сравнивает символьные массивы buf 1 и buf2 и длиной count байтов в лексикогра- фическом порядке. Возвращает: □ -1, если содержимое bufl меньше содержимого buf2; □ 0, если содержимое bufl совпадает с содержимым buf2; □ +1, если содержимое bufl больше содержимого buf2. void * memcpy(void *dest, const void *src, size t count); Копирует count байтов из символьного массива src в символьный массив dest. Возвращает указатель на dest. void * memmove(void *dest, const void *src, size t count); Копирует count байтов из символьного массива src по адресу dest. Массивы мо- гут перекрываться. Возвращает указатель на dest. void * memset(void *dest, int ch, size t count): Заполняет первые count байтов символьного массива dest символом, взятым из младшего байта ch. Возвращает указатель на dest.
224 Приложение А. Функции для работы с символьными массивами Следующие функции выполняют операции с символьными массивами как с С-строками и учитывают наличие нулевого байта в конце С-строки. sizet strlenCconst char *s); Возвращает количество символов в строке s (длину строки) без учета нулевого байта. char * strcatCchar *Dest, const char *Source); Добавляет строку Source в конец строки с Dest и возвращает указатель на Dest. Символьный массив Dest должен иметь размер не менее strlen(Dest) + strlen(Source) + 1 байт. char * strncat(char *Dest, const char *Source, size t n); Добавляет не более n символов из строки Source в конец строки Dest и возвраща- ет указатель на Dest. В массиве Dest должно быть не менее strlen(Dest) + n + 1 байтов. const char* strchr(const char* string, int ch): char* strchr(const char* string, int ch); Выполняет поиск символа с кодом ch слева направо в строке string; возвращает указатель на первое вхождение символа. Если символ не обнаруживается, воз- вращает NULL (нулевой указатель). const char* strrchr(const char* string, int ch); char* strrchr(const char* string, int ch); Делает то же самое, что и функция strchrO, но в порядке справа налево. const char* strstr(const char* string, const char* Search): char* strstr(const char *string, const char *Search); Выполняет поиск строки Search в строке string; возвращает указатель на первое вхождение Search. Если строка не обнаружена, то возвращает нулевой указатель (NULL). size t strcspn(const char *string, const char *CharSet); Возвращает указатель на первое вхождение любого символа из строки CharSet в строке string или возвращает NULL, если такого символа не обнаружено. size t strspn(const char *string, const char *CharSet); Возвращает указатель на первый же символ из строки CharSet, который не вхо- дит в строку string. В противном случае возвращает NULL. const char* strpbrk(const char* str, const char* CharSet): char* strpbrk(const char *str, const char *CharSet); Возвращает указатель на символ, являющийся первым вхождением любого из символов строки CharSet в строку str. Если символ не найден, возвращает NULL. char * strtok(char *Token, const char *Delim); Возвращает указатель на следующую лексему из строки Token, отделенную лю- бым из символов-разделителей строки Delim. Первый и последующие вызовы различаются.
Функции для работы с массивами chart ] 225 #1nclude <cstring> #include <iostream> using namespace std; int mainO { char input[100] = "abc,d.teacher.count"; char *p = strtok(input. if (p) cout « p « endl; while(p!=NULL) { p = strtok(NULL. if (p) cout <<@060> p « endl; } return 0; } // ищет первую запятую // выводит 'abc' // ищет следующую запятую // выводит следующую лексему Программа выведет на экран abc d teacher count char * strcpy(char *Dest, const char *Source); Копирует строку Source в другую строку Dest и возвращает указатель на Dest. В массиве Dest должно быть не менее strlen(Source) + 1 байтов. char * strncpy(char *Dest, const char *Source, sizet count); Копирует не более чем count символов из строки Source в другую строку Dest и возвращает Dest. В массиве Dest должно быть не менее n + 1 байтов. int strcmp(const char *stringl, const char *string2); Сравнивает строки stringl и string? лексикографически. Возвращает: □ -1, когда содержимое stringl меньше содержимого string2; □ 0, когда содержимое stringl равно содержимому string2; □ +1, если содержимое stringl больше содержимого string2. int strncmp(const char *stringl, const char *string2, size t n); Сравнивает строку stringl и первые n символов строки string2. Возвращает результат аналогично функции strcmpO. Следующие функции требуют установки локального контекста с помощью С-функции setlocal е(). int strcoll(const char *stringl, const char *string2); Сравнивает строки как функция strcmpO, учитывая установки локализации. size t strxfrm(char *Dest, const char *Source, size t count): Преобразует строку Source и помещает ее в строку Dest на основе текущей лока- ли. Преобразуется не более count символов. Возвращает полученную длину стро- ки Dest без учета завершающего символа.
226 Приложение А. Функции для работы с символьными массивами Функции для работы с массивами wchar_t[ ] Следующие функции выполняют обработку «широких» символьных массивов и «широких» С-строк. wchart * wcscat(wchar_t *Dest, const wchart *Source): Функция аналогична функции strcatO. const wchar t* wcschr(const wchar t* string, wchar t c); wchar_t* wcschr(const wchar t* string, wint_t c); Функция аналогична функции strchrO. int wcscmpCconst wchar t *stringl, const wchar t *string2): Функция аналогична функции strcmpO. int wcscoll(const wchar t *stringl, const wchar t *string2): Функция аналогична функции strcoll О. wchar t * wcscpy(wchar_t *Dest, const wchar t *Source): Функция аналогична функции strcpyO. size t wcscspn(const wchar t *string, const wchar t *CharSet); Функция аналогична функции strcspnO. size t wcslen(const wchar_t *string); Функция аналогична функции strlenO. wchar t * wcsncat(wchar_t *Dest, const wchar t *Source, size t n); Функция аналогична функции strncatO. int wcsncmp(const wchar t *stringl, const wchar t *string2, size_t n): Функция аналогична функции strncmpO. wchar t * wcsncpy(wchar_t *Dest, const wchar t *Source, size_t n); Функция аналогична функции strncpyO. const wchar t* wcspbrkCconst wchar_t* string, const wchar t* CharSet): wchar t* wcspbrk(const wchar t* string, const wchar t* CharSet); Функция аналогична функции strpbrkO. const wchar t* wcsrchr(const wchar t* string, wchar t ch); wchar t* wcsrchr(const wchar t* string, wchar t ch); Функция аналогична функции strrchrO. size t wcsspn(const wchar t *string, const wchar_t *CharSet); Функция аналогична функции strspnO. const wchar t* wcsstr(const wchar t* string, const wchar t* CharSet); wchar t* wcsstr(const wchar t* string, const wchar t* CharSet): Функция аналогична функции strstrO.
Функции для работы с массивами wchar t[ ] 227 wchart * wcstokCwchart *Token, const wchart *Delim): Функция аналогична функции strtokO. size t wcsxfrmCwchart *Dest, const wchar t *Source, size t n): Функция аналогична функции strxfrmO. int wmemcmp(const wchar t * bufl, const wchar t * buf2, size t n); Функция аналогична функции memcmpO. const wchar t* wmemchrCconst wchar t * buf, wchar t ch, size t n): wchar t* wmemchrCconst wchar t * buf, wchar t ch, size t n); Функция аналогична функции memchrO. wchar t * wmemcpyCwchar t *sl, const wchar t *s2, size t n); Функция аналогична функции memcpyO. wchar t * wmemmoveCwchart *sl, const wchar t *s2, size t n); Функция аналогична функции memmoveC). wchar_t * wmemsetCwchar t *s, wchar t c, size t n); Функция аналогична функции memset О.
ПРИЛОЖЕНИЕ Б Обобщенные алгоритмы В этом приложении рассматриваются все алгоритмы в алфавитном порядке (кроме алгоритмов работы с кучей — они практически не применяются начи- нающими программистами). Каждый алгоритм представлен в следующем виде: сначала описывается прототип функции, затем его действие. Для некоторых ал- горитмов приводится простейший пример их использования. accumulate() template <class Inputiterator, class Type> Type accumulate! Inputiterator first. Inputiterator last, Type init); template <class Inputiterator, class Type, class B1naryOperation> Type accumulate! Inputiterator first. Inputiterator last. Type init. Binaryoperation op): Первый вариант accumulate!) вычисляет сумму значений элементов интервала [first, last) с начальным значением init. Например, дана последовательность {1, 2, 3, 4, 5} и начальное значение 0; результат работы алгоритма равен 15. Во втором варианте вместо оператора сложения к элементам применяется пере- данная бинарная операция ор. Если передать алгоритму accumulated объект- функцию multiplies<int> и начальное значение 1, результат будет равен 120. Применение accumulated требует включения заголовочного файла <numeric>. adjacent_difference() template <class Inputiterator, class Outputlterator> Outputiterator adjacent_di fference! Inputiterator first. Inputiterator last'. Output Iterator result); template <class Inputiterator, class Outputiterator, class BinaryOperation>
binary search() 229 Outputiterator adjacent_di fference( Inputiterator first. Inputiterator last. Outputiterator result, BinaryOperation op); Первый вариант adjacentdifferenceO вычисляет и записывает новую последова- тельность элементов по следующему правилу: □ *f 1 rst — для нулевого элемента; □ *(first+i) - *(first+i-l) — для последующих элементов. Например, если дана последовательность {0, 2, 4, 5, 7, 8}, то новая последователь- ность будет иметь вид {0, 2 - 0, 4 - 2, 5 - 4, 7 - 5, 8 - 7} = {0, 2, 2, 1, 2, 1}. Во втором варианте вместо вычитания соседних элементов выполняется задан- ная операция ор. Например, для той же исходной последовательности {0, 2, 4, 5, 7, 8} стандартный функтор multiplies<int> вычислит {0, 2 х 0, 4 х 2, 5 х 4, 7 х 5, 8x7} = {0, 0, 8, 20, 35, 56}. Итератор Outputiterator после выполнения алгоритма указывает на элемент, рас- положенный за последним элементом новой последовательности. Использование adjacent differenceO предполагает включение заголовочного файла <numeric>. adjacent_find() template <class Forwardlterator> Forwarditerator adjacent_find( Forwarditerator first. Forwarditerator last); template <class Forwarditerator, class BinaryPredicate> Forwarditerator adjacent_find( Forwarditerator first. Forwarditerator last, Predicate pred); Алгоритм adjacentfindO ищет первую пару одинаковых соседних элементов в интервале [first, last). Если дубликаты найдены, возвращает итератор на пер- вый элемент пары; в противном случае возвращается 1 ast. Например, если дана последовательность {0, 1, 2, 2, 3, 3}, то будет найдена пара [2, 2] и возвращен ре- зультат, указывающий на первую двойку. binary_search() template <class Forwarditerator, class Type> bool binary_search( Forwarditerator first. Forwarditerator last, const Type &value); template <class Forwarditerator, class Type> bool binary_search( Forwarditerator first. Forwarditerator last, const Type &value, Compare comp);
230 Приложение Б. Обобщенные алгоритмы Алгоритм binarysearchO ищет значение value в отсортированном интервале [first, last). Если это значение найдено, возвращается true, иначе — false. Пер- вая версия работает с контейнером, упорядоченным с помощью оператора «мень- ше». Во втором случае порядок определяется объектом-функтором. сору() template <class Inputiterator, class Outputlterator> Outputiterator copy( Inputiterator firstl. Inputiterator last. Outputiterator first2) Алгоритм соруО копирует последовательность элементов из интервала [first, last) в другой контейнер начиная с позиции first2. Алгоритм возвращает итера- тор на элемент второго контейнера, следующий за последним скопированным. Выходной итератор может указывать на элемент входного контейнера. copy_backward() template <class BidirectionalIteratorl. class BidirectionalIterator2> Bidirectionallterator2 copy_backward( BidirectionalIteratorl first. BidirectionalIteratorl lastl. Bidirectionallterator2 last2) Алгоритм copybackwardO работает аналогично соруО, только элементы копиру- ются в обратном порядке: копирование начинается с 1 astl - 1 и продолжается до fi rst. Кроме того, элементы помещаются в целевой контейнер с конца, от пози- ции 1 ast2 - 1, пока не будет скопировано lastl - first элементов. Целевой и ис- ходный контейнеры могут быть одним и тем же. count() template <class Inputiterator, class Type> iterator_traits<lnputlterator>::distance_type count! Inputiterator first. Inputiterator last, const Type& value); Алгоритм count!) сравнивает каждый элемент co значением value в интервале [first, last) на равенство. Возвращаемое значение — количество элементов, рав- ных val ue. count_if() template <class Inputiterator, class Predicate> iterator_traits<lnputlterator>;;distance_type count_1f( Inputiterator first. Inputiterator last. Predicate pred); Алгоритм count ifO применяет предикат pred к каждому элементу из интерва- ла [first, last). Результат информирует, сколько раз сравнение было истинным (true).
fill» 231 equal() tempiate<class Inputlteratorl, class Inputlterator2> bool equal( Inputlteratorl firstl, Inputlteratorl lastl, Inputlterator2 first2): tempiate<class Inputlteratorl. class Inputlterator2, class BinaryPredicate> bool equal! Inputlteratorl firstl. Inputlteratorl lastl. Inputlterator2 first2. Binarypredicate pred); Алгоритм equal О возвращает true, если последовательность элементов в интер- вале [firstl, lastl) совпадает со второй последовательностью, начинающейся с позиции first2. Для сравнения применяется операция проверки на равенство. Во втором варианте equal О возвращает true, если для всех пар элементов первой и второй последовательностей предикат pred возвращает true. Если число элементов второй последовательности больше, лишние игнорируют- ся. Если второй контейнер содержит меньше элементов, чем первый, поведение программы не определено. equal_range() tempiate<class Forwarditerator, class Type> pair<ForwardIterator. Forwardlteratoo equal_range( Forwarditerator first. Forwarditerator last. const Type &value); tempiate<class Forwarditerator, class Type, class Compare> pair<ForwardIterator. Forwardlteratoo equal_range( Forwarditerator first. Forwarditerator last. const Type &value. Compare comp): Алгоритм equal range О возвращает пару значений: в first помещается значение итератора на выходе алгоритма lower boundO, в second — из алгоритма upper boundO. Контейнер должен быть отсортирован. В первом варианте алгоритма при сравнении используется оператор «меньше», определенный для типа элементов контейнера; во втором — заданный предикат comp. fillo tempiate<class Forwarditerator, class Type> void fill( Forwarditerator first, Forwarditerator last, const Type& value): Алгоритм fill!) заполняет интервал [first, last) значением value.
232 Приложение Б. Обобщенные алгоритмы fill_n() tempiate<class Forwarditerator, class Size, class Type> void fill_n( Forwarditerator first. Size n, const Type& "value); Алгоритм fillnO присваивает n элементам интервала [first, first + n) значение val ue. find() tempiate<class Inputiterator, class T> Inputiterator find! Inputiterator first. Inputiterator last, const T &value); Элементы из интервала [first, last) сравниваются co значением value на равен- ство; операция должна быть определена для типа элементов контейнера. Как только соответствие найдено, поиск прекращается. Алгоритм find!) возвращает Input Iterator, указывающий на найденный элемент; в противном случае возвра- щается last. find_if() tempiate<class Inputiterator, class Predicate> Inputiterator find_if( Inputiterator first. Inputiterator last. Predicate pred); К каждому элементу интервала [first, last) применяется предикат pred. Если он возвращает true, поиск прекращается. Алгоритм find ifO возвращает итератор Input Iterator, указывающий на найденный элемент; в противном случае возвра- щается 1 ast. find_end() tempiate<class Forwardlteratorl, class Forwardlterator2> Forwardlteratorl find_end( Forwardlteratorl firstl, Forwardlteratorl lastl. Forwardlterator2 first2, Forwardlterator2 last2); tempiate<class Forwardlteratorl. class Forwardlterator2. class BinaryPredicate> Forwardlteratorl find_end( Forwardlteratorl firstl. Forwardlteratorl lastl, Forwardlterator2 first2. Forwardlterator2 last2, BinaryPredicate pred); В последовательности [firstl, lastl) выполняется поиск последнего вхождений последовательности [first2, last2). Если вторая последовательность не входит в первую, возвращается 1 astl.
generate n() 233 В первой версии алгоритма используется операция равенства, определенная для типа элементов контейнера, а во втором — заданный бинарный предикат. find_first_of() tempiate<class Forwardlteratorl, class Forwardlterator2> Forwardlteratorl find_first_of( Forwardlteratorl firstl. Forwardlteratorl lastl, Forwardlterator2 f1rst2, Forwardlterator2 last2): tempiate<class Forwardlteratorl, class Forwardlterator2. class B1naryPred1cate> Forwardlteratorl f1nd_first_of( Forwardlteratorl firstl, Forwardlteratorl lastl. Forwardlterator2 first2. Forwardlterator2 last2, BlnaryPredicate pred): Последовательность [first2, last2) содержит элементы, поиск которых ведется в интервале [firstl, lastl). Алгоритм findfirstofO возвращает итератор, ука- зывающий на первое вхождение любого элемента последовательности [first2, 1 ast2). Если же первая последовательность не содержит ни одного элемента из второй, возвращается lastl. В первом варианте используется операция равенства, определенная для типа элементов контейнера, а во втором — заданный бинарный предикат pred. for_each() tempiate<class Inputiterator, class Function> Function for_each( Inputiterator first. Inputiterator last. Function func); Алгоритм foreachO применяет функтор func к каждому элементу интервала [first, last). Функтор func не может изменять элементы, поскольку итератор за- писи не гарантирует поддержки присваивания. Если же модификация необходи- ма, следует воспользоваться алгоритмом transform!). Функтор func может воз- вращать значение, но оно игнорируется. generate() tempiate<class Forwarditerator, class Generator void generate! Forwarditerator first, Forwarditerator last. Generator gen): Алгоритм generate!) заполняет интервал [first, last), вызывая функтор gen. generate_n() tempiate<class Outputiterator, class Size, class Generator void generate_n( OutputIterator first.
234 Приложение Б. Обобщенные алгоритмы Size п. Generator gen): Алгоритм generate d) заполняет первые п элементов последовательности, начи- ная с f 1 rst, п раз вызывая функтор gen. includes() templates! ass Inputlteratorl. class Inputlterator2> bool includes! Inputlteratorl firstl. Inputlteratorl lastl. Inputlterator2 first2, Inputlterator2 last2); tempiate<class Inputlteratorl, class Inputlterator2, class Compare> bool includes! Inputlteratorl firstl, Inputlteratorl lastl, Inputlterator2 first2. Inputlterator2 last2, Compare comp); Алгоритм includes!) проверяет, входит ли каждый элемент последовательности [firstl, lastl) в интервал [first2, last2). Первый вид алгоритма предполагает, что последовательности отсортированы по возрастанию; второй — что порядок задается предикатом comp. inner_product() tempiate<class Inputlteratorl. class Inputlterator2, class Type> Type inner_product( Inputlteratorl firstl. Inputlteratorl last. Inputlterator2 first2. Type init); tempiate<class Inputlteratorl, class Inputlterator2, class Type, class BinaryOperationl, class BinaryOperation2> Type inner_product( Inputlteratorl firstl. Inputlteratorl last, Inputlterator2 first2, Type init. BinaryOperationl opl, BinaryOperation2 op2); Первый вид алгоритма вычисляет обычное скалярное произведение последова- тельностей и прибавляет результат к начальному значению init. Например, если даны последовательности {2, 3, 4, 5} и {1, 2, 3, 4}, результат вычисляется как init + (2х1+Зх2 + 4хЗ + 5х4). Если начальное значение равно 0, результатом будет 40. Во втором случае вместо сложения используется бинарная операция opl, а вме- сто умножения — бинарная операция ор2. Например, если для приведенных по- следовательностей задать вычитание как opl и сложение как ор2, то результат бу- дет вычисляться так: init + ((2 + 1) - (3 + 2) - (4 + 3) - (5 + 4)).
Iexicographical compare() 235 Применение алгоритма innerproductO подразумевает включение заголовочного файла <numeric>. inplace_merge() tempiate<class BidirectionalIterator> void inpl ace_merge( Bidirectionallterator first, Bidirectional Iterator middle. Bidirectionallterator last): tempiate<cl ass Bidirectionallterator, class Compare> void inplace_merge( Bidirectionallterator first, Bidirectionallterator middle. Bidirectionallterator last. Compare comp): Алгоритм inplacemergeO объединяет соседние отсортированные последователь- ности элементов [first, middle) и [middle, last) из одного контейнера и помещает объединенную последовательность в тот же контейнер начиная с позиции first. Первый вариант алгоритма предполагает, что последовательности упорядочены с помощью операции «меньше», определенной для типа элементов контейнера; второй алгоритм предполагает, что последовательности упорядочены с помощью операции сравнения, задаваемой бинарным предикатом comp. iter_swap() tempiate<class Forwardlteratorl, class Forwardlterator2> void iter_swap( Forwardlteratorl a, Forwardlterator2 b): Алгоритм iter swapO обменивает значения элементов, на которые указывают итераторы а и Ь. Iexicographical_compare() tempiate<class Inputlteratorl, class Inputlterator2> bool lexicographical_compare( Inputlteratorl firstl, Inputlteratorl lastl, Inputlteratorl first2, Inputlterator2 last2): tempiate<class Inputlteratorl, class Inputlterator2, class Compare> bool lexicographical_compare( Inputlteratorl firstl, Inputlteratorl lastl, Inputlteratorl first2. Inputlterator2 last2. Compare comp): Алгоритм lexicographical compared сравнивает соответственные пары элементов из двух интервалов [firstl, lastl) и [first2, last2). Сравнение продолжается, пока не будет найдена первая пара различных элементов, или же достигнута пара [lastl, last2], или хотя бы один из элементов lastl или last2 (если последова-
236 Приложение Б. Обобщенные алгоритмы тельности имеют разные длины). Если первая последовательность лексикогра- фически меньше второй, возвращается true, иначе fai se. Этот результат выраба- тывается по следующим правилам: □ если меньше элемент первой последовательности, то true, иначе fai se; □ если 1 astl достигнут, а 1 ast2 нет, то true (первая короче второй); □ если 1 ast2 достигнут, а 1 astl нет, то fai se (первая длиннее второй); □ если достигнуты и 1 astl, и 1 ast2 (то есть все элементы одинаковы), то fai se. Во второй версии вместо операции сравнения используется предикат comp. Iower_bound() tempiate<class Forwarditerator, class Type> Forwarditerator lower_bound( Forwarditerator first. Forwarditerator last. const Type &value): tempiate<class Forwarditerator, class Type, class Compare> Forwarditerator lower_bound( Forwarditerator first. Forwarditerator last, const Type &value, class Compare): Алгоритм lower boundO возвращает итератор, указывающий на первую позицию в отсортированном интервале [first, last), в которую можно вставить значение value, не нарушая сортировки. В этой позиции находится значение, большее либо равное val ue. Например, если дана такая последовательность: int a[12j = {12. 15. 17. 19. 20. 22, 23. 26, 29. 35. 40, 51}: то обращение lower_bound(a,a+12,21) возвращает итератор, указывающий на 22. В первом варианте алгоритма используется операция «меньше», определенная для типа элементов контейнера, а второй подход предполагает, что для упорядо- чения элементов применяется предикат comp. max() tempiate<class Type> const Type& max( const Type &x, const Type &y); tempiate<class Type, class Compare> const Type& max( const Type &x. const Type &y. Compare comp); Алгоритм max() возвращает большее из двух значений х и у. Первый тип исполь- зует для сравнения операцию «больше», определенную для типа Туре; во вто- ром случае аргументы сравниваются с помощью предиката comp.
merge!) 237 max_element() tempiate<class Forwardlteratoo Forwarditerator max_element( Forwarditerator first, Forwarditerator last); tempiate<class ForwardIterator, class Compare> Forwarditerator max_element( Forwarditerator first, Forwarditerator last. Compare comp); Алгоритм max elementO возвращает итератор, указывающий на максимальный элемент последовательности [first, last). Как обычно, первая форма использует для сравнения операцию «больше», определенную для типа элементов контейне- ра; во втором алгоритме применяется предикат comp. min () tempiate<class Type> const Type& mini const Type &x, const Type &y); tempiate<class Type, class Compare> const Type& mini const Type &x, const Type &y. Compare comp); Алгоритм min() вычисляет меньшее из двух значений х и у. Первый алгоритм ис- пользует для сравнения операцию «меньше», определенную для типа Туре; во втором алгоритме аргументы сравниваются с помощью предиката comp. min_element() tempi ate<class Forwardlteratoo Forwarditerator min_element( ForwardIterator first. Forwarditerator last): tempiate<class Forwarditerator, class Compare> Forwarditerator min_e1ement( Forwarditerator first. Forwarditerator last. Compare comp); Алгоритм max elementO возвращает итератор, указывающий на минимальный элемент последовательности [first.last). Первая форма использует для сравне- ния операцию «меньше», определенную для типа элементов контейнера; во вто- рой версии используется предикат comp. merge() tempiate<class Inputlteratorl. class Inputlterator2, class Outputlterator> Outputiterator merge! Inputlteratorl firstl, Inputlteratorl lastl. Inputlterator2 first2.
238 Приложение Б. Обобщенные алгоритмы Inputlterator2 last2. Outputiterator result); tempiate<cl ass Inputlteratorl, class Inputlterator2. class Outputiterator, class Compare> Outputiterator merge! Inputlteratorl firstl, Inputlteratorl lastl, Inputlterator2 first2, Inputlterator2 last2. Outputiterator result. Compare comp); Алгоритм merge!) объединяет две отсортированные последовательности [firstl, lastl) и [first2, last2) в единую отсортированную последовательность, начинаю- щуюся с позиции result. Результирующий итератор записи указывает на эле- мент за концом новой последовательности. В первом варианте алгоритма упорядочение выполняется согласно операции «меньше», определенной для типа элементов контейнера; во втором — применя- ется предикат comp. mismatch() tempiate<class Inputlteratorl. class Inputlterator2> pair<lnputlteratorl, Inputlterator2> mismatch! Inputlteratorl first, Inputlteratorl last. Inputlterator2 f1rst2); tempiate<class Inputlteratorl, class Inputlterator2, class BinaryPred1cate> pa1r<lnputlteratorl, Inputlterator2> mismatch! Inputlteratorl first. Inputlteratorl last. Inputlterator2 first2. BinaryPredicate pred); Алгоритм mismatch!) сравнивает две последовательности и находит первую пози- цию, где элементы различны. Возвращается пара итераторов, каждый из которых указывает на эту позицию в соответствующей последовательности. Если все эле- менты одинаковы, то каждый итератор в паре указывает на элемент 1 ast в своем контейнере. Первый вариант алгоритма использует для сравнения элементов операцию ра- венства, а второй — операцию сравнения, задаваемую в качестве аргумента. Если вторая последовательность длиннее первой, лишние элементы игнорируются; если же она короче, поведение программы не определено. next_permutation() template <class BidirectionalIteratoo bool next_permutation( Bidirectionallterator first. Bidirectional Iterator last); template <class Bidirectionallterator, class Compare> bool next_permutation( Bidirectionallterator first.
nth element() 239 Bidirectional Iterator last. Compare comp); Алгоритм nextpermutationO считает последовательность [first, last) переста- новкой и возвращает следующую перестановку. Если таковой не существует, ал- горитм возвращает fal se, иначе — true. Напомним, что такое перестановка — рассмотрим последовательность из трех элементов: {1, 2, 3}. Для нее существует шесть различных перестановок: 123, 132, 213, 231, 312 и 321, упорядоченных на основе оператора «меньше». Если элемен- ты интервала представляют перестановку 213, то следующей для нее будет 231, а предыдущей — 132. Для перестановки 123 нет предшествующей, а для 321 — следующей. Первый алгоритм для определения следующей перестановки использует опера- цию «меньше» для типа элементов контейнера, а второй — операцию сравнения, задаваемую предикатом comp. Последовательные обращения к next permutationO генерируют все возможные перестановки только в том случае, когда исходная последовательность отсорти- рована. nth_element() template <class RandomAccess Iteratoo void nth_element( RandomAccessIterator first. RandomAccessIterator nth. RandomAccessIterator last): template <class RandomAccessIterator. class Compare> void nth_element( RandomAccessIterator first. RandomAccessIterator nth, RandomAccessIterator last. Compare comp); Алгоритм nth_element() переупорядочивает интервал [first, last) так, что все элементы, меньшие указываемого итератором nth, оказываются перед этим эле- ментом, а все большие элементы — после него. При этом не гарантируется, что элементы, расположенные по обе стороны от nth, упорядочены. Например, если есть массив int а[12] = {29, 23. 20. 22, 17. 15. 26, 51. 19. 12. 35. 40}; то вызов nth указывает на 7-й элемент; его значение равно 26. Вызов nth_element(a, &а[6], а+12); генерирует последовательность, в которой семь элементов, меньших 26, оказыва- ются слева от 26, а четыре элемента, больших 26, справа: {23, 20, 22, 17, 15, 19, 12, 26, 51, 35, 40, 29}. Первый вариант алгоритма для сравнения использует операцию «меньше», опре- деленную для типа элементов контейнера, второй — бинарную операцию сравне- ния, заданную в качестве входного параметра.
240 Приложение Б. Обобщенные алгоритмы partial_sort() template <class RandomAccessIteratoo void partial_sort( RandomAccessIterator first. RandomAccessIterator middle. RandomAccessIterator last); template <class RandomAccessIterator, class Compare> void partial_sort( RandomAccessIterator first. RandomAccessIterator middle, RandomAccessIterator last. Compare comp); Алгоритм partialsortO перемещает наименьшие элементы интервала [first, last) в интервал [first, middle) и сортирует их. Элементы в интервале [middle, 1 ast) не сортируются и порядок их не определен. Например, если дан массив int а[12] = {29. 23. 20. 22. 17. 15. 26. 51, 19. 12. 35. 40}: то вызов partial_sort(a. &а[5], а+12); перемещает в начало массива наименьшие пять элементов: {12, 15, 17, 19, 20, 29, 23, 22, 26, 51, 35, 40}. Первый алгоритм для сравнения использует операцию «меньше», определенную для типа элементов контейнера, а второй — заданный бинарный предикат comp. partial_sort_copy() template <class Inputiterator, class RandomAccessIteratoo RandomAccessIterator partial_sort_copy( Inputiterator first, Inputiterator last. RandomAccessIterator result_fi rst. RandomAccessIterator result_last); template <class Inputiterator, class RandomAccessIterator. class Compare> RandomAccessIterator partial_sort_copy( Inputiterator first. Inputiterator last. RandomAccessIterator result_first. RandomAccessIterator result_last. Compare comp); Алгоритм partialsortcopyO работает аналогично partialsortO, только результат помещается в контейнер, ограниченный диапазоном [result_first, result !ast). Размеры исходного интервала и интервала-результата могут не совпадать. На- пример, даны два массива: int а[] = {29. 23, 20. 22, 17. 15, 26. 51, 19. 12. 35,. 40}; int а2[5];
partition!) 241 Тогда обращение partial_sort_copy(a, &а[7], а+12, а2. а2+5): заполняет массив а2 пятью отсортированными элементами а: {12, 15, 17, 19, 20}. Оставшиеся два элемента отсортированы не будут. partial_sum() template <class Inputiterator, class Outputlterator> Outputiterator partial_sum( Inputiterator first. Inputiterator last, Outputiterator result); template <class Inputiterator, class Outputiterator, class BinaryOperation> Output Iterator partial_sum( Inputiterator first. Inputiterator last, Outputiterator result, BinaryOperation op); Первый вариант алгоритма parti al sumO создает из последовательности [first, last) новую последовательность, в которой значение каждого элемента равно сумме всех предыдущих, включая и данный. Так, из последовательности {0, 1, 1, 2, 3, 4, 5} будет сформирована последователь- ность {0, 0+1, 0+1 + 1, 0 + 1 + 1 +2, 0+1 + 1 + 2 + 3, 0 + 1 + 1 + 2 + 3 + 4, 0+1 + 1 + 2 + 3 + 4 + 5} = {0, 1, 2, 4, 7, И, 20}. Второй алгоритм вместо операции сложения использует бинарную операцию, указываемую в качестве параметра. Предположим, мы задали последователь- ность {1, 2, 3, 4} и функтор multiplies<int>. Результатом будет {1, 2, 6, 24}. В обоих случаях итератор записи Outputiterator указывает на элемент за послед- ним элементом новой последовательности. Для использования parti alsumO необходимо включить в программу стандарт- ный заголовочный файл <numeric>. partition() template <class Bidirectionallterator, class UnaryPredicate> Bidi rectionalIterator partition( Bidirectionallterator first. Bidirectionallterator last. UnaryPredicate pred); Алгоритм partition!) переупорядочивает элементы в интервале [first, last): все элементы, для которых предикат pred равен true, помещаются перед элемен- тами, для которых он равен false. Исходный порядок следования элементов не гарантируется. Сохранение относительного порядка обеспечивает алгоритм stablepartitionO. Например, если дана последовательность {0, 1, 2, 3, 4, 5, 6} и предикат, проверяю- щий целое число на четность, то результат может быть таким: {0, 2, 4, 6, 1, 3, 5}.
242 Приложение Б. Обобщенные алгоритмы Хотя гарантируется, что четные элементы будут помещены перед нечетными, их первоначальный порядок может быть нарушен, то есть 6 может оказаться перед О, а 5 — перед 3. prev_permutation() template <class Bi directional Iterator^ bool prev_permutation( Bidirectional Iterator first. Bidirectional Iterator last): template <class Bidirectional Iterator, class Compare> bool prev_permutation( Bidirectional Iterator first. Bidirectional Iterator last, class Compare): Алгоритм prevpermutationO генерирует предыдущую перестановку (см. алго- ритм nextpermutationO). random_shuffle() template <class RandomAccessIterator void random_shuffle( RandomAccessIterator first, RandomAccessIterator last); template <class RandomAccessIterator, class RandomNumberGenerator> void random_shuffle( RandomAccessIterator first, RandomAccessIterator last, RandomNumberGenerator rand); Алгоритм randomshuffleO переставляет элементы из интервала [first, last) в слу- чайном порядке. Вторая форма алгоритма использует функтор, генерирующий случайные числа. Предполагается, что генератор rand возвращает значение типа double в интервале [0, 1]. remove() tempiate<class Forwarditerator, class Type> Forwarditerator remove! Forwarditerator first, Forwarditerator last, const Type &value): Алгоритм removed «удаляет» из интервала [first, last) все элементы, равные value. На самом деле алгоритм (как и remove !fО) не исключает элементы из контейне- ра: размер контейнера сохраняется. На место удаляемого элемента перемещается следующий оставляемый элемент. Возвращаемый итератор указывает на элемент, следующий за позицией, в которую помещен последний неудаленный элемент. Если бы элементы физически удалялись из контейнера, это была бы позиция last «укороченного» контейнера. Рассмотрим последовательность {6, 7, 6, 2, 6, 5, 6, 4}. Предположим, нужно уда- лить все шестерки. В результате получится последовательность {7, 2, 5, 4, 6, 4, 6, 4}.
replace() 243 Возвращенный итератор указывает на первую оставшуюся шестерку. Физически удалить элементы можно методом eraseO, который определен во всех классах- контейнерах. remove_copy() tempiate<class Inputiterator, class Outputiterator, class Type> Outputiterator remove_copy( Inputiterator first. Inputiterator last. Outputiterator result. const Type &value): Алгоритм removecopy () копирует все элементы из интервала [first, last), кроме имеющих значение value, в контейнер, на начало которого указывает result. Воз- вращаемый итератор указывает на элемент за последним скопированным. Ис- ходный контейнер не изменяется. remove_if() tempiate<class Forwardlterator, class Predicate» Forwarditerator remove_if( Forwarditerator first. Forwarditerator last. Predicate pred); Алгоритм removeifO «удаляет» из интервала [first, last) все элементы, для ко- торых значение предиката pred равно true. Алгоритм remove ! f () работает анало- гично remove!), фактически не исключая удаляемые элементы из контейнера. Возвращаемый итератор указывает на элемент, следующий за позицией, в кото- рую помещен последний неудаленный элемент. remove_copy_if() tempiate<class Inputiterator, class Outputiterator, class Predicate» Outputiterator remove_copy_if( Inputiterator first, Inputiterator last, Outputiterator result, Predicate pred): Алгоритм removecopyi f () копирует все элементы интервала [first, last), для ко- торых предикат pred равен fal se, в контейнер, на начало которого указывает ите- ратор result. Возвращаемый итератор указывает на элемент, расположенный за последним скопированным. Исходный контейнер остается без изменения. replace() tempiate<class Forwarditerator, class Type» void replace! Forwarditerator first. Forwarditerator last, const Type& old_value, const Type& new_value);
244 Приложение Б. Обобщенные алгоритмы Алгоритм replace О заменяет в интервале [first, last) все элементы, равные о1 d_val ue, на newval ue. replace_copy() tempiate<class Inputiterator, class Inputiterator, class Type> Outputiterator replace_copy( Inputiterator first. Inputiterator last, class Outputiterator result. const Type& old_value, const Type& new_value): Алгоритм replace_copy() работает аналогично replaced, но новая последователь- ность копируется в контейнер начиная с resul t. Возвращаемый итератор указы- вает на элемент, расположенный за последним скопированным. Исходный кон- тейнер остается без изменения. replace_if() tempiate<class Forwarditerator, class Predicate, class Type> void replace_if( Forwarditerator first, Forwarditerator last. Predicate pred, const Type& new_value); Алгоритм replaceifd заменяет значения всех элементов в интервале [first, last), для которых предикат pred равен true, на new val ue. replace_copy_if() tempiate<class Forwarditerator, class Outputiterator, class Predicate, class Type> Outputiterator replace_copy_if( Forwarditerator first. Forwarditerator last, class Outputiterator result. Predicate pred. const Type& new_value); Алгоритм replace copy ifd ведет себя аналогично replace ifd, только новая по- следовательность копируется в контейнер начиная с result. Возвращаемый ите- ратор указывает на элемент, расположенный за последним скопированным. Ис- ходный контейнер не изменяется. reverseO tempi ate<cl ass Bi di recti onal Iterator void reverse! Bidirectionallterator first. Bidirectionallterator last): Алгоритм reverseO меняет порядок следования элементов контейнера в интер- вале [first, last) на обратный. Например, последовательность {0, 5, 1, 8, 3} после обращения станет {3, 8, 1, 5, 0}.
search () 245 reverse_copy() tempiate<cl ass Bidirectional Iterator, class Outputlterator> Outputiterator reverse_copy( Bidirectional Iterator first, Bidirectional Iterator last. Outputiterator result); Алгоритм reversecopy () поступает аналогично reverseO, с той разницей, что но- вая последовательность копируется в контейнер начиная с result. Возвращае- мый итератор указывает на элемент, расположенный за последним скопирован- ным. Исходный контейнер остается без изменения. rotateO tempiate<class Forwardlterator> void rotate! Forwarditerator first. Forwarditerator middle. Forwarditerator last); Алгоритм rotateO перемещает элементы интервала [first, middle) в конец кон- тейнера. Элемент, на который указывает middle, становится первым. С помощью этого алгоритма можно выполнить циклическое «вращение» элементов контей- нера. rotate_copy() tempiate<class Forwarditerator, class Outputlterator> Outputiterator rotate_copy( Forwarditerator first. Forwarditerator middle. Forwarditerator last, Outputiterator result); Алгоритм rotate_copy() работает аналогично rotateO. Здесь новая последова- тельность копируется в контейнер начиная с result. Возвращаемый итератор указывает на элемент, расположенный за последним скопированным. Исходный контейнер не изменяется. search() tempiate<class Forwardlteratorl. class Forwardlterator2> Forwarditerator search! Forwardlteratorl firstl, Forwardlteratorl lastl, Forwardlterator2 first2, Forwardlterator2 last2): tempiate<class Forwardlteratorl, class Forwardlterator2. class BinaryPredicate> Forwarditerator search! Forwardlteratorl firstl, Forwardlteratorl lastl, Forwardlterator2 first2. Forwardlterator2 last2. BinaryPredicate pred);
246 Приложение Б. Обобщенные алгоритмы Алгоритм search () ищет вторую последовательность в первой и возвращает ите- ратор, указывающий на первую позицию в интервале [firstl, lastl), начиная с которой втбрая подпоследовательность входит в первую. Если подпоследова- тельность не найдена, возвращается 1 astl. Первый вариант алгоритма для ср