Текст
                    В.В.ПОДБЕЛЬСКИЙ
СТАНДАРТНЫЙ
Допущено
Учебно-методическим объединением
по образованию в области прикладной математики
и управления качеством
в качестве учебного пособия
для студентов высших учебных заведений,
обучающихся по направлению подготовки 230400
“Прикладная математика”
МОСКВА
"ФИНАНСЫ И СТАТИСТИКА"
2008


УДК 004.438(075.8) ББК 32.973.26-018.1.я73 П44 РЕЦЕНЗЕНТЫ: кафедра «Системы обработки информации и управления» Московского государственного технического университета им. Н.Э. Баумана; С.М. Лавренов, кандидат технических наук, доцент Подбельский В.В. П44 Стандартный Си++: учеб, пособие / В.В. Подбельский. - М.: Финансы и статистика, 2008. — 688 с.: ил. ISBN 978-5-279-03243-3 Содержит доступное для начинающего программиста описание соответствующего международному стандарту языка Си++, его стандартной библиотеки и входящей в нее библиотеки шаблонов STL. Материал пособия позволяет изучить синтаксис и семантику базовых конструкций языка, а также механизмы и возможности стандартной библиотеки. На многочисленных примерах программ разъясняются наиболее тонкие и трудные вопросы процедурного, объектного, объектно-ориентированного и обобщенного программирования с помощью средств языка Си++ и его библиотеки. Для студентов вузов, учащихся колледжей и специализированных школ. Может использоваться в качестве самоучителя и справочного пособия. 2404000000-039 УДК 004.438(075.8) П 010(01) — 2008 141 -2007 ББК 32.973.26-018.1.я73 © Подбельский В.В., 2008 © Издательство «Финансы ISBN 978-5-279-03243-3 и статистика», 2008
3 Оглавление Предисловие 7 Глава 1. Неформальное введение в Си++ 13 1.1. Первая программа на языке Си-И- 13 1.2. Пространство имен и стандартные заголовки 17 1.3. Программа с вводом данных в цикле 20 1.4. Строки в языке Си 4-4- 22 Гл а в а 2. Лексические основы языка Си++ 25 2.1. Общие сведения о программах, лексемах и алфавите 25 2.2. Идентификаторы и служебные слова 27 2.3. Константы-литералы и перечисления 28 2.4. Знаки операций 42 2.5. Разделители 60 Гл а в а 3. Скалярные типы и выражения 67 3.1. Базовые и производные типы 67 3.2. Объекты и их атрибуты 73 3.3. Определения и описания 94 3.4. Выражения и преобразования типов 101 Глава 4. Операторы языка Си++ 111 4.1. Последовательно выполняемые операторы 111 4.2. Операторы выбора (ветвления) 113 4.3. Операторы цикла 118 4.4. Операторы передачи управления 124 Глава 5. Адреса, указатели, массивы 131 5.1. Указатели и адреса объектов 131 5.2. Адресная арифметика, типы указателей и операции над ними 137 5.3. Массивы и указатели 150 5.4. Многомерные массивы, массивы указателей, динамические массивы 162 Глава 6. Функции, указатели, ссылки 177 6.1. Определения, описания и вызовы функций 177 6.2. Функции с переменным количеством параметров (аргументов) 185 6.3. Рекурсивные функции 192
4 Оглавление 6.4. Подставляемые (inline-) функции 196 6.5. Функции и массивы 198 6.6. Указатели на функции 209 6.7. Ссылки 223 6.8. Перегрузка функций 235 Глава 7. Препроцессорные средства 239 7.1. Стадии и команды препроцессорной обработки .... 239 7.2. Замены в тексте 241 7.3. Включение текстов из файлов 245 7.4. Условная компиляция 246 7.5. Макроподстановки средствами препроцессора 249 7.6. Препроцессорные операции и дополнительные директивы 254 7.7. Встроенные (предопределенные) макроимена 256 Глава 8. Структуры и объединения 260 8.1. Структура как совокупность данных 260 8.2. Объединения разнотипных данных 270 8.3. Битовые поля структур и объединений 275 Глава 9. Класс как абстрактный тип 282 9.1. Класс как расширение понятия структуры 282 9.2. Конструкторы, деструкторы и статусы доступа 290 9.3. Поля данных и методы класса 302 9.4. Указатель this 315 9.5. Друзья классов 320 Глава 10. Библиотечный класс string 328 Ю.1. Строки в языках Си и Си++ 328 10.2. Конструкторы класса string 329 10.3. Операции над строками 330 10.4. Методы класса string 333 10.4.1. Доступ к символу, конкатенация, присваивание 333 10.4.2. Размеры строк 334 10.4.3. Вставки, удаления, замены частей строк.... 337 10.4.4. Поиск в строке и извлечение подстрок 339 10.4.5. Сравнение строк и их частей 345 10.4.6. Обращение к данным объекта класса string 347 10.4.7. Массивы строк и обмены значениями строк 349 10.5. Консольный ввод-вывод строк и обмены с файлами 350
Оглавление 5 Глава 11. Перегрузка операций и классы ресурсоемких объектов 356 11.1. Расширение действия (перегрузка) стандартных операций 356 11.2. Изменение интерфейса существующего класса.... 369 11.3. Классы ресурсоемких объектов 372 Глава 12. Исключения 383 12.1. Общие сведения об исключениях 383 12.2. Синтаксис и семантика механизма исключений 389 12.3. Исключения в конструкторах 396 Гл а в а 13. Включение и наследование классов 398 13.1. Отношение включения классов 398 13.2. Общие сведения о наследовании в Си++ 401 13.3. Синтаксис наследования и доступность компонентов 406 13.4. Множественное наследование и виртуальные классы 413 13.5. Локальные классы 417 Глава 14. Специальные методы классов и перегрузка операций при наследовании 419 14.1. Методы при наследовании классов 419 14.2. Присваивание при наследовании 420 14.3. Конструкторы при наследовании 424 14.4. Деструкторы при наследовании 428 14.5. Перегрузка операций при наследовании 428 14.6. Принцип подстановки и его реализация на языке Си++ 432 14.7. Наследование и ресурсоемкие классы 437 Глава 15. Виртуальные функции и абстрактные классы.... 443 15.1. Виртуальные функции 443 15.2. Присваивания при наследовании 446 15.3. Деструкторы при наследовании 450 15.4. Реализация виртуальных функций 452 15.5. Абстрактные классы 455 15.6. Массивы и списки указателей на абстрактные классы 462 Гл а в а 16. Шаблоны функций и классов 467 16.1. Шаблоны функций 467 16.2. Явная специализация шаблонной функции 478 16.3. Шаблоны классов 480
6 Оглавление 16.4. Внешнее определение методов и дружественные функции шаблонных классов 484 16.5. Специализации шаблонов классов 492 16.6. Частичная пользовательская специализация 498 16.7. Объекты и массивы объектов шаблонных классов 501 Глава 17. Механизмы, использованные при построении STL 509 17.1. Краткие сведения о STL 509 17.2. Шаблоны функций и обобщенные алгоритмы 510 17.3. Контейнеры и итераторы 515 17.4. Взаимодействие средств STL с контейнерами и алгоритмами пользователя 528 Г л а в а 18. Основные средства библиотеки STL 533 18.1. О концепции построения STL 533 18.2. Контейнеры STL 534 18.3. Основные методы контейнеров 537 18.4. Итераторы в STL 547 18.5. Функциональные объекты (функторы) 550 18.6. Алгоритмы STL 554 Гл а в а 19. Стандартная библиотека и ввод-вывод 564 19.1. Обзор стандартной библиотеки Си++ 564 19.2. Ввод-вывод в языке Си++ 568 19.3. Форматирование данных при обменах с потоками 575 19.4. Функции для обмена с потоками 583 19.5. Работа с файлами 587 Приложения 597 Приложение 1. Разработка консольных приложений в среде Microsoft Visual Studio.Net 2005 597 Приложение 2. Константы предельных значений 610 Приложение 3. Таблицы кодов 612 Приложение 4. Вывод на консоль русского текста 619 Приложение 5. Методы класса string 622 Приложение 6. Стандартные функции библиотеки Си 632 Приложение 7. Алгоритмы STL 638 Приложение 8. Средства ввода-вывода в Си++ 650 Приложение 9. Комплексные числа в Си++ 659 Приложение 10. Свободно распространяемый компилятор DJGPP 662 Библиографический список 667 Указатель символов 670 Предметный указатель 672
7 Предисловие В настоящее время Си++ является языком, наиболее полно представляющим основные парадигмы современного программирования. С его помощью можно писать процедурно-ориентированные программы и создавать библиотеки функций. Си++ поддерживает объектно-ориентированное программирование и позволяет разрабатывать библиотеки классов. Механизм шаблонов языка Си++ и его стандартная библиотека дают возможность создавать программы, применяя методы обобщенного программирования. В то же время язык Си++ дает возможность программисту "находиться" наиболее близко к аппаратным средствам той платформы, на которой исполняется программа. В программе на Си++ можно динамически управлять памятью, использовать адресную арифметику, обращаться к отдельным разрядам двоичного представления данных и т. д. Язык Си++ постоянно развивается и совершенствуется в общем русле эволюции средств программирования и информатики в целом. Назовем наиболее значимые события этого процесса. • В библиотеку языка Си++ была включена стандартная библиотека шаблонов (STL — Standard Template Library), что "кардинально изменило всю библиотеку" и возможности базового языка в целом. • В 1998 г. вышел международный стандарт языка Си++ ISO/IEC 14882. (С 2003 г. действует вторая редакция этого стандарта 2003-10-15: ISO/IEC/ANSI/ITI.) • Мировое сообщество фирм, создающих программное обеспечение, разработало несколько компиляторов языка Си++, соответствующих международному стандарту Си++. Компиляторы языка Си++ разработаны практически для всех аппаратно-программных платформ. Программы, написанные на языке Си++, транслируются в исполняемые модули, работающие под управлением операционных систем UNIX (и ее разновидностей, таких, как, LINUX, Solaris), Windows, Mac OS. В отличие от С#, Java и Visual Basic язык Си++ позволяет создавать программы, для выполнения которых не требуется устанавливать
8 Предисловие на компьютер специальное программное обеспечение, создающее среду исполнения программ. Интересно отметить следующий факт. Наиболее современная разработка фирмы Microsoft — Visual Studio.NET включает компиляторы нескольких языков: Си++, С#, JScript, Visual Basic, J#. Для каждого из названных языков соответствующий компилятор создает так называемый управляемый модуль (manager module). Особенность такого модуля состоит в том, что он не может исполняться на том компьютере, на котором не установлены специальные программные средства — "общеязыковая исполняющая среда". И только для языка Си++ сделано исключение — текст программы на языке Си++ в Visual Studio.NET может транслироваться и в управляемый модуль, и в традиционный исполняемый модуль (ехе-модуль). А традиционный ехе-модуль не требует присутствия на компьютере специальной среды исполнения. С учетом вышеизложенного на основе опыта преподавания и применения языка Си++ написано данное пособие. Материал излагается таким образом, что начинающий программист, начав с элементов языка, может профессионально освоить самые трудные и самые современные средства Си++ и его стандартной библиотеки. В книге девятнадцать глав и десять приложений. Глава 1 содержит примеры, позволяющие познакомиться с общей схемой подготовки и выполнения программ на языке Си++. В главе 2 рассматриваются лексические основы языка, используемые в дальнейшем на протяжении всей книги. Глава 3 посвящена объектам (переменным), их определениям, описаниям и использованиям в выражениях. При изучении выражений внимание уделено не только операциям языка Си++, но и особенностям преобразований типов. В главе 4 рассмотрены операторы, т. е. конструкции, определяющие логику (порядок) выполнения действий в программе. Этот материал традиционен для алгоритмических языков и обычно не вызывает затруднений при изучении. Глава 5 потребует от начинающего программиста значительных усилий. Адресная арифметика и указатели — это одна из фундаментальных и трудных тем языка Си и его наследника Си++.
Предисловие 9 Рассмотрены связи массивов с указателями, а также показано, как с помощью массивов указателей моделируются многомерные массивы с динамически задаваемыми размерами. В главе 6 изучаются функции и их взаимоотношение с другими языковыми конструкциями: указателями, массивами, ссылками. Вводятся понятия сигнатуры функции и процедурного полиморфизма (перегрузки функций). Глава 7 подробно рассматривает препроцессорные механизмы. В последнее время им незаслуженно мало уделяется внимания в пособиях по Си++. Однако в ряде случаев именно препроцессор дает возможность получить элегантные решения, недоступные без его применения. Примером служит макрос для вывода изображения и значения выражения, использованного в качестве аргумента макроса. Глава 8 готовит читателя к изучению классов. Здесь изучены частные случаи классов — классы без методов, т.е. структуры и объединения. Именно в таком виде они унаследованы из языка Си. Особое внимание уделено размещению структур и объединений в памяти, а также их битовым полям. В главе 9 рассмотрен вводимый программистом структурированный тип, действия над объектами которого выполняются с помощью методов класса. Особое внимание уделено конструкторам и деструктору, а также специальному указателю this. Глава 10 знакомит читателя со строками в стиле Си++. Подробно описаны методы класса string, при использовании которого каждый символ строки имеет тип char. Методы сгруппированы по их функциональным назначениям. Далее приведены стандартные функции для ввода и вывода строк, а также базовые сведения по обменам с файлами в текстовом режиме. В главе 11 рассматривается перегрузка операций и показана особая роль дружественных функций в этом механизме. Здесь же вводится понятие "ресурсоемкие объекты" и показано, как для их классов определять конструкторы копирования и выполнять перегрузку операции присваивания. Глава 12 посвящена уникальному механизму управления ходом выполнения программы — исключениям. Показано, как определять класс исключений, как генерировать (посылать) исключения, как их перехватывать (ловить), обрабатывать и ретранслировать.
10 Предисловие В главе 13 рассмотрены два отношения между классами: включение и наследование. Подробно изучаются перегрузка операций при наследовании и особенности наследования классов ресурсоемких объектов. Глава 14 продолжает тему наследования классов и посвящена методам при наследовании. Особое внимание уделено правилам определения при наследовании специальных методов: конструкторов умолчания, конструкторов копирования, деструкторов и операций-функций присваивания. Показано, как нужно организовывать в Си++ наследование классов, чтобы для их объектов выполнялся принцип подстановки Лискова. Глава 15 знакомит читателя с понятиями статического и динамического типов указателей и ролью виртуальных функций в обеспечении динамического связывания. Показано, как определять виртуальные функции, и объясняется, почему деструктор рекомендуется делать виртуальным методом. Объясняются причины появления в Си++ чисто виртуальных функций и абстрактных классов. Глава 16 посвящена шаблонам. Цель их появления в языке — обеспечить параметризованное определение функций и классов. Описаны конструкции для определений шаблонов и средства для получения их специализаций. Показаны различия между типизирующими и не типизирующими параметрами шаблонов. Рассмотрены особенности определения методов и дружественных функций шаблонных классов. Материал этой главы необходим для понимания механизмов STL. Глава 17 начинается с объяснения принципов, положенных в основу построения STL. Вводится понятие последовательности, затем показано, как определить обобщенный алгоритм, контейнер, итератор. Основная цель - научить читателя определять собственные контейнеры, которые можно использовать с алгоритмами STL, и алгоритмы, которые можно применять к контейнерам STL. Глава 18 посвящена контейнерам, алгоритмам и итераторам STL. Подчеркивается, что контейнеры не связаны между собой (нет общего базового класса контейнеров), но каждый реализует все стандартные контейнерные интерфейсы. Рассмотрены последовательные и ассоциативные контейнеры, приведены их методы и итераторы, делающие их доступными для алгоритмов STL. Далее показано, как определять конкретные экземпляры
контейнеПредисловие 11 ров и как применять к этим экземплярам обобщенные алгоритмы, при необходимости вводя и используя функторы. Глава 19 названа "Стандартная библиотека и ввод-вывод". К сожалению, подробно рассмотреть все средства стандартной библиотеки Си++ в учебном пособии невозможно. Это задача справочной литературы. Поэтому в начале главы только перечислены 10 библиотек, составляющих стандартную библиотеку Си++, и приведены все стандартные заголовки, необходимые для работы с этой библиотекой. Библиотека ввода-вывода описывается достаточно подробно. Приведена иерархия шаблонов потоковых классов ввода-вывода. Рассмотрены средства форматирования при обменах с потоками, манипуляторы потоков, функции ввода-вывода и возможности потоков для работы с файлами. Материал вспомогательного характера помещен в приложения. Материал приложения 1 адресован читателю, который хочет использовать при изучении языка Си++ среду Microsoft Visual Studio.Net. Приложение 2 содержит сведения о предельных значениях арифметических величин, используемых в программах на Си++. Приложение 3 содержит таблицы кодов, этими кодами можно воспользоваться, например, разбирая особенности внутреннего представления символов или перекодировку из кодов MS-DOS в коды Windows. Приложение 4 содержит тексты функций, которые использованы в пособии для перекодировки русского текста из MS Windows в MS-DOS при выводе в консольное окно. Приложение 5 дополняет главу 10, в него включены сведения о методах для работы со строками в стиле Си++. Методы представлены прототипами, расположенными в алфавитном порядке. Приложение 6 полезно при решении задач вычислительного характера в тех случаях, когда удобно использовать средства стандартной библиотеки языка Си. Приложение 7 дополняет главу 18. Из приложения можно получить справку о том или ином алгоритме STL. Алгоритмы сгруппированы по их функциональным назначениям, и указаны заголовки, которые необходимы для их применения в программе. Приложение 8 дополняет главу 19. Приведены прототипы методов классов ввода-вывода, флаги, манипуляторы, режимы файлов. Приложение 9 дополняет главу 19. Приведены прототипы методов класса для работы с комплексными числами.
12 Предисловие Приложение Ю - это инструкция по применению для работы на языке Си++ свободно распространяемого компилятора DJGPP. Учебное пособие предназначено для читателей с разным уровнем знакомства с языками программирования. Имея только начальные сведения по алгоритмическим языкам, минимальный опыт программирования, читатель, последовательно прорабатывая материал, может изучить Си++, начиная с его азов. Рекомендуя читать книгу последовательно главу за главой, следует отметить одно исключение, относящееся к главе 2. В ней рассматриваются лексические основы языка, и при первоначальном знакомстве этот материал может просто утомить читателя. Однако полностью пропустить сведения этой главы невозможно — они необходимы для понимания следующих глав. Поэтому при первом чтении прочтите главу 2 очень бегло и возвращайтесь к ней по мере возникновения вопросов, т. е. используйте материал главы 2 как справочное руководство. Напомним, что язык Си практически полностью вошел в Си++. Поэтому для читателя, хорошо знакомого с языком Си, следует в первых главах обращать внимание на то новое, что появилось именно в Си++ по сравнению с Си. Для человека, хорошо знакомого с Си++, интерес представят главы, посвященные шаблонам (параметризованным классам) и внутренним механизмам построения стандартной библиотеки шаблонов (STL). Эти сведения позволят читателю самостоятельно расширять возможность этой библиотеки. Он научится разрабатывать алгоритмы, пригодные для обработки структур данных STL, и создавать собственные структуры данных (контейнеры), к которым будут применимы обобщенные алгоритмы STL. За конструктивные замечания, сделанные по первому варианту рукописи пособия, автор глубоко благодарен рецензентам: доценту МГИЭМ С.М. Лавренову, сотрудникам кафедры "Системы обработки информации и управления" МГТУ им. Н.Э. Баумана, ее заведующему профессору В.М. Черненькому и доценту А.Д. Козлову.
13 Глава 1 НЕФОРМАЛЬНОЕ ВВЕДЕНИЕ в Си++ 1.1. Первая программа на языке Си++ При изучении нового языка программирования многим пользователям проще усвоить конкретные приемы программирования, т.е. научиться составлять типовые несложные программы, чем сначала досконально разобраться в синтаксисе и семантике языка. Связано это с тем общепонятным фактом, что синтаксис и семантика языка определяют форматы и поведение отдельных внешне разрозненных конструкций и элементов, а все эти элементы и конструкции бесполезны при их отрыве друг от друга. Только объединенные в законченную программу конструкции языка образуют взаимосвязанную систему, решающую ту или иную задачу. Именно поэтому начинать изучение языка гораздо легче с небольших конкретных программ, а не с правил построения (с синтаксиса) языковых конструкций. Учитывая сказанное, начнем с небольших задач, решение которых позволяет проиллюстрировать особенности применения основных средств языка Си++. Следуя классикам [28], приведем программу, выводящую на экран дисплея фразу “Hello, World!” (Здравствуй, Мир!): //hello, срр - имя файла с программой. #include <iostream> intmainf) { std::cout« "Hello, World!"«std::endl; return 0; } Результат выполнения программы в консольном окне экрана: Hello, World! В первой строке текста программы — необязательный однострочный комментарий, где указано имя файла hello.срр, в котором хранится исходный текст программы.
14 Глава 1 Во второй строке — команда (директива) препроцессора, обеспечивающая включение в программу средств связи со стандартными потоками ввода и вывода данных. Указанные средства подключаются к программе при использовании заголовка с именем iostream (мнемоника: / (input) — ввод; о (output) — вывод; stream — поток). Стандартным потоком вывода по умолчанию является вывод на экран дисплея (в консольное окно). Стандартный поток ввода обеспечивает чтение данных от клавиатуры. Третья строка является заголовком функции с именем main. Перед main помещено служебное слово int, указывающее, что функция main( ) возвращает целое (integer) значение. Возвращаемое функцией main( ) значение должно быть равно нулю, если исполнение программы прошло успешно. Круглые скобки после main требуются в соответствии с синтаксисом заголовка любой функции. В них помещается необязательный для главной функции список параметров. В нашем примере параметры не нужны и список пуст. Тело любой функции - это заключенная в фигурные скобки последовательность описаний, определений и операторов. Каждое описание, определение или оператор заканчивается символом "точка с запятой". В теле нашей функции main( ) явных описаний и определений нет и есть только два оператора. Первый из них: std::cout« "Hello, World!"«std::endl; Конструкция stdr.cout в соответствии с информацией, содержащейся в заголовке iostream, является именем объекта, который обеспечивает вывод информации на экран дисплея (в стандартный поток вывода). Данные для вывода передаются объекту stdr.cout с помощью операции << ("поместить в"). То, что нужно вывести, помещается справа от знака операции <<. В данном случае это строка (строковая константа) "Hello, World!". Строковая константа в языке Си++ — это заключенная в кавычки последовательность почти любых символов. Вслед за строкой помещена еще одна операция «, а за ней манипулятор stdr.end! (endl —сокращение от "end of line" — "конец строки"). Его роль - очистить буфер выходного потока и поместить в выходной поток символ перехода на новую строку. Таким образом, программа выведет на экран фразу «Hello, World!» и переведет курсор в начало следующей строки консольного окна экрана.
Неформальное введение в Си++ 15 Уже сейчас следует отметить одну из принципиальных особенностей языка Си++, называемую перегрузкой, или расширением действия стандартных операций. Конструкция (лексема) << играет роль операции вставки ("поместить в") именно потому, что слева от нее находится имя объекта stdr.cout. В противном случае пара символов << означает, как и в языке Си, операцию сдвига влево двоичного представления левого операнда. Второй оператор в нашей программе return О; - "оператор возврата". Он завершает исполнение программы и передает в точку ее вызова значение того выражения, которое помещено перед символом "точка с запятой". Так как программа "запускается" на исполнение по команде операционной системы, то и возврат будет выполнен к операционной системе. Точнее говоря, обращение к программе выполняется из стандартного модуля, который автоматически «прикомпоновывается» к ней и из которого выполняется вызов функции main( ). Именно в этот модуль происходит возврат из программы. Как уже отмечено, при успешном завершении программа должна передавать в точку вызова нулевое значение, поэтому после return помещено значение 0. До выполнения программы необходимо: • подготовить ее текст; • передать этот текст на компиляцию и устранить синтаксические ошибки, выявленные компилятором; • безошибочно откомпилировать (получится объектный файл); • дополнить объектный файл нужными библиотечными функциями (компоновка) и получить исполняемый модуль программы. В конкретных компиляторах исходный текст программы на языке Си++ обычно находится в файле, имя которого имеет расширение "срр", объектный файл имеет обозначение с расширением ".obj", а исполняемый модуль помещается в файл, имя которого имеет расширение ".ехе". Классическая схема подготовки исполняемой программы приведена на рис. I.I. Заметим, что при использовании современных интегрированных сред разработки (ИСР) схема подготовки программ на языке Си++ более сложная (см., например, Приложение I, а также [15, 24]). Однако в этой главе мы не можем отвлекаться на знакомство с ИСР, а сосредоточим внимание на текстах программ на языке Си++. На рис. I. I
16 Глава 1 *.срр *. ii *. obj *. exe hello, cpp Directory (Рабочий каталог) Include directory Включаемые файлы hello, ii hello, obj Output directory Library directory Библиотечные файлы hello, exe Output directory Рис. 1.1. Схема подготовки исполняемой программы (исходный текст в одном файле) перед шагом компиляции показан этап препроцессорной обработки текста программы. В нашем примере препроцессор обрабатывает директиву #include <io$tream> и подключает к исходному тексту программы средства для обмена с дисплеем (для поддержки операции <<). Результат препроцессорной обработки при включении специальной опции компилятора помещают в файл, имя которого заканчивается расширением Пусть исходный текст программы подготовлен в файле hello.срр. Препроцессор, выполнив директивы препроцессора, сформирует полный текст программы — единицу трансляции
Неформальное введение в Си++ 17 (translation unit). Компилятор создаст объектный файл, выбрав (по умолчанию) для него имя hello.obj. Компоновщик (редактор связей, Linker) дополнит программу библиотечными функциями (например, для работы с объектом std::cout) и построит исполняемый модуль, например, с именем hello.exe. Запустив на выполнение файл hello.exe, получим на экране желаемую фразу «Hello, World!». Особенности выполнения перечисленных действий зависят от конкретного компилятора языка Си++ и той операционной системы, в которой он работает. Технические подробности следует изучить по документации конкретного программного продукта. (Приложение 10 описывает действия, которые нужны для создания и исполнения программ в ИСР Microsoft Visual C++.NET 2005.) При использовании свободно распространяемого компилятора DJGPP построение исполняемой программы, текст которой размещен в файле hello.cpp, обеспечивает такая команда: >дхх hello.срр -о hello.exe <ENTER> Здесь "дхх" — название (имя) версии компилятора, в которой в качестве языка по умолчанию установлен язык Си++ и при компоновке автоматически подключаются стандартные библиотеки Си++. (Вместо дхх можно использовать дрр.) "-о" - ключ (опция), вслед за которым указывается имя создаваемого файла (hello.exe), где будет размещен исполняемый модуль программы. Здесь и далее <ENTER> обозначает нажатие пользователем клавиши Enter. Если исполняемый модуль создан в каталоге [examples] на диске С:, то для запуска нашей программы в консольном окне с клавиатуры нужно ввести: >C:\examples\hello. exe<ENTER> 1.2. Пространство имен и стандартные заголовки Прежде чем переходить к следующим примерам, остановимся на влиянии, которое оказал принятый в 1998 г. международный стандарт 1SO/1EC 14882 на такую крохотную программу, как 2 -2762
18 Глава 1 "hello.cpp". Это позволит обратить внимание на особенности перехода от "старомодных" компиляторов к современным и даст возможность читателю понимать тексты программ, написанных и опубликованных до введения Стандарта. (Сейчас действует версия стандарта, принятая в 2003 г., см. [4].) До введения Стандарта обозначения заголовочных файлов программ имели расширение ".h". Таким образом, устаревший вариант препроцессорной директивы, подключения к программе библиотечных средств консольного ввода-вывода был таким: #include <iostream.h>. Второе отличие стандартной программы от ее устаревшего варианта - префикс "stdв именах стандартного выходного потока std::cout и манипулятора std::endl. Первое изменение (удаление суффикса ".h" из названия заголовка) — это принятое в Стандарте соглашение, адресованное скорее разработчикам компиляторов и библиотек. Смысл появления префикса "std::" гораздо больше затрагивает и авторов библиотек, и программистов-пользователей. "std" — принятое обозначение пространства имен стандартной библиотеки языка Си++. Лексема :: - это операция указания области видимости. Пространство имен — это средство, позволяющее группировать и локализовать обозначения, использованные в библиотеке или программе. Если пространство имен при разработке программы не указано, то предполагается, что все глобальные имена (например, имя main главной функции программы), использованные в программе, находятся в единственном глобальном пространстве имен. Именно в это глобальное пространство имен до введения Стандарта помещались имена объектов библиотек классов и функций языка Си++. Для обращения к именам глобального пространства никакого префикса с операцией указания видимости не требуется. Поэтому ранее при использовании нестандартного заголовка <iostream.h> имена cout и endl в программах употреблялись без префиксов. Стандарт поместил имена из стандартной библиотеки классов и функций в пространство имен std, т.е. отделил эти имена от глобального пространства. Теперь к именам библиотечных средств можно обратиться, указав, что разыскивать их нужно в пространстве имен std (а не в глобальном пространстве имен).
Неформальное введение в Си++ 19 Так как при частых обращениях к библиотеке довольно обременительно снабжать каждое имя библиотечного средства префиксом stdто возможно применение в программе специального описания такого вида: using namespace имя_пространства_имен; Это описание позволяет обращаться к именам названного в нем пространства с помощью неполного имени, не содержащего префикса. Описание using namespace std; делает доступными имена из std в той части программы, перед которой это описание размещено. С учетом сказанного модифицируем нашу программу "hello.cpp”'. //P01J01.cpp - вариант программы hello.cpp с using-описанием #include <iostream> using namespace std; //доступ к пространстгу имен std intmainf) { cout«"Hello, World and namespace.r<<endl; return 0; } Результат выполнения программы: Hello, World and namespace! Старые компиляторы не справятся с обработкой этой программы. Если у вас такой компилятор, как, например, Borland C++ 3.0, обойти это затруднение несложно — замените в программе POl-Ol.cpp строки #include <iostream> using namespace std; на одну строку #include <iostream.h> Теперь ее будет компилировать и устаревший компилятор. Разработчики современных компиляторов с целью сохранения преемственности встраивают в них возможность применения в программах старых заголовков (с расширением .И). Но это уже не относится к Стандарту. 2*
20 Глава 1 1.3. Программа с вводом данных в цикле Рассмотрим программу для вычисления суммы и количества целых чисел, введенных пользователем с клавиатуры. Окончание работы программы — ввод нулевого значения. //Р01_02. срр - Ввод данных в цикле //1 #include <iostream>//Библиотечные средства ввода-вывода //2 using namespace std; //Доступ к пространству имен std //3 intmainf) { //4 int х; // Очередное читаемое число //5 int summa=0; // Накапливаемая сумма //6 int counter=0; // Количество просуммированных чисел //7 cout«"Enter integers (0 is the end):”«endl; //8 while (cout«"x''«counter+1«"=", cin»x, x /= 0) { //9 counter++; // Увеличение счетчика на 1. //10 summa+=x; // Увеличение суммы на х. //11 } //12 cout«"counter="«counter<<", sum=,,«summa«endl; //13 return 0; // Завершение программы. //14 ) //15 Результаты выполнения программы: Enter integers (0 is the end): x1=10<ENTER> x2=2<ENTER> x3=8<ENTER> x4=0<ENTER> counter=3, sum-20 В тексте программы помещены подробные комментарии. Каждый комментарий вводится парой символов // и заканчивается неизображаемым кодом конца строки. Все строки текста программы справа пронумерованы. Номера оформлены в виде комментариев и нужны только для ссылок на строки программы при пояснениях. По сравнению с предыдущей программой в этой программе использовано гораздо больше средств языка Си++. Первые четыре строки (1-4) и две последних (14-15) уже нам понятны - они практически совпадают с первыми и последними строками
прогНеформальное введение в Си++ 21 раммы Р01_01.срр. "Новости" начинаются в строках 5—7, где определены и инициализированы целочисленные переменные: intx; //Очередное читаемое число //5 int summa=0; // Накапливаемая сумма //6 int counter=0; // Количество просуммированных чисел //7 В строке 8 размещен оператор, выводящий на экран (в стандартный выходной поток) приглашение пользователю вводить целые числа с указанием, что ввод будет завершен, если ввести нулевое значение: cout«"Enter integers(0 is the end):"«endi; //8 В строках 9—12 цикл, заголовок которого начинается со служебного слова while: while (cout«"x"«counter+1<<"=", cin»x, x != 0) { //9 counter++; // Увеличение счетчика на 1. //10 summa+=x; // Увеличение суммы на х. //11 } //12 В круглых скобках условие выполнения итераций цикла. Условие достаточно сложное и требует пояснений. В условие входят три выражения, разделенные запятыми: cout«"x”«counter+1«"='', cin»x, х!= О Первое из них служит для вывода информации в стандартный выходной поток, представленный объектом cout. Выводится изображение символа ‘х’, затем увеличенное на единицу значение переменной counter и, наконец, изображение символа Если переменная counter равна 0, то на экране появится х1=. Следующее выражение cin»x обеспечивает чтение данных из стандартного входного потока в переменную х. По умолчанию стандартный входной поток “настроен” на чтение с клавиатуры, т. е. переменная получает значение, которое ввел пользователь перед тем, как нажал клавишу ENTER. Третье выражение х != О логическое, в нем значение переменной х сравнивается с нулем. Если в х введено не нулевое значение, то результат сравнения истинный (true). Этот результат воспринимается как значение всего условия выполнения итераций цикла, и выполняется тело цикла (строки 10—12).
22 Глава 1 В теле цикла два оператора. Первый из них counter++; представляет собой выражение с унарной операцией ++, действие которой состоит в увеличении на 1 операнда (переменной counter). Второй оператор summa+=x; содержит выражение с операцией, в которой суммирование операндов совмещено с присваиванием результата левому операнду. (Как мы узнаем позже, этот оператор можно было бы записать в виде summa=summa+n;.) Выполнение этого оператора увеличивает значение переменной summa на значение переменной х. Итак, заголовок цикла обеспечивает ввод данных и проверку каждого введенного значения. Если введенное число отлично от нуля, то в теле цикла увеличиваются на 1 счетчик (переменная counter) и на величину введенного числа сумма (переменная summa). После завершения итераций цикла выполняется оператор cout<< ”counter= ”«counter<<", sum="<<summa; //13 Тем самым в выходной поток выводятся: количество введенных пользователем ненулевых чисел и их сумма. Приведенные выше конкретные результаты выполнения программы поясняют сказанное. 1.4. Строки в языке Си++ Язык Си++ получил в наследство от языка Си странный аппарат для работы с текстовой информацией. Со строковыми константами дело обстоит достаточно хорошо. Для представления строковых (текстовых) констант используются заключенные в кавычки последовательности символов. Такие константы мы уже использовали, например “Hello, World!”. Строковых переменных, которым в процессе выполнения программы можно присваивать разные значения, ни в языке Си, ни в языке Си++ просто нет! Вместо них язык использует символьные массивы со специальными правилами заполнения и обработки. К такому представлению строк (строк в стиле Си) мы еще вернемся. А сейчас покажем на примере, как Стандарт языка Си++ позволяет упростить работу с текстовой информацией. В библиотеке классов, соответствующей Стандарту, определен класс string, позволяющий работать со строками как с переменными базовых типов
Неформальное введение в Си++ 23 языка, т. е. можно определять объекты класса string так же, как переменные базовых типов (например, типа int), присваивать объектам-строкам разные значения и выполнять над ними некоторые операции. Приведем программу, в которой пользователя просят ввести свое имя, а затем приветствуют его по имени. В отличие от предыдущих программ для представления строк (и констант, и переменных) применим объекты класса string. Текст программы: //РО 1_03.срр - Строки в стиле Си++ //1 #include <iostream> //Для средств ввода-вывода //2 #include <string> //Для библиотечного класса string //3 using namespace std; //Доступ к пространству имен std //4 intmain(){ //5 string response = "Enter Your name: //6 cout << response «end!; //7 string name; //8 cin»name; //9 string greeting = "Hello, //10 greeting = greeting + name + 7"; //11 cout«greeting< < endl; //12 return 0; //13 } //14 Результаты выполнения: Enter Your name: Tom<ENTER> Hello, Tom! Строки программы справа пронумерованы. В заголовке программы (строка 3) директива #include <string> подключает к программе библиотечный класс string. В теле функции main() (строки 6, 8, 10) определены три объекта-строки класса string. Объект с именем response (в строке 6) инициализирован значением строковой константы "Enter Your name: " (Введите ваше имя:). Этот текст выводится (строка 7) в стандартный поток вывода в качестве приглашения пользователю. В строке 8 определен пустой (не инициализированный) объект-строка с именем name, в который помещается вводимая пользователем информация (имя
пользо24 Глава 1 вателя). Передачу данных из входного потока (от клавиатуры) обеспечивает оператор cin » пате; (строка 9), где cin - имя объекта, представляющего входной поток, >> — операция чтения данных. В строке 10 определен и инициализирован строковой константой "Hello" объект-строка greeting. В строке 11 этому объекту присваивается новое значение — конкатенация (сцепление) строк greeting, пате и строковой константы Для конкатенации строк-объектов класса string используется операция + (которая для операндов арифметических типов традиционно является операцией суммирования). Это еще один пример перегрузки или расширения действия стандартных операций. Оператор cout «greeting; в строке 12 выводит значение объекта-строки greeting, затем оператор return 0; завершает выполнение программы. Приведенные выше результаты выполнения программы дополняют сделанные пояснения.
Глава 2 ЛЕКСИЧЕСКИЕ ОСНОВЫ ЯЗЫКА СИ++ 2.1. Общие сведения о программах, лексемах и алфавите Общая схема обработки программы и пробельные разделители. Основная программная единица на языке Си++ - это текстовый файл с названием имя.срр, где срр — принятое расширение для программ на Си++, а имя выбирается достаточно произвольно. Для удобства ссылок и сопоставления программ с их внешними именами целесообразно помещать в начале текста каждой программы строку комментария с именем файла, в котором она находится. Это уже сделано в программах предыдущего параграфа. Текстовый файл с программой на Си++ вначале обрабатывает препроцессор (см. рис. l.l), который распознает и выполняет команды (директивы) препроцессора (каждая такая команда начинается с символа '#'). В приведенных выше программах использованы препроцессорные команды #include <имя_заголовка> Команда #include вставляет в программу заранее подготовленный текст файла, определяемого именем заголовка. Выполняя препроцессорные директивы, препроцессор изменяет исходный текст программы. Сформированный таким образом измененный текст программы поступает на компиляцию. Компилятор, во-первых, выделяет из поступившего к нему текста программы лексические элементы, т.е. лексемы (tokens), а затем на основе грамматики языка распознает смысловые конструкции языка (выражения, определения, описания, операторы и т.д.), построенные из этих лексем. Фазы работы компилятора здесь рассматривать нет необходимости. Важно только отметить, что в результате работы компилятора формируется объектный модуль программы (или ее части). Компилятор, выполняя анализ текста программы на языке Си++, для распознавания начала и (или) конца отдельных
26 Глава 2 конструкций использует пробельные разделители. К пробельным разделителям относятся собственно символы пробелов, символы табуляции, символы перехода на новую строку. Кроме того, к пробельным разделителям относятся комментарии. В языке Си++ есть два способа задания комментариев. Традиционный способ (ведущий свое происхождение от многих предшествующих языков, например, ПЛ/1, Си и т.д.) определяет комментарий как последовательность символов, ограниченную слева парой символов /*, а справа — парой символов */• Между этими граничными парами может размещаться почти любой текст, в котором разрешено использовать не только символы из алфавита языка Си++, но и другие символы (например, русские буквы). Пример: /* Комментарий, допустимый во многих языках */ Текст такого комментария может размещаться в нескольких строках, поэтому его иногда называют многострочным. Стандартом запрещено вкладывать такие комментарии друг в друга. Второй способ (введенный в Си++) определяет однострочный комментарий — последовательность символов, началом которой служат символы //, а концом — не изображаемый код перехода на новую строку. Пример: // Это однострочный комментарий Пары // и /* в тексте однострочного комментария воспринимаются как обычные символы, т. е. внутри однострочного комментария не может начаться другой комментарий. Пара // может появиться в многострочном комментарии (выделенном ограничителями /*, */) и воспринимается как пара обычных символов. Алфавит и лексемы языка СИ++. В алфавит языка Си++ входят 96 символов. Среди них 91 изображаемые: • прописные и строчные буквы латинского алфавита; • цифры 0, 1,2, 3, 4, 5, 6, 7, 8, 9; • 29 специальных знаков: " {} , I []()+- / % \ ; ':?< = > ! & # ~ Л . *
Лексические основы языка Си++ 27 К неизображаемым относятся пробел и управляющие символы: горизонтальная табуляция, вертикальная табуляция, перевод страницы, начало новой строки. Из изображаемых символов алфавита формируются лексемы (tokens) языка: • идентификаторы (identifier); • служебные (иначе ключевые) слова (keyword); • константы (literal); • знаки операций (operator); • разделители (знаки пунктуации — punctuator). Рассмотрим эти лексические элементы языка подробнее. 2.2. Идентификаторы и служебные слова Идентификатор — последовательность произвольной длины из букв латинского алфавита, десятичных цифр и символов подчеркивания, начинающаяся не с цифры: RUN run hard_RAM_disk сору_54 Прописные и строчные буквы различаются. Таким образом, в этом примере два первых идентификатора различны. На длину различаемой части идентификатора конкретные реализации могут накладывать ограничение, но Стандарт этого не предусматривает. Ключевые (служебные) слова — это идентификаторы, зарезервированные в языке для специального использования. asm else new this auto enum operator throw bool explicit private true break export protected try case extern public typedef catch false register typeid char float reinterpret_cast typename class for return union const friend short unsigned constjcast goto signed using continue if sizeof virtual default inline static void delete int staticjcast volatile do long struct wcharj double mutable switch while dynamicjcast namespace template
28 Глава 2 Не все из перечисленных ниже служебных слов необходимы каждому программисту, однако их запрещено использовать в качестве произвольно выбираемых имен, и список служебных слов нужно иметь уже на начальном этапе знакомства с языком. Кроме приведенных служебных слов Стандарт определил следующие идентификаторы, которые могут использоваться для альтернативного представления некоторых знаков операций и разделителей. Знать их необходимо, так как их запрещено использовать для других целей. Зарезервированные идентификаторы C++ and && bitor | not_eq != xor and_eq &= compl ~ or II xor_eq Л= bitand & not ! or_eq | = Идентификаторы, включающие два подряд символа подчеркивания ( ), резервируются для реализаций компиляторов языка Си++ и его стандартных библиотек. Идентификаторы, начинающиеся с одного символа подчеркивания (_), используются в компиляторах языка Си. В связи с этим начинать выбираемые пользователем идентификаторы с символа подчеркивания и использовать в них два подряд символа подчеркивания не рекомендуется. 2.3. Константы-литералы и перечисления В языке Си++ существует несколько видов констант: константы-литералы (literal), именованные константы, константы перечислений и препроцессорные константы. Рассмотрим вначале только константы-литералы. Константа-литерал — это лексема, представляющая изображение фиксированного значения. Константы-литералы делятся на пять групп: целые (integer-literal); вещественные (floating-literal — с плавающей точкой); логические (boolean-literal — булевские); символьные (character-literal - литерные); строковые (строки или литерные строки — string-literal).
Лексические основы языка Си++ 29 Компилятор, выделив константу в качестве лексемы, относит ее к определенной группе, а внутри группы — к тому или иному типу данных по ее "внешнему виду" (по форме записи) в исходном тексте и по числовому значению. Для констант всех типов препроцессор определяет предельные значения (см. Приложение 2), числовые величины которых могут быть различны для конкретных реализаций языка (компилятора). Целые константы могут быть десятичными (decimal), восьмеричными (octal) и шестнадцатеричными (hexadecimal). Десятичная целая константа определена как последовательность десятичных цифр, начинающаяся не с нуля, если это не число нуль: 16, 484216, 0, 4. Отрицательные константы - это константы без знака, к которым применена операция изменения знака: -48, -3. Восьмеричные целые константы начинаются всегда с нуля: 016 имеет десятичное значение 14. Если в записи восьмеричной константы встретится недопустимая цифра 8 или 9, то это воспринимается как ошибка. Последовательность шестнадцатеричных цифр, которой предшествует Ох, считается шестнадцатеричной константой. В набор шестнадцатеричных цифр, кроме десятичных, входят латинские буквы от л (илиЛ) до/(или F). Таким образом, Ох 16 имеет десятичное значение 22, а 0 х F - десятичное значение 15. В зависимости от значения целой константы компилятор по- разному представляет ее в памяти ЭВМ, относя ее к тому или иному типу данных. В связи с целочисленными данными упомянем некоторые из тех препроцессорных констант, заданных заголовком <climits>, которые определяют предельные значения целых величин разных типов. (О препроцессорных константах речь пойдет позже, сейчас достаточно понимать, что их имена представляют в программе некоторые фиксированные значения.) Для типа int (“целый” или “целочисленный”) диапазон допустимых значений определяют пре процессорные константы INT_MIN и INT_MAX. Для 32-разрядных компиляторов их значения обычно равны 2147483647 и -2147483647. Для типа unsigned int (“беззнаковый целый”) минимальное значение равно 0, а максимальное задает препроцессорная константа UINT_MAX- 4294967295. Если константа принадлежит диапазону [INT MIN, INT_MAX\, то компилятор приписывает ей тип int. Если константа больше
30 Глава 2 fNT MAX, но не превышает UINTJMAX, то ей соответствует тип unsigned int. При использовании целых чисел, превышающих UINT MAX, возникает ошибка компиляции. (Сказанное относится к реализациям, в которых не различаются типы int - “целое” и long - “длинное целое”.) Если программиста по каким-либо причинам нс устраивает тот тип, который компилятор приписывает константе, то он может явным образом повлиять на его выбор. Для этого служат суффиксы L, I (long) и U, и (unsigned). Например, константа 64L будет иметь тип long, хотя значению 64 должен быть приписан тип int,. Для одной константы можно использовать два суффикса U(u) и L(l), причем в произвольном порядке. Например, константы 0x22UI, 0x11Lu, 0x330000UL, 0х551и будут иметь тип unsigned long. При использовании одного суффикса выбирается тот тип данных, который более всего соответствует типу, выбираемому для константы по умолчанию (т.е. без суффикса). Например, 04L есть константа типа long, 04U имеет тип unsigned int и т.д. Чтобы оценить предельные значения целочисленных констант и размеры участков памяти, выделяемых для их представления в ЭВМ, можно использовать следующую программу: //Р02_01 .срр - Предельные значения и размеры целых констант #include <iostream> #include <climits> //Для препроцессорных констант using namespace std; //Доступ к пространству имен std int main() { cout« "INT_MIN = " « INT_MIN « endI; cout« INT MAX = " « INT_MAX « endl; cout « "UINT_MAX = "« UINT_MAX« endl; cout « "LONG_MAX = "« LONG_MAX« endl; cout « "sizeof 111 = "« sizeof 111 «endl; cout« "sizeof 111L- "« sizeof 111L«endl; return 0; } Директива #include <climits> включает в программу определения уже упомянутых препроцессорных констант. Значения некоторых из них выводятся в стандартный выходной поток (в консольное окно на экране). В программе использована унарная
Лексические основы языка Си++ 31 операция языка Си++ sizeof, позволяющая вычислять размер в байтах участка памяти, выделяемой для стоящего справа операнда. При использовании разных компиляторов результаты выполнения программы могут быть различными. В том 32-разрядном компиляторе, который не делает различия между целыми типов int и long, результаты будут такими: INT_MIN = -2147483648 INT_MAX = 2147483647 U INT_МАХ = 4294967295 LONG_MAX = 2147483647 sizeof 111=4 sizeof 111L = 4 К целочисленным константам Стандарт относит и перечислимые константы (константы перечислений). Перечислимые константы (или константы перечисления, иначе константы перечислимого типа) вводятся с помощью служебного слова епит. По существу это обычные целочисленные константы (типа int), которым приписаны уникальные и удобные для использования обозначения. В качестве обозначений выбираются произвольные идентификаторы, не совпадающие со служебными словами и именами других объектов программы. Обозначения присваиваются константам с помощью определения, например, такого вида: епит {one = 1, two - 2, three = 3}; Здесь епит — служебное слово, определяющее тип данных "перечисление"; one, two, three — условные имена, введенные программистом для обозначения констант 1, 2, 3. После такого определения в программе вместо константы 2 (и наряду с ней) можно использовать ее обозначение two и т.д. Если в определении перечислимых констант опускать знаки "=" и не указывать числовых значений, то они будут приписываться идентификаторам (именам) по умолчанию. При этом самый левый в фигурных скобках идентификатор получит значение 0, а каждый последующий увеличивается на 1. Например, в соответствии с определением епит {zero, one, two, three };
32 Глава 2 перечислимые константы примут значения: zero—0, оле==1, two—2, three—3. Правило о последовательном увеличении на 1 значений перечислимых констант действует и в том случае, когда первым из них (слева в списке) явно присвоены значения. Например, определение епит {ten = 10, three = 3, four, five, six}; вводит следующие константы: ten== 10, three—3, four—A, five—5, s/x==6. Имена перечислимых констант должны быть уникальными, однако к значениям констант это не относится. Одно значение могут иметь разные константы. Например, определение епит {zero, nought = 0, one, two, pair = 2, three} ; вводит следующие константы: zero—0, nought—0, one—1, two—2, pair—2, three^=3. Значения, принимаемые перечислимыми константами, могут быть заданы не только в виде целочисленных констант, но и в виде выражений. Например, конструкция епит {two = 2, four = two * 2 }; определит константы two—2 и four—4. Так как отрицательная целая константа — это константа без знака, к которой применена унарная операция “ (минус), то перечислимые константы могут иметь и отрицательные значения. Вещественные константы, т.е. константы с плавающей точкой, даже не отличаясь по значению от целых констант, имеют другую форму внутреннего представления в ЭВМ. Эта форма требует использования арифметики с плавающей точкой при операциях с такими константами. Компилятор распознает вещественные константы по внешним признакам. Константа с плавающей точкой может включать следующие шесть частей: • целая часть (десятичная целая константа); • десятичная точка;
Лексические основы языка Си++ 33 • дробная часть (десятичная целая константа); • признак (символ) экспоненты е или Е; • показатель десятичной степени (десятичная целая константа, возможно со знаком); • суффикс F (или 0 либо L (или /). В записях вещественных констант могут опускаться: целая или дробная часть (но не одновременно), десятичная точка или признак экспоненты с показателем степени (но не одновременно); суффикс. Примеры вещественных констант: 66. .0 .12 3.14159F 1.12е-2 2E+6L 2.71 При отсутствии суффиксов F (0 или L (/) вещественные константы имеют форму внутреннего представления, которой в языке Си++ соответствует тип данных double. Добавив суффикс f или F, константе придают тип float. Константа имеет тип long double} если в ее представлении используется суффикс L или /. Диапазоны возможных значений данных вещественного типа определяются для каждой реализации компилятора и доступны в программах с помощью стандартизованных препроцес- сорных констант заголовка <cfloat>. Для каждого из типов {float, double, long double) препроцессорными константами определены (см. Приложение 2): количество верных десятичных цифр, минимальное значение, отличное от машинного нуля при сравнении с 1, максимальное значение, предельное значение показателя (экспоненты) и др. Для иллюстрации в табл. 2.1 приведены диапазоны возможных значений и длины внутреннего представления (размеры в битах) данных вещественного типа в конкретной 32-разрядной реализации Си++ (компилятор DJGPP, см. Приложение 10). Табл и ца 2.1 Данные вещественного типа Тип данных Размер, бит Диапазон значений float 32 от 3.4Е—38 до 3.4Е+38 double 64 от 1.7Е—308 до 1.7Е+308 long double 96 от 3.4Е—4932 до 1.1Е+4932 з- 2762
34 Глава 2 Следующая программа позволяет оценить размеры (в байтах) участков памяти, выделяемой вещественным константам разного типа. //Р02_02. срр - Размеры памяти для вещественных констант #include <iostream> using namespace std; intmainf) { cout« ”sizeof3.141592653589793 = cout« sizeof3.141592653589793 « endl; cout« "sizeof3.14159 = " << sizeof3.14159« endl; cout« "sizeof3.14159f= "« sizeof3.14159f« endl; cout« "sizeof3.14159L = " << sizeof3.14159L« endl; return 0; } Результаты выполнения программы для 32-разрядного компилятора: sizeof3.141592653589793 = 8 sizeof3.14159 = 8 sizeof3.14159f = 4 sizeof3.14159L = 12 Обратите внимание, что размеры участков памяти не зависят от количества цифр в записи констант, а определяются их типами. Булевские (логические) константы — это два литерала типа bool: true (ИСТИНА) и false (ЛОЖЬ). Их можно использовать в логических выражениях и присваивать логическим переменным. Наряду с литералами true и false в Си++ продолжают действовать унаследованные из языка Си правила, в соответствии с которыми значению ЛОЖЬ соответствует число 0, а любое, отличное от 0 значение воспринимается в логическом выражении как ИСТИНА. Символьные (литерные) константы - это один или несколько символов, заключенные в апострофы. Перед начальным апострофом может размещаться буква L. Константа без ведущего символа L считается ординарной или узкой (narrow-character literal). Ординарная константа из одного символа имеет тип char, а ее значение эквивалентно числовому
Лексические основы языка Си++ 35 значению кода из применяемой в реализации кодовой таблицы символов (Приложение 3). Ординарная символьная константа из нескольких символов называется мультисимвольной (multicharacter literal) и имеет тип int. Значение мультисимвольной константы полностью зависит от реализации. Константа, включающая ведущий символ L, имеет тип wcharjt и называется широкой, или широкосимвольной (wide- character literal). Значением широкой константы из одного символа является числовое значение кода из применяемой в реализации кодовой таблицы символов. Значение широкой константы из нескольких символов полностью зависит от реализации, поэтому мы их использовать и рассматривать в пособии не будем. Примеры ординарных односимвольных констант (типа char): ■Z\ ■*', ■\012\ W, V»’ Примеры ординарных мультисимвольных констант (типа int): 'db', "\x07\x07\ -\n\t\ ‘mab’ Примеры широких односимвольных констант (типа wcharjt): L'Z', L'*', L'\012', L'\0', L'\n' В этих примерах заслуживают внимания последовательности, начинающиеся со знака '\\ Они называются эскейп-после- довательностями (escape-sequence). Стандарт выделяет простые (simple), восьмеричные и шестнадцатеричные эскейп-последо- вательности. Простые используется для символьных кодов, не имеющих графического изображения, и символов, используемых в специальных целях: апостроф (’), обратная косая черта (\), знак вопроса (?) и кавычка ("). Восьмеричные и шестнадцатеричные эскейп-последовательности позволяют вводить произвольные символьные константы, явно задавая их коды соответственно в восьмеричном или шестнадцатеричном виде. В табл. 2.2 приведены допустимые значения эскейп-последова- тельностей. В двух последних строках табл. 2.2 ооо — последовательность от 1 до 3 восьмеричных цифр; hhh — последовательность шестнадцатеричных цифр. Восьмеричная эскейп-последователь- з‘
36 Глава 2 Таблица 2.2 Допустимые ESC-последователыюсти в языке Си++ Изображение Внутренний код Обозначаемый символ (название) Реакция или смысл \а 0x07 bel (audible bell) Звуковой сигнал (alert) \ь 0x08 bs (backspace) Возврат на шаг (забой) \f ОхОС ff (form feed) Перевод страницы \п ОхОА If (new-line) Перевод строки (новая строка) \г OxOD cr (carriage return) Возврат каретки \t 0x09 ht (horizontal tab) Табуляция горизонтальная \v ОхОВ vt (vertical tab) Табуляция вертикальная \\ 0х5С \ (backslash) Обратная косая черта V 0x27 ' (single quote) Апостроф (одинарная кавычка) V 0x22 " (double quote) Двойная кавычка \? 0x3F ? (question mark) Вопросительный знак \ооо ООО Любой (octal number) Восьмеричный код \xhhh Oxhhh Любой (hex number) Шестнадцатеричный код ность может содержать любое целое восьмеричное число в диапазоне от 0 до 377. Превышение этого верхнего значения приводит к ошибке. Наиболее часто в программах используется последовательность '\0\ обозначающая символ-ограничитель строк в стиле Си (терминальный символ). В шестнадцатеричной эс- кейп-последовательности вслед за \х может быть записано любое количество шестнадцатеричных цифр. Таким образом допустимы, например, константа \x0004F и ее аналог \x4F. Однако числовое значение не должно выходить за диапазон от 0 х 0 до О х FF. Если непосредственно за символом *\* поместить символ, не предусмотренный табл. 2.2, то результат будет неопределенным. Если среди восьмеричных цифр последовательности \ооо или шестнадцатеричных в \xhhh встретится неподходящий символ, то это считается концом восьмеричного или соответственно шестнадцатеричного кода.
Лексические основы языка Си++ 37 Для использования внутренних кодов символов иногда нужна таблица, в которой каждому изображаемому на экране символу соответствует числовое значение его кода в десятичном, восьмеричном, шестнадцатеричном представлениях. На IBM-совместимых ПЭВМ применяется таблица кодов ASCII (см. Приложение 3). Выбирая из кодовой таблицы подходящее значение, можно (если это полезно по каким-либо причинам) использовать их в программе вместо явных изображений символов. Например: //Р02_03.срр - Использование кодов символьных констант #include <iostream> using namespace std; intmain() { cout« '\x48'« '\x65'« '\x6C'« '\x6C'; cout« '\x6F'« '\x2C7 cout« '\40'« '\ 127' « *\ 157'; cout << '\162'« '\154'« '\144'« '\41'«endl; return 0; } Программа выведет на экран: Hello, World! В первом и втором из операторов вывода использованы шестнадцатеричные, а в третьем и четвертом — восьмеричные коды символов: '\х48' - ’Н';...; '\40' - "пробел"; '\127' - W;...; ,\41* - Т. Как говорилось, в Си++ ординарная односимвольная константа имеет тип char. Ординарные многосимвольные константы вида f\t\nf или г\r\079 представляются значениями типа int% широкие символьные константы отнесены к типу wcharjt. В зависимости от особенностей реализации и типа константы ей выделяется участок памяти соответствующего размера. Следующая программа вычисляет размеры памяти, выделенной для символьных констант разных типов. //Р02_04.срр - Длины символьных констант It include <iostream> using namespace std; intmain() { cout << "sizeof \'z\'="« sizeof z' « endl; cout« "sizeof\'\\n\\t\'= "«sizeof ’\n\t'« endl;
38 Глава 2 cout« "sizeof L\'\\x4F\'= "« sizeof L'\x4F'« endl; cout« "sizeof \'\\111\,= "« sizeof '\ 11V « endl; return 0; } Результаты выполнения программы с 32-разрядным компилятором: sizeof 'z'= 1 sizeof '\n\t' = 4 sizeof L'\x4F' = 4 sizeof V11'= 1 Строковая константа, иногда называемая литерной строкой, определяется как последовательность символов, заключенная в кавычки (не в апострофы). Перед открывающей кавычкой может помещаться символ L. Когда символ L отсутствует, говорят об ординарной или узкой (narrow) строковой константе. При наличии символа L константу называют широкой (wide) строковой константой. Примеры: "Это литерная строка, называемая также строковой константой" С’Это широкая литерная строка, или широкая строковая константа" Как мы объясним далее, ординарная строковая константа имеет тип массива из n+1 элементов типа char. Широкая строковая константа имеет тип массива из п+1 элементов типа wchar_t. Здесь п — количество символов в строковой константе. Так как широкие строковые константы существенно зависят от реализации, то будем рассматривать только ординарные (узкие) строковые константы и к ним применять названия строковая константа или литерная строка. Среди символов строковой константы могут быть эскейп- последовательности, соответствующие неизображаемым символьным константам или символам, задаваемым значениями их внутренних кодов. В этом случае, как и в представлениях отдельных символов, эскейп-последовательности начинаются с обратной косой черты Покажем на примере, как можно применять эскейп-последовательности в строковых константах: //Р02_05.срр - Строковые константы с эскейп-последовательностями ft include <iostream>
Лексические основы языка Си++ 39 using namespace std; intmainf) { cout << "Bjarne Stroustrup is the author of \n"; cout <<"\"The C++ Programming Language\” and \n"; cout« "the creator of C++ Language. ”« endl; return 0; Обратите внимание на наличие символа 'V перед двойной кавычкой внутри строковой константы. Именно по наличию этого символа компилятор отличает внутреннюю кавычку от кавычки, ограничивающей строковую константу. Эскейп-последовательности \п и \м внутри строковых констант обеспечат при выводе результатов выполнения программы такое размещение и представление информации: Bjarne Stroustrup is the author of "The C++ Programming Language" and the creator of C++ Language. Строковые константы, записанные в программе подряд или через пробельные разделители (табуляция, код новой строки и др.), при компиляции конкатенируются (склеиваются). Таким образом, в тексте программы последовательность из двух строковых констант: "Тип строковой константы - char[ ]. “ “Тип символа - char." эквивалентна одной строковой константе: "Тип строковой константы - char[ ]. Тип символа - char." Длинную строковую константу можно еще одним способом разместить в нескольких строках текста программы, используя специальное обозначение переноса — '\'. Пример одной строковой константы, размещенной на трех строках в тексте программы: ”Обычно транслятор отводит \ каждой строковой константе \ отдельное место в памяти ЭВМ."
40 Глава 2 Символ ‘Y и последующие пробельные символы до конца строки текста программы (включая код перехода на новую строку) удаляются из текста, и формируется одна строковая константа. Отметим, что пробелы в началах строк-продолжений не удаляются и переносятся в результирующую строковую константу. Следующая программа иллюстрирует разные формы "склеивания" строк в одну: //Р02_06.срр - строковые константы и их конкатенация #include <iostream> using namespace std; intmainf) { cout« "Malum"" consilium " "est" " guod"; // При выводе пробелы будут удалены cout« " mutari поп\ protest. ”<<endl; //Пробелы этой строки сохранятся return 0; } Результат выполнения программы: Malum cosilium est, guod mutari non protest. Обратите внимание на количество пробелов в результате перед словом "protest". Продолжением перенесенной с помощью символа *\' константной строки считается любая информация на следующей строке, в том числе и пробелы. Размещая строку в памяти, транслятор автоматически добавляет в ее конец так называемый терминальный символ *\0\ Таким образом, количество символов во внутреннем представлении строки на 1 больше числа символов в ее записи. Пустая строка хранится как один символ М\0М. Строковую константу можно использовать для инициализации символьного массива (массива типа char [ ]) и в дальнейшем обращаться к ней по имени массива. Следующая программа выполняет указанные действия: //Р02_07.срр - Инициализация массива строковой константой #include <iostream> using namespace std; intmainf) {
Лексические основы языка Си++ 41 char stroka[] = "REPETITIO EST MATER STUDIORUM"; cout« "sizeof stroka = "« sizeof stroka « endl; cout« "stroka = " << stroka « endl; return 0; } Результат выполнения программы: sizeof stroka = 30 stroka = REPETITIO EST MATER STUDIORUM Обратите внимание, что при определении массива char после его имени stroke в скобках [ ] не указано количество элементов. Размер массива подсчитывается автоматически во время инициализации и равен количеству символов в строковой инициализирующей константе (в нашем случае 29) плюс один элемент для терминального символа '\0\ Кавычки не входят в строковую константу, а служат ее ограничителями при записи в программе. В строковой константе может быть один символ, например, "А" — строковая константа из одного символа. Однако в отличие от символьной константы 'А' длина внутреннего представления константы ”А” равна 2. Строковая константа может быть пустой ,,и (две двойные кавычки), при этом ее длина равна I. Однако символьная константа не может быть пустой, т.е. запись " (два апострофа) в большинстве реализаций недопустима. //Р02_08. срр - Длины строковых и символьных констант #include <iostream> using namespace std; intmainf) { cout« "sizeof \ "\" = "« sizeof cout« "\tsizeof \A\' = " << sizeof A'; cout« "\tsizeof \"A\" = " « sizeof "A" « endl; cout« "sizeof \'\\п\' = "«sizeof'\n'; cout« "\tsizeof\"\\n\" = ” << sizeof ”\n"« endl; cout« "sizeof \ '\\*FF\ << sizeof '\xFF'; cout« "\tsizeof \"\\xFF\" = " << sizeof "\xFF"« endl; }
42 Глава 2 Результат выполнения программы: sizeof'"'= 1 sizeof'A'= 1 sizeof"A” = 2 sizeof '\л' = 1 sizeof’\n" = 2 s/zeof '\xFF-1 sizeof "\xFF" = 2 2.4. Знаки операций Знаки операций обеспечивают формирование и последующее вычисление выражений. Выражение есть правило для получения значения. Один и тот же знак операции может употребляться в различных выражениях и по-разному интерпретироваться в зависимости от контекста. Для изображения операций в большинстве случаев используется несколько символов. В ANSI-стандарте языка Си определены следующие знаки операций (все они входят в язык Си++): [] 0 . -> ++ ~ & * + - ~ ! sizeof / % « » < > <= >= == |= Л i && II ?: = *_ /= &= %= Л= += |= » «= (тип) »= Кроме того, определены две препроцессорные операции: # ## Дополнительно к перечисленным в Си++ введены: .* ->* new delete typeld dynamic_cast staticjcast reinterpret_cast const_cast mn() throw В приведенном списке операций тип — условное обозначение одного из базовых типов языка либо типа, определенного программистом. О типах речь впереди. Большинство знаков операций распознаются компилятором как отдельные лексемы. В зависимости от контекста одна и та же лексема может обозначать разные операции. Например, бинарная операция & — это
поразрядЛексические основы языка Си++ 43 ная конъюнкция, а унарная префиксная операция & — это операция получения адреса. Одним из принципиальных отличий языка Си++ от предшествующего ему языка Си является возможность расширения действия, иначе перегрузки {overload) стандартных операций, т.е. распространения их действия на нестандартные для них операнды. Материал, относящийся к расширению действия (перегрузке) операций, будет рассматриваться в следующих главах. Сейчас опишем кратко стандартные возможности отдельных операций. Унарные операции К унарным относятся следующие операции: & операция получения адреса операнда (операнд — имя какого-либо объекта); * операция обращения по адресу, иначе операция разыменования (доступа по адресу к значению того объекта, на который указывает операнд). Операндом должен быть адрес (указатель); - унарный минус — изменяет знак арифметического операнда; + унарный плюс (введен для симметрии с унарным минусом); ~ поразрядное инвертирование внутреннего двоичного кода целочисленного аргумента (побитовое отрицание); ! логическое отрицание (НЕ) значения операнда; результат false (если операнд истинный) или true (если операнд ложный). В качестве логических значений в языке Си используют целые числа: 0 - ложь и не нуль — истина. Эти правила унаследовал и язык Си++. Поэтому отрицанием любого ненулевого числа будет 0 (т. е. false), а отрицанием нуля будет 1 (т. е. true). Таким образом: !1 равно 0; !2 равно 0;!(—5) равно 0; !0 равно 1; ++ увеличение на единицу (инкремент или автоувеличение) значения операнда: префиксная форма операции — увеличение значения операнда на 1 до использования его значения; постфиксная форма операции — увеличение значения операнда на 1 после использования его значения; -- уменьшение на единицу (декремент или автоуменьшение) значения операнда: префиксная форма операции — уменьшение значения операнда на 1 до использования его значения;
44 Глава 2 постфиксная форма операции — уменьшение значения операнда на 1 после использования его значения. • Для операций ++ и — операнд не может быть константой. Записи ++5 или 84++ будут неверными. Операндом не может быть и произвольное выражение. Например, ++(j+k) также неверная запись. Операндом унарных операций ++ и -- должны быть всегда леводопустимые выражения, например переменная. Понятие леводопустимого выражения введем чуть позже при рассмотрении операций присваивания; throw - операция генерации исключения. Подробно возможности и особенности этой операции будут рассматриваться при изучении механизма исключений. Формат применения операции: throw выражение _формирующее исключение typeid - операция определения типа операнда. Применение операции требует понимания правил использования классов, поэтому объяснение ее возможностей придется отложить. Отметим, что для ее использования в программу должен быть включен заголовок <typeinfo>. Две формы применения операции: typeid (выражение) — для идентификации типа на этапе компиляции, когда определяется тип значения выражения; typeid (имя_типа) - для идентификации типа на этапе компиляции программы; {тип) операция приведения (преобразования) типов. Она унаследована из языка Си. Не изменяя самого операнда, операция преобразует его значение к типу, обозначение которого помещено в скобках. Его принято называть целевым типом. Выражение с этой операцией имеет следующий формат: (имя_целевого_типа)операнд В качестве операнда используется унарное выражение, которое в простейшем случае может быть переменной, константой или любым выражением, заключенным в круглые скобки. Например, следующие преобразования изменяют длину внутреннего представления целых констант, не меняя их значений: (long) 1 - внутреннее представление имеет длину 4 байта; (char)^ - внутреннее представление имеет длину 1 байт. В этих преобразованиях константа не меняла значения и оставалась целочисленной. Однако возможны более глубокие преобразования, например (long double) 1 или (float) 1 не только изменяют длину константы, но и структуру ее внутреннего представления. При преобразовании длинных целочисленных констант к вещественному типу (например, к типу float) возможна потеря значащих цифр (потеря точности).
Лексические основы языка Си++ 45 Если вещественное значение преобразуется к целому, то возможна ошибка при выходе полученного значения за диапазон допустимых значений для целых. В этом случае результат преобразования непредсказуем и целиком зависит от реализации. тип ( ) функциональная форма преобразования типа. Она может использоваться только в тех случаях, когда тип (целевой тип) имеет простое (несоставное) наименование (обозначение). В скобках размещается список выражений, на основе которого формируется некоторое значение с типом, указанным перед скобками. Если в списке одно выражение, то преобразование выглядит так: имя_целевого_типа( операнд) Примеры для конкретного 32-разрядного компилятора: long(2) - внутреннее представление результата имеет длину 4 байта; double( 2) - внутреннее представление результата имеет длину 8 байтов. Однако будет недопустимым выражение: unsigned long(2) // Ошибка! dynamic jcast - операция приведения типов с проверкой допустимости приведения во время выполнения кода программы. Формат применения операции: dynamicjcast <целевой тип> (выражение) Значение выражения приводится к типу, имя которого указано в угловых скобках. Примеры использования этой и следующих операций приведения типов рассмотрим позже в связи с наследованием классов. statlc^cast операция приведения типов (обычно "родственных”) с проверкой допустимости приведения во время компиляции. Формат применения операции: static_cast <целевой тип> (выражение) Значение выражения приводится к типу, имя которого указано в угловых скобках. Для базовых (встроенных) типов применение этой операции эквивалентно применению операции функционального вида тип(выраже- ние). reinterpret jcast — операция приведения типов без проверки допустимости приведения. Формат применения операции: reinterpret jcast <целевой тип> (выражение) Значение выражения приводится к типу, имя которого указано в угловых скобках. Операция reinterpretjcast обеспечивает преобразование несвязных типов, когда смысл преобразования может быть "неясен"
компи46 Глава 2 лятору, и вся ответственность ложится на программиста. Такие преобразования существенно зависят от реализации. Применение этой операции не гарантирует получения исходного значения, если к результату применить обратное преобразование. const_cast операция приведения типов, которая аннулирует действие модификатора const (см. [1], с. 172). Формат применения операции: const _cast <целевой тип> (выражение) Значение выражения приводится к типу, имя которого указано в угловых скобках. Эта операция позволяет превратить константу в леводопустимое значение. sizeof- операция вычисления размера (в байтах) для объекта того типа, который имеет операнд. Разрешены два формата операции: sizeof унарное _выражение и sizeoffrnn). Примеры использования операции sizeof приводились в связи с изложением материала о константах (см., например, Р02_04.срр). Проиллюстрируем применение этой операции со стандартными типами: //Р02-09.срр - размеры разных типов данных #include <iostream> using namespace std; intmainf) { cout« "sizeoffint) = " « sizeoffint); cout« "\tsizeof( short) = " << sizeoff short); cout« "\tsizeof(long) = " << sizeofflong) « endl; cout« "sizeof(float) = " << sizeofffloat); cout« "\tsizeof(double) = "« sizeoffdouble); cout« "\tsizeof(char) = " << sizeoff char) « endl; cout« "sizeoffbool) = " << sizeoffbool)«endl; return 0; } Результаты выполнения программы (для конкретного компилятора): sizeoffint) = 4 sizeoff short) = 2 sizeofflong) = 4 sizeoff float) = 4 sizeoffdouble) = 8 sizeoffchar) = 1 sizeoff bool) = 1
Лексические основы языка Си++ 47 :: унарная операция указания (разрешения) области видимости иначе операция указания контекста) позволяет получить из тела функции доступ к внешнему для функции объекту (например, к переменной). Следующая программа поясняет ее возможности. В программе определены две одноименные переменные int к. Одна (глобальная, или внешняя) определена вне функции, другая (внутренняя, или локальная) определена в ее теле. Текст программы: //Р02_10.срр - Изменение видимости внешней переменной #include <iostream> # include "cyrToDos.h" using namespace std; int к = 15; //Глобальная переменная int main() { int к = 10; //Локальная переменная cout« cyrToDosf "Внешняя переменная k- ”)« ::k«endl; cout« cyrToDosf "Внутренняя переменная k = ") « k«endl; ::k = 0; cout« cyrToDosf "Внешняя переменная k = ") « ::k<<endl; cout« cyrToDosf "Внутренняя переменная k = ") « k«endl; return 0; Обратим внимание на препроцессорную директиву It include "cyrToDos.h" С ее помощью в текст программы добавлен код из файла cyrToDos.h, размещенного в каталоге с программами этой книги. В файле находятся определения нескольких вспомогательных функций, обеспечивающих перекодировку русских букв из кода MS Windows в код MS DOS. Тексты этих функций с необходимыми пояснениями приведены в Приложении 4. Сейчас нам достаточно знать, что оператор cout« cyrToDos.hf "русский текст"); обеспечивает вывод в консольное окно этого самого "русского текста" в кодировке MS DOS, т. е. его правильное отображение.
48 Глава 2 Результаты выполнения программы: Внешняя переменная к = 15 Внутренняя переменная к = 10 Внешняя переменная к = 0 Внутренняя переменная к - 10 Выражение ::к позволяет обратиться из тела функции к глобальной переменной. Как видно из примера, с помощью унарной операции можно организовать доступ из тела функции к внешней переменной, если переменная с тем же именем определена внутри функции. Остальное очевидно из сопоставления результатов с текстом программы. new - операция для динамического выделения памяти. Формы использования операции: new имя типа new имя типа инициализатор Особенности применения операции new и “противоположной” ей операции delete рассматриваются позже, так как их применение требует знакомства с указателями. delete - операция для освобождения динамически выделенной памяти. Формы использования операции: delete указатель delete [ ] указатель Бинарные операции Эти операции делятся на следующие группы: • аддитивные (additive operators); • мультипликативные (multiplicative operators); • сдвигов {shift operators); • поразрядные {bitwise operators); • операции отношений {relational operators)', • операции сравнения на равенство {equality operators)', • логические {logical operators)', • присваивания {assignment operators)', • доступа к компоненту класса {class member access operators); • доступа к адресуемому компоненту класса {pointer to member operators);
Лексические основы языка Си++ 49 • определения контекста иначе — разрешения области видимости {scope operator); • операция "запятая" {comma operator); • скобки в качестве операций. Аддитивные операции К аддитивным относятся следующие операции: + бинарный плюс (сложение арифметических операндов или сложение указателя с целочисленным операндом); - бинарный минус (вычитание арифметических операндов или указателей). Мультипликативные операции К мультипликативным относятся следующие операции: * умножение операндов арифметического типа; / деление операндов арифметического типа. При целочисленных операндах абсолютное значение результата округляется до целого. Например, 20/3 равно 6, -20/3 равняется -6, (-20)/3 равно -6, 20/(-3) равно -6; % получение остатка от деления целочисленных операндов (деление по модулю). При неотрицательных операндах остаток положительный. В противном случае остаток определяется реализацией. Например (для конкретного компилятора): 13%4 равняется 1, (-13)%4 равняется-1, 13%(—4) равно +1, (—13)%(—4) равняется -1. При ненулевом делителе всегда выполняется соотношение: (a/b)*b + а%Ь равно а. Операции сдвига Операции сдвига определены только для целочисленных операндов. Формат выражения с операцией сдвига: операнд_левый операция_сдвига операнд_правый « сдвиг влево битового (двоичного) представления значения левого целочисленного операнда на количество разрядов, равное значению правого целочисленного операнда; » сдвиг вправо битового (двоичного) представления значения левого целочисленного операнда на количество разрядов, равное значению правого целочисленного операнда. 4-2762
50 Глава 2 В операциях сдвигов тип результата соответствует типу левого операнда. Выполнение операции не определено, если правый операнд отрицательный либо его значение превысит число разрядов, отведенное для представления левого операнда. Поразрядные операции К поразрядным операциям относятся следующие: & поразрядная конъюнкция (И) битовых представлений значений целочисленных операндов; | поразрядная дизъюнкция (ИЛИ) битовых представлений значений целочисленных операндов; А поразрядное исключающее ИЛИ битовых представлений значений целочисленных операндов. Следующая программа иллюстрирует особенности операций сдвига и поразрядных операций. //Р02_11.срр - Операции сдвига и поразрядные операции #include <iostream> #include "cyrToDos.h" using namespace std; intmainf) { cout« cyrToDosf "4«3 равняется ") «(4«3); cout« cyrToDosf "\t5>> 1 равняется ") «(5»1)«endl; cout« cyrToDosf "6&5 равняется 99) «(6&5); cout« cyrToDosT\t6\5равняется ") «(6|5); cout« cyrToDosf "\t6*5 равняется 99) «(6*5)«endl; return 0; } Результаты выполнения программы: 4«3 равняется 32 5»1 равняется 2 6&5 равняется 4 615 равняется 7 6Л5 равняется 3 Напоминаем, что двоичный код для 4 равен 100, для 5 — это 101, для 6 — 110 и т.д. При сдвиге влево на три позиции код 100 становится равным 100000 (десятичное значение равно 32). Остальные результаты операций сдвига и поразрядных операций могут быть прослежены аналогично.
Лексические основы языка Си++ 51 Обратите внимание, что сдвиг влево на п позиций эквивалентен умножению значения на 2", а сдвиг вправо кода уменьшает соответствующее значение в 2п раз с отбрасыванием дробной части результата. (Поэтому 5>>1 равно 2.) Операции отношений (сравнения) К операциям отношений относятся следующие операции: < меньше, чем; > больше, чем; <= меньше или равно; >= больше или равно; == равно (сравнение на равенство); != не равно (сравнение на неравенство); Для двух последних операций введено специальное название — "операции сравнения на равенство". Операнды в операциях отношений арифметических типов или указатели. Результат буле- вого типа: false (ложь, т. е. О по традициям Си) или true (истина, т. е. 1 по традициям Си). Последние две операции (операции сравнения на равенство) имеют более низкий приоритет по сравнению с остальными операциями отношения. Таким образом, выражение (х < В == А < х) есть true тогда и только тогда, когда значение х находится в интервале от А до В. (Вначале вычисляются х<ВиА<х,ак результатам применяется операция сравнения на равенство ==.) Логические бинарные операции К логическим бинарным операциям относятся следующие: && конъюнкция (И) арифметических операндов или отношений. Результат false (ложь) или true (истина); 11 дизъюнкция (ИЛИ) арифметических операндов или отношений. Результат false (ложь) или true (истина). Следующая программа иллюстрирует некоторые особенности операций отношений и логических операций: //Р02_12.срр - Операции отношения и логические операции #include <iostream> #include "cyrToDos.h" 4*
52 Глава 2 using namespace std; intmainf) { cout << cyrToDos("3<5 равняется ") « (3<5); cout« cyrToDos("\t3>5 равняется ”) «(3>5)«endl; cout « cyrToDos("3==5 равняется ") «(3==5); cout« cyrToDos("\t3!=5равняется ") «(3!=5)«endl; cout« cyrToDos("3!=5 \ \ 3~5равняется ") «(3!=5 \ \3==5)«endl; cout« cyrToDos("3+4>5 && 3+5>4 && 4+5>3 равняется ”) « (3+4>5 && 3+5>4 && 4+5>3)«endl; return 0; Результаты выполнения программы: 3<5 равняется 1 3>5 равняется О 3==5 равняется 0 3!=5 равняется 1 3!=5 11 3--5 равняется 1 3+4>5 && 3+5>4 && 4+5>3 равняется 1 Обратите внимание, что вместо true выводится 1, а вместо false - 0. Операции присваивания В качестве левого операнда в операциях присваивания может использоваться только модифицируемое леводопустимое ("/-значение) значение - ссылка на некоторую именованную область памяти, значение которой доступно изменениям. Термин леводопустимое — значение (left value), иначе — леводопустимое выражение, происходит от объяснения действия операции присваивания Е = D, в которой операнд Е слева от знака операции присваивания может быть только модифицируемым леводопустимым значением. Нужно отметить, что иногда леводопустимое значение ссылается на неизменный участок памяти. В таком случае это значение можно использовать в определении константы, но оно недопустимо в левой части присваивания. Именно поэтому добавлено уточнение "модифицируемое". Примером модифицируемого леводопустимого значения служит переменная, которой выделена память и соответствует некоторый класс памяти. Итак, перечислим операции присваивания:
Лексические основы языка Си++ 53 = присвоить значение выражения-операнда из правой части операнду левой части: Р= 10.3-2*х; *= присвоить операнду левой части произведение значений обоих операндов: Р *= 2 эквивалентно Р-Р*2\ /= присвоить операнду левой части частное от деления значения левого операнда на значение правого: Р/- 2.2 - d эквивалентно Р = Р/(2.2 - d); %= присвоить операнду левой части остаток от деления целочисленного значения левого операнда на целочисленное значение правого операнда: N %- 3 эквивалентно N - N %3; += присвоить операнду левой части сумму значений обоих операндов: А += В эквивалентно А = А + В; -= присвоить операнду левой части разность значений левого и правого операндов: X -= 4.3 - Z эквивалентно Х = Х -(4.3 - Z); «= присвоить целочисленному операнду левой ча^ти значение, полученное сдвигом влево его битового представления на количество разрядов, равное значению правого целочисленного операнда: а «-4 эквивалентно а = а << 4; »= присвоить целочисленному операнду левой части значение, полученное сдвигом вправо его битового представления на количество разрядов, равное значению правого целочисленного операнда: а >>= 4 эквивалентно а = а >> 4; &= присвоить целочисленному операнду левой части значение, полученное поразрядной конъюнкцией (логическое И) его битового представления с битовым представлением целочисленного операнда правой части: е <£= 44 эквивалентно е = е & 44\ | = присвоить целочисленному операнду левой части значение, полученное поразрядной дизъюнкцией (логическое ИЛИ) его битового представления с битовым представлением целочисленного операнда правой части: а | = b эквивалентно а = а | Ь; л= присвоить целочисленному операнду левой части значение, полученное применением поразрядной операции исключающего логического ИЛИ к битовым представлениям значений обоих операндов: z А= х + у эквивалентно z = z А (х + у).
54 Глава 2 Обратите внимание, что для всех операций сокращенная форма присваивания Е1 ор= Е2 эквивалентна Е1 - Е1 op (Е2), где ор — обозначение операции. Для иллюстрации некоторых особенностей выполнения операций присваивания рассмотрим следующую программу: //Р02_13.срр - операции присваивания #include <iostream> ttinclude "cyrToDos.h" using namespace std; intmain() { int k; cout« cyrToDos(”k = 35/4 равняется ") «(k=35/4); cout« cyrToDos(''\t к/= 1 + 1 + 2равняется ") «(k/=1 + 1+2)«endl; cout« cyrToDos("k *= 5 - 2равняется ”) «(k*=5-2); cout« cyrToDos("\t к %- 3 + 2равняется ”) «(k%=3+2)«endl; cout « cyrToDos(”k +=21/3 равняется ") «(k+=21/3); cout« cyrToDos("\t к -= 6 - 6/2 равняется ”) «(k-=6-6/2)«endl; cout« cyrToDosfk «= 2 равняется ") «(k«=2); cout« cyrToDos("\t к »= 6 - 5 равняется ") «(k»=6-5)«endl; cout« cyrToDos(,fk &=9 + 4 равняется ") «(k&=9+4); cout« cyrToDos("\tк | = 8 - 2равняется ”) «(k\=8-2)«endl; cout« cyrToDos("k Л= 10 равняется ”) « (k*=10); return 0; } Тело функции main() начинается с определения целочисленной переменной int к;. Далее этой переменной присваиваются значения с помощью разных вариантов операции присваивания. В первом присваивании обратите внимание на выполнение деления целочисленных операндов, при котором выполняется округление за счет отбрасывания дробной части результата. Результаты выполнения программы: к = 35/4 равняется 8 к *= 5 - 2 равняется 6 к += 21/3 равняется 8 к «= 2 равняется 20 к&=9 + 4 равняется 8 к Л= 10 равняется 4 к/= 1 + 1 + 2 равняется 2 к %= 3 + 2 равняется 1 к -= 6 - 6/2 равняется 5 к»= 6 - 5 равняется 10 к | = 8 - 2 равняется 14
Лексические основы языка Си++ 55 Результаты, во-первых, подтверждают эквивалентность записей Е\ op = Е2 и Е\ = Е\ op (Е1). Кроме того, анализируя результаты, мо^кно еще раз рассмотреть особенности поразрядных операций. Двоичный код для к, равного 5, будет 101. Сдвиг влево на 2 дает 10100 (десятичное 20). Затем сдвиг на 1 вправо формирует код 1010 (десятичное 10). Поразрядная конъюнкция 1010&1101 дает 1000 (десятичное 8). Затем 1000| 110 дает значение 1110 (десятичное 14). Результатом 1110л1010 будет 0100 (десятичное 4). Операции доступа к компонентам структурированного объекта Так как следующие операции используются со структурами, объединениями, классами, то необходимые пояснения и примеры мы приведем позже, при изучении перечисленных понятий и, кроме того, определив указатели. . (точка) прямой доступ к компоненту структурированного объекта, например структуры или объединения. Формат применения операции: имя_объекта. имя компонента -> косвенный доступ к компоненту структурированного объекта, адресуемого указателем. При использовании операции требуется, чтобы с объектом был связан указатель. В этом случае формат применения операции имеет вид: указатель_на_объект -> имя компонента Операции доступа к адресуемым компонентам классов К таким операциям Относятся следующие: .* прямое обращение к компоненту класса по имени объекта и указателю на компонент. Формат применения операции: имя_объекта.*указатель_на_компонент ->* косвенное обращение к компоненту класса через указатель на объект и указатель на компонент. Формат применения операции: указатель_на_объект- > *указатель_на ^компонент
56 Глава 2 :: операция указания (разрешения) области видимости, или операция определения контекста имеет две формы: бинарную и унарную. Унарную форму мы уже рассмотрели. Бинарная форма применяется для отнесения имени к конкретному пространству имен. В первой программе первой главы эта операция использовалась для отнесения имен cout, cin, endl к пространству имен стандартной библиотеки (std). В последующих главах будет показано применение этой бинарной операции для классов и пространств имен, введенных программистом. Формат применения бинарного варианта операции: квалификатор_имени::имя Квалификатором может служить имя класса или название (имя) пространства имен. Имя справа от операции :: относится в первом случае к компоненту соответствующего класса, во втором случае - к названному слева пространству имен. Запятая в качестве операции , несколько выражений, разделенных запятыми, вычисляются последовательно слева направо. Таким образом, операция "запятая'’ группирует вычисления слева направо. Тип и значение результата определяются самым правым из разделенных запятыми операндов (выражений). Значения всех левых операндов игнорируются. Например: //Р02_14.срр - Запятая в качестве знака операции #include <iostream> #include "cyrToDos.h" using namespace std; //Доступ к пространству имен std intmainf) { intd; cout« cyrToDosf "Выражение (d = 4, d*2) равно ") «(d=4,d*2); cout« cyrToDosf", d равно ") « d «endl; return 0; Результаты выполнения программы: Выражение (d = 4, d*2) равно 8, dравно 4 Второй пример использования операций "запятая" — заголовок цикла из программы Р01_02.срр: while (cout <<"x"<<counter+ 1 <<"=", с/л>>х, х!=0)
Лексические основы языка Си++ 57 В скобках три разделенных запятыми операнда, вычисления выполняются слева направо. Первый операнд выводит "подсказку" пользователю, второй - считывает значение х, третий - сравнивает это значение с нулем. Результат имеет тип bool (логический), т.е. значением выражения в скобках будет либо true, либо false. Круглые '()' и квадратные '[ ]' скобки играют роль бинарных операций при вызове функций и индексировании элементов массивов. Для программиста, мало знакомого с техникой использования указателей и адресацией памяти, мысль о том, что скобки в ряде случаев являются бинарными операциями, часто даже не приходит в голову, и это тогда, когда он в каждой программе обращается к функциям или применяет индексированные переменные. Круглые скобки как операция обязательны в обращении к функции: имя_функции(список_аргументов) Здесь операндами служат имя_фунщии и список_аргументов. Результат вызова вычисляется в теле функции, структуру которого задает ее определение. В выражении имя_массива[индекс] операндами для операции [ ] служат имя_массива и индекс. В языках Си и Си++ принято, что индексы массивов начинаются с нуля, т.е. массив int Z[3] из трех элементов включает индексированные элементы Z[0], Z[ 1], Z[2]. Это соглашение языка становится очевидным, если учесть, что индекс определяет не номер элемента, а его смещение относительно начала массива. Таким образом, Z[0] — обращение к первому элементу, Z[ 1 ] — обращение ко второму элементу и т.д. В следующей программе показано, как обычно используют квадратные скобки при работе с элементами массива: //Р02_15.срр - Работа с элементами массива #include <iostream> using namespace std; intmainf) { charx[] = "DIXI”; // "Я СКАЗАЛ” (высказался) int i = 0; while (x[i] != '\0') cout« x[i++] « endl; return 0; }
58 Глава 2 Результат - слово "DIXI", написанное в столбик (сверху вниз): D I X I Как упоминалось, каждая строковая константа содержит в конце последовательности ее символов дополнительный терминальный символ '\0'. После инициализации массива сЛагх[ ] количество его элементов становится равным пяти. Оператор цикла с заголовком while выполняется, пока верно выражение в скобках, т.е. пока очередной символ массива не равен '\0'. При каждом вычислении выражения х[/++] используется текущее значение /, которое затем увеличивается на 1. В данном случае квадратные скобки играют роль бинарной операции, а операндами служат имя массива х и индекс /++. Условная (тернарная) операция В отличие от унарных и бинарных операций условная операция используется с тремя операндами. В выражении с условной операцией два размещенных не подряд символа *?*, и и три операнда-выражения: выражение_1 ? выражение_ 2: выражение_3 Первым вычисляется значение выражения_1. Если оно истинно (т.е. true и не равно нулю), то вычисляется значение выра- жения_2, которое становится результатом. Если при вычислении выражения_1 получится false (нулевое значение), то в качестве результата берется значение выражения_3. Классический пример: х < 0 ? -х: х; Значение выражения — абсолютное значение переменной х. Ранги операций Приведем таблицу приоритетов, или рангов операций. Грамматика языка Си++ определяет приоритеты (ранги) операций. Операции ранга I имеют наивысший приоритет. Операции одного ранга имеют одинаковый приоритет, и если их в вы-
Лексические основы языка Си++ 59 Таблица 2.3 Приоритеты операций Ранг Операции Ассоциативность 1 :: (контекст) —> 2 . —> (доступ к компоненту) —> 3 () [ ] тип() ++ — (постфиксные формы) typeidf) dynamic_cast static_cast reinterpret_cast const_cast <— 4 size of ++ -- (префиксные формы) ! ~ + - & * (унарные формы) (тип) sizeof new delete <- 5 * — >*(доступ к компоненту по адресу) —> 6 * / % (мультипликативные операции) —> 7 + — (аддитивные бинарные операции) —> 8 << »(сдвиги) —> 9 <<=>=> (сравнения) -> 10 == != (сравнения на равенство) -> 11 & (поразрядная конъюнкция) —> 12 А (поразрядное исключающее или) —> 13 | (поразрядная дизъюнкция) —> 14 && (конъюнкция) —> 15 || (дизъюнкция) —> 16 ? : (условная операция) —> 17 II * II 1* Я II + II 1 II II > II Т А А II V V II <- 18 throw 19 , (операция "запятая") —> ражении несколько, то они выполняются в соответствии с правилом ассоциативности либо слева направо (->), либо справа налево (<-). Если один и тот же знак операции приведен в таблице дважды, то его первое появление (с меньшим по номеру, т.е. старшим по приоритету, рангом) соответствует унарной операции, а второе — бинарной. Отметим, что кроме стандартных режимов использования операций язык Си++ допускает расширение их действия (перегрузка), т. е. распространение действия на объекты классов,
вво60 Глава 2 димых пользователем или уже определенных в конкретной реализации языка. Примером такого расширения (перегрузки) являются операции поразрядных сдвигов >> и «. Когда слева от них в выражениях находятся обозначения входных и выходных потоков, они воспринимаются как операция извлечения данных из потока » и операция передачи данных в выходной поток <<. 2.5. Разделители Разделители, или знаки пунктуации, как уже говорилось, входят в число лексем языка: [ ] () { } , ; : ... * = #&<><::><%%>%:%: %: Многие разделители уже использовались в приведенных выше программах. Без их участия не обходится почти ни одна конструкция языка, поэтому подробно объяснять их роль в каждом конкретном случае удобнее при рассмотрении соответствующих выражений, операторов, производных типов и т.д. Здесь только кратко укажем основное назначение отдельных разделителей. Квадратные скобки [ ] как разделители вводят размеры одно- и многомерных массивов. Примеры: double А[9]; А — одномерный массив из девяти элементов типа double; int, е[3][2]; е - двумерный массив — матрица размерности 3x2. Круглые скобки ( ): 1) выделяют условные выражения (в операторе if): if(x < 0) х = -х; // Модуль значения арифметической переменной; 2) входят как обязательные элементы в заголовок определения и в описание (в прототип) функции, где выделяют список спецификаций параметров: float F(float х, Int k) // Заголовок определения функции { тело_функции} float F(float, Int); // Описание функции - ее прототип;
Лексические основы языка Си++ 61 3) скобки обязательны в определении указателя на функцию. Пример — определение указателя с именем func на функцию с параметром типа double, возвращающую значение типа int: int (*func)(double); 4) группируют выражения, изменяя естественную последовательность выполнения операций: у - (а + Ь) / с; // Изменение приоритета операций 5) входят как обязательные элементы в операторы циклов: for (i = 0, j = 1; i < j; i += 2, j++) тело параметрического цикла; while (i < j) тело_цикла_ с_предусловием; do тело_цикла_с постусловием while (k > 0); 6) необходимы при явном преобразовании типа. Как показано выше при описании операций, в Си++ существуют шесть форм задания явного преобразования типов: традиционное приведение типа (имя_muna)операнд', функциональное приведение — имя_типа(операнд); dynamic_cast; static_cast; reinterpret_ cast; constjcast. Во всех операциях явного приведения типа круглые скобки обязательны; 7) применение круглых скобок настоятельно рекомендуется в препроцессорных макроопределениях. Пример: #define R(x,y) sqrt((x)*(x) +(у)*(у)). Заключение параметров макроса в круглые скобки позволяет использовать в качестве аргументов макровызовов арифметические выражения любой сложности и не сталкиваться с нарушениями приоритетов операций (см. гл. 7); 8) круглые скобки необходимы в заголовке обработчика исключений (см. гл. 12). Фигурные скобки { } обозначают соответственно начало и конец составного оператора или блока. Пример использования составного оператора в условном операторе: lf(d > х) { d-; х++; }
62 Глава 2 Пример блока, являющегося телом функции: int fi bon (int n) { int a - 1, b = 0, c = 0; for (int i = 0; i < n; I ++ ) { c = a + b; a = b; b = c; return c; } (Функция вычисляет значение n-го члена ряда Фибоначчи. Для п < 1 возвращается значение 0. Номер члена ряда задан аргументом п.) Обратите внимание на отсутствие точки с запятой после закрывающейся скобки }, обозначающей конец составного оператора или блока. Фигурные скобки ограничивают списки компонентов в определениях структурных типов, объединений, классов. struct cell { // Определение структурного типа char *b; int ее; double U[6]; }; union smes { // Определение объединяющего типа unsigned int ii; char cc[2]; }; class sir { // Определение класса int В; public: int X, D; sir (int); }; Обратите внимание на необходимость точки с запятой после определения каждого типа. Фигурные скобки используются при инициализации массивов и структур в их определениях.
Лексические основы языка Си++ 63 Инициализация определяемого массива: /ntmonth[] = {1, 2, 3, 4, 5, 6,7, 8,9, 10, 11, 12}; Фигурные скобки нужны при определении перечислимых констант (см. выше примеры перечислений, вводимых служебным словом епит). Фигурные скобки используются при определении пространства имен (namespace), где выделяют список отнесенных к этому пространству элементов (имен переменных, функций, классов и т.д.). Запятаяслужит разделителем в списках для отделения элементов. Мы рассмотрели списков начальных значений, присваиваемых индексированным элементам массивов и компонентам структур при их инициализации. Приведем еще примеры. charname[] = {'С','и','р','а','н','о'}; // Это не строка - это массив! struct A {intx; float у; charz; }F={3, 18.4, •с•}; Запятая как разделитель используется также в описаниях и определениях объектов (переменных) одного типа: Int i, n; float х, у, z, p1, p2; Еще одно использование списков — это списки аргументов и спецификаций параметров в функциях. При определении перечислимых констант запятая также используется как разделитель: ел um {Sunday, monday, tuesday); Другие применения запятой в качестве разделителя мы не станем перечислять, так как приводить примеры и объяснять их не стоит без рассмотрения тех вопросов, которым будут посвящены "дальние" главы книги. Без списков не обходится практически ни один механизм языка Си++ и в большинстве из них запятая служит разделителем. Запятая в качестве операции уже рассматривалась. Следует обратить внимание на необходимость с помощью круглых скобок отделять запятую-операцию от запятой-разделителя. Например,
64 Глава 2 в следующей программе для элементов массива с именем m используется список с тремя начальными значениями. //Р02_ 16. срр - Запятая как разделитель и как знак операции #include <iostream> using namespace std; intmainf) { inti = 1, m[] = { i, (i=2,i*i), i}; cout« 7 = "« /« "\tm[0] = "« m[0]; cout« "\tm[1]= 99« m[1] « "\tm[2] = "« m[2] «endl; return 0; > Результаты выполнения программы: / = 2 m[0] = 1 m[1] = 4 m[2] = 2 В данном примере запятая в круглых скобках выступает в роли знака операции. Операция присваивания "=" имеет более высокий приоритет, чем операция "запятая". Поэтому вначале / получает значение 2, затем вычисляется произведение i*i, и этот результат служит значением выражения в скобках (и потому второй элемент массива инициализируется значением 4). Однако переменная /остается равной 2. Разделитель точка с запятой завершает каждый оператор, каждое определение (кроме определения функции) и каждое описание. Любое допустимое выражение, за которым следует V, воспринимается как оператор. Это справедливо и для пустого выражения, т.е. отдельный символ "точка с запятой" считается пустым оператором. Пустой оператор иногда используется как тело цикла. Примеры операторов-выражений: /++; // Результат выполнения - только изменение значения / F(z,4); // Результат определяется телом функции с именем F Особым случаем являются выражения, входящие в заголовок цикла for. Они разделяются точкой с запятой, но не являются операторами. Разделитель двоеточие Y служит для отделения (соединения) метки и помечаемого ею оператора: метка: оператор;
Лексические основы языка Си++ 65 Метка - это идентификатор. Таким образом, допустимы, например, такие помеченные операторы: XYZ: a=(b-c) *(d - с); сс: z *= 1; Двоеточие в качестве разделителя применяется в определениях классов и их компонентов. Примеры сейчас приводить рано, поэтому только назовем соответствующие конструкции. • Определение производного класса, где имя класса отделяется от списка базовых классов двоеточием: ключ_кпасса имя_класса: список_базовых_классов {определениякомпонентов_класса}; • Завершение спецификаторов (public, protected, private), определяющих доступность компонентов класса. • В начале списка инициализаторов конструктора. Многоточие - это три точки без пробелов между ними. Оно используется для обозначения переменного числа параметров у функции при ее определении и описании (при задании ее прототипа). При работе на языке Си программист очень часто использует библиотечные функции со списком параметров переменной длины для форматных ввода и вывода. Их прототипы выглядят следующим образом: intprintf (char * format, ...); int scant (char * format, ...); Здесь с помощью многоточия указана возможность при обращении к функциям использовать разное количество параметров (не меньше одного, так как параметр format должен быть указан всегда и не может опускаться). Подготовка своих функций с переменным количеством па-раметров на языке Си++ требует применения средств адресной арифметики, например, предоставляемых заголовком <cstdarg>. Правила применения вводимых там макросов vajarg, vajend, va_start для организации доступа из тела такой функции к спискам ее параметров приведены в гл. 6. Еще одно применение многоточия — «ловушка» (обработчик) исключений catch(...). В этом случае роль многоточия — уШ2
66 Глава 2 указать, что ловушка перехватывает исключения всех типов (см. гл. 12). Звездочка как уже упоминалось, используется в качестве знака операции умножения и знака операции разыменования (получения значения через указатель). В описаниях и определениях звездочка означает, что описывается (определяется) указатель на значение использованного в объявлении типа: int *point; // Указатель на величину типа int char **refer;//Указатель на указатель на величину типа char Разделитель Знак =, как уже упоминалось, является обозначением операции присваивания. Кроме того, в определении он отделяет имя объекта от списка его инициализации. Примеры: struct {charx, int у} А = {'z', 1918}; int F = 66; В списке параметров функции знак = указывает на выбираемое по умолчанию значение аргумента: charCC (intZ= 12, charL = {...} По умолчанию параметру Z соответствует аргумент со значением 12, параметру L соответствует значение '\0\ В определении метода класса с помощью знака = вводится чистый спецификатор виртуальной функции (об этом позже в гл. 15). Разделитель '#' (sharp - знак номера или диеза в музыке) используется для обозначения директив (команд) препроцессора. Если этот символ является первым отличным от пробела символом в строке программы, то строка воспринимается как директива препроцессора. Символ играет роль разделителя при определении переменных типа ссылки: int В; // Описание переменной int &А = В; //А - ссылка на В (А еще одно имя переменной В) Отметив использование символа & в качестве разделителя при определении ссылок, отложим подробное рассмотрение ссылок до следующей главы.
67 СКАЛЯРНЫЕ ТИПЫ И ВЫРАЖЕНИЯ 3.1. Базовые и производные типы В языке Си++ переменная считается частным случаем объекта. В свою очередь, объект - это непрерывный (зачастую именованный) участок памяти, для которого типом объекта определены: размеры, структура, множество возможных значений и набор допустимых операций. В традиционных пособиях по языкам программирования переменную чаще всего определяют как пару "имя - значение". Имени соответствует адрес участка памяти, выделенный переменной, а значением является содержимое этого участка. Именем служит идентификатор, а значение соответствует типу переменной, определяющему множество допустимых значений м набор операций, для которых переменная может служить операндом. Множество допустимых значений переменной обычно совпадает со множеством допустимых констант того же типа. Познакомившись в предыдущей главе с константами-литералами, мы уже догадываемся, что существуют вещественные, целые, булевские (логические) и символьные типы и соответствующие переменные. (Обратите внимание, что строкового типа нет, и нет строковых переменных.) Целые, символьные и логические типы совместно называют обобщенными целыми, или интегральными типами. Интегральные и вещественные типы считаются арифметическими типами. (Арифметические типы являются частным случаем скалярных типов. К скалярным типам, кроме арифметических, относятся указатели, ссылки и перечисления епит, но о них будет сказано позже). Переменные типизируются с помощью определений и описаний. Сразу же введем терминологические соглашения. В отличие от описания определение не только вводит объект (например, переменную), но и предполагает, что на основе этого определения 5*
68 Глава 3 компилятор выделит память для объекта (переменной). Для определения и описания переменных базовых (фундаментальных) типов используются следующие служебные слова, каждое из которых в отдельности может выступать в качестве имени типа: • bool (логический); • char (символьный); • wchar_t (широкосимвольный); • short (короткий целый); • int (целый); • long (длинный целый); • float (вещественный); • double (вещественный с удвоенной точностью); • void (отсутствие значения). Переменная может быть определена в любом месте программы, но обязательно до ее использования. При определении переменных им можно приписывать начальные значения, которые заносятся в выделяемую для них память в процессе инициализации. Примеры определений (описания с инициализацией): double epsilon = 1е-15; char newsymbol = '\п'; long filebegin = OL; В обозначении типа может использоваться одновременно несколько служебных слов. Например, определение long double pi = 3.1415926535897932385; вводит переменную с именем pi вещественного типа расширенной точности и явно присваивает этой переменной начальное значение. Употребляемые как отдельно, так и вместе с другими именами типов служебные слова unsigned (беззнаковый) и signed (знаковый) позволяют для арифметического или символьного типа выбирать способ учета знакового разряда: unsigned Int /', j, к; unsigned long L, M ,N; При таком определении переменные i, j, k могут принимать только целые положительные значения в диапазоне от 0 до UINTMAX и т.д.
Скалярные типы и выражения 69 Применение в обозначениях типов отдельных служебных слов int, char, short, long эквивалентно signed int, signed char, signed short, signed long. Именно поэтому служебное слова signed обычно опускается в определениях и описаниях. Использование при задании типа только одного unsigned эквивалентно unsigned int. Переменные одного типа занимают в памяти одно и то же количество байтов, и это количество байтов может быть всегда вычислено с помощью операции sizeof, как мы это делали в описании ее возможностей. В следующей программе дается еще несколько примеров: //Р03_01.срр - Размеры разных типов данных #include <iostream> using namespace std; intmainf) { int i; unsigned int ui; long I; unsigned long ui, double d; long double Id; cout« "sizeof (int) = " << sizeoffi); cout« "\t sizeof (unsigned int) = "<< sizeof(ui)«endl; cout« "sizeof (long) = "« sizeoffl); cout« "\t sizeof (unsigned long) = ” << sizeof(ul)«endl; cout« "sizeof (double) = " << sizeof(d); cout« "\t sizeof (long double) = ” << sizeof(ld)«endl; return 0; ) Результаты выполнения программы: sizeof (int) = 4 sizeof ( unsigned int) = 4 sizeof (long) = 4 sizeof (unsigned long) = 4 sizeof (double) = 8 sizeof (long double) = 12 Разнообразие базовых типов может смущать начинающего программиста. Б. Страуструп ([1], с. 108) рекомендует следующее. ”В большинстве приложений можно обойтись типами bool для логических значений, int - для целых, char - для символов и double - для чисел с плавающей точкой. Остальные
фундамен70 Глава 3 тальные типы являются вариациями, предназначенными для оптимизации и решения других специальных задач. Ими лучше не пользоваться, пока не возникла острая необходимость.” Используя спецификатор typedef, можно в своей программе вводить удобные названия для сложных обозначений типов. В следующем примере typedef unsigned char COD; COD symbol; имя COD есть сокращенное название для типа unsigned char, symbol - переменная этого типа, значениями которой могут быть беззнаковые числа в диапазоне от 0 до 255. Кроме перечисленных базовых типов, определен тип void, используемый для указания на отсутствие значения. Переменные типа void определять нельзя. Синтаксически тип void является базовым, однако его можно использовать только как часть обозначения производного типа (список производных типов приведен ниже). Рассматривая переменные, мы пока использовали базовые (фундаментальные) типы, для обозначения которых употребляются по отдельности и в допустимых сочетаниях служебные слова bool, char, int, signed, double, long, unsigned, float, short. Из этих базовых типов и типа void с помощью знаков операций и разделителей *,&,[],() и механизмов определения типов структурированных данных (классов, структур, объединений) можно конструировать множество производных типов. Стандарт к производным типам относит: • массивы объектов конкретного типа; • функции; • указатели на void, на объекты, на функции; • ссылки на объекты и на функции; • классы (сюда включены как частный случай и структуры, и объединения); • перечисления; • указатели на нестатические компоненты (поля данных, методы) классов.
Скалярные типы и выражения 71 С некоторыми из этих производных типов мы уже встречались в примерах программ, а рассматривая перечислимые константы использовали тип, называемый перечислением. Дело в том, что для каждого набора перечислимых констант может быть введено имя типа, соответствующего именно этому списку констант. Имя типа - это произвольно выбираемый уникальный идентификатор, помещаемый между служебным словом епит и открывающейся фигурной скобкой {. Например, определение enum week {Sunday, monday, tuesday, Wednesday, thursday, friday, Saturday}; не только определяет константы sunday==0, monday==l,..., но и вводит перечислимый тип с именем week. Чтобы получить представление об остальных производных типах и механизмах их конструирования, приведем форматы основных производных типов. (Вы можете бегло просмотреть следующие ниже форматы и примеры производных типов, обратив внимание на основные концепции. Используемые здесь понятия подробно рассматриваются в следующих главах.) Пусть именем type обозначен некоторый допустимый тип, имя — название вводимого понятия. type имя[ ] массив объектов типа type. Например: long int М[5]; - пять объектов типа long int% доступ к которым обеспечивают индексированные переменные М[0], М[1], М[2], М[3], М[4]. typel имя(гуре2); функция, принимающая аргумент типа type2 и возвращающая значение типа typel. Например: Int f1(void)\ - функция, не требующая аргументов и возвращающая значение типа int\ void f2(double)\ - функция, принимающая аргумент типа double и не возвращающая значений. type *имя; указатель на объекты типа type. Например, char *ptr\ определяет указатель ptr на объекты типа char. type *имя[ ]; массив указателей на объекты типа type.
72 Глава 3 type (*имя)[ ]; указатель на массив объектов типа type, typel *HMB(type2); функция, принимающая аргумент типа type2 и возвращающая указатель на объект типа type 1. type 1 (*HMB)(type2); указатель на функцию, принимающую параметр типа type2 и возвращающую значение типа typel. Например, описание int (*p\r)(char); определяет указатель ptr на функцию, принимающую параметр типа char и возвращающую целое значение типа int. typel *(*HMB)(type2); указатель на функцию, принимающую параметр типа type2 и возвращающую указатель на объект типа typel. type & имя = имя_объекта_типа^ре; инициализированная ссылка на объект типа type. Например, unsigned char &сс = symbol; определяет ссылку сс на объект с именем symbol типа unsigned char. Предполагается, что ранее в программе присутствует определение unsigned char symbol; type 1(&HMB)(type2); ссылка на функцию, принимающую параметр типа type2 и возвращающую значение типа type 1. struct имя {typel имя1; type2 имя2; }; структурный тип с двумя компонентами (полями данных), которые имеют типы typel и type2. Например, struct ST {int х; chary; double z;}; — определяет структурный тип ST с тремя полями данных разных типов: целая х, символьная у, вещественная z. (Количество полей данных в определении структурного типа может быть произвольным.) union имя {typel имя1; type2 имя2;}; определение объединяющего типа (в данном случае это объединение двух полей данных с типами typel, type2). Например, union UN{int m; char c[2];}; - тип UN объединяет (т. с. помещает в один участок памяти) целую переменную m и два элемента символьного массива с[0], с[1]. (Количество полей данных в объединении может быть любым.) class имя { typel имя1; type2 имя2(type3); }; определение класса, включающего (в данном случае) два компонента — поле данных типа typel и метод — функцию, возвращающую значение типа type2 и имеющую параметр типа type3. Например: class A {int N;
Скалярные типы и выражения 73 double F(char);}] - здесь А — класс, компонентами которого служат целочисленное поле данных с именем N и функция (метод) Fс параметром типа char, возвращающая значение типа double. (Количество компонентов класса может быть произвольным. Классам будет посвящено в нашей книге еще очень много страниц.) Привести формат типа указателя на компоненты класса затруднительно, не рассмотрев подробно классы, поэтому отложим рассмотрение этих указателей. Все возможные производные типы принято разделять на скалярные (scalar), агрегатные (agregate) и функции (function). К скалярным типам относят арифметические типы, перечислимые типы, указатели и ссылки (ссылки введены только в Си++, их не было в языке Си). Агрегатные типы, которые также называют структурированными, включают массивы, структуры, объединения и классы (последние только в Си++). 3.2. Объекты и их атрибуты Как уже было сказано, переменная — это частный случай объекта как непрерывной области памяти. Отличительной чертой переменной является возможность связывать с ее именем различные значения, совокупность которых определяется типом переменной. При определении значения переменной в соответствующую ей область памяти помещается некоторый код. Это может происходить либо во время компиляции, либо во время исполнения программы. В первом случае говорят об инициализации, во втором случае — о присваивании. Операция присваивания Е= В содержит имя переменной Еи некоторое выражение В. Имя переменной есть частный случай более общего понятия - «леводопустимое выражение» (left value expression), или /-значение. Название «леводопустимое выражение» произошло как раз от выражения с операцией присваивания, так как только /-значение может быть использовано в качестве левого операнда. Леводопустимое выражение определяет в общем случае ссылку на некоторый объект или функцию. Частным случаем является имя переменной. Итак, объект определяется как некоторая непрерывная область памяти. Это понятие вводится как понятие времени
испол74 Глава 3 нения программы, а не понятие языка Си++. В языке Си++ термин объект зарезервирован как термин объектно-ориентированного программирования. При этом объект всегда принадлежит некоторому классу, и такие объекты мы будем рассматривать в главах, посвященных классам. Вернемся к объектам как к участкам памяти и к их частному случаю, к переменным. Так как объекту присуще значение, то кроме /-выражения для объекта задается тип, который: • определяет требуемое для объекта количество памяти при ее начальном распределении; • задает совокупность операций, допустимых для объектов; • интерпретирует двоичные коды значений при последующих обращениях к объекту; • используется для контроля типов с целью обнаружения возможных случаев недопустимого присваивания. Имя объекта как частный случай леводопустимого выражения обеспечивает как получение значения объекта, так и изменение этого значения. Не любые выражения являются леводопустимыми. Клеводопустимым выражениям относятся: • имена скалярных арифметических и символьных переменных; • имена переменных, принадлежащих массивам (индексированные переменные); • имена указателей; • уточненные (расширенные) имена компонентов структурированных данных (структур, объединений, классов): имя_структуры. имя компонента; • выражения, обеспечивающие косвенный выбор компонентов структурированных данных (структур, объединений, классов): указатель_на_объект -> имя компонента; • леводопустимые выражения, заключенные в круглые скобки; • ссылки на объекты; • выражения с операцией разыменования • вызовы функций, возвращающих ссылки на объекты. Кроме леводопустимых выражений, Стандарт отдельно определяет праводопустимые выражения, которые невозможно использовать в левой части оператора присваивания. Например:
Скалярные типы и выражения 75 • имя функции; • имя массива; • имя константы; • вызов функции, не возвращающей ссылки. Кроме типов, для объектов явно либо по умолчанию определяются: • класс памяти (задает размещение объекта); • область (сфера) действия, связанного с объектом идентификатора (имени); • видимость объекта; • продолжительность существования объектов и их имен; • тип компоновки (связывания). Все перечисленные атрибуты (свойства) взаимосвязаны и должны быть либо явно указаны, либо они выбираются по контексту неявно при определении и (или) описании конкретного объекта. Рассмотрим подробнее их возможности и особенности. Тип определяет размер памяти, выделенной для значения объекта, правила интерпретации двоичных кодов значений объекта и набор допустимых операций. Типы рассмотрены в связи с константами и переменными. Класс памяти определяет размещение объекта в памяти и продолжительность его существования. Для явного задания класса памяти при определении (описании) объекта используются или подразумеваются по умолчанию следующие спецификаторы: auto - автоматически выделяемая, локальная память. Спецификатор auto может быть задан только при определении объектов внутри блока, например в теле функции. Этим объектам память выделяется при входе в блок и освобождается при выходе из него. Вне блока объекты класса auto не существуют. register - автоматически выделяемая, по возможности регистровая память. Спецификатор register аналогичен auto, но для размещения значений объектов используются регистры, а не участки основной памяти. Такая возможность имеется не всегда, и в случае отсутствия регистровой памяти (если регистры заняты другими данными) объекты класса register компилятор обрабатывает как объекты автоматической памяти.
76 Глава 3 static - внутренний тип компоновки и статическая продолжительность существования. Объект, описанный со спецификатором static, будет существовать в пределах того файла с исходным текстом программы (модуля), где он определен. Класс памяти static может приписываться переменным и функциям. extern - внешний тип компоновки и статическая продолжительность существования. Объект класса extern глобален, т.е. доступен во всех модулях (файлах) программы. Спецификатор памяти extern может быть приписан переменной или функции. mutable - отменяет константность отдельного компонента (поля данных) объекта класса в том случае, когда объект в целом является неизменяемым, т.е. константным. Кроме явных спецификаторов, на выбор класса памяти существенное влияние оказывают размещение определения и описаний объекта в тексте программы. Такими определяющими частями программы являются блок, функция, файл с текстом кода программы и т.д. Таким образом, класс памяти, т.е. размещение объекта (в регистре, стеке, в динамически распределяемой памяти, в сегменте) зависит как от синтаксиса определения, так и от размещения определения в программе. Область (сфера) действия идентификатора (имени) — это часть программы, в которой идентификатор может быть использован для доступа к связанному с ним объекту. Область действия зависит от того, где и как определены объекты и идентификаторы. Здесь имеются следующие возможности: блок, функция, прототип функции, файл, класс и шаблон класса или функции. Если идентификатор описан (определен) в блоке, то область его действия — от точки описания до конца блока. Когда блок является телом функции, то в нем, кроме того, определены и указанные в заголовке функции параметры. Таким образом, сфера действия параметров в определении функции есть тело функции. Например, следующая функция вычисляет факториал значения своего аргумента. (Сфера действия для параметра z и переменных, определенных в теле функции, есть блок — тело функции): long fact(intz) { long т= 1; if(z < 0) return 0; //1 //2 //3
Скалярные типы и выражения 77 for (int i = 1; i< z; m = ++/ * m); return m; } //4 //5 //6 Область действия: для переменной m - строки 2-6; для переменной i - строка 4; для параметра z — блок в целом. Метки операторов в тексте определения функции имеют в качестве сферы действия функцию. В пределах тела функции они должны быть уникальны, а вне функции они недоступны. Прототип функции является сферой действия идентификаторов, указанных в списке параметров. Конец этой сферы действия совпадает с концом прототипа функции. Например, в прототипе double expon (double d, int т)\ определены идентификаторы d, m, неизвестные в других частях той программы, где помещен данный прототип. Для примера рассмотрим программу, в которой используется функция с приведенным прототипом. Ее назначение — вычислить целую степень m вещественного числа d. Для простоты не будем включать проверок значений параметров, будем считать, что второй параметр не отрицателен. //Р03_02.срр - Сфера действия имен параметров #include <iostream> using namespace std; int main() { int m = 0; double d = 5.0; double expon(double d, int m);//Прототип функции for(; m <= 2; m++) cout« "expon(”« d « ",M << m « ") = " << exponfd, m) « endl; return 0; } // Определение функции: double expon(double d, intm) { double res = 1.0; for (inti = 1; i <= m; /'++, res *= d); return res; }
78 Глава 3 Результаты выполнения программы: ехроп(5,0) = 1 ехроп(5,1) =5 ехроп(5,2) = 25 Программа иллюстрирует независимость идентификаторов списка параметров прототипа функции от других идентификаторов программы. Имена d и т в прототипе функции и имена переменных т и с/, определенных в тексте основной программы, имеют разные сферы действия и полностью независимы. Файл с текстом программы является сферой действия для всех глобальных имен, т.е. для имен объектов, описанных вне любых функций и классов. Каждое глобальное имя определено от точки описания до конца файла. С помощью глобальных имен удобно связывать функции по данным, т.е. создавать общее "поле данных" для всех функций файла. Простейший пример: //Р03_03.срр - Область действия глобальных имен #include <iostream> using namespace std; int LC; char scope[] = "Declaration region"; void WW { LC = sizeoff scope); } void Prin { cout« "sizeof scope = " « LC; } intmainf) { WW(); Prin(); return 0; } Результаты выполнения программы: sizeof scope = 19 В программе три функции и два глобальных объекта — массив scope и целая LC, через которые реализуется связь функций по данным. Обратите внимание на числовое значение результата.
Скалярные типы и выражения 79 В строковой константе явно присутствуют 18 букв, а длина массива 19 байт, так как учтено присутствие терминального символа '\0'. Определение класса задает множество его компонентов, включающее данные и функции (методы). Имеются специальные правила доступа и определения сферы действия, когда речь идет о классах. Подробным рассмотрением этих вопросов мы займемся позже в связи с классами и принадлежащими им объектами. С учетом области, в пределах которой идентификатор должен быть "уникальным", используемые в программе идентификаторы делятся на следующие группы. • Имена меток, используемых в операторе goto. Эти имена должны быть уникальными в той функции, где они введены. • Имена структур, классов, объединений и перечислений должны быть уникальными в пределах того блока, где они определены. Если эти имена описаны вне функций и классов, то они должны быть уникальными относительно всех таких же глобальных имен. • Имена компонентов структур, объединений, классов должны быть уникальными в пределах соответствующего определения. В разных структурах, объединениях, классах допустимы компоненты с одинаковыми именами. • Имена переменных и функций, названия типов, которые введены пользователем (с помощью служебного слова typedef), и имена элементов перечислений должны быть уникальными в сфере определения. Например, внешние идентификаторы должны быть уникальными среди внешних и т.д. • Имена параметров шаблонов функций и шаблонов классов должны быть уникальными в тексте определения шаблона. Понятие видимость объекта понадобилось в связи с возможностью повторных определений идентификатора внутри вложенных блоков (или функций). В этом случае разрывается исходная связь имени с объектом, который становится "невидимым" из блока, хотя сфера действия имени сохраняется. Следующая программа поясняет эту ситуацию: //Р03_04.срр - Переопределение внешнего имени внутри блока #include <iostream> using namespace std;
80 Глава 3 # include "cyrToDOS.h" intmainf) { char cc[ ] = "Число // Массив автоматической памяти float pi = 3.14; // Переменная типа float cout« cyrToDOS ("Обращение к переменной типа float: pi = ") « pi« endl; {//Переменная типа double переопределяет pi: double pi = 3.1415926535897932385; // Видимы double pi и массив cc[ ]: cout « cyrToDOS (cc) « " double pi ="« pi« endl; } //Видимы float pi и массив cc[ ]: cout« cyrToDOS (cc) « " float pi ="« pi« endl; return 0; } Результаты выполнения программы: Обращение к переменной типа float: pi = 3.14 Число double pi = 3.14159 Число float pi = 3.14 Обратите внимание, что значение типа double выведено с ограниченной точностью. В средствах вывода по умолчанию наложено ограничение на количество значащих цифр (см. гл. 11). Однако приведенная программа демонстрирует другое. Достаточно часто сфера (область) действия идентификатора и видимость связанного с ним объекта совпадают. Область действия может превышать видимость, но обратное невозможно, что иллюстрирует данный пример. За описанием переменной double pi внутри блока внешнее имя переменной float pi становится невидимым. Массив charcc[ ] определен и видим во всей программе, а переменная float pi видима только вне вложенного блока, внутри которого действует описание double pi. Таким образом, float pi невидима во внутреннем блоке, хотя сферой действия для имени pi является вся программа. Для переменной double pi и сферой действия, и сферой видимости служит внутренний блок, вне которого она недоступна и не существует. Второй пример изменения видимости объектов при входе в блок:
Скалярные типы и выражения 81 //Р03_05.срр - Переопределение имени внутри блока #include <iostream> # include "cyrToDos.h" using namespace std; intmainf) { int k = 0,j= 15; {cout« cyrToDosf "Внешняя для блока переменная k = ") « k « endl; char k = A'; //Определена внутренняя переменная cout« cyrToDosf "Внутренняя переменная k = ") « k « endl; cout« cyrToDosf"Видимая в блоке переменная j = ") « j « endl; j = 30; // Изменили значение внешней переменной } // Конец внутреннего блока cout« cyrToDos("BHe блока: к = ") « к « ", j - " <</ «endl; return 0; } Результаты выполнения программы: Внешняя для блока переменная к = 0 Внутренняя переменная к = А Видимая в блоке переменная / = 15 Вне блока: k = 0,j = 30 Как видно из примера, внутри блока сохраняется сфера действия внешних для блока имен до их повторного использования в качестве имен в определениях (переменная к). Определение объекта внутри блока действует от точки размещения определения до конца блока (до выхода из блока). Внутреннее определение изменяет видимость внешнего объекта с тем же именем (объект невидим). Внутри блока видимы и доступны объекты, определенные во внешних блоках (переменная /). После выхода из блока восстанавливается видимость внешних объектов, переопределенных внутри блока. (Вывод значения переменной к после выхода из блока.) Язык Си++ позволяет изменить видимость объектов с помощью операции Программа Р2-10.срр, иллюстрирующая возможность такого изменения видимости, приведена при описании операции указания области видимости Рассмотрим еще один пример обращения к ’’невидимому" внутри функции б"2762
82 Глава 3 внешнему массиву с помощью операции указания области видимости. //Р03_06.срр - Доступ из функции к внешнему объекту, // имя которого переопределено в теле функции #include <iostream> # include "cyrToDos.h,f using namespace std; char cc[] = "Внешний массив"; int main() { void func(); // Прототип функции func(); // Вызов функции return 0; } void func() { // Определение функции char cc[] = "Внутренний массив"; // Обращение к локальному объекту сс: cout« cyrToDosfcc) « end!; // Обращение к глобальному объекту сс: cout« cyrToDos(::cc) « endl; } Результаты выполнения программы: Внутренний массив Внешний массив Следующая программа и соответствующая ей схема (рис. 3.1) обобщают соглашения о сферах действия идентификаторов и о видимости объектов. Программа оформлена в виде одного текстового файла. В программе три функции, из которых одна главная (main): //Р03_07.срр - Сферы действия имен и видимость объектов #include <iostream> using namespace std; char dc[ ] = "Object 1"; // Глобальный объект 1 void func 1() { cout« "f1.dc= "« dc « endl; //Виден глобальный объект 1 chardcf ] = "Object 2"; //Локальный для func1() объект 2 cout« "f1.dc = "« dc « endl; // Виден локальный объект 2
Скалярные типы и выражения 83 Текстовый файл программы (модуль) type 1 имя; ◄ - функция 1 имя - видимость — ! type2 имя; ◄ ! имя - видимость — • г блок - ! | имя - видимость — ; I type3 имя; < ; j имя - видимость — • |: имя - • ■ видимость имя - видимость — :: им я- видимость * * Object 3 - функция 2 (type имя) - имя - видимость :: имя - • * видимость г блок j type 4 имя; ◄ j имя - видимость * > Object 4 - функция main — • имя - видимость * type5 имя; ^ имя - видимость Object 5 Рис. 3.1. Видимость объектов, связанных с одним идентификатором (именем), в однофайловой программе { // Внутренний блок для func Ц) // Виден локальный для func 1() объект 2: cout« "fl. block, dc = " << dc « endl; //Локализованный в блоке объект 3: chardc[ ] = "Object 3”; // Виден локальный объект 3: 6* Object 1
84 Глава 3 cout« "f1.block.dc = " << dc « endl; //Виден глобальный объект 1: cout« "f1.block.::dc = "« ::dc « endl; } //Конец блока // Виден локальный для func 1() объект 2: cout« "f1.dc = " «dc « endl; //Виден глобальный объект 1: cout << "f1.::dc = " << ::dc << endl; } //Конец функции func1() void func2( char dc [ ]) // dc - массив-параметр функции { cout << 72.dc.parameter^" <<dc« endl;//Виден параметр //Виден глобальный объект 1: cout« "f2.::dc = ' « ::dc << endl; {// Внутренний блок для func2(). //Локализованный в блоке объект4: chardc[] = "Object 4"; // Виден локальный для func2() объект 4: cout << "f2.dc = "« dc « endl; } //Конец блока } // Конец функции func2() int main { //Виден глобальный объект 1: cout« "fmain.dc = "« dc « endl; char dc[] = "Object 5"; //Локальный для main() объект 5 func Ц); func2(dc); // Виден локальный для main() объект 5: cout << "fmain.dc = "« dc « endl; return 0; ) Результаты выполнения программы: fmain.dc = Object 1 f1.dc = Object 1 f1.dc = Object 2 f1. block, dc = Object 2 f1.block.dc = Object 3 f1.block.::dc = Object 1
Скалярные типы и выражения 85 f 1 .dc = Object 2 f1.::dc = Object 1 f2.dc.parameter=0bject 5 f2.::dc = Object 1 f2.dc = Object 4 fmain.dc = Object 5 На рис. 3.1 для имени объекта с типом typel областью действия служит файл в целом, однако видимость этого объекта различна внутри блоков и функций. Если определение typel имя; (т. е. chardc[ ] = "Object 1м;) поместить в конце файла, то ничего хорошего не получится — действие определения не распространяется вверх по тексту программы. Все попытки обратиться к глобальному "Объекту 1" в этом случае приведут к синтаксическим ошибкам на этапе компиляции. Глобальный объект, определенный в тексте программы ниже (позже) своего первого использования, должен быть описан в той функции, где он используется, с атрибутом extern. Эти особенности иллюстрирует следующая мини-программа. //Р03_08.срр - Опережающее описание глобального объекта #include <iostream> using namespace std; void f1() { extern int ex; // Описание внешней переменной cout« "f1:ex = " << ex « endt; > intmainf) { fi(); return 0; } int ex = 33; // Определение с инициализацией Результаты выполнения программы: f1:ex = 33 Продолжительность существования объектов определяет период, в течение которого идентификаторам в программе соответствуют конкретные объекты в памяти. Определены три вида продолжительности: статическая, локальная и динамическая.
86 Глава 3 Объектам со статической продолжительностью существования память выделяется в начале выполнения программы и сохраняется до окончания ее обработки. Статическую продолжительность имеют все функции и все файлы. Остальным объектам статическая продолжительность существования может быть задана с помощью явных спецификаторов класса памяти static и extern. При отсутствии явной инициализации объекты со статической продолжительностью существования по умолчанию инициализируются нулевыми, или пустыми значениями. При статической продолжительности существования объект не обязан быть глобальным. Следующий пример содержит переменную К со статической продолжительностью существования, сферой действия для которой является только тело функции (блок), т.е. переменная К локализована в теле функции counter: //Р03_09.срр - Инициализация и существование локальных // статических объектов #include <iostream> #include ''cyrToDos.h'' using namespace std; int counter^) // Определение функции // Статическая переменная, //локализованная в теле функции //Локальная переменная функции main { static int К; return ++К; } int main { int К = 3; for(; К != 0; K~) { cout « cyrToDosf ’!Автоматическая К =") « К; cout« cyrToDos("\tC4eT4HK='’) « counted) « end!; } return 0; } Результаты выполнения программы: Автоматическая К = 3 Автоматическая К = 2 Автоматическая К = / Счетчик= 1 Счетчик-2 Счетчик=3 В данном примере статическая переменная К, локализованная в теле функции counter, по умолчанию инициализируется
Скалярные типы и выражения 87 нулевым значением, а затем сохраняет значение после каждого выхода из функции, так как продолжительность существования переменной К статическая, и выделенная ей память будет уничтожена только при выходе из программы. В основной функции примера определена целая переменная К с локальной продолжительностью существования. Такие переменные называются переменными автоматически выделяемой памяти, или для краткости — автоматическими. Они создаются при каждом входе в блок или функцию, где они определены, и уничтожаются при выходе. Объекты с локальной продолжительностью должны быть инициализированы только явным образом, иначе их начальные значения непредсказуемы. Область действия для объекта с локальной продолжительностью существования всегда локальна - это блок или функция. Для задания локальной продолжительности при определении объекта можно использовать спецификатор класса памяти auto, однако он всегда избыточен, так как этот класс памяти по умолчанию приписывается всем объектам, определенным в блоке или функции. Следует обратить внимание, что не для всех объектов с локальной областью действия определена локальная продолжительность существования. Например, в функции counter() переменная К имеет локальную область действия (тело функции), однако для нее спецификатором static определена статическая продолжительность существования. Объекты с динамической продолжительностью существования создаются (получают память) и уничтожаются с помощью явных операторов в процессе выполнения программы. Для создания используется операция new или функции malloc() и са//ос(), а для уничтожения — операция delete или функция free(). Операция new имя_типа либо new имя_типа инициализатор позволяет выделить и сделать доступным участок в основной памяти, размеры которого соответствуют типу данных, определяемому именем типа. В выделенный участок заносится значение, определяемое инициализатором, который не является
обязатель88 Глава 3 ным элементом. В случае успешного выполнения операция new возвращает адрес начала выделенного участка памяти. Если участок нужных размеров не может быть выделен (нет памяти), то операция new возвращает нулевое значение. Синтаксис применения операции: указатель = new имя_типа инициализатор Здесь необязательный инициализатор - это выражение в круглых скобках. Указатель, которому присваивается получаемое значение адреса, должен относиться к тому же типу данных, что и имя_типа в операции new. О размерах участков памяти уже упоминалось в связи с константами разных типов. Поэтому приведем несложные примеры. Выражение new float выделяет участок памяти, соответствующий типу float. Выражение new int(l5) выделяет участок памяти для значения типа int и инициализирует этот участок целым значением 15. Синтаксис использования операций new и delete предполагает применение указателей. Предварительно каждый указатель должен быть определен. Как уже упоминалось, определение указателя имеет вид: тип * имя указателя; Имя_указателя - это идентификатор. В качестве типа можно использовать, например, базовые типы int, long, float, double, c/iar, wcharjt. Таким образом, int *h; - определение указателя h, который в дальнейшем может быть связан с участком памяти, выделенным для величины целого типа. Введя с помощью определения указатель, можно присвоить ему возвращаемое операцией new значение: h = new int (15); В дальнейшем доступ к выделенному участку памяти обеспечивает выражение *h. В случае отсутствия инициализатора в операции new значение, которое заносится в выделенный участок памяти, не определено и не следует рассчитывать, что там будет, например, нуль. Если в качестве имени типа в операции new используется массив, то для массива должны быть полностью определены все размеры. Но и при этом инициализация участка памяти,
выделяСкалярные типы и выражения 89 емого для массива, запрещена. Подробнее о выделении памяти для массивов речь пойдет в гл. 5. Продолжительность существования выделенного с помощью операции new участка памяти — от точки создания до конца программы или до явного его освобождения операцией delete. Формат ее применения: delete указатель, где указатель адресует освобождаемый участок памяти, ранее выделенный с помощью операции new. Например, оператор delete h; освободит участок памяти, связанный с указателем h. Повторное применение операции delete к тому же указателю дает неопределенный результат. Также непредсказуем результат применения этой операции к указателю, получившему значение без использования операции new. Однако применение delete к указателю с нулевым значением не запрещено, хотя и не имеет особого смысла. Иллюстрацией к сказанному служит следующая программа, в которой с одним указателем на объекты целого типа последовательно связываются четыре динамически выделяемых участка памяти: //Р03_10.срр - Динамическое распределение памяти #include <iostream> #include "cyrToDos.h" using namespace std; int main() { int *i; i = new int( 1); cout« "*i="« */'« "\f /=" «/'; / = new int( 5); cout« "\t*i="« */« "\ti="« i« endl; i = new int (2"i); cout << "*/= "<<*i<<"\ti=" « i; i = new int (2**i); cout« "\t*i="« *i« "\t i="«/« endl; delete i; cout« cyrToDosf "После освобождения памяти:") « endl; cout << "*/•="« *i« "\t i="«i; delete i; //Некорректное применение операции cout« "\t*i=" « *i« "\t /=" « /« endl; return 0; }
90 Глава 3 Результат выполнения программы: *i=1 i=0xb1900 *i=10 i=0xb1920 *i=5 i=0xb1910 *i=20 i=0xb1930 После освобождения памяти: *i=0 i=0xb1930 4=727340 i=0xb1930 Обратите внимание, что после выполнения операторов delete /; значение указателя i неопределенно. В ряде случаев (в некоторых компиляторах) при выполнении такой программы выдается сообщение об ошибке вида Null pointer assignment Для освобождения памяти, выделенной для массива, используется следующая модификация того же оператора: delete [ ] указатель-, где указатель связан с выделенным для массива участком памяти. Подробнее об этой форме оператора освобождения памяти будем говорить в связи с массивами и указателями в гл. 5. Приведем программу, в которой для динамического управления памятью используются библиотечные функции malloc() и free(). Указанные функции находятся в стандартной библиотеке языка Си и его наследника — языка Си++, а их прототипы включаются в программу при использовании заголовка cstdlib. Прототип функции для выделения памяти: void * mall ос (size.t size)-. Исходной информацией для функции malloc() служит целое значение size, определяющее в байтах требуемый размер выделяемой памяти. Функция возвращает адрес выделенного участка памяти. Возвращаемый адрес равен 0, если выделить память не удается. В следующей программе при выделении памяти проверка для упрощения не выполняется: //Р03_11.срр - Динамическое выделение памяти для объектов #include <iostream> #include "cyrToDos.h" ttinclude <cstdlib> //Прототипы mallocf) и free ()
Скалярные типы и выражения 91 using namespace std; //Доступ к пространству имен std intmainf) { int *t; // Память выделена только для указателя t // Память выделена для m и *т: int *т =(int *)malloc(sizeoffint)); *m= 10; t = m; // Запомнили значение указателя m //Память выделена для *m: m = (int *) malloc(sizeof(int)); *m = 20; cout« cyrToDos ("Второе значение *m = ") « *m « end!; free(m); // Освободить память, выделенную для *m cout« cyrToDos ("Первое значение *m = ") « *t« endl; free(t); // Освободить память, выделенную для *m return 0; } Результаты выполнения программы: Второе значение *т = 20 Первое значение *т= 10 При определении указатель t получил память, но память для целого *t не выделена. При определении указателя т функцией malloc() в инициализаторе выделена память для целого значения */77. Затем в этот участок памяти заносится значение 10. При втором использовании malloc() указателю т присваивается адрес нового участка памяти. В */77 заносится значение 20 и выполняется печать. Остальное поясняют комментарии. Tun компоновки, или тип связывания, определяет соответствие идентификатора конкретному объекту или функции в программе, исходный текст которой размещен в нескольких файлах (модулях). Каждому имени, используемому в нескольких файлах, может соответствовать либо один объект (функция), общий для всех файлов, либо по одному и более объекту в каждом файле. Файлы программы (модули) могут транслироваться отдельно, и в этом случае возникает проблема установления связи между одним и тем же идентификатором и единственным объектом, которому он соответствует. Такие объекты (функции) и их имена нуждаются во внешнем связывании, которое выполняет компоновщик (редактор связей) при объединении отдельных
92 Глава 3 объектных модулей программы. Для остальных объектов (функций), которые локализованы в файлах, используется внутреннее связывание. Тип компоновки (связывания) никак специальным образом не обозначается при определении объектов и описании имен, а устанавливается компилятором по контексту, местоположению этих объявлений и использованию спецификаторов класса памяти static и extern. Например, имена статических объектов (static) локализованы в своем файле и могут быть использованы как имена других объектов и функций в других независимо транслируемых файлах. Такие имена имеют внутренний тип компоновки (связывания). Если имена объектов или функций определены со спецификатором extern, то имя будет иметь внешний тип компоновки (связывания). Рассмотрим следующую программу, функции которой и определения переменных рассредоточены по трем текстовым файлам: //Р03_12_1.срр - Первый файл программы //#include <iostream> int К = 0; //Для К - внешнее связывание (глобальная) void counted) { //Для counter - внешнее связывание static int KJN = 0; //Для KJN - внутреннее связывание К+= ++KJN; > //РОЗ-12-2. СРР - второй (основной) файл программы #include <iostream> using namespace std; void counter!); //Прототип - внешнее связывание void display(); // Прототип - внешнее связывание intmainf) { //К-локальная переменная - внутреннее связывание: for (int К = 0; К< 3; К++) { cout« "The for-cycle: К = " << К « endl; counterf); // Изменяет внешнюю переменную К display(); } return 0; } //Р03_12_3.срр - третий файл программы
Скалярные типы и выражения 93 #include <iostream> #include "cyrToDos.h" using namespace std; void displayf void) //Для display - внешнее связывание { extern int К; //Для К - внешнее связывание (глобальная) static int KJN = 0; //Для KJN - внутреннее связывание cout« сугТоОозГ^Глобальная К - ”) « K++ << endl; cout« cyrToDos("\tKJN из функции display = ”) « KJN++ « endl; ) При использовании компилятора DJGPP для одновременной трансляции трех файлов одной программы с получением исполнимого модуля с именем P03_12.exe можно в командной строке набрать следующую директиву: >дхх Р03_ 12_1.срр Р03_12_2.срр Р03_12_3.срр -о P03_12.exe<ENTER> Результат выполнения исполнимого модуля P03_12.exe: The for-cycle: К = 0 Глобальная К - 1 KJN из функции display = О The for-cycle: К = 1 Глобальная К = 4 KJN из функции display = 1 The for-cycle: К-2 Глобальная К = 8 KJN из функции display = 2 Анализируя текст и результаты выполнения программы, обратите внимание на следующее: • внешняя (глобальная) переменная с именем К является общей для файлов 1 и 3 (внешнее связывание); • внутренняя (локальная) переменная с именем К существует только в основном модуле; • статические (локальные) переменные с именем KJN различны в модулях 1 и 3, т.е. это различные объекты с одинаковым именем. Для каждого из них реализуется внутреннее связывание. Для некоторых имен тип компоновки не существует. К ним относятся параметры функций, имена объектов, локализованных
94 Глава 3 в блоке (без спецификаторов extern), идентификаторы, не являющиеся именами объектов и функций (например, имя введенного пользователем типа). 3.3. Определения и описания Различия между определениями и описаниями. Все взаимосвязанные атрибуты объектов (тип, класс памяти, область (сфера) действия имени, видимость, продолжительность существования, тип компоновки) приписываются объекту с помощью определений и описаний, а также контекстом, в котором эти определения и описания появляются. Определения устанавливают атрибуты объектов, резервируют для них память и связывают эти объекты с именами (идентификаторами). Кроме определений, в тексте программы могут присутствовать описания, каждое из которых делает указанные в них идентификаторы известными компилятору. В переводной литературе и особенно в документации наряду с терминами "описание" и "определение" используют "объявление" и "декларация". Не втягиваясь в терминологическую дискуссию, остановимся на варианте, применяемом в русскоязычных книгах по языкам программирования. Итак, появление двух терминов "определение" и "описание" объясняется тем фактом, что каждый конкретный объект может быть многократно описан, однако в программе должно быть только единственное определение этого объекта. Определение обычно выделяет объекту память и связывает с этим объектом идентификатор — имя объекта, а описания только сообщают свойства того объекта, к которому относится имя. Говорят, что описание ассоциирует тип с именем, а определение, кроме того, задает все другие свойства (объем памяти, внутреннее представление и т.д.) объекта и выполняет его инициализацию. Достаточно часто описание одновременно является и определением. Однако это не всегда возможно и не всегда требуется. Определять и описывать можно следующие объекты: • переменные; • функции; • константы (значения) заданного типа; • классы, их компоненты и указатели на компоненты;
Скалярные типы и выражен ия 95 • типы, вводимые пользователем с помощью typedef] • типы и имена структур, объединений, перечислений; • компоненты (элементы данных) структур и объединений; • компоненты (методы и данные) классов; • массивы объектов заданного типа; • перечислимые константы и типы-перечисления; • метки операторов; • препроцессорные макросы; • указатели на объекты или функции заданного типа; • ссылки на объекты или функции заданного типа; • шаблоны функций; • шаблоны классов. Определения и описания переменных. Из всего перечисленного разнообразия объектов только переменным и константам соответствуют базовые типы. Определение переменных заданного типа имеет следующий формат: s т тип имя1 и ниц. 1, имя2 иниц.2, ...; где s — спецификатор класса памяти (auto, static, extern, register) либо typedef - подробно описаны в п. 3.2; т — модификатор const или volatile’, тип — один из базовых типов; имя — идентификатор; иниц. — необязательный инициализатор, определяющий на этапе компиляции начальное значение соответствующего объекта (переменной). Синтаксис инициализатора (иниц.) переменной: = инициализирующее_выражение либо (инициализирующее _выражение) Наиболее часто в качестве инициализирующего выражения используется константа. Обратите внимание, что инициализация возможна только при определении объекта. Например, если в качестве спецификатора класса памяти используется extern, то инициализатор недопустим. Напомним, что описание является определением, если: • вводит переменную; • содержит инициализатор; • полностью описывает функцию (включает тело функции);
96 Глава 3 • вводит объединение или структуру (включая компоненты); • вводит класс (включая его компоненты); • вводит шаблон классов или функций. Описание не может быть определением, если: • это прототип функции; • содержит спецификатор extern; • вводит статический компонент класса; • вводит имя класса; • вводит имя типа, определяемое пользователем (typedef); • вводит прототип шаблона функций или классов. Приведем примеры описаний: extern int д; // Внешняя переменная float fnfint, double); // Прототип функции extern const float pi; // Внешняя константа struct st; // Имя структуры (класса) typedef unsigned char symbol; // symbol - название типа template <typename T> void displayfint k, T ar[ ]); // Прототип шаблона функций Примеры определений: char sm; // Глобальная или локальная переменная float dim = 10.0; //Инициализированная переменная double Eulerf2.718282); //Инициализированная переменная const float pi = 3.14159; // Именованная константа floatх2(floatх) { return х*х; }; //Функция struct { char a; int b; } st; // Структура enum {zero, one, two); //Перечисление. Некоторая неоднозначность есть в определениях переменных. Их статус зависит от контекста, в котором использовано определение или описание. Рассмотрим, например, charsm;. Если такое описание находится в блоке, то это определение неинициализированной переменной автоматической памяти. Если такое описание появляется вне блоков и классов, то это определение статической внешней переменной, которой по умолчанию присваивается нулевое значение, а для символьной переменной char — пробел. В тех файлах, где нужен доступ к этой глобальной переменной, должно быть помещено ее описание вида extern char sm;. Подобное описание необходимо для доступа из функций к
Скалярные типы и выражения 97 внешним объектам, определения которых размещены в том же файле, но ниже текста определения функции. Пример: //Р03_13.срр - определения и описания переменных #include <iostream> #include "cyrToDos. h" using namespace std; float pi = 3.141593; // Определение с явной инициализацией int sO; // Определение sO (инициализация по умолчанию) int s2 = 5; // Определение s2 с явной инициализацией int main( ) { extern int sO; // Описание sO extern char s 1; // Описание s 1 int s2(4); // Определение s2 с явной инициализацией cout « cyrToDosf "Инициализация по умолчанию: sO = ") « sO « endl; cout« cyrToDosf "Явная инициализация: s1 -■ ") « s1 « endl; cout« cyrToDos (”Внутренняя переменная: s2 = ") « s2 « endl; cout« cyrToDos ("Внешняя переменная: s2 = ") « ::s2 « endl; cout« cyrToDos ("Внешняя переменная: pi = ") « pi« endl; return 0; } char s1='@ V // Определение s1 с явной инициализацией //Конец файла с текстом программы Результаты выполнения программы: Инициализация по умолчанию: s0 = О Явная инициализация: s1 =@ Внутренняя переменная: s2-4 Внешняя переменная: s2 = 5 Внешняя переменная: pi = 3.14159 Для инициализации переменной s2, относящейся к автоматической памяти, использована "скобочная" (функциональная) форма задания начального значения. Внешние переменные таким образом инициализировать нельзя - компилятор воспринимает их как прототипы функций. В программе обратите внимание на переменную pi, которая определена (и инициализирована) вне функции /т?а/п(), а внутри нее не описана. Так как программа оформлена в виде одного файла, то все внешние у-2762
98 Глава 3 переменные, определенные до текста функции, доступны в ней без дополнительных описаний. Таким образом, описание extern int $0; в данной однофайловой программе излишнее, но описание extern chars 1; необходимо. Следующая программа еще раз иллюстрирует доступ к внешним переменным из разных функций однофайловой программы: //Р03_14.срр - обмен между функциями через внешние переменные #include <iostream> using namespace std; int x; // Определение глобальной переменной void func(); // Прототип функции intmainf) { extern int x; // Излишнее описание x = 4; func(); cout« "x = " << x << endl; return 0; } void func() { extern int x; // Излишнее описание x += 2; } Результат выполнения: х — 6 Отметим некоторые особенности спецификаторов класса памяти. Спецификатор auto редко появляется в программах. Действительно, его запрещено использовать во внешних определениях, а применение внутри блока или функции излишне - локальные объекты блока по умолчанию (если нет спецификаторов static или extern) являются объектами автоматической памяти. Спецификатор register также запрещен во внешних определениях, однако его применение внутри блоков или функций может быть вполне обосновано. Появление typedef среди спецификаторов класса памяти (auto, static,...) объясняется не семантикой, а синтаксическими аналогиями. Служебное слово typedef специфицирует новый тип данных, который в дальнейшем можно использовать в
описаСкалярные типы и выражения 99 ниях и определениях. Однако не следует считать, что typedef действительно вводит новый тип. Вводится только новое название (имя, синоним) типа, который программист хочет иметь возможность называть по-другому. Сравним два предложения: static unsigned int ui; typedef unsigned int NAME; В первом определена статическая целая беззнаковая переменная ui, а во втором никакой объект не определен, а введено NAME — новое имя типа для еще не существующих беззнаковых целых объектов. В дальнейшем NAME можно использовать в описаниях и определениях. Например, запись register NAME rn = 44; // Допустим спецификатор класса памяти эквивалентна определению register unsigned int rn = 44; Заметим, что register не имя типа, а спецификатор класса памяти. Имена типов, введенные с помощью typedef, нельзя употреблять в одном описании (определении) с другими именами типов. Например, будет ошибочной запись long NAME start; // Ошибочное сочетание имен типов Однако определение const NAME cn = 0; вполне допустимо. const - не имя типа, а квалификатор. Спецификатор typedef нельзя употреблять в определениях функций, однако можно в их описаниях (прототипах). Имя типа, введенное с помощью typedef, входит в то же пространство имен, что и прочие идентификаторы программ (за исключением меток), и подчиняется правилам области (сферы) действия имен. Например: typedef long NL; unsigned int NL = 0; // Ошибка - повторное определение NL void func() { int NL = 1; // Верно - определен новый локальный объект }
100 Глава 3 Квалификаторы const и volatile. Эти квалификаторы, названые в Стандарте CV-квалификаторами (CV-qualifiers), позволяют сообщить компилятору об изменчивости или постоянстве определяемого объекта. Если переменная определена с квалификатором const, то ее нельзя изменять во время выполнения программы. Единственная возможность присвоить ей значение -инициализация при определении. Объекту с квалификатором const не только нельзя присваивать значения, но для него запрещены операции инкремента (++) и декремента Таким образом, используя в определении квалификатор const, мы создаем не переменную, а именованную константу. Одна из очень важных возможностей применения именованных констант — задание размеров массивов автоматической памяти, что запрещено Стандартом, с помощью переменных. (Отметим, что в некоторых реализациях размер массива допускается задавать с помощью переменной.) Пример, соответствующий Стандарту: const int К = 4; char car[K]; Указатель, определенный с квалификатором const, нельзя изменять, однако может быть изменен объект, который им адресуется. Примеры с константами: const zero = 0; // По умолчанию добавляется тип int const char *ptrconst = "variable"; //Константный указатель на строку char * point = "строка"; //Обычный указатель на строку char const *ptr = "константа"; // Указатель на константную строку char *varptr = ptr; // Запрещено zero += 4; // Ошибка - нельзя изменить константу ptrconst = point; // Ошибка - значение указателя неизменно Отметим ошибочную попытку присвоить указателю (не константному) varptr значение указателя на константу. Это запрещено, так как в противном случае можно было бы через указатель varptr изменить константную литерную строку. Квалификатор volatile указывает, что в процессе выполнения программы значение объекта может изменяться в промежутке между явными обращениями к нему. Например, на объект может повлиять внешнее событие. Поэтому компилятор не должен
поСкалярные типы и выражения 101 мещать его в регистровую память и не должен делать никаких предположений о постоянстве объекта в те моменты, когда в программе нет явных операций, изменяющих значение объекта. Квалификаторы const и volatile имеют особое значение при работе с классами, и мы к ним еще обратимся. 3.4. Выражения и преобразования типов Выражение — это последовательность операндов, разделителей и знаков операций, задающая правило вычисления значения некоторого типа. Порядок применения операций к операндам определяется рангами (приоритетами) операций (см. табл. 2.3) и правилами группирования операций (их ассоциативностью). Для изменения порядка выполнения операций и их группирования используют разделители (круглые скобки). Таким образом, х = у = z означает х = (у = z), х + у - z означает (х + у) - z. Кроме формирования результирующего значения, вычисление выражения может вызвать побочные эффекты. Например, значением выражения z = 3, z + 2 будет 5, а в качестве побочного эффекта z примет значение 3. В результате вычисления выражения х > 0 ? х~: х будет получено значение х (положительное либо отрицательное), а в качестве побочного эффекта положительное значение х будет уменьшено на I. С помощью ассоциативности и рангов операций не удается определить правила вычисления всех выражений, в которые входит условная операция. Например, (см. [I] с. 162) выражение a = b<c?d = e:f=g означаема = ((Ь < с) ? (d = e)(f= д)). Чтобы избежать ошибочного толкования подобных выражений, рекомендуем использовать круглые скобки. С их помощью программист может по-своему определить порядок выполнения операций. В языке Си++ программист может расширить действие стандартных операций (overload — перегрузка), т.е. придавать им новый смысл при работе с нестандартными для них операндами. Отметим, что операции могут быть распространены на вводимые пользователем типы, однако у программиста нет возможности
102 Глава 3 изменить действие операций на операнды стандартных типов. Эту связанную с классами возможность языка Си++ рассмотрим позже, а сейчас остановимся на некоторых свойствах операций, стандартно определенных для тех типов, для которых эти операции введены. Формальный синтаксис языка Си++ предусматривает рекурсивное определение выражений. Рекурсивность синтаксических определений (не только для выражений) и широкие возможности конструирования новых типов делают попытки "однопроходного" изложения семантики сразу всего языка Си++ практически безнадежными. Поясним это, рассмотрев выражения. Основным исходным элементом любого выражения является первичное выражение. К ним относятся: • константа-литерал (literal) • this • (выражение) • именующее выражение (id-expression) Константы (как мы уже рассмотрели в предыдущей главе) могут быть целыми, символьными, вещественными, строковыми и булевскими (логическими). Указатель this может использоваться только в теле нестатической компонентной функции класса, поэтому его невозможно рассматривать, не вводя понятие класса. Следующим первичным выражением является выражение, заключенное в скобки - налицо рекурсия, т. е., не введя понятия «выражение», невозможно формально определить понятие «выражение в скобках». Именующее выражение (id-expression) может быть: •• неквалифицированным именованием (unqualified-id); • квалифицированным именованием (qualified-id)) К сожалению, материала предшествующих разделов недостаточно, чтобы объяснять семантику всех видов именующих выражений. О невозможности «линейного» изложения синтаксиса языка мы уже говорили, поэтому перечислим все виды именующих выражений, но смысл большинства из них будет понятен только при дальнейшем изучении конструкций языка.
Скалярные типы и выражения 103 Неквалифицированное именование: идентификатор может использоваться в качестве первичного выражения только в том случае, если он введен с помощью подходящего определения. Самый распространенный представитель — идентификатор как имя переменной или константы. именование операции-функции (operator-function-id) вводится только при расширении действия (при перегрузке) операций. Форма этого первичного выражения: operator операция. Механизм перегрузки можно объяснить только после определения понятия класс, именование функции преобразования (conversion-function-id) функции преобразования являются методами классов. Форма этого первичного выражения: operator операция_преобра- зования_типа. Такой метод позволяет приводить значение объекта класса к нужному пользователю типу. Для объяснения их семантики требуется ввести соответствующие понятия, относящиеся к классам. -имя класса обозначает обращение к специальному методу класса - к деструктору. именование шаблона (в Стандарте template-id) Форма этого первичного выражения: имя_шаблона < список_аргументов шаблона >. К сожалению, невозможно объяснить смысл этой конструкции, не введя понятие шаблона. Давайте подождем! квалифицированное именование - это: • ::opt спецификатор _вложенного_имени template^ неквалифицированное именование •:: идентификатор •:: именование операции-функции •:: именование шаблона. В свою очередь спецификатор_вложенного_имени - это: • имя_класса_илипространства_имен:: спецификатор_вложенного_имени0(}Х • имя_класса_или_пространства_имен:: template спецификатор_вложенного_имени.
104 Глава 3 Индекс opt после термина или символа означает необязательность появления предшествующего элемента в конкретном случае. Из всего богатого набора первичных выражений, вводимых приведенными синтаксическими правилами, материал предшествующих разделов подготовил читателя к использованию только некоторых конструкций. Мы знакомы: • с константами-литералами, • идентификаторами как именами переменных и именованных констант, • идентификаторами, перед которыми использован знак операции :: • конструкцией имя_пространста_имен :: идентификатор. Из этих элементов можно с помощью уже описанных знаков операций формировать более сложные выражения, а заключив любое из них в круглые скобки, - получить еще один вид первичного выражения. На основе первичных выражений Стандарт определяет синтаксис постфиксных выражений, затем унарных выражений, и т.д. Как показывает пример первичных выражений при изучении языка следовать логике построения синтаксических определений конструкций языка практически невозможно, поэтому вернемся к неформальному изложению материала. Для знакомства с формальной грамматикой языка читатель может обратиться к Стандарту [4] либо к книге Б.Страуструпа [I]. Обратите внимание, что в обоих названных источниках полная грамматика помещена в приложения. Итак, зная операции и перечисленные первичные выражения, можно конструировать более сложные выражения, смысл большинства которых интуитивно понятен (например, присваивание или суммирование). Однако при выполнении операций (в особенности знакомых читателю еще из элементарной математики) нужно учитывать особенности представления в программе (с учетом ее реализации на компьютере) данных разных типов. В связи с этим рассмотрим преобразование и приведение типов, допустимые языком Си++. Явное приведение типа. Постфиксное выражение type (список_выражений)
Скалярные типы и выражения 105 служит для формирования значений типа type на основе спис- ка_выражений, помещенного в круглых скобках. Если выражений больше одного, то тип должен быть классом, и данное постфиксное выражение вызывает конструктор класса. Если в списке выражений всего одно выражение, a type - имя простого типа, то имеет место уже рассмотренное в разд. 2.4 непосредственное функциональное приведение типа. Примеры: int( 3.14), float(2/5), int(TV). Функциональная запись не может применяться для типов, не имеющих простого имени. Например, попытка трактовать конструкции unsigned long (х/3+2) или char *(0777) как функциональные преобразования вызовет ошибку компиляции. Напомним (см. п. 2.4), что кроме функциональной записи для явного преобразования типа можно использовать операцию приведения к требуемому типу, унаследованную из Си. Для ее изображения используется обозначение типа в круглых скобках. Те же примеры можно записать с помощью операции приведения типа так: (int)3.14, (float)2/5, (int)'А'. Такая операция приведения к типу может применяться и для типов, имеющих сложные обозначения. Например, можно записать (unsigned long) (х/3+2) или (char *)0777 и тем самым выполнить нужные преобразования. Другую возможность явного преобразования для типов со сложным наименованием обеспечивает введение собственных обозначений типов с помощью typedef. Например: typedef unsigned long int ULI; typedef char * PCH; После введения пользователем таких простых имен типов можно применять функциональную запись преобразования типа: ULI(x/3+2) или РСН(0777). Язык Си++ ввел еще четыре операции приведения типов: dynamic_cast, sfaf/c_casf, reinterpret_cast, const_cast.
106 Глава 3 Каждая из них включает в угловых скобках название того типа, к которому нужно привести значение операнда. Таким образом формат выражения с каждой из этих операций имеет следующий вид: Ha3eaHne_cast < целевой_тип> операнд Отметим, что все операции приведения типов не изменяют ни значения, ни типа самого операнда. Операции устанавливают только тип значения, определенного операндом и возвращаемого выражением. Если целевой тип, указанный в операции, ссылочный, то результат выражения леводопустимый. Операция static_cast используется для преобразования родственных типов, например, целых величин или указателей на объекты классов, имеющих общий базовый класс. Для базовых типов применение операции эквивалентно использованию функционального или традиционного приведения типов. Особые возможности появляются при приведении типов указателей, объектов и ссылок для классов, находящихся в отношении наследования. Правила получения результата для этих случаев нужно рассмотреть при изучении наследования. Операция reinterpret_cast обеспечивает преобразование независимых типов. Поэтому компилятор не может проверить допустимость заданного преобразования, и вся ответственность ложится на программиста. В отличие от static_cast операция dynamic_cast предполагает проверку корректности преобразования на этапе исполнения программы. Это позволяет делать тип операнда нефиксированным, т. е. зависящим от условий выполнения. dynamic_cast применяется для приведения типов указателей и ссылок на объекты базовых и производных классов одной иерархии наследования. Мы вернемся к этой операции в гл. 13, посвященной наследованию классов. Перечисленные три операции не устраняют константность значения операнда. Действие квалификатора const анулирует операция const_cast. Приведенным выше примерам с традиционной и функциональной операциями приведения соответствуют такие выражения: static_cast <unsigned long>(x/3+2) static_cast <char *>0777
Скалярные типы и выражения 107 Стандартные преобразования типов. При вычислении выражений некоторые операции требуют, чтобы операнды имели соответствующий тип, а если требования к типу не выполнены, принудительно вызывают выполнение нужных преобразований. Та же ситуация возникает при инициализации, когда тип инициализирующего выражения приводится к типу определяемого объекта. Напомним, что присваивание является бинарной операцией, поэтому сказанное относительно преобразования типов относится и ко всем формам операции присваивания. Как пишет Б.Страуструп [I], «базовые типы могут преобразовываться один в другой невероятно большим количеством способов». Рассматривать их все просто бесполезно, достаточно понять основные принципы механизма преобразований. Среди преобразования типов, выполняемых неявно, выделяют: • преобразования указателей'. любой указатель может быть неявно преобразован в void *; указатель на производный класс может быть преобразован в указатель на базовый класс; значение 0 преобразуется в любой указательный тип; неконстантный указатель преобразуется в константный того же типа; • преобразования ссылок: ссылка на производный класс может быть преобразована в ссылку на базовый класс; неконстантная ссылка преобразуется в константную того же типа; • преобразования в логические значения: в значение типа boot преобразуются обобщенные целые числа, вещественные числа и указатели; ненулевые значения преобразуются в true, а нулевые — в false; • преобразования указателей на компоненты классов — указатель на компонент производного класса преобразуется к типу указателя на компонент базового класса, но не наоборот; • преобразования операндов в арифметических выражениях нуждаются в более подробном рассмотрении. При преобразовании типов нужно различать преобразования, изменяющие внутреннее представление данных, и преобразования, изменяющие только интерпретацию внутреннего
представ108 Глава 3 ления. Например, когда данные типа unsigned int переводятся в тип int, менять их внутреннее представление не требуется — изменяется только интерпретация. При преобразовании типа double в тип int недостаточно изменить только интерпретацию, необходимо изменить длину участка памяти для внутреннего представления и кодировку. При таком преобразовании из double в int возможен выход за диапазон допустимых значений типа int, и реакция на эту ситуацию существенно зависит от конкретной реализации. Именно поэтому для сохранения мобильности программ в них рекомендуется с осторожностью применять преобразование типов. Рассмотрим этапы (последовательность выполнения) преобразования операндов в арифметических выражениях. 1. Все короткие целые типы преобразуются в типы не меньшей длины в соответствии с табл. 3.1. При этом получаемый тип выбирается из условия, что он может представить все значения исходного типа. Затем оба значения, участвующие в операции, принимают тип int или float либо double в соответствии со следующими ниже правилами. Таблица 3.1 Правила стандартных арифметических преобразований для обобщенных целых Исходный тип Преобразуется в Правила преобразований char int Расширение нулем или знаком в зависимости от умолчания для char unsigned char int Старший байт заполняется нулем signed char int Расширение знаком short int Сохраняется то же значение unsigned short unsigned int Сохраняется то же значение bool int true - в 1, false - в 0 enum int Сохраняется то же значение wchar_t int unsigned int long unsigned long Сохраняется то же значение битовое поле int Сохраняется то же значение
Скалярные типы и выражения 109 2. Если один из операндов имеет тип long double, то второй тоже будет преобразован в long double. 3. Если п. 2 не выполняется и один из операндов есть double, другой приводится к типу double. 4. Если пп.2—3 не выполняются и один из операндов имеет тип float, то второй приводится к типу float. 5. Если пп.2-4 не выполняются (оба операнда целые) и один операнд long int, а другой unsigned int, то, если long int может представить все значения unsigned int, последний преобразуется к long int; иначе оба операнда преобразуются к unsigned long int. 6. Если пп.2—5 не выполняются и один операнд есть long, другой преобразуется к long. 7. Если пп.2—6 не выполнены и один операнд unsigned, то другой преобразуется к unsigned. 8. Если пп.2—7 не выполнены, то оба операнда принадлежат типу int. Используя арифметические выражения, следует учитывать приведенные правила и не попадать в "ловушки" преобразования типов, так как некоторые из них приводят к потерям информации, а другие изменяют интерпретацию битового (внутреннего) представления данных. На рис. 3.2, взятом с некоторыми сокращениями из проекта Стандарта языка Си++ [3], стрелками отмечены арифметические Рис 3.2. Последовательности арифметических преобразований типов, гарантирующие сохранение значимости
110 Глава 3 преобразования, гарантирующие сохранение точности и неизменность численного значения. При преобразованиях, которые не отнесены схемой (см. рис. 3.2) к безопасным, возможны существенные информационные потери. Для оценки значимости таких потерь рекомендуется проверить обратимость преобразования типов. При арифметических преобразованиях необратимость вполне объяснима и естественна. Преобразование целочисленных значений к вещественному типу осуществляется настолько точно, насколько это предусмотрено аппаратурой. Если целочисленное значение не может быть точно представлено как вещественное, то младшие значащие цифры теряются. Преобразование вещественного значения к целому типу выполняется за счет отбрасывания дробной части. Обратное преобразование целой величины к вещественному значению может привести к потере точности. Следующая программа иллюстрирует сказанное. //Р03_15.срр - Потери информации при преобразованиях типов #include <iostream> using namespace std; intmainf) { long к = 123456789; float g = (float)k; cout« "k = " << к « endl; // Печатает: к = 123456789 cout« "g = ” << g « endl; // Печатает: g = 1.23457e+08 к = (long)g; cout« ”k ="« к « endl; // Печатает: к = 123456792 g = (floa t)2.222222e+2; int m = (int)g; cout« "g = ”« g « endl; // Печатает: g = 222.222 cout« ”m = "« m « endl; // Печатает: m = 222 g = (float) m; cout« "g = ” << g « endl; // Печатает: g = 222 return 0; } К менее предсказуемым результатам может привести необратимость преобразования типов для указателей, ссылок и указателей на компоненты классов.
111 Глава 4 ОПЕРАТОРЫ ЯЗЫКА СИ++ 4.1. Последовательно выполняемые операторы Материал, относящийся к операторам, по-видимому, наиболее традиционный. Здесь язык Си++ почти полностью соответствует языку Си, который в свою очередь наследует конструкции классических алгоритмических языков лишь с небольшими вариациями. Операторы, как обычно, определяют действия и логику (порядок) выполнения действий в программе. Среди операторов выделяют операторы, выполняемые последовательно, и управляющие операторы (условные операторы, циклы, переключатели, передачи управления). Рассмотрим в этом подразделе первую группу. Каждый оператор языка Си++ заканчивается и идентифицируется разделителем "точка с запятой". Любое выражение, после которого поставлен символ "точка с запятой", воспринимается компилятором как отдельный оператор. (Исключения составляют выражения, входящие в заголовок цикла for.) Часто оператор-выражение служит для вызова функции, не возвращающей никакого значения. Например: //Р04_01.срр - Обращение к функции как оператор-выражение #include <iostream> using namespace std; void cod_char( char c) { cout <<"code("« c « ") = " << (unsigned int)c « endl; } intmainf) { cod_char('A'); // Оператор-выражение cod_char( ’x'); // Оператор-выражение return 0; }
112 Глава 4 Результат выполнения программы: code(A) = 65 code(x) = 120 Еще чаще оператор-выражение - это не вызов функции, а выражение присваивания. Именно в связи с тем, что присваивание относится к операциям и используется для формирования бинарных выражений, в языке Си++ (и в Си) отсутствует отдельный оператор присваивания. Оператор присваивания всего-навсего является частным случаем оператора-выражения. Специальным случаем оператора служит пустой оператор. Он представляется символом "точка с запятой", перед которым нет никакого выражения или не завершенного разделителем оператора. Пустой оператор не предусматривает выполнения никаких действий. Он используется там, где синтаксис языка требует присутствия оператора, а по смыслу программы никакие действия не должны выполняться. Пустой оператор чаще всего используется в качестве тела цикла, когда все циклически выполняемые действия определены в его заголовке: // Вычисляется факториал: 5! long р = 1; for (int i = 0; i < 5; ++/, p *= i); Перед каждым оператором может быть помещена метка, отделяемая от оператора двоеточием. В качестве метки используется произвольно выбранный программистом уникальный идентификатор: АВС: х = 4+ х *3; Метки локализуются в сфере действия функции. Описания и определения, после которых помещен символ "точка с запятой", считаются операторами. Поэтому перед ними также могут помещаться метки: metka: int z-0,d-4; // Метка перед определением С помощью пустого оператора (перед которым имеет право стоять метка) метки можно размещать во всех точках программы, где синтаксис разрешает использовать операторы. Прежде чем привести пример, определим составной оператор как
заключенОператоры языка Си++ 113 ную в фигурные скобки последовательность операторов. Если среди операторов, находящихся в фигурных скобках, имеются определения и описания, то составной оператор превращается в блок, где локализованы все определенные в нем объекты. Синтаксически и блок, и составной оператор являются отдельными операторами. Однако ни блок, ни составной оператор не должны заканчиваться точкой с запятой. Для них ограничителем служит закрывающая фигурная скобка. Внутри блока (и составного оператора) любой оператор должен оканчиваться точкой с запятой: {int a; char b='0'; а = (int)b; } //Это блок { func(z +1.0, 22); е = 4 *х - 1; } //Составной оператор // Составной оператор с условным переходом к его окончанию: {if(i > k) goto МЕТ; к++; МЕТ:; } // Помечен пустой оператор Говоря о блоках, нужно помнить правила определения сферы действия имен и видимости объектов. Так как и блок, и составной оператор пользуются правами операторов, то разрешено их вложение, причем на глубину вложения синтаксис не накладывает ограничений. О вложении составных операторов и блоков удобнее говорить в связи с циклами, функциями и операторами ветвления (выбора), к которым мы и перейдем. О выходе из блока речь пойдет в связи с операторами передачи управления (разд. 4.4). О входе в блок стоит сказать уже сейчас — запрещено обращаться к внутренним операторам блока, минуя размещенные в блоке определения локальных объектов автоматической памяти, для которых выполняется инициализация. Примеры: goto L; {int п; L: п=5; р +=п; } //Допустимо, но не рекомендуется goto L; {int n-4; L: p +=п; } // Запрещено 4.2. Операторы выбора (ветвления) К операторам выбора, называемым операторами ветвления, относят: условный оператор (if...else) и переключатель (switch). Каждый из них служит для выбора пути выполнения программы. Синтаксис условного оператора: if (выражение) оператор_1 else оператор_2 g-2762
114 Глава 4 Выражение должно быть скалярным и может иметь булевский тип, арифметический тип или тип указателя. Если арифметическое выражение не равно нулю (или не есть пустой указатель), то его значение приводится к true, условие считается истинным и выполняется оператор_1. В противном случае, когда выражение равно false, выполняется оператор_2. В качестве операторов нельзя использовать описания и определения. Однако здесь могут быть составные операторы и блоки: if (х > 0) { х = -х; Цх ‘ 2); } else {int i = 2; х *= i; f(x);} При использовании блоков (т.е. составных операторов с определениями и описаниями) нельзя забывать о локализации определяемых в блоке объектов. Например, ошибочна будет последовательность: if O' > 0) {int i; i = 2 * j\} else i = —y; Переменная / локализована в блоке и не существует вне блока. Допустима сокращенная форма условного оператора, в которой отсутствует else и оператор_2. В этом случае при ложности проверяемого условия (равенстве нулю арифметического выражения или указателя) никакие действия не выполняются: if (а < 0) а = -а; В свою очередь, оператор_1 и оператор_2 могут быть условными, что позволяет организовать цепочку проверок условий любой глубины вложенности. В этих цепочках каждый из условных операторов (после проверяемого условия и после else) может быть как полным условным, так и иметь сокращенную форму записи. При этом могут возникать ошибки неоднозначного сопоставления if и else. Синтаксис языка предполагает, что при вложениях условных операторов каждое else соответствует ближайшему к нему предшествующему if. Пример неверного толкования этого правила: if(x == 1) if(y== 1)cout« "х равно 1 и у равно 1"; else cout« "х не равно 1
Операторы языка Си++ 115 При х, равном 1, и у, равном 1, совершенно справедливо печатается фраза "х равно 1 и у равно 1". Однако фраза "хне равно 7" может быть напечатана только при х, равном 1, и при у, не равном 1, так как else относится к ближайшему if. Внешний условный оператор, где проверяется х==7, является сокращенным и в качестве оператора_1 включает полный условный оператор, в котором проверяется условие у==7. Таким образом, проверка этого условия выполняется только при х, равном 1. Простейшее правильное решение этой микрозадачи можно получить, применив фигурные скобки, т.е. построив составной оператор. Нужно фигурными скобками ограничить область действия внутреннего условного оператора, сделав его неполным. Тем самым внешний оператор превратится в полный условный: if(x== 1) {if (у == 1) cout« "х равно 1 и у равно 1 } else cout« "х не равно 1 Теперь else относится к первому if, и выбор выполняется верно. В качестве второго примера вложения условных операторов рассмотрим функцию, возвращающую максимальное из значений трех аргументов: int max3(int х, inty, intz) { if (х < у) iffy < z) return z; else return y; else iffx < z) return z; else return x; } В тексте соответствие if и else показано с помощью отступов. Переключатель является наиболее удобным средством для организации множественного (мульти-) ветвления. Синтаксис переключателя таков: switch (переключающее _выражение) {case константное _выражение_1: операторы_1; case константное _выражение_2: операторы_2; 8‘
116 Глава 4 case константное_выражение_п: операторы_п; default: операторы; Управляющая конструкция switch передает управление к тому из помеченных с помощью case операторов, для которого значение константного выражения совпадает со значением переключающего выражения. Значение переключающего выражения должно быть целочисленным или приводиться к целому. Значения константных выражений, помещаемых за служебными словами case, также приводятся к целому типу переключающего выражения. В одном переключателе все константные выражения должны иметь различные значения, но быть одного типа. Любой из операторов, помещенных в фигурных скобках после конструкции switch(...)} может быть помечен одной или несколькими метками вида case константное_выражение: Если значение переключающего выражения не совпадает ни с одним из константных выражений, то выполняется переход к оператору, отмеченному меткой defaultВ каждом переключателе должно быть не больше одной метки defaultt однако эта метка может и отсутствовать. В случае отсутствия метки default при несовпадении переключающего выражения ни с одним из константных выражений, помещаемых вслед за case, в переключателе не выполняется ни один из операторов. Сами по себе метки case константное_выражениеJ: и default: не изменяют последовательности выполнения операторов. Если не предусмотрены переходы или выходы из переключателя, то в нем последовательно выполняются все операторы, начиная с той метки, на которую передано управление. Пример программы с переключателем: //Р04_02.срр - Названия нечетных целых цифр, не меньших заданной #include <iostream> using namespace std; intmainf) { int ic; //Введите любую десятичную цифру: cout« "Enter decimal digit:
Операторы языка Си++ 117 с in »ic; switch (ic) {case 6: case 1: cout« "one, case 2: case 3: cout« "three, case 4: case 5: cout« "five, case 6: case 7: cout« "seven, case 8: case 9: cout« "nine, "«end!; break; // Выход из переключателя default: cout« "Error! It isn't digit!" «endl; } // Конец переключателя return 0; } Результаты двух выполнений программы: Enter decimal digit: 4<Enter> five, seven, nine. Enter decimal digit: z<Enter> Error! It isn't digit! Приведенная программа иллюстрирует действие оператора break. С его помощью выполняется выход из переключателя. Если поместить операторы break после вывода каждой цифры, то программа будет печатать название только одной нечетной цифры или "Error...". Несмотря на то что в формате переключателя после конструкции switch() приведен составной оператор, это не обязательно. После switchQ может находиться любой оператор, помеченный с использованием служебного слова case. Однако без фигурных скобок такой оператор может быть только один, и смысл переключателя теряется: он превращается в разновидность сокращенного условного оператора. В переключателе могут находиться описания и определения объектов, т.е. составной оператор, входящий в переключатель, может быть блоком. В этом случае, как и при входе в блок, нужно избегать ошибок "перескакивания" через определения: switch (п) { char d = 'D'; // Переключатель с ошибками // Никогда не обрабатывается
118 Глава 4 case 1: double f=3.14; //Обрабатывается только для п == 1 case 2:... if (int (d) != int (f))... // Ошибка: d и (или) f не определены } 4.3. Операторы цикла Операторы цикла задают многократное исполнение операторов тела цикла. Определены три разных оператора цикла: • цикл с предусловием: while (выражение-условие) тело_цикла • цикл с постусловием: do тело_цикла while (выражение-условие); • параметрический цикл: for (инициализатор_цикла; выражение-условие; выражение) тело_цикпа Тело_цикпа не может быть описанием или определением. Это либо отдельный (в том числе пустой) оператор, который всегда завершается точкой с запятой, либо составной оператор, либо блок (заключаются в фигурные скобки). Выражение-условие - это во всех операторах скалярное выражение (чаще всего логическое или арифметическое выражение), определяющее условие продолжения выполнения итераций (если его значение true, т.е. не равно нулю). Прекращение выполнения цикла возможно в следующих случаях: • ложное (нулевое) значение проверяемого выражения-условия; • выполнение в теле цикла оператора передачи управления (break, goto, return) за пределы цикла. Последнюю из указанных возможностей проиллюстрируем позже, рассматривая особенности операторов передачи управления.
Операторы языка Си++ 119 Оператор while (оператор "повторять, пока истинно условие") называется оператором цикла с предусловием. При входе в цикл вычисляется выражение-условие. Если его значение отлично от нуля, то выполняется тело_цикла. Затем вычисление выражения-условия и выполнение операторов тела_цикла повторяются последовательно, пока значение выражения-условия не станет ложным, т.е. равным false или 0. Оператором while удобно пользоваться для просмотра всевозможных последовательностей, если в конце каждой из них находится заранее известный признак. Например, по определению, строковая константа есть последовательность символов типа char, в конце которой находится нулевой символ ‘\0\ Следующая функция подсчитывает длину строковой константы, заданной с помощью адресующего ее указателя-параметра: int length (char *stroka) { int len = 0; while (*stroka++) len++; return len; } Указатель-параметр изменяется при выполнении цикла в теле функции. Выход из цикла — равенство нулю того элемента строковой константы, который адресуется указателем stroka. (Обратите внимание на порядок вычисления проверяемого выражения. Вначале будет выбрано значение указателя stroka, затем оно будет использовано для доступа по адресу. Выбранное значение будет значением выражения в скобках и затем значение указателя будет увеличено на I.) В качестве проверяемого выражения-условия в циклах часто используются отношения. Например, следующая последовательность операторов вычисляет сумму квадратов первых К натуральных чисел (членов натурального ряда): int / = 0; // Счетчик int s = 0; // Будущая сумма while (i < К) //Цикл вычисления суммы /++; s += i * /;
120 Глава 4 Если в выражении-условии нужно сравнивать указатель с нулевым значением (с пустым указателем), то следующие проверки эквивалентны (хотя вторая понятнее и потому предпочтительнее): while (point)... while (point != 0)... Используя оператор цикла с предусловием, необходимо следить за тем, чтобы операторы тела_цикла воздействовали на выражение-условие?, либо оно еще каким-то образом должно изменяться во время вычислений. (Например, за счет побочных эффектов могут изменяться операнды выражения-условия. Часто для этих целей используют унарные операции ++ и Только при изменении выражения-условия можно избежать зацикливания. Например, следующий оператор обеспечивает бесконечное выполнение пустого оператора в теле цикла: while (1); // Бесконечный цикл с пустым //оператором в качестве тела Такой цикл может быть прекращен только за счет событий, происходящих вне потока операций, явно предусмотренных в программе. Вариант такого события - указание операционной системе «Снять задачу!». Оператор do (оператор "повторять") называется оператором цикла с постусловием. Он имеет следующий вид: do тело_цикла while (выражение-условие); При входе в цикл do обязательно выполняется тело_цикла. Затем вычисляется выражение-условие и, если его значение равно true, вновь выполняется тело_цикпа. При обработке некоторых последовательностей применение цикла с постусловием оказывается удобнее, чем цикла с предусловием. Это бывает в тех случаях, когда обработку нужно заканчивать не до, а после появления концевого признака. Например, следующая функция переписывает заданную строковую константу, адресованную указателем star в заранее подготовленный символьный массив, адресованный указателем nov:
Операторы языка Си++ 121 void copy_str(char *star, char *nov) { do *nov = *star++; while (*nov++); } Еще один вариант решения той же задачи с пустым телом _цикла: do г while( *nov ++= *star++); Даже если строковая константа пустая, в ней (по определению) в конце присутствует признак *\0'- Именно его наличие проверяется после записи по адресу nov каждого очередного символа. К выражению-условию в цикле do требования те же, что и для цикла while — оно должно изменяться при итерациях либо за счет операторов тела цикла, либо при вычислениях. Пример бесконечного цикла: do; while(1); Оператор параметрического цикла for имеет формат: for (инициализатор_цикла; выражение-условие; выражение) тело_цикла Инициализатор_цикла - выражение или определение объектов одного типа. Обычно здесь определяются и инициализируются некие параметры цикла. Обратим внимание, что эти параметры должны быть только одного типа. Если в качестве инициали- затора_цикла используется не определение, а выражение, то чаще всего его операнды разделены запятыми. Все выражения, входящие в инициализатор цикла, вычисляются только один раз при входе в цикл. Инициализатор_цикла в цикле for всегда завершается точкой с запятой, т.е. отделяется этим разделителем от последующего выражения-условия, которое также завершается точкой с запятой. Даже при отсутствии инициализатора_цикла, выражения-условия и выражения в цикле for разделяющие их символы "точка с запятой" всегда присутствуют.
122 Глава 4 Выражение-условие такое же, как и в циклах while и do. Если оно равно false (нулю), то выполнение цикла прекращается. В случае отсутствия выражения-условия следующий за ним разделитель "точка с запятой" сохраняется, и предполагается, что его значение всегда истинно. Выражение (в цикле for) - часто представляет собой последовательность скалярных выражений, разделенных запятыми. Это выражение (а запятая, напомним, это операция) вычисляется на каждой итерации после выполнения операторов тела цикла и до следующей проверки выражения-условия. Для определенности можно назвать его завершающим выражением цикла for. Тело_цикла может быть блоком, отдельным оператором, составным оператором и пустым оператором. Определенные в инициализаторе цикла объекты существуют только в заголовке и в теле цикла. Если результаты выполнения цикла нужны после его окончания, то их нужно сохранять во внешних относительно цикла объектах. В следующей программе приведены три формы оператора for, в каждом из которых решается одна и та же задача суммирования квадратов первых к членов натурального ряда: //Р04_03.срр - разные формы цикла for #include <iostream> using namespace std; intmainf) { int k = 3, s=0, i-1; for (; i <= k; i++) s += i*i; // Первый цикл cout< < ="< <s« endl; s = 0; // Восстанавливаем начальное значение s for (int i = 0, k = 4; i < k;) s += ++/ * /; // Второй цикл cout«"s(4)="«s«endl; for (i = 0, s = 0, k = 5; i <= k; s += i * i, i++); // Третий цикл cout«,,s(5)=,,«s«endl; return 0; } Результаты выполнения программы: s(3)=14 s(4)=30 s(5)=55
Операторы языка Си++ 123 Все переменные в первом цикле внешние, отсутствует инициализатор цикла, в завершающем выражении заголовка изменяется параметр цикла /. После выполнения цикла результат сохраняется в переменной s. Перед вторым циклом значение s обнуляется. Инициализатор второго цикла определяет локализованные в цикле переменные /, к. (Их определение экранирует действие внешних для цикла переменных с теми же именами.) В заголовке отсутствует завершающее выражение, а параметр цикла изменяется в его теле (вне заголовка). Инициализатор третьего цикла — выражение, операнды которого разделены запятыми. В этом выражении подготавливается выполнение цикла — обнуляются значения / и s, и присваивается значение переменной к. Остальное, надеюсь, понятно. Итак, еще раз проследим последовательность выполнения цикла for. Определяются и инициируются объекты или вычисляется выражение из инициализатора_цикла. Вычисляется значение выражения-условия. Если оно равно true (отлично от нуля), выполняются операторы тела_цикла. Затем вычисляется завершающее выражение, вновь вычисляется выражение-условие и проверяется его значение. Далее цепочка действий повторяется. В Стандарте Си++ [4, с. 97] приводится следующая форма оператора, эквивалентного оператору for: { инициализатор_цикла while (выражение-условие) { операторы_тела_цикла завершающее _выражение; ) } При выполнении цикла for выражение-условие может изменяться либо при вычислении его значений, либо под действием операторов тела цикла, либо под действием завершающего выражения. Если выражение-условие не изменяется либо отсутствует, то цикл бесконечен. Следующие операторы обеспечивают бесконечное выполнение пустых операторов: for(;;); // Бесконечный цикл for(; 1;);// Бесконечный цикл
124 Глава 4 Разрешено и широко используется вложение любых циклов в любые циклы. 4.4. Операторы передачи управления К операторам передачи управления относят оператор безусловного перехода, иначе — оператор безусловной передачи управления goto, оператор возврата из функции return, оператор выхода из цикла или переключателя break и оператор перехода к следующей итерации цикла continue. Оператор безусловного перехода имеет вид: goto идентификатор; где идентификатор - имя метки оператора, расположенного в той же функции, где используется оператор безусловного перехода. Передача управления разрешена на любой помеченный оператор в теле функции. Однако существует одно важное ограничение: запрещено "перескакивать" через определения объектов, содержащие инициализацию. Это ограничение не распространяется на вложенные блоки, которые можно обходить целиком. Следующий фрагмент иллюстрирует сказанное: goto В; //Ошибочный переход, минуя определение double х = 0.0; // Определение с инициализацией goto В; //Допустимый переход, минуя блок {intn= 10; //Внутри блока определена переменная х = п *х + х; } В: cout« "\tx = " « х; Все операторы блока достижимы для перехода к ним из внешних блоков. Однако при таких переходах необходимо соблюдать то же самое правило: нельзя передавать управление в блок, обходя определение с инициализацией. Следовательно, будет ошибочным переход к операторам блока, перед которыми помещены определения с явной или неявной инициализацией. Это же требование обязательного выполнения инициализации справедливо и при внутренних переходах в блоке. Следующий фрагмент содержит обе указанные ошибки:
Операторы языка Си++ 125 {... //Внешний блок goto АВС; // Во внутренний блок, минуя определение Н {intii= 15; // Внутренний блок АВС: goto XYZ; // Обход определения СС char СС = ' XYZ: } } Принятая в настоящее время дисциплина программирования рекомендует либо вовсе отказаться от оператора goto, либо свести его применение к минимуму и строго придерживаться следующих рекомендаций: • не входить внутрь блока извне; • не входить внутрь условного оператора, т.е. не передавать управление операторам, размещенным после служебных слов if или else; • не входить извне внутрь переключателя (switch); • не передавать управление внутрь цикла. Следование перечисленным рекомендациям позволяет исключить возможные нежелательные последствия бессистемного использования оператора безусловного перехода. Полностью отказываться от оператора goto вряд ли стоит. Есть случаи, когда этот оператор обеспечивает наиболее простые и понятные решения. Один из них — это ситуация, когда в рамках текста одной функции необходимо из разных мест переходить к одному участку программы. Если по каким-либо причинам эту часть программы нельзя оформить в виде функции, то наиболее простое решение — применение безусловного перехода с помощью оператора goto. Второй случай возникает, когда нужно выйти из нескольких вложенных друг в друга циклов или переключателей. Оператор
126 Глава 4 break прерывания цикла и выхода из переключателя здесь не поможет, так как он обеспечивает выход только из самого внутреннего вложенного цикла или переключателя. Пример такого использования goto см. далее на с. 130. Оператор возврата из функции имеет вид: return выражение; или return; Выражение, если оно присутствует, может быть только скалярным. Например, следующая функция вычисляет и возвращает куб значения своего аргумента: double cubefdouble z) { return z * z* z; } Выражение в операторе return не может присутствовать в том случае, если возвращаемое функцией значение имеет тип void. Например, следующая функция выводит в стандартный выходной поток значение куба своего аргумента и не возвращает в точку вызова никакого значения: void cube_print(double z) { cout « ”cube(“«z«") = " « z *z *z; return; } В данном примере оператор возврата из функции не содержит выражения. Оператор break служит для принудительного выхода из цикла или переключателя. Определение "принудительный" подчеркивает безусловность перехода. Например, в случае цикла не проверяются и не учитываются условия дальнейшего продолжения итераций. Оператор break прекращает выполнение оператора цикла или переключателя и осуществляет передачу управления (переход) к следующему за циклом или переключателем оператору. При этом в отличие от перехода с помощью goto оператор, к которому выполняется передача управления, может быть не помечен. Оператор break нельзя использовать нигде, кроме циклов и переключателей.
Операторы языка Си++ 127 Необходимость в использовании оператора break в теле цикла возникает, когда условия продолжения итераций нужно проверять не в начале итерации (циклы for, while), не в конце итерации (цикл do), а в середине тела цикла. В этом случае тело цикла может иметь такую структуру: {операторы if (условие) break; операторы } Например, если начальные значения целых переменных /, у таковы, что / < у, то следующий цикл определяет наименьшее целое, не меньшее их среднего арифметического: while (i < j) { /++; if(i ==j) break; У-; Для / == 0, j == 3 результат / == j == 2 достигается при выходе из цикла с помощью оператора break. (Запись / == у == 2 не в тексте программы означает равенство значений переменных /,у и константы 2.) Для / == 0, у == 2 результат / == у == 1 будет получен при естественном завершении цикла. Оператор break практически незаменим в переключателях, когда с их помощью надо организовать разветвление. Например, следующая программа печатает название любой, но только одной, восьмеричной цифры: //Р04_04.срр - оператор break в переключателе #include <iostream> using namespace std; intmainf) { int ic; cout« "Enter octal digit: cin »ic; cout«ic; switch (ic) (case 0: cout« " - zero"; break;
128 Глава 4 case 1: cout«"- one"; break; case 2: cout«"- two"; break; case 3: cout«"- three"; break; case 4: cout«"- four"; break; case 5: cout« " - five"; break; case 6: cout« " - six"; break; case 7: cout« " - seven"; break; default: cout« "-It isn't octal digit!"; } cout« endI« "The End!"«endl; return 0; } Результаты выполнения программы: Enter octal digit: 4<ENTER> 4 - four The End! Программа напечатает название только одной введенной цифры и прекратит работу. Если в ней удалить операторы break, то в переключателе будут последовательно выполнены все операторы, начиная с помеченного нужным (введенным) значением. Циклы и переключатели могут быть многократно вложенными. Однако следует помнить, что оператор break позволяет выйти только из самого внутреннего цикла или переключателя. Например, в следующей программе в цикл вложен переключатель. Программа в символьном массиве (с элементами типа char) подсчитывает количество нулей (кО) и единиц (Ас1): //Р04_05.срр - break при вложении переключателя в цикл #include <iostream> ft include "cyrToDos.h" using namespace std; intmainf) { char c[] = "ABC 100111"; intkO = 0, k1 = 0; for(int i = 0; c[i] != '\0'; i++) switch (c[i]) { case 'O': k0++; break; case 'Г: k1++; break;
Операторы языка Си++ 129 ; cout« cyrToDos("B строке нулей: ") « кО; cout <,< cyrToDosf", единиц: ") « к1 « endI; return 0; } Результаты выполнения программы: В строке 2 нуля,4 единицы Оператор break в данном примере передает управление из переключателя, но не за пределы цикла. Цикл продолжается до естественного завершения. При многократном вложении циклов и переключателей оператор break не может вызвать передачу управления из самого внутреннего уровня непосредственно на самый внешний. Например, при решении задачи поиска в матрице хотя бы одного элемента с заданным значением удобнее пользоваться не оператором break, а оператором безусловной передачи управления goto. Пусть А[п][т] - двумерный массив, который нужно проверить на наличие элемента, равного х. Возможна следующая схема решения: for (int i = 0; i < n; i++) for (int j = 0; j < m; j++) {if(A[i][j] == x) goto success; } //Конец цикла //Действия при отсутствии элемента в матрице success: cout« "Элементхнайден."; В качестве примера, когда при вложении циклов целесообразно применение оператора break, рассмотрим задачу вычисления произведений элементов строк матрицы. Вычисление произведения элементов можно прервать, если один из сомножителей окажется равным 0. Возможный вариант реализации может быть таким: for (i = 0; i < n; i++) // Перебор строк матрицы // Перебор элементов строки: 9-2762
130 Глава 4 for(j = О, p[i] = 1; j < m; j++) if(A[i][j] == 0.0) // Обнаружен нулевой элемент { РШ = 0.0; break; } else p[i] *= A[i][j]; При появлении в строке нулевого элемента оператор break прерывает выполнение только внутреннего цикла, однако внешний цикл перебора строк всегда выполнится для всех значений / от 0 до п - 1. Оператор continue употребляется только в операторах цикла. С его помощью завершается текущая итерация и начинается проверка условия дальнейшего продолжения цикла, т.е. условий начала следующей итерации. Для объяснений действия оператора continue в Стандарте [4] рекомендуется рассматривать операторы цикла в следующем виде: while (too) { do { for(;foo;) { contin:; contin:; contin:; } } while (foo); } В каждой из форм многоточием обозначены операторы тела цикла. Вслед за ними размещен пустой оператор с меткой contin. Если среди операторов тела цикла есть оператор continue и он выполняется, то его действие эквивалентно оператору безусловного перехода на метку contin. Пример использования оператора continue: подсчитать среднее значение только положительных элементов одномерного массива х[п]\ for(s = 0.0, к = 0, /' = 0; / < п; i++) { if(x[i] <= 0.0) continue; /C++; // Количество положительных элементов s += x[i]; // Сумма положительных элементов } if (к > 0) s = s/k; // Среднее значение
131 Глава 5 АДРЕСА, УКАЗАТЕЛИ, МАССИВЫ 5.1. Указатели и адреса объектов Специальными объектами в программах на языках Си и Си++ являются указатели. О них уже говорилось в связи с операциями new и delete для динамического управления памятью (разд. 3.2). Различают указатели-переменные (именно их мы будем называть указателями) и указатели-константы. Значениями указателей служат адреса участков памяти, выделенных для объектов конкретных типов. Поэтому в определении и описании указателя всегда присутствует обозначение соответствующего ему типа. Эта информация позволяет в последующем с помощью указателя получить доступ ко всему сохраняемому объекту. Указатели делятся на две категории — указатели на объекты и указатели на функции. Выделение этих двух категорий связано с отличиями в свойствах и правилах использования. Например, указатели функций не допускают применения к ним арифметических операций, а указатели объектов разрешено использовать в некоторых арифметических выражениях. Начнем с указателей объектов (на объекты). В простейшем случае определение и описание указателя-переменной на некоторый объект имеют вид: type *имяуказателя; где type - обозначение типа; имя_указателя - это идентификатор; * - унарная операция раскрытия ссылки (операция разыменования; операция обращения по адресу; операция доступа по адресу), операндом которой должен быть указатель (именно в соответствии с этим правилом вслед за ней следует имя_указате- ля). В совокупности имя типа и символ * перед именем воспринимаются как обозначение особого типа данных "указатель на объект данного типа". 9*
132 Глава 5 Признаком указателя при лексическом разборе определения или описания служит символ *, помещенный перед именем. Таким образом, при необходимости определить несколько указателей на объекты одного и того же типа этот символ * помещают перед каждым именем. Например, определение int *Нр, *i2p, *i3p, i; вводит три указателя на объекты целого типа Ир, /2р, /Зр и одну переменную / целого типа. При определении указателя в большинстве случаев целесообразно выполнить его инициализацию. Формат определения станет таким: type *имя_указателя инициализатор; Как упоминалось, инициализатор имеет две формы записи, поэтому допустимы следующие две формы определения указателей: type *имя_указателя = инициализирующее_выражение; type *имя_указателя (инициализирующее_выражение); В качестве инициализирующего_выражения должно использоваться константное выражение, частными случаями которого являются: • явно заданный адрес участка памяти; • указатель, уже имеющий значение; • выражение, позволяющее получить адрес существующего объекта с помощью операции &. Если значение константного выражения равно нулю, то это нулевое значение преобразуется к пустому (иначе нулевому) указателю. Синтаксис языка "гарантирует, что этот указатель отличен от указателя на любой объект" [2]. Кроме того, внутреннее (битовое) представление пустого указателя может отличаться от битового представления целого значения 0. Примеры: charcc = 'сГ; char *рс = &сс; char *ptr(0); char *р; // Символьная переменная (типа char) // Инициализированный указатель на объект // типа char //Нулевой указатель на объект типа char // Неинициализированный указатель на // объект типа char
Адреса, указатели, массивы 133 Переменная сс инициализирована значением символьной константы 'сГ. После определения (с инициализацией) указателя рс доступ к значению переменной сс возможен как с помощью ее имени, так и с помощью адреса, являющегося значением указателя-переменной рс. В последнем случае должна применяться операция разыменования * (доступ к значению через указатель). Таким образом, при выполнении оператора cout« "\п сс равно "« сс«" и *рс = "<< *рс; будет выведено: сс равно d и *рс = d Указатели ptr и р, определенные в нашем примере, пользуются различными "правами". Указатель ptr получил нулевое начальное значение (пустой указатель), и попытка его разыменования будет бесперспективной. Не нужно надеяться, что пустой указатель связан с участком памяти, имеющим нулевой адрес или хранящим нулевое значение. Синтаксис языка Си++ этого не гарантирует. Однако, присвоив затем ptr значение адреса уже существующего объекта, можно затем осмысленно применять операцию разыменования. Например, любой из операторов присваивания ptr = &сс; или ptr = рс; свяжет ptr с участком памяти, выделенным для переменной сс, т.е. после их выполнения значением * ptr будет 'сТ. Присвоив указателю адрес конкретного участка памяти, можно, используя операцию разыменования, не только получать, но и изменять содержимое этого участка памяти. Например, оператор присваивания: *рс = или пара операторов ptr = рс; *ptr='+'; сделают значением переменной сс символ ’+'. Унарное выражение *указатель обладает в некотором смысле правами имени переменной, т.е. *рс и *ptr служат
синонима134 Глава 5 ми (псевдонимами, другими именами) имени сс. Выражение Указатель может использоваться практически везде, где допустимо использование имен объектов того типа, к которому относится указатель. Однако это утверждение справедливо лишь в том случае, если указатель уже имеет ненулевое и ^конкретное значение. В нашем примере не инициализирован указатель р. Поэтому попытки использовать выражение *р в левой части оператора присваивания или в операторе ввода неправомерны. Значение указателя р неизвестно, а результат занесения значения в неопределенный участок памяти непредсказуем и иногда может привести к аварийному событию. Пример: *р = // Ошибочное применение неинициализированного р Если присвоить указателю адрес конкретного объекта (р = &сс\) или значение уже инициализированного указателя (р = рс;), то это превратит *р в синоним (псевдоним) уже имеющегося имени объекта. Чтобы связать неинициализированный указатель с новым участком памяти, еще не занятым никаким объектом программы, используется операция new: р = new char; // Выделили память для переменной типа char // и связали указатель р с этим участком памяти После этого оператора можно использовать *р для записи в память нужных символьных значений. Например, станут допустимы операторы: *р = или с/п >> *р; При определении указателя, как сам он, так и его значение могут быть объявлены константами. Для этого используется квалификатор const: type const * const имя_указателя инициализатор; const - это необязательные элементы определения. Ближайший к имени указателя const относится собственно к указателю, а const перед символом * определяет "константность" начального значения, связанного с указателем. Мнемоника очевидна, так как выражение * имя указателя есть обращение к содержимому соответствующего указателю участка памяти. Таким образом,
опАдреса, указатели, массивы 135 ределение неизменяемого (константного) указателя имеет следующий формат: type * const имя_указателя инициализатор; Так как значение указателя-константы изменить невозможно, то имя указателя-константы можно считать наименованием конкретного фиксированного адреса участка основной памяти. Содержимое этого участка памяти с помощью разыменования указателя-константы в общем случае доступно как для чтения, так и для изменений. Попытку изменить значение самого указателя-константы не допустит компилятор и выдаст, например, такое сообщение об ошибке: Error...: Cannot modify a const object Формат определения указателя на константу: type const * имя_указателя инициализатор; Например, введем указатель на константу целого типа со значением 0: constintzero = 0; //Определение константы int const * point_to_const = & zero; // Указатель на константу О Операторы вида *pointJojconst = 1; cin » *pointJojconst; недопустимы, так как каждый из них — это попытка изменить значение константы 0. Однако операторы pointJojconst = &СС; pointJojconst = 0; вполне допустимы. Они разрывают связь указателя point Jojconst с константой 0, однако не меняют значения этой константы, т.е. не изменяют ее код в фиксированном участке памяти. Можно определить неизменяемый (постоянный) указатель на константу. Например, иногда полезен так определенный указатель-константа на константное значение:
136 Глава 5 const double pi = 3.141593; double const *const pointpi = & pi; Невозможно изменить значение константы, обращаясь к ней с помощью выражения *pointpi. Нельзя изменить и значение указателя pointpi, т.е. он всегда "смстрит" на константу 3.141593. Работая с указателями, постоянно используют операцию & — получение адреса объекта. Самое общее правило: операндом для операции & может быть леводопустимое выражение или квали- цированное именование. Таким образом, для этой операции существуют естественные ограничения: • нельзя определять адрес неименованной константы, т.е. недопустимы выражения <53.141593 или <£7Г; • нельзя определять адрес значения, получаемого при вычислении скалярных выражений, т.е. недопустимы конструкции &(44 *x-z) или & (а + Ь) != 12 • нельзя определить адрес переменной, относящейся к классу памяти register. Следовательно, ошибочной будет последовательность операторов: int register Numb = 1; int *prt_Numb = & Numb; Обобщая сказанное, можно сделать вывод, что операция & применима к объектам, имеющим имя и размещенным в памяти. Ее нельзя применять к выражениям, неименованным константам, битовым полям структур и объединений, к регистровым переменным и внешним объектам (файлам). Однако допустимо получать адрес именованной константы, т.е. правомерна, например, такая последовательность определений: const double Euler = 2.718282; double *pEuler = (double *) & Euler; (Обратите внимание на необходимость явного приведения типов, так как &Euler имеет тип const double *, а не double *.)
Адреса, указатели, массивы 137 5.2. Адресная арифметика, типы указателей и операции над ними Во многих языках, предшествовавших языкам Си и Си++, например в ПЛ/1, указатель относился к самостоятельному типу указателей, который не зависел от существования в языке других типов. В языках Си и Си++ каждый указатель связан с некоторым типом данных. В качестве типа при определении указателя может быть использован как базовый тип, так и производный. В языке Си++ производных типов может быть бесконечно много, однако правила их конструирования из более простых (а в конечном итоге из базовых) типов точно определены. К производным типам (как мы уже рассматривали) отнесены массивы, функции, указатели, ссылки, константы, классы, структуры, объединения и, наконец, определенные пользователем типы. Начнем с указателей, относящихся к базовым типам, массивам, указателям, ссылкам и константам. Базовые типы определяются служебными словами: char, wchar_t, int, float, long, double, short, unsigned, signed, void. Примеры указателей, относящихся к базовым типам char, int и double, уже рассматривались. Вот несколько других определений: long double Id = 0.0; long double *ldptr = & Id; void *vptr; unsigned char *cucptr; unsigned long int *uliptr = 0; // Id - переменная // Idptr - указатель // vptr - указатель типа void * // cucptr - указатель без значения // uliptr - указатель... Если операция & получения адреса объекта всегда дает однозначный результат, который зависит от размещения объекта в памяти, то операция разыменования, т. е. значение выражения *указатель зависит не только от значения указателя, но и от его типа. Дело в том, что при доступе к памяти с помощью разыменования указателя требуется информация не только о размещении, но и о размерах участка памяти, который будет использоваться. Эту дополнительную информацию компилятор получает из типа указателя. Указатель char *ср; при обращении к памяти "работает" с участком в 1 байт. Указатель long double *ldp\ будет
"доста138 Глава 5 вать" данные из sizeof (long double *) смежных байт памяти и т.д. Иллюстрирует сказанное следующая программа, где указателям разных типов присваиваются значения адреса одного участка памяти: //Р05_01.срр - указатели разных типов #include <iostream> using namespace std; intmainf) { unsigned int I = 0x12345678; char *cp = (char *)&l; int *ip = (int *)&!; cout« "sizeof *cp = " << sizeof *cp « endl; cout« "sizeof *ip = ” << sizeof *ip « endl; cout« hex; // Шестнадцатеричное представление // выводимых значений cout« ’The address of\’l\’(&l) = ” << &l« endl; cout« "ip = "« (void *)ip « "\t*lp = Ox"« *ip « endl; for(; cp < (char *)ip +4; cp++) { cout« "cp = " «(void *)cp « "\t*cp = Ox"«(int)*cp « endl; } return 0; } Результаты выполнения программы (зависящие от реализации): sizeof *ср = 1 sizeof *ip = 4 The address of T(&l) = 0xa95a4 ip = 0xa95a4 *ip = 0x12345678 cp = 0xa95a4 *cp = 0x78 cp = 0xa95a5 *cp = 0x56 cp = 0xa95a6 *cp = 0x34 cp = 0xa95a7 *cp = 0x12 В программе определены переменная unsigned int I и два указателя разных типов. Значения указателей совпадают — им присвоен адрес переменной I. Так как адрес &I имеет тип
Адреса, указатели, массивы 139 unsigned int *, то при инициализации указателей его значение явно преобразуется соответственно к типам char * и int *. С по- мощыр операции sizeof определены и затем выведены значения размеров участков памяти, которые адресуются соответствующими указателями. Затем выведен адрес переменной I, совпадающее с этим адресом значение указателя ip и результат разыменования этого указателя (*ip). Далее в цикле изменяется указатель ср и выводятся его значения и результаты его разыменования (*ic). В этом цикле указатель ср последовательно адресует байты участка памяти, выделенной для переменной unsigned int I. Результат каждого разыменования *ср — значение соответствующего байта. При выводе значений указателей они преобразуются к типу void *. Кроме того, выходной поток “настроен” на вывод чисел в шестнадцатеричном виде. Для этого использован новый для нашего изложения элемент - манипулятор hex форматирования выводимого значения. Этот манипулятор обеспечивает вывод в выходной поток cout числовых значений в шестнадцатеричной системе счисления. Подробнее о форматировании вводимых и выводимых данных будем говорить при описании потоков ввода-вывода (гл. 19). При выводе значения *ср использовано явное преобразование типа (inf), так как при его отсутствии будет выведен не код байта, а соответствующий ему символ ASCII-кода. Еще один неочевидный результат выполнения программы связан с аппаратными особенностями компьютеров на базе процессоров фирмы Intel — размещение числовых кодов в памяти, начиная с младшего адреса. За счет этого пары младших разрядов шестнадцатеричного числового кода размещаются в байтах памяти с меньшими адресами. Именно поэтому начальное значение *ср равно 0x78, а не 0x12. Сказанное иллюстрирует рис. 5.1. Обратите внимание, что начальные значения указателей разных типов (ip и ср) в примере совпадают, а количество байтов, “извлекаемых” из памяти при разыменовании указателя, зависит от его типа. Явное преобразование типов при работе в одном выражении с указателями разных типов необходимо для всех указателей, кроме тех, которые имеют тип void *. При использовании указателя типа void * операция преобразования типов применяется по
140 Глава 5 &1=0ха95а4 Адреса байтов: 5a4 5a5 5a6 5a7 7 8 5 6 3 4 1 2 < <CP » **p . Рис. 5.1. Схема размещения в памяти переменной типа unsigned int (младшие разряды числа в байте с большим адресом) умолчанию. Напомним, что в отличие от других типов тип void предполагает отсутствие значения. Указатель типа void * отличается от других указателей отсутствием сведений о размере соответствующего ему участка памяти. Указатель типа void * как бы создан "на все случаи жизни", но, как всякая абстракция, ни к чему не может быть применен без конкретизации, которая в данном случае заключается в приведении типа. Возможности "связывания" указателя void * с объектами разных типов иллюстрирует следующая программа: //Р05_02.срр - Приведение типа void * к базовым типам #include <iostream> using namespace std; #include "cyrToDos.h" intmainf) { void *vp; int i = 77; double Euler = 2.718282; cout« cyrToDos("Начальное значение vp = ") « vp « endl; vp = &i; // "Настроились"на int cout« "vp = "« vp « "\t*(int *)vp = "« *(int *)vp « endl; vp = &Euler; // "Настроились" на double cout« "vp = " << vp « "\t*(double *)vp = " << Ydouble *)vp « endl; return 0;
Адреса, указатели, массивы 141 Результаты выполнения программы: Начальное значение vp = ОхббсЬО vp = ОхббсЭс *(int *)vp = 77 vp = 0хббс94 *( double *)vp = 2.71828 Во время преобразования типов нулевое арифметическое значение преобразуется к нулевому указателю, который иногда называется пустым указателем (null pointer). В компиляторах (но не в Стандарте) это значение указателя обозначается именем NULL. Синтаксис языка гарантирует, что этот указатель не адресует никакой объект. Однако синтаксис не гарантирует, что внутреннее представление значения пустого указателя будет совпадать с кодом целого числа 0. Разрешено неявное (умалчиваемое) преобразование любого неконстантного и не имеющего модификатора volatile указателя к указателю типа void *. Никакие другие преобразования типов указателей по умолчанию не выполняются. Например, в предыдущей программе неверной будет такая последовательность операторов: void *vp; int *ip; ip = vp; // Ошибка Транслятор сразу же выводит приблизительно такое сообщение об ошибке: Error...: Cannot convert 'void *' to 'int *' Запрет на преобразование типа void * к другим типам объясняется недопустимостью ситуации, когда к одному и тому же объекту будет доступ с помощью указателей разных типов. Например, в той же программе Р05_02.срр последовательность операторов vp = & Euler; int *ip; ip = vp; // Ошибка позволила бы обращаться к значению типа double (2.718282) с помощью */р. Приблизительно по тем же причинам не все будет допустимо в следующих операторах: Int I; Int *ip = 0; void *vp; char *cp;
142 Глава 5 vp = i? vp : cp; //Допустимый оператор vp = ip ? ip : cp; // Ошибка в выражении: операнды должны // иметь одинаковый тип В первом из операторов присваивания выполняется неявное преобразование значения ср к типу void *, и никаких ошибок не возникает. Во втором операторе присваивания выражение содержит указатели ip, ср разных типов, которые не могут быть неявно преобразованы к одному типу. Операции над указателями можно сгруппировать таким образом: • операция разыменования или доступа по адресу (*); • преобразование типов (приведение типов); • присваивание; • получение (взятие) адреса (<5); • сложение и вычитание (аддитивные операции); • инкремент или автоувеличение (++); • декремент или автоуменьшение (--); • операции отношений (операции сравнения). Разыменование, приведение типов, присваивание мы уже рассмотрели и иллюстрировали примерами. О получении адреса указателя можно сказать очень кратко: указатель есть объект и как объект имеет адрес соответствующего ему участка памяти. Значение этого адреса доступно с помощью операции &, применяемой к указателю: unsigned int *uip1 = 0, **uip2; uip2 =&uip1; Здесь определены два указателя, первый из которых uip1 получает нулевое значение при инициализации, а второму uip2 в качестве значения присваивается адрес указателя uip1. Начинать изучение аддитивных операций удобнее с вычитания. Вычитание применимо к указателям на объекты одного типа и к указателю и целой константе. Вычитая два указателя одного типа, можно определять "расстояние" между двумя участками памяти. "Расстояние" определяется в единицах, кратных длине (в байтах) объекта того типа, к которому отнесен указатель. Таким образом, разность указателей, адресующих два смежных объекта любого типа, по абсолютной величине всегда равна 1. Сказанное иллюстрирует следующая программа:
Адреса, указатели, массивы 143 //Р05_03.срр - Вычитание указателей #include <iostream> #include "cyrToDos.h" using namespace std; int main() { char ас = T, be = '2 char *pac = &ac, *pbc = & be; long int al = 3, bl = 4; long int *pal = & al, *pbl = & bl; cout« cyrToDosf"Значения и разности указателей:”) « end!; cout« "рас = " << (void *)pac « "\tpbc = "«(void *)pbc « endl; cout« 7рас - pbc) = "<< рас - pbc « endl; cout« "pal ="« pal« "\tpbl = " << pbl« endl; cout« "(pbl - pal) = " «(pbl - pal) << endl; cout« cyrToDosf "Разности числовых значений указателей:") << endl; cout << "(int)pac - (int)pbc = " << (int)pac - (int)pbc << endl; cout << "(int)pbl - (int)pal ="« (int)pbl - (int)pal << endl; return 0; } Результаты выполнения программы (зависят от реализации): Значения и разности указателей: рас = 0х670аЗрЬс = 0х670а2 (рас - pbc) = 1 pal = 0x67094 pbl = 0x67090 (pbl - pal) = -1 Разности числовых значений указателей: (int)pac - (int)pbc = 1 (int)pbl - (int)pal = -4 Анализируя результаты, нужно обратить внимание на два важных факта. Первый относится собственно к языку Си++ (или к Си). Он подтверждает различие между разностью однотипных указателей и разностью числовых значений этих указателей. Хотя (int)pac - (int)pbc равно 1, а (int)pbl - (int)pal равно -4, разности соответствующих указателей в обоих случаях по абсолютной величине равны 1. Второй факт относится не к самому языку
144 Глава 5 Си++, а к реализации. Те переменные, определения которых помещены в программе рядом, размещаются в смежных участках памяти. Это (см. Р05_03.срр) видно из значений связанных с ними указателей (адресов). Однако совершенно неочевиден тот факт, что переменная, определенная в тексте программы позже, имеет меньший адрес, чем предшествующие ей в тексте программы объекты. Именно поэтому разности рас - pbc и (int)pac - (int)pbc равны I, а разности pbl - pal и (int)pbl - (int)pal отрицательны. "Обратный" порядок размещения объектов в памяти объясняется особенностями работы компилятора. При разборе текста программы компилятор последовательно распознает и помещает в стек имена всех объектов, для которых нужно выделить место в памяти. Затем, после окончания лексического анализа, на этапе распределения памяти имена объектов выбираются из стека, и им отводятся смежные последовательно размещенные участки памяти. Так как порядок записи в стек обратен порядку чтения (выбора) из стека, то размещение объектов в памяти оказывается обратным по сравнению с их взаимным расположением в определениях текста программы. Еще один пример (с результатами для компилятора DJGPP) иллюстрирует правила вычитания указателей и их отличия от вычитания численных значений адресов. //Р05_04.срр - Вычитание адресов и указателей разных типов #include <iostream> #include "cyrToDos. h " using namespace std; intmainf) { double aa = 0.0, bb = 1.0; double *pda = &aa, *pdb = & bb; int *pia = (int *)&aa, *pib = (int *)&bb; char *pca = (char *)&aa, *pcb = (char *)&bb; cout« cyrToDosf "Адреса объектов: & aa = ") « &aa « "\t&bb = "« &bb « endl; cout« cyrToDosf "Разность адресов: (&bb - & aa) = ") « (& bb - & aa) « endl; cout« cyrToDosf "Разность числовых значений адресов: ") « "((int)&bb - (int)&aa) = ”
Адреса, указатели, массивы 145 << ((int)Abb - (int)&aa) « endl; cout« cyrToDos("Разности указателей:") « endl; cout« "double *: (pdb - pda)="«(pdb - pda) « endl; cout« ".int *: (pib - pia)="«(pib - pia) « endl; cout« "char *: (pcb - pea)-"«(peb - pea) « endl; return 0; } Результаты выполнения программы (зависят от реализации): Адреса объектов: &аа = 0x6709с &bb = 0x67094 Разность адресов: (&bb - &аа) = -1 Разность числовых значений адресов: ((int)&bb ~(int)&aa) = -8 Разности указателей: double *: (pdb - pda )= -1 int *: (pib - pia) = -2 char *: (pcb - pea) = -8 Из результатов видно, что определенные последовательно объекты аа и bb, имея тип double, размещаются в памяти рядом на "расстоянии" 8 байт. Однако разность адресов равна 1. Это подтверждает тот факт, что значение, получаемое с помощью операции &имя_объекта, имеет права указателя того типа, к которому принадлежит объект. Остальные результаты очевидны — разности указателей вычисляются в единицах, кратных длине участка памяти для соответствующего типа данных. Важно отметить, что разности указателей зависят от реализации и настроек компилятора. Для уменьшения этой зависимости Стандарт для разности указателей ввел специальный тип ptrdiff_t, определенный в заголовке <cstddef>. Добавив в программу этот заголовок, разность указателей следует определить так: int(ptrdiff_t(&bb-&aa)). Явное приведение к типу int не обязательно, но позволяет устранить предупреждения компилятора об опасном преобразовании. Из указателя можно вычитать целочисленные значения. При этом числовое значение указателя уменьшается на величину k * slzeofftype) где к - "вычитаемое", type - тип объекта, к которому отнесен указатель. ,0-2762
146 Глава 5 Аналогично выполняется и операция сложения указателя с целочисленным значением. (Отметим, что суммировать два указателя запрещено синтаксисом языка Си++. Таким образом, операция сложения по сравнению с операцией вычитания еще беднее для указателей.) Следующая программа иллюстрирует особенности увеличения указателей на целую величину: //Р05_05.срр - Увеличение указателя #include <iostream> using namespace std; intmain() { double zero = 0.0, pi = 3.141593, Euler = 2.718282; double *ptr = & Euler; cout« "ptr = "« ptr « "\t\fptr = "« *ptr « endl; cout« "(ptr + 1) = "«(ptr + 1) « "\t*(ptr + 1) = "« *(ptr+1) « endl; cout« "(ptr + 2) = "«(ptr + 2) « " \t*(ptr + 2) = "« *(ptr + 2) « endl; return 0; } Результаты выполнения программы (зависят от реализации): ptr = Охббсвс *ptr = 2.71828 (ptr + 1) = 0х66с94 *(ptr + 1) = 3.14159 (ptr + 2) = ОхббсЭс *(ptr + 2) = О Как видно из результата, изменяя значение указателя, можно перемещаться по участкам памяти и получать доступ к разным объектам. Однако при этом не следует полагаться на то, что объекты всегда будут размещаться в памяти подряд в соответствии с положением их определений в тексте программы. Синтаксис языка Си++ этого не гарантирует, и все зависит от реализации. Полную уверенность в последовательном размещении объектов дает только их объединение в массивы. Декремент (автоуменьшение) указателей (унарная операция --) и инкремент (автоувеличение) указателей (унарная операция ++) не имеют никаких новых особенностей. Как и вычитание единичной константы, операция — изменяет конкретное численное значение указателя типа type на величину
Адреса, указатели, массивы 147 sizeof(type), где type * - тип указателя. Тем самым указатель "перемещается" к соседнему объекту с меньшим адресом. Аналогично и действие операции единичного приращения ++. В зависимости от положения (до операнда-указателя или после него) выполнение унарных операций ++ и — осуществляется либо до, либо после использования значения указателя. Но это обычное свойство инкрементных и декрементных операций, которое не связано с особенностями указателей. В ряде нестандартных случаев при работе с указателями нужно "смещать" их значения на величины, не кратные размеру участка памяти, соответствующего их типу. Непосредственное изменение значения указателя на целую величину такой возможности не дает. Вместо этого можно использовать выражения, операндами которых являются численные значения адресов. Покажем это на примере: //Р05_06.срр - Изменение указателя на произвольную величину #include <iostream> #include "cyrToDos.h" using namespace std; int main() { long double L1 =9.10Q2e-2Q; // Масса покоя электрона (г), int ii = 6; char ch = '& double d = 299792.5; // Скорость света (км/с). long double L2- 1.0011596; // Аномальный момент электрона. cout« cyrToDosCHe кратные для long double адреса:") « endl; cout« "&L1 = ” « &L1 « "\t&L2 = " « &L2 « endl; long dis;// "Расстояние"в памяти между L2 и LI dis = long(&L2) - long(&L1); cout« cyrToDosf "Разность числовых значений адресов: ") « dis « endl; long double *pI = & L1; //Явно "переместим"указатель: pi = (long double *)(long(pl) + dis); cout« cyrToDos( "Измененный указатель: ") « endl; cout« "pi = "« pi« " *pl = "« *pl; return 0; } 10*
148 Глава 5 Результаты выполнения программы (зависят от реализации): Не кратные для long double адреса: &L1 = OxbOeOQ &L2 = OxbOdeQ Разность числовых значений адресов: -32 Измененный указатель: pl = 0xb0de8 *pl= 1.00116 Длина переменной типа long double 12 байт. Переменные L1 и L2, имея шестнадцатеричные адреса ...е08 и ...de8, отстоят в памяти друг от друга на 32 (десятичное число) байта (вычисляется как значение переменной dis). Таким образом, "расстояние” между L1 и L2 не кратно длине переменной типа long double. При определении указатель pi "настроен" на переменную L1. Добавив к его целочисленному значению значение переменной dis и выполнив приведение типов к long double, получаем адрес L2. Обладая правами объекта (как именованного участка памяти), указатель имеет адрес, длину и значение. Следующая программа печатает значения адресов и длин некоторых типов указателей: //Р05_07.срр - Адреса и длины указателей разных типов #include <iostream> #include "cyrToDos.h" using namespace std; intmain() { char *pac, *pbc; long *palf *pbl; cout« cyrToDosf "Адреса указателей:") « endl; cout« "&pac = " << & рас « ”\t&pbc = "<< &pbc « endl; cout« "&pal = " << & pal« ”\t&pbl - "<< & pbl« endl; cout« cyrToDosf "Длины указателей некоторых типов:") « endl; cout« "sizeoffvoid *) = "<< sizeoffvoid *) « endl; cout« "sizeoffchar *; = "<< sizeoffchar *) « endl; cout« ".sizeoffwcharj *) = ” << sizeof(wchar_t *) « endl; cout« "sizeofflong *) = "« sizeofflong *) « endl; cout« "sizeoffdouble *) = " << sizeoffdouble *) « endl; return 0; }
Адреса, указатели, массивы 149 Результаты выполнения программы (зависят от реализации): Адреса указателей: &рас = 0хб70а0 &pbc = 0x6709с &pal = 0x67098 &рЫ = 0x67094 Длины указателей некоторых типов: sizeoff void *) =4 sizeoff char *) = 4 sizeoff wcharj *)= 4 sizeoff long *) = 4 sizeoff double *) = 4 Раз указатель - это объект в памяти, то можно определять указатель на указатель и т. д. сколько нужно раз. Например, в следующей программе определены такие указатели и с их помощью выполнен доступ к значению переменной: //P05J0Q.cpp - цепочка указателей на указатели #include <iostream> using namespace std; intmainf) { int i = 88; int *pi = &i; int **ppi = &pi; int ***pppi = &ppi; cout« "***pppi = "« ***pppi« end!; return 0; } Результат выполнения программы: ***pppi = 88 В выражении ***pppi выполняется обращение к участку памяти с адресом ррр/, затем к участку с адресом (*pppi) == рр/, затем к (*рр/) == pi, затем к (*р/) == /. С помощью скобок последовательность разыменований можно пояснить таким выражением: *(*(*рррГ>). Работая с адресами и указателями, нужно внимательно относиться к ассоциативности и рангам операций *, ++, &, так как они в выражениях могут употребляться в самых разнообразных
150 Глава 5 сочетаниях. Программировать в таком стиле не стоит, однако нужно уметь понимать смысл запутанных выражений с адресами и указателями. Кроме того, используя в одном выражении перечисленные унарные операции, следует не забывать требования к операндам операций ++, Если пате - имя некоторого объекта, то будут недопустимы такие записи: ++&пате; //Ошибка, требуется 1-выражение --&пате++; //Ошибка, требуется 1-выражение Смысл ошибки очевиден, ведь адрес участка памяти не есть леводопустимое выражение, адрес — это константа, и его нельзя изменять. 5.3. Массивы и указатели В предыдущих главах уже введены и проиллюстрированы некоторые понятия, относящиеся к массивам. Например, продемонстрирована инициализация массива типа char[ ] значением строковой константы: char имя_массива[] = ипоследовательность_символов”; (Напомним, что количество элементов в таком символьном массиве на 1 больше, чем количество явно присутствующих символов в строковой константе, использованной для инициализации. Последний элемент массива в этом случае всегда равен 'ХО'.) Несколько раз показано на примерах обращение к элементам массива с помощью индексирования. Отмечались роль разделителей [ ] (при описании и определении массивов) и существование в языке Си++ операции [ ]. С помощью этой операции обеспечивается доступ к элементу массива по имени массива и индексу — целочисленному смещению от начала: имя_ма ссива[ индекс] Теперь необходимо тщательно разобрать соотношение между массивами и указателями. Самое загадочное в массивах языков Си и Си++ — это их различное "поведение" на этапах определения и использования. При определении массива ему выделяется память так же, как массивам других алгоритмических языков (например, ПЛ/1 или
ПасАдреса, указатели, массивы 151 каль). Но как только память для массива выделена, имя массива воспринимается как константный указатель того типа, к которому отнесены элементы массива. Существуют исключения, например применение имени массива в операции sizeof. В этой операции массив "вспоминает" о своем отличии от обычного указателя, и результатом является размер в байтах участка памяти, выделенного не для указателя, а для массива в целом. В остальных случаях значением имени массива является адрес первого элемента массива, и это значение невозможно изменить. Таким образом, для любого массива соблюдается равенство: имя_массива == &имя_массива == &имя_массива[0] Итак, массив — это один из агрегатных типов языка Си++. Массив отличается тем, что все его элементы имеют один и тот же тип и что элементы массива расположены в памяти подряд. Определение одномерного массива типа type: type имя_массива [константное_выражение]; Здесь имя_массива - идентификатор; константное _выра- жение, если оно присутствует, определяет размер массива, т.е. количество элементов в массиве. В некоторых случаях допустимо описание массива без указания количества его элементов, т.е. без константного выражения в квадратных скобках. Например: extern unsigned long UL[ ]; это описание внешнего массива, который определен в другой части программы, где ему выделена память и присвоены значения его элементам. При определении массива может выполняться его инициализация, т.е. элементы массива получают конкретные значения. Инициализация выполняется по умолчанию (без вмешательства программиста), если массив статический или внешний. В этих случаях всем элементам массива компилятор автоматически присваивает нулевые значения: void Ц) {static double F[4]; // Внутренний статический массив long double А[ 10]; // Массив автоматической памяти }
152 Глава 5 int main() { extern int D[ ]; // Описание массива } int D[8];// Внешний массив (определение) Массивы D[8] и F[4] инициализированы нулевыми значениями. В основной программе /т?а/п( ) массив D описан без указания количества его элементов. Массив Л[10] не получает конкретных значений своих элементов при определении. Явная инициализация элементов массива разрешена только при его определении и возможна двумя способами: либо с указанием размера массива в квадратных скобках, либо без явного указания (без конкретного выражения) в квадратных скобках: char СН[] = { 'А', Ъ\ 'С, 'D//Массив из 4 элементов int IN[6] = { 10, 20, 30, 40 ); // Массив из 6 элементов char STR[] = "ABCD”; // Массив из 5 элементов Количество элементов массива СН компилятор определяет по числу начальных значений в списке инициализации, помещенном в фигурных скобках при определении массива. В массиве IN шесть элементов, но только первые четыре из них явно получают начальные значения. Элементы /Л/[4], /Л/[5] либо не определены, либо имеют нулевые значения, когда массив внешний или статический. Массив STR инициализирован строковой константой, элемент STR[4] равен '\0\ а всего в этом массиве 5 элементов. При отсутствии константного выражения в квадратных скобках список начальных значений в определении массива обязателен. Если размер массива явно задан, то количество элементов в списке начальных значений не должно превышать размера массива. Ошибочные определения: double А[ ]; // Ошибка в определении массива - нет размера double В[4] = { 1, 2, 3, 4, 5, 6 };// Ошибка инициализации В тех случаях, когда массив не определяется, а описывается, список начальных значений задавать нельзя. В описании массива может отсутствовать и его размер: extern double Е[ ]; // Правильное описание внешнего массива
Адреса, указатели, массивы 153 Предполагается, что в месте определения массива Е для него выделена память и выполнена инициализация. Описание массива (без указания размера и без списка начальных значений) может использоваться в списке параметров определения функции и в спецификации параметров прототипа функции. Примеры: double MULTYfdouble G[], double F[]) //Определение функции MULTY {... операторы_тела_функции...} void print_array(int l[ ]); // Прототип функции print_array Доступ к элементам массива с помощью индексирования мы уже несколько раз демонстрировали на примерах. Приведем еще один, но предварительно обратим внимание на полезный прием, позволяющий контролировать диапазон изменения индекса массива при его "просмотре", например в цикле. С помощью операции з'1геоЦимя_массива) можно определить размер массива в байтах, т.е. размеры участка памяти, выделенного для массива. Так как все элементы массива имеют одинаковые размеры, то частное sizeoffимя_массива)/з1гео%имя_массива[0]) определяет количество элементов в массиве. Следующий фрагмент программы печатает значения всех элементов массива: intm[] = { 10,20, 30,40}; for (int i = 0; i < sizeof(m)/sizeof(m[0]); i++) cout« "m[ « i« "] = "« m[i] « " Результат выполнения: m[0] = 10 m[1] = 20 m[2] = 30 m[3] = 40 Еще раз отметим, что для первого элемента массива индекс равен 0. Цикл завершается при достижении i значения 4. По определению, имя массива является указателем-константой, значением которой служит адрес первого элемента массива (с индексом 0). Таким образом, в нашем примере &т == т. Раз имя массива есть указатель, то к нему применимы все правила адресной арифметики, связанной с указателями. Более того, запись имя_массива[индекс] является выражением с двумя операндами. Первый из них, т.е. имя_массива, - это константный указатель - адрес начала мае-
154 Глава 5 сива в основной памяти. Индекс - это выражение целого типа, определяющее смещение от начала массива. Используя операцию обращения по адресу * (раскрытие ссылки, разыменование), действие бинарной операции [ ] можно объяснить так: '(имя_массива + индекс) В языках Си и Си++ принято, что индексы массивов начинаются с нуля, т.е. массив int Z[3] из трех элементов включает элементы Z[0], Z[1 ], Z[2]. Это соглашение языка становится очевидным, если учесть, что индекс определяет не номер элемента, а его смещение относительно начала массива. Таким образом, *Z - обращение к первому элементу Z[0], *(Z + 1) - обращение ко второму элементу Z[1] и т.д. В следующей программе показано, как можно не использовать квадратные скобки при работе с элементами массива: //Р05_09.срр - Работа с элементами массива без скобок [] #include <iostream> using namespace std; int main() { char x[] = "DIXI"; // "Я сказал (высказался)" int i = 0; while (*(x + i) /= '\0') cout« *(x + i++) « endi; return 0; } Результат выполнения программы: слово "DIXI", написанное в столбик (см. Р02_15.срр). В данном примере оператор цикла с заголовком while выполняется, пока истинно выражение в скобках, т.е. пока очередной символ массива не равен '\0'. Это же условие можно проверять и при таком заголовке цикла: while (*(х + /)) В теле цикла при каждом вычислении выражения х + /++ используется текущее значение /, которое затем увеличивается на 1. Обращение к элементу массива Стандарт относит к постфиксному выражению вида РЕ[1Е]. Здесь РЕ должно быть указателем нужного типа, выражение IE в квадратных скобках должно быть
Адреса, указатели, массивы 155 целочисленным. Таким образом, если РЕ — указатель на массив, то РЕ[1Е] — индексированный элемент этого массива. *(РЕ + IE) — другой «путь» доступа к тому же элементу массива. Поскольку сложение коммутативно, то возможна такая эквивалентная запись *(/£ + РЕ) и, следовательно, 1Е[РЕ] именует тот же элемент массива, что и РЕ[1Е]. Сказанное иллюстрирует следующая программа: //Р05_10.срр - Коммутативность операции [] ttinclude <iostream> using namespace std; intmainf) { int m[ ] = { 10,20,30, 40); intj = 1; cout« "m[j] = " << m[j]; cout« "\t\t*(m + j++) = ” << *(m + j++) « end!; cout« "*(++) + m) = "« *(++j + m); cout« "\t\tj[m] = "« j[m] « end!; cout« "*(j— + m) = "« *(j— + m); cout« "\t\tj—[m] = ” << j—[m] « endl; cout« "*(—j + m) = "« *( —j + m); cout« "\t\t—j[m] = ” << —j[m] « endl; cout« "3[m] = ”<< 3[m] « "2[m]="<<2[m] « ” 1[m] = " « 1[m] «" 0[m] = " << 0[m] << endl; return 0; } Результаты выполнения программы: m[j] = 20 *(++j + m) =40 *(j— + m) =40 *(—j + m) = 10 3[m] = 40 2[m]=30 Y m + j++) = 20 j[m] = 40 j—[m] = 30 —j[m] = 9 1[m] = 20 0[m] = 9 Обратите внимание на порядок вычислений. В выражении j—[m\. вычисляется j[m], а затем у—. В выражении —j[m]: вычисляется j[m], и результат уменьшается на 1, т.е. --(/‘[ш]). В некоторых не совсем обычных конструкциях можно использовать постфиксное выражение РЕ[1Е] с отрицательным значением индекса. В этом случае РЕ должен указывать не на
нача156 Глава 5 ло массива, т.е. не на его нулевой элемент. Например, последовательность операторов lntA[ ] = {1,3,5,7}; int *U = &А[3]; cout« U[0] « U[-1] « U[-2] « U[-3]; приведет к выводу последовательности 7531. То же самое будет выведено на экран при таком использовании вспомогательной переменной-индекса: int i = 3; cout« i[A] « i[A-1 ] « i[A-2] « i[A-3]; Как видно из приведенных примеров, перемещение указателя от одного элемента к другому выполняется в естественном порядке, т.е. при увеличении индекса или указателя на 1 переходим к элементу с большим номером. Внутри массива нет проблемы, зависящей от реализации "обратного" размещения в памяти последовательно определенных в программе объектов. Так как имя массива есть не просто указатель, а указатель- константа, то значение имени массива невозможно изменить. Попытка получить доступ ко второму элементу массива int Z[4] с помощью выражения *(++Z) будет ошибочной. А выражение *(Z+1) вполне допустимо. Инициализация символьных массивов может быть выполнена не только с помощью строковых констант, но и с помощью списка инициализации, где последовательно указаны значения каждого отдельного элемента. При такой инициализации списком в конец символьного массива можно явно записать символ '\0': charstroka[] = { 'S', Т, 'С, '\0'}; Только при этом одномерный символьный массив (в данном случае stroka) получает свойства строки в стиле языка Си, и его можно использовать, например, в библиотечных функциях языка Си для работы со строками или при выводе с помощью оператора cout« stroka; Продолжая изучать массивы и указатели, рассмотрим конструкцию: type *имя;
Адреса, указатели, массивы 157 В зависимости от контекста она описывает или определяет различные объекты типа type *. Если она размещена вне любой функции, то объект есть внешний указатель, инициированный по умолчанию нулевым значением. Внутри функции это тоже указатель, но не имеющий определенного значения. В обоих случаях его можно связать с массивом элементов типа type несколькими способами, как во время определения, так и в процессе выполнения программы. В определениях существуют следующие возможности: type *имя = имя_ужеопределенного_MaccHBa_TnnaJype; type *имя = new 1уре[размер_массива]; type *имя = (type *)malloc( размер * sizeoff type)); Например: longarlong[] = { 100, 200, 300, 400, 500};//Определили массив long *arlo = arlong; // Определили указатель, связали его с массивом lnt*arint = new lnt[5]; // Определили указатель и выделили участок // памяти double *ardouble = // Определили указатель и (double *)malloc(5 * sizeof(double)); // выделили участок памяти В примерах определены три массива каждый из пяти элементов. Массив arlong инициализирован списком начальных значений в фигурных скобках. Массив, связанный с указателем arint, с помощью операции new получил участок памяти нужных размеров, однако эта память явно не инициализирована. Память для элементов массива, связанного с указателем ardouble, выделена с помощью библиотечной функции языка Си malloc(). В ее параметре приходится указывать количество выделяемой памяти (в байтах). Так как эта функция возвращает значение указателя типа void *, то потребовалось явное преобразование типа (double *). Выделенная память явно не инициализирована. В отличие от имени массива указатель, адресующий массив, никогда не "вспоминает" о том, что адресует массив, а не отдельный объект. Операция sizeof, применяемая к такому указателю, вернет количество байтов, занятых именно этим указателем, а вовсе не размер массива. Выражение &указатель возвращает адрес указателя в основной памяти, а никак не адрес начала
масси158 Глава 5 ва, на который настроен указатель. Таким образом, для наших примеров: sizeofarint == 4 - размер (в байтах) указателя int * sizeof arlong == 20 - размер (в байтах) массива sizeof arlo == 4- размер (в байтах) указателя. Как и массивы типа char, указатели char * могут инициализироваться с помощью строковых констант: char *имяуказателя = "последовательность символов char * имя указателя = { "последовательность символов " }; char *имя_указателя( "последовательность символов "); В этом случае количество элементов в символьном массиве, связанном с указателем, как обычно, на 1 больше, чем количество символов в последовательности символов инициализирующей строковой константы. Примеры определения указателей типа char: char *car1 = "строка-1"; char *саг2 = { "строка-2"}; char *сагЗ("строка-3"); Длины символьных массивов, связанных с указателями саг/, саг2, сагЗ, одинаковы. В последнем элементе каждого из этих массивов находится символ '\0'. Чтобы подчеркнуть различие между именем массива и указателем на массив, определим массив: chararch[ ]-"строка-4"; Операция sizeof, примененная к указателю на массив любого типа, возвращает длину не массива, а самого указателя, например sizeof(carl) == 4. Та же операция, примененная к имени массива, вернет длину массива (в байтах). Таким образом, sizeof arch == 9. Использование имени массива и указателя на массив в качестве аргумента функции при ее вызове эквивалентны. В обоих случаях в функцию передается адрес, и внутри функции (в ее теле) невозможно определить размер массива с помощью операции sizeof. Тот факт, что при выводе с помощью объекта cout используется специальная функция (операция-функция) будет нам
Адреса, указатели, массивы 159 понятен при изучении механизма перегрузки операций (гл. 9). Поэтому сейчас просто рассмотрим выражение cout« строка_в_стиле _Си; Здесь строка_в_стиле_Си - либо строковая константа, либо указатель на символьный массив, в котором присутствует терминальный символ ‘\0\ В обоих случаях правый операнд воспринимается как адрес, начиная с которого нужно выводить данные (символы) строки. (Напомним, что имя символьного массива представляет собой константный указатель на его начало.) В выходной поток выводятся все символы строки в стиле Си до терминального символа. Примеры: cout« саг2; // Вывод: строка-2 cout« arch; // Вывод: строка -4 cout« Aarch [3] // Вывод: строка-4 В последнем примере обращение выполняется не с начала массива (строки в стиле Си). Как и при обычном определении массивов к элементам массивов, связанных с указателями, существует несколько путей доступа. Принципиально различных путей два: с помощью операции [ ] и с помощью операции разыменования. Примеры: cout« саг1[0] « саг1[2] « саг1[3] « саг1[4]; //Вывод: срок Последовательность операторов cout« carl[2]; cout« *(car1+=3); cout« *++car1; приведет к выводу слова «рок». Обратите внимание, что в примере указатель саг1 изменяется, что невозможно для имени массива. При определении указателя ему может быть присвоено значение другого указателя, уже связанного с массивом того же типа: IntpH []={ 1,2, 3,4}; int *pi2 = pH; //pi2 - "другое имя" для pH double pd1 []={ 10, 20, 30, 40, 50 }; double *pd2 = pd1; //pd2- "другое имя"для pd 1 После таких определений к элементам каждого из массивов возможен доступ с помощью двух разных имен. Например:
160 Глава 5 cout« pi2[0]; *pi1 = 0; //Выводится 1 //Изменяется pi1[0] cout« *pi2; // Выводится 0 // Выводится 40 cout« pd1 [3] *(pd2 + 3) = 77 cout« pd1[3] // Изменяется pd2[3] // Выводится 77 Такие же присваивания указателям допустимы и в процессе исполнения программы, т.е. последовательность операторов int * pi3; i3 = pH; свяжет еще один указатель р/3 с тем же самым массивом int из четырех элементов. Возможность доступа к элементам массива с помощью нескольких указателей не следует путать с присваиванием одному массиву значений элементов другого массива. Рассмотрим такой пример: char str[ ] = ммассив// Определили массив с именем str char *pstr = str; // Определили указатель pstr и Присваивание указателю pstr не переписывает строковую константу "строка" в массив str, вместо этого изменится значение самого указателя pstr. При определении он указывал на начало массива с именем str, а после присваивания его значением станет адрес того участка памяти, в котором размещена строковая константа "строка". Чтобы в процессе выполнения программы изменить значения элементов массива, необходимо, явно или опосредованно (с помощью указателей или средств ввода данных), выполнить присваивания этим элементам новых значений. Например, заменит содержимое массива-строки str такой дополнительный оператор while (str++ = pstr++); или его аналог с индексированными переменными: for (int i = 0; str[i] = pstr[i]; /++); pstr= "строка"; // "настроили" его на массив str // Изменили значение указателя, // но никак не изменили массив str
Адреса, указатели, массивы 161 Окончание цикла в обоих случаях — достижение элемента с терминальным символом ‘\0\ При переписывании одного массива в другой длина заполняемого массива должна быть не меньше длины копируемого массива, так как никаких проверок предельных значений индексов язык Си++ не предусматривает, а выход за границу индекса часто приводит к аварийной ситуации. В обоих операторах учтено, что длины строковых констант "массив" и "строка" одинаковы, а в конце строковой константы всегда размещается терминальный символ, по достижении которого цикл завершается. Возможно "настроить" на массив указатели других типов, однако при этом потребуются явные приведения типов. Продолжая наши примеры, можно ввести такие определения: char *pch = (char *) pH; double *pfI = (double *) pH; Определенные так указатели позволят по-другому "перебирать" коды элементов массива. Но, наверное, это не скоро потребуется читателю. Итак, допустимо присваивать указателю адрес начала массива. Однако имени массива нельзя присвоить адрес другого массива — имя массива есть указатель-константа. Рассмотрим пример: long arl[ ]={ 10, 20, 30, 40 }; long *pl = new long[4]; Определены два массива по 4 элемента в каждом. Операторы присваивания для имен этих массивов обладают разными правами: arl = pi; // Недопустимый оператор pi = arl; // Опасный оператор Первый оператор недопустим, так как имя массива arl — указатель-константа. Второй оператор синтаксически верен, однако приводит к опасным последствиям — участок памяти, выделенный операцией new long[4], становится недоступным. Его нельзя теперь не только использовать, но и освободить, так как в операции delete нужен адрес начала освобождаемой памяти, а его значение потеряно.
162 Глава 5 5.4. Многомерные массивы, массивы указателей, динамические массивы Многомерный массив в соответствии с синтаксисом языка есть массив массивов, т.е. массив, элементами которого служат массивы. Определение многомерного массива в общем случае должно содержать сведения о типе, размерности и количествах элементов каждой размерности: type имя_массива[К1][К2]...[КЫ]; Здесь type - допустимый тип (основной или производный), имя_массива - идентификатор, N - размерность массива, К1 - количество в массиве элементов-массивов размерности N-1 каждый и т.д. Например: inf arr3[4][3][6]; Трехмерный массив аггЗ состоит из четырех элементов, каждый из которых — двумерный массив с размерами 3 на 6. В памяти массив аггЗ размещается в порядке возрастания самого правого индекса (рис. 5.2), т.е. самый младший адрес имеет элемент а/т3[0][0][0], затем идет аллЗ[0][0][1] и т.д. апгЗ[ 3] аггЗ[2] апгЗ[ 1] агг3[0] Массив 3 на 6 Массив 3 на 6 Массив 3 на 6 Массив 3 на 6 ◄ Возрастание адресов arr3[i][2] а/гЗ[/][1] arr3[i][ 0] Массив из 6 Массив из 6 Массив из 6 элементов элементов элементов < Возрастание адресов а/гЗ[/][/][5] аггти4] ... аггЩЦЩ Скалярный Скалярный Скалярный элемент элемент элемент ■4 Возрастание адресов Рис. 5.2. Схема размещения в памяти трехмерного массива
Адреса, указатели, массивы 163 Следующая программа иллюстрирует перечисленные особенности размещения в памяти многомерных массивов: //Р05_11 .срр - Адреса элементов многомерных массивов #include <iostream> #include "cyrToDos.h" using namespace std; intmainf) { int arr3[4][3][6]; cout« "&arr3[0] = " << &arr3[0]«endl; cout« "&arr3[1] = " << &arr3[1]«endl; cout« ”&arr3[2] = "« &arr3[2]«endl; cout« "&arr3[3] = ” << &arr3[3]«endl; cout« "&arr3[2][2][2] = "« &arr3[2][2][2]«endl; cout« ”&arr3[2][2][3] = " << &arr3[2][2][3]«endl; cout« cyrToDosf ”\ ,,Расстояние\” в байтах:\п (unsigned long)&arr3[ 1]") <<M - (unsigned iong)&arr3[0] = " << (unsigned long)&arr3[ 1 ] - (unsigned Iong)&arr3[0]; return 0; } Результаты выполнения программы (зависят от реализации): & arr3[0] = 0x66dQ4 &arr3[ 1] = 0x66dcc &arr3[2] = Охббе 14 &агг3[3] = Охббебс &аггЗ[2][2][2] = 0хббе4с &агг3[2][2][3] = ОхббебО "Расстояние" в байтах: (unsigned long)&arr3[1] - (unsigned Iong)&arr3[0] = 72 Обратите внимание на равную четырем разность адресов элементов а/т3[2][2][3] и аггЗ[2][2}[2]. Самый «внутренний» одномерный массив целочисленный, "длина" одного его элемента типа int равна четырем байтам. "Расстояние" в байтах от элемента а/тЗ[1] до а/т3[0] равно 72, что соответствует двумерному целочисленному массиву с размерами 3 на 6. С учетом порядка расположения в памяти элементов многомерного массива нужно размещать начальные значения его элементов и в списке инициализации. (Поправка на правостороннее 11*
164 Глава 5 написание фраз и слов в европейских языках в отличие от направления возрастания адресов на рис. 5.2 совершенно естественна.) intarr3[4][3][6] = {0, 1.2. 3, 4, 5, 6, 7}; При таком определении начальные значения получили только "первые" 8 элементов трехмерного массива, т.е. агл3[0][0][0] == О аггЗ[0][0][1] == 1 агг3[0] [0][2] == 2 а/т3[0][0][3] == 3 аггЗ[0][0][4] == 4 аггЗ[0][0][5] == 5 агг3[0] [ 1 ][0] ==6 аггЗ[0][1 ][1 ] ==7 Остальные элементы массива аггЗ остались не инициализированными и получат начальные значения в соответствии со статусом массива. Если необходимо инициализировать только часть элементов многомерного массива, но они размещены не в его начале или не подряд, то можно вводить дополнительные фигурные скобки, каждая пара которых выделяет последовательность значений, относящихся к одной размерности. (Нельзя использовать скобки без информации внутри них.) Следующее определение с инициализацией трехмерного массива Int А[4][5][6] = {{{()}}, {{100}, {110, 111}}, {{200}, {210}, {220, 221,222}}; так задает значения некоторых его элементов: А[0][0][0] == 0, А[1][0][0] == 100, А[ 1][ 1][0] == 110, А[1][1][1] ==111 А[2][0][0] == 200, А[2][1][0] == 210, А[2][2][0] == 220, А[2][2][ 1]== 221, А[2][2][2] == 222 Остальные элементы массива явно не инициализируются.
Адреса, указатели, массивы 165 Если многомерный массив при определении инициализируется, то его самая левая размерность может в скобках не указываться. Количество элементов компилятор определяет по числу членов в инициализирующем списке. Например, определение double matrix [ ][5] = {{1}, {2}, {3}}; формирует массив matrix с размерами 3 на 5, но не определяет явно начальных значений всех его элементов. Начальные значения получают только mafr/x[0][0] == 1 mafr/x[1][0] == 2 matrix[2][0] == 3 Оператор cout« "\nsizeof(matrix) = ”« sizeof (matrix); выведет sizeof (matrix) = 120 Как и в случае одномерных массивов, доступ к элементам многомерных массивов возможен с помощью индексированных переменных и с помощью указателей. Возможно объединение обоих способов в одном выражении. Чтобы не допускать ошибок при обращении к элементам многомерных массивов с помощью указателей, нужно помнить, что при добавлении целой величины к указателю его внутреннее значение изменяется на "длину" элемента соответствующего типа. Имя массива всегда константа- указатель. Для массива, определенного как type AR [A/][M][L], AR — константный указатель, поставленный в соответствие элементам типа type [М\[Ц. Добавление 1 к указателю AR приводит к изменению значения адреса на величину sizeof(type) * М * L . Именно поэтому выражение *(AR + 1) есть адрес элемента AR[ 1] - указатель на массив меньшей размерности, отстоящий от начала массива, т.е. от &4Я[0], на размер одного элемента type[M][L]. Сказанное иллюстрирует следующая программа: //Р05_ 12.срр - Многомерные массивы - доступ по указателям «include <iostream> using namespace std;
166 Глава 5 intmainf) { int b[3][2][4] = { 0, 1, 2, 3, 10, 11, 12, 13, 100, 101, 102, 103, 110, 111, 112, 113, 200, 201, 202, 203, 210, 211, 212, 213 }; cout« "b = "« b « endl; // Адрес массива b[ ][ ][ ] cout« ”*b = ” << *b « endl; //Адрес массива b[0][][] cout« "**b = " << **b « endl; //Адрес массива b[0][0][] // Элемент b[0][0][0]: cout« "***b = "« ***b « endl; //Адрес массива b[ 1][][]: cout« ”*(b + 1) = "« *(b + 1) « endl; //Адрес массива b[2][][]: cout« "*(b + 2) = "« *(b + 2) « endl; //Адрес массива b[0][ 1 ][]: cout« ”*(*b + 1) = M« *(*b + 1) « endl; cout << "*(*(*(b + 1)+1)+ 1) = "« YYYb+ 1)+ 1)+ 1) « endl; cout << "*(b[1][1]+ 1) = ”« *(b[1][1]+ 1) «endl; // Элемент b[2][0][0]: cout « M*(b[1] + 1)[1] = M « *(b[1] + 1 )[1] << endl; return 0; Результаты выполнения программы (зависят от реализации): b = 0x67044 *Ь=0x67044 **Ь = 0x67044 ***ь = о *(Ь + 1) = 0x67064 *(Ь + 2) = 0x67084 *(*Ь+ 1) = 0x67054 *(*(*(Ь + 1)+ 1)+ 1)= 111 *(Ь[1][1]+ 1)=111 *(Ь[1]+1)[1] = 200
Адреса, указатели, массивы 167 В программе доступ к элементам многомерного массива осуществляется с помощью операций с указателями. В общем случае для трехмерного массива индексированный элемент b[i\\j\[k\ соответствует выражению *(*(*(Ь + /) + у) + к). В нашем примере: *(*(*(£> + 1) + 1) + 1) == Ь[1][1][1]== 111. Допустимо в одном выражении комбинировать обе формы доступа к элементам многомерного массива, т. е. записывать выражение *(Ь[1][1] + 1)==111. Как бы ни был указан путь доступа к элементу многомерного массива, внутренняя адресная арифметика, используемая компилятором, всегда предусматривает действия с конкретными числовыми значениями адресов. Компилятор всегда реализует доступ к элементам массива с помощью указателей и операции разыменования. Если в программе использована, например, такая индексированная переменная: ЛЯ[/][/][/с], принадлежащая массиву type АЯ[Л/][М][/.], где N, М, L - целые положительные константы, то последовательность действий компилятора такова: • выбирается адрес начала массива, т.е. целочисленное значение указателя AR, равное (unsigned tong)AR\ • добавляется смещение / * (М * L) * sizeof{type) для вычисления начального адреса i-ro массива с размерами М на L, входящего в исходный трехмерный массив; • добавляется смещение для вычисления начального адреса у-й строки (одномерный массив), включающей L элементов. Теперь смещение равно (/ * (М * L) + j * L) * sizeof(type); • добавляется смещение для получения адреса &-го элемента в строке, т.е. получается адрес (unsigned long)(i * (М * L) + j * L + к) * sizeof(type)\ применяется разыменование, т.е. обеспечивается доступ к содержимому элемента по его адресу: *((unsigned tong){\ * (М * L) + j*L + к)). Массивы указателей. Синтаксис языка Си++ в отношении указателей непротиворечив, но весьма далек от ясности. Для понимания, что же определено с помощью набора звездочек, скобок и имен типов, приходится аккуратно применять
синтак168 Глава 5 сические правила, учитывающие последовательность выполнения операций. Например, следующее определение int *аггау[6]; вводит массив указателей на объекты типа int. Имя массива array, и он состоит из шести элементов-указателей, тип каждого int *. Определение int (*ptr)[6]\ вводит указатель ptr на массив из шести элементов, каждый из которых имеет тип int. Таким образом, выражение (array + 1) соответствует перемещению в памяти на sizeof(int *) байтов от начала массива (т.е. на длину указателя типа int *). Если прибавить 1 к ptr, то адрес изменится на величину sizeof(int[6]). Возможность создавать массивы указателей порождает интересные следствия, которые удобно рассмотреть в контексте многомерных массивов. По определению массива, его элементы должны быть однотипными и поэтому будут иметь один "размер". Предположим, что мы хотим определить массив для представления списка фамилий (учеников класса, сотрудников фирмы и т.п.). Если определять его как двумерный массив элементов типа char, то в определении для элементов массива необходимо задать предельные размеры каждого из двух индексов. Таким образом, "прямолинейное" определение массива для хранения списка фамилий может быть таким: char spisok[25] [20]; Для примера здесь предполагается, что количество фамилий в списке не более 25 и длина каждой фамилии не превышает 19 символов (букв). После такого определения или с помощью инициализации в самом определении в элементы spisok[0]} sp/so/c[1], ... можно занести конкретные фамилии, представленные в виде строковых констант. Размеры таким образом определенного массива всегда фиксированы. При определении массива один из его предельных размеров (самого левого индекса) можно не указывать. В этом случае количество элементов массива определяется инициализацией
Адреса, указатели, массивы 169 charspisok[][20] = { "Иванов", "Петров", "Сидоров"}; Теперь в массиве spisok только 3 элемента, каждый из них длиной 20 элементов типа char (рис. 5.3). spisok ► (имя массива - указатель- константа) 20 байт Массивы char[20] Иванов\00000000000000 Петров\00000000000000 Сидоров\0000000000000 pointer ► (имя массива - указатель- константа) Указатели- Строковые переменные константы * —► Иванов\0 * —► ПетровЮ * —► Сидоров\0 (char*) Рис. 5.3. Двумерный массив char spisok[3] [20] и одномерный массив указателей char *pointer[3], инициализированные одинаковыми строковыми константами Нерациональное использование памяти и в этом случае налицо — даже для коротких строк всегда выделяется одно и то же количество байтов, заранее указанное в качестве предельного значения второго индекса массива spisok. В противоположность этому при определении и инициализации теми же строковыми константами одномерного массива указателей типа char * память можно распределять гораздо рациональнее: char ^pointer [] = { "Иванов", "Петров", "Сидоров"}; Для указателей из массива pointer, в котором при таком определении 3 элемента и каждый является указателем-переменной типа char *, выделяется всего 3*sizeof(char *) байтов. Кроме того, компилятор размешает в памяти три строковые константы
170 Глава 5 "Иванов" (7 байт), "Петров" (7 байт), "Сидоров" (8 байт), а их адреса становятся значениями элементов pointer[0], pointer^], pointer[2]. Сказанное иллюстрирует рис. 5.3. Применение указателей и массивов указателей позволяет рационально решать задачи сортировки сложных объектов с неодинаковыми размерами. Например, для упорядочения (хотя бы по алфавиту) списка строк можно менять местами не сами строки, а переставлять значения элементов массива указателей на эти строки. Такой одномерный массив pointer[ ] использован в только что приведенном примере (см. рис. 5.3). При этой "косвенной” сортировке списков объектов дополнительно требуется память, необходимая для массива указателей. Выигрыш — существенное ускорение сортировки. В качестве конкретной задачи такого рода рассмотрим сортировку строк матрицы. Матрица с элементами типа double представлена двумерным массивом double array[n][m], где п и т - целочисленные константы. Предположим, что целью сортировки является упорядочение строк матрицы в порядке возрастания сумм их элементов. Чтобы не переставлять сами строки массива аггау[п][т], введен вспомогательный одномерный массив указателей double * раг[п]. Инициализируем его элементы адресами одномерных массивов типа double[m], составляющих двумерный массив аггау[п][т]. После такой инициализации массива указателей к элементам исходного массива появляются два пути доступа: • прямой — с помощью индексации имени массива array[i\\j\ и • косвенный - с помощью указателей вспомогательного массива par[f][f]. Чтобы не усложнять программу, применим самый простой метод сортировки, а в качестве начальных значений элементов сортируемого массива выберем номера строк, к которым элементы относятся. В программе три раза напечатаем матрицу — до и после сортировки с помощью вспомогательного массива указателей и (после сортировки) с использованием основного имени массива. Комментарии в тексте программы поясняют остальные детали: //Р05_13.срр - Перестановка указателей на одномерные массивы #include <iostream>
Адреса, указатели, массивы 171 #include "cyrToDos.h" using namespace std; int main()'{ const int n = 5; const int m = 7; int i, j, к; double array[n][m]; // Количество строк матрицы // Количество столбцов матрицы // Индексы // Основной массив (матрица) for (i = 0; i < п; i++) for(j = 0; j < m; j++) //Заполнение матрицы array[i][j] = n- i; double *par[n]; // Вспомогательный массив указателей for (i = 0; i < n; i++) // Цикл перебора строк par[i] = (double *)&array[i]; //Печать массива по строкам (через массив указателей): cout« cyrToDosf "До перестановки элементов массива " Указателей:"); for (i = 0; i < n; /++; // Цикл перебора строк { cout« cyrToDos("\пстрока ”) « (i+1) « for(j = 0; j < m; j++)//Цикл печати cout«,,\t"«par[i][j];//элементов строки } // Упорядочение указателей на строки массива double si,sk; for(i = 0; i < n - 1; i++) { for (j = 0, si = 0.0; j < m; /++; si += par[i][j]; // Сумма элементов i-й строки for(k = / + 1; k < n; k++) { for (j = 0, sk = 0.0; j < m; j++) sk += par[k][j]; // Сумма элементов k-й строки if (si > sk) {double *pa = par[i]; par[i] = par[k]; par[k] = pa; si = sk; } } } //Печать массива по строкам (через массив указателей): cout« cyrToDosf "\пПосле перестановки элементов массива "указателей:");
172 Глава 5 for(\ - 0; i < n; i++) // Цикл перебора строк {cout« cyrToDos("\ncTpoKa ") «(i + 1) « for(j = 0; j < m;j++)//Цикл печати cout« "\Г << par[i][j]; // элементов строки } //Печать исходного массива по строкам(обращение через //имя массива): cout« cyrToDosf ”\пИсходный массив остался без изменений:"); for(i = 0; / < n; i++) // Цикл перебора строк {cout« cyrToDosf "\пстрока ") «(i + 1) « for (j = 0; j < m; j++) // Цикл печати cout« "\t"« array[i][j]; //строки } cout« endl; return 0; } Результаты выполнения программы: До перестановки элементов массива указателей: строка 1: 5 5 5 5 5 5 5 строка 2: 4 4 4 4 4 4 4 строка 3: 3 3 3 3 3 3 3 строка 4: 2 2 2 2 2 2 2 строка 5: 1 1 1 1 1 1 1 После перестановки элементов массива указателей: строка 1: 1 1 1 1 1 1 1 строка 2: 2 2 2 2 2 2 2 строка 3: 3 3 3 3 3 3 3 строка 4: 4 4 4 4 4 4 4 строка 5: 5 5 5 5 5 5 5 Исходный массив остался без изменений: строка 1: 5 5 5 5 5 5 5 строка 2: 4 4 4 4 4 4 4 строка 3: 3 3 3 3 3 3 3 строка 4: 2 2 2 2 2 2 2 строка 5: 1 1 1 1 1 1 1 Обратите внимание на неизменность исходного массива array[n][m] после сортировки элементов вспомогательного мае-
Адреса, указатели, массивы 173 сива указателей. Для иллюстрации действия механизма сортировки нарисуйте схему взаимосвязи массивов аггау[ ][ ] и раг[ ]. В качестве образца можно воспользоваться рис. 5.4. Одномерный массив о 1 2 ... л-1 указателей 0 1 2 л- 1 * —► —► 1 0 0 ... 0 * 0 1 0 ... 0 ★ —► 0 0 1 ... 0 Указатели типа double* f одномерных массивов 0 0 0 ... 1 Элементы типа double Рис. 5.4. Схема моделирования двумерного динамического массива с помощью массива указателей и набора одномерных массивов Многомерные массивы динамической памяти. В соответствии с синтаксисом выражение с операцией new для массива имеет следующий формат: new тип_массива Тип_массива определяет количество элементов и их тип. Выражение позволяет выделить в динамической памяти участок для размещения массива соответствующего типа, но не позволяет его инициализировать. Результат выражения — указатель, значением которого служит адрес первого элемента массива. Обратите внимание, что при выделении динамической памяти для массива его размеры должны быть полностью определены. Примеры: long (*1р)[2][4]; // Определили указатель Ip = new long[3][2][4]; // Выделили память для массива В данном примере использован указатель на объекты в виде двумерных массивов, каждый из которых имеет фиксированные
174 Глава 5 размеры 2 на 4 и содержит элементы типа long. В определении указателя следует обратить внимание на круглые скобки, без которых обойтись нельзя. После выполнения приведенных операторов указатель /р становится средством доступа к участку динамической памяти с размерами 3 * 2 * 4 * sizeof(long) байтов. В отличие от имени массива (имени у массива из примера нет) указатель /р есть переменная, что позволяет изменять его значение и тем самым, например, перемещаться по элементам массива. Изменять значение указателя на динамический массив нужно с осторожностью, чтобы не "забыть", где же находится начало массива, так как указатель, значение которого определяется при выделении памяти для динамического массива, используется затем для освобождения памяти с помощью операции delete. Следующая программа иллюстрирует сказанное: //Р05_14.срр - Выделение и освобождение памяти для массива #include <iostream> using namespace std; intmainf) { long (*lp)[2][4], (*beg)[2][4]; beg = lp = new long [3][2][4]; forfint i = 0; Ip < beg + 3; lp++, i++) { for(intj = 0; j < 2; j++) forfint k = 0; k< 4; k++) { lp[Omk] = long(i+j + k); cout « '\t'« lp[0][j][k]; } cout« endl; } delete [] beg; return 0; } Результаты выполнения программы: 0 1 2 3 1 2 3 4 1 2 3 4 2 3 4 5 23453456 В отличие от определения массивов, не относящихся к динамической памяти, инициализация динамических массивов не
Адреса, указатели, массивы 175 выполняется. Поэтому при выделении памяти для динамических массивов их размеры должны быть полностью определены явно. Только из типа_массива операция new получает информацию о его размерах: new 1опд[ ]// Ошибка, размер неизвестен newlong[ ][2][4] // Ошибка, размер неизвестен new 1опд[3][ ][4] // Ошибка, размер неизвестен Существует еще одно ограничение на размеры динамических массивов. Только первый (самый левый) размер массива может быть задан с помощью переменной. Остальные размеры многомерного массива могут быть определены только с помощью констант. Это несколько затрудняет работу с многомерными динамическими массивами. Например, если пытаться создать матрицы в виде двумерных массивов, то затруднения возникнут при попытке написать функцию, формирующую в динамической памяти транспонированную матрицу по исходной матрице с заранее не определенными размерами. Обойти указанное ограничение многомерных динамических массивов позволяет применение массивов указателей. Однако при использовании массивов указателей для имитации многомерных динамических массивов усложняется не только их формирование, но и освобождение динамической памяти. В следующей программе формируется, заполняется данными, затем печатается и уничтожается массив, представляющий прямоугольную диагональную единичную матрицу, порядок которой (размеры массива) вводится пользователем с клавиатуры: //Р05_15.срр - Единичная матрица с изменяемым порядком #include <iostream> #include "cyrToDos.h" using namespace std; intmainf) { intn; // Порядок матрицы cout« cyrToDosCВведите порядок матрицы: ”); cin > > n; // Определяются размеры массива double **matr; // Указатель для массива указателей matr = new double *[п]; //Массив указателей double * iffmatr == 0) {
176 Глава 5 cout« cyrToDos("He создан динамический массив!"); return 0; // Завершение программы } forfint / = 0; / < n; i++) { //Строка-массив значений типа double: matr[i] = new doublefn]; if(matr[i] == 0) { cout« cyrToDos("He создан динамический массив!"); return 0; // Завершение программы } forfintj = 0;i < n; j++) //Заполнение строки матрицы matr[i][j] = (i !=j ? 0 : 1); } forfint i = 0; i < n; i++) { //Цикл перебора строк cout« cyrToDosf "строка ")«(i+ 1) « // Цикл печати элементов строки: forfint j = 0; j < n; j++) cout« "\t"« matr[i][j]; cout« end!; } forfint i = 0; i < n; i++) delete [] matr[i]; delete[ ]matr; return 0; } Результаты выполнения программы: Введите порядок матрицы: 4 <Enter> строка 1: 1 0 0 0 строка 2: 0 1 0 0 строка 3: 0 0 1 0 строка 4: 0 0 0 1 На рис. 5.4 изображена схема взаимосвязи п-одномерных массивов, из п элементов каждый. Эти п массивов совместно имитируют квадратную матрицу с изменяемыми размерами, формируемую в программе Р05_15.срр.
177 ФУНКЦИИ, УКАЗАТЕЛИ, ССЫЛКИ 6.1. Определения, описания и вызовы функций Если в таких языках, как Алгол, Фортран, ПЛ/1, Паскаль и другие, делается различие между программами, подпрограммами, процедурами, функциями, то в языке Си++ и в его предшественнике — языке Си — используются только функции. При программировании на языке Си++ функция — это основное понятие, без которого невозможно обойтись. Во-первых, каждая программа обязательно должна включать единственную функцию с именем main (главная функция). Именно функция main обеспечивает создание точки входа в откомпилированную программу. Кроме функции с именем main, в программу может входить произвольное количество неглавных функций, выполнение которых инициируется прямо или опосредованно вызовами из функции main. Всем именам функций программы по умолчанию присваивается класс памяти extern, т.е. каждая функция имеет внешний тип компоновки и статическую продолжительность существования. Как объект с классом памяти extern каждая функция глобальна, т.е. ее имя принадлежит глобальному пространству имен и при определенных условиях функция доступна во всех единицах трансляции (translation unit) программы. Для доступности в единице трансляции функция должна быть в нем определена или описана до первого вызова. Итак, каждая программа на языке Си++ — это совокупность функций, каждая из которых должна быть определена или по крайней мере описана до ее использования. В определении функции указываются последовательность действий, выполняемых при ее вызове, имя функции, тип возвращаемого ею значения (т.е. тип результата) и совокупность параметров. Отметим, но пока не будем объяснять (см. главу об исключениях), что в опре- 1 Г2762
178 Глава 6 деление и описание функции может входить спецификация исключений, генерация которых запланирована в теле функции. Каждый параметр не только перечисляется, но и специфицируется, т.е. для него задается тип. Спецификация параметров входит в сигнатуру функции. Этот термин активно используется в связи с перегрузкой функций (см. разд. 6.8). Сигнатура функции зависит от количества параметров, их типов и от порядка их размещения в спецификации. Кроме того, в сигнатуру входят имя функции и пространство имен, которому принадлежит имя. (Тип возвращаемого значения не входит в сигнатуру функции.) В этой главе будем рассматривать только функции, имена которых принадлежат безымянному глобальному пространству имен. Определение функции, в котором выделяются две части — заголовок и тело, имеет следующий формат: тип имя_функции(спецификацияпараметров) тело_функции Здесь тип — это тип возвращаемого функцией значения, в том числе void, если функция никакого значения не возвращает. Имя функции - идентификатор. Имена функций как имена внешние (тип extern) должны быть уникальными среди других имен из глобального пространства имен. Спецификация параметров — это либо пусто, либо void, либо список спецификаций отдельных параметров, в конце которого может быть поставлено многоточие. Спецификация каждого параметра в определении функции имеет одну из форм: тип имя параметра тип имя_параметра = умалчиваемое_значение Как следует из формата, для параметра может быть задано (а может отсутствовать) умалчиваемое значение. Синтаксис языка разрешает спецификации параметров без имен, если последние не используются в теле функции. Использование спецификации параметра без имени "полезно для резервирования места в списке параметров". В дальнейшем этот параметр может быть введен в функции без изменения интерфейса, т.е. без изменения вызывающей программы. Такая возможность бывает удобной при заранее запланированном развитии уже существующей программы за счет изменения входящих в нее функций.
Функции, указатели, ссылки 179 Тело_функции - это всегда блок или составной оператор, т.е. последовательность описаний, определений и операторов, заключенная в фигурные скобки. Очень важным оператором тела функции является оператор возврата в точку вызова: return выражение; или return; Выражение в операторе return определяет возвращаемое функцией значение. Именно это значение будет результатом обращения к функции. Тип возвращаемого значения указывается перед именем функции в ее определении и описании. Если функция не возвращает никакого значения, т.е. указан тип void, то выражение в операторе return опускается. В этом случае необязателен и сам оператор return в теле функции. В этом случае необходимые коды команд возврата в точку вызова компилятор языка Си++ добавит в объектный модуль функции автоматически. В теле функции может быть и несколько операторов return. Оператор return, как мы уже знаем, используется и в функции main. Значение, возвращаемое функцией main, анализируется операционной системой. Принято, что при благоприятном завершении программы возвращается значение 0. В противном случае возвращаемое значение отлично от 0. Даже в том случае, когда функция не должна выполнять никаких действий и не должна возвращать никаких значений, тело функции будет состоять из фигурных скобок {}. Такая функция может потребоваться при отладке программы в качестве "заглушки". Примеры определений функций с разными сигнатурами: void print (char *name, int value) {cout« "\n"« name « value; } double minfdouble a, double b) { if (a < b) return a; return b; //Ничего не возвращает // Нет оператора return // В функции два оператора //возврата // Возвращает минимальное //из значений целых аргументов } 12*
180 Глава 6 double cube(double x) { return x * x * x; } int maxfint n, intm) { return n < m ? m : n; } void write(void) { cout« П\п НАЗВАНИЕ:"; } // Возвращает значение типа double // Возведение в куб вещественного числа // Вернет значение типа int // Возвращает максимальное // из значений целочисленных аргументов //Ничего не возвращает, // ничего не получает // Всегда печатает одно и то же Заголовок последней функции может быть записан без типа void в списке параметров: void write() // Отсутствие параметров эквивалентно void При обращении к функции параметры заменяются аргументами, причем соблюдается строгое соответствие по типам. Говорят, что язык Си++ обеспечивает "строгий контроль типов". В связи с этой особенностью языка Си++ проверка соответствия типов аргументов и параметров выполняется на этапе компиляции. Строгое согласование по типам между аргументами и параметрами требует, чтобы в единице трансляции до первого обращения к функции было помещено либо ее определение, либо ее описание (прототип), содержащее все сведения, нужные для ее вызова. Именно наличие такого прототипа либо полного определения позволяет компилятору выполнять контроль соответствия типов параметров и аргументов. Прототип (описание) функции может внешне почти полностью совпадать с заголовком ее определения: тип имя_функции (спецификацияпараметров); Основное различие — точка с запятой в конце описания (прототипа). Второе отличие — необязательность имен параметров в прототипе даже тогда, когда они есть в заголовке определения функции. Прототипы определенных выше функций: void printf char *, int); // Опустили имена параметров double min(double a, double b);
Функции, указатели, ссылки 181 double cube(double х); Int maxflnt, Int m); // Опустили одно имя void write(vold); // Список параметров может быть пустым Обращение к функции (иначе - вызов функции) - это выражение с операцией "круглые скобки". Операндами служат имя функции (либо указатель на функцию) и список аргументов: имя_функции (список_аргументов) Значением выражения "вызов функции" является возвращаемое функцией значение, тип которого указан перед именем функции в ее определении и описании. Аргумент функции - это в общем случае выражение. Соответствие между аргументами и параметрами устанавливается по их взаимному расположению в списках. Аргументы передаются из вызывающей программы в функцию по значению, т.е. вычисляется значение каждого выражения, представляющего аргумент, и именно это значение используется в теле функции вместо соответствующего параметра. Таким образом, список_аргументов — это либо пусто, либо void, либо разделенные запятыми выражения. Проиллюстрируем сказанное о функциях программой, в которую включим некоторые из приведенных выше функций. Предположим, что текст программы размещен в одном файле: //Р06_01.срр - Определения, прототипы и вызовы функций #include <iostream> using namespace std; //Определение функции до ее вызова: intmax(intn, intm) { return n < m ? m : n; } void printfehar *, int); // Прототип до определения double cube(double x = 0); // Прототип до определения int main() { // Главная функция int sum = 5, k = 2; // Вложенные вызовы функций: sum = max((int)cube(double(k)), sum); print("sum = ”,sum); return 0; >
182 Глава 6 // Определение функции: void printf char * name, int value) { cout« name « value « endl; } double cube(double x) { // Определение функции return x * x * x; } Результаты выполнения программы: sum = 8 Отметим необходимость преобразований типов аргументов при вызовах функций тах() и cube( ). Преобразования требуются для согласования типов аргументов и параметров. В иллюстративных целях формы записи преобразований взяты различными — каноническая и функциональная. Для функции тах() прототип не потребовался — ее определение размещено в том же файле до вызова функции. Прототипы функций print( ) и cube( ) в программе необходимы, так как определения функций размещены после обращения к ним. Если в качестве эксперимента убрать (например, превратить в комментарий с помощью скобок /* */ или //) прототип любой из функций print( ) или cube( ), то компилятор выдаст сообщение об ошибке. При использовании DJGPP оно будет таким: Р06_01. срр: In function int main() P06_01.cpp:14: implicit declaration of function 'int cube(...y Подобное же сообщение появится, если перенести определение функции тах( ) в конец текста программы и не ввести прототипа до вызова тах( ). При наличии прототипов вызываемые функции не обязаны размещаться в одном исходном файле с вызывающей функцией, а могут оформляться в виде отдельных файлов либо могут находиться уже в оттранслированном виде в библиотеке объектных модулей. Сказанное относится не только к функциям, которые готовит программист для включения в свою программу, но и к функциям из стандартных библиотек используемого компилятора. В последнем случае определения библиотечных функций, уже оттранслированные и оформленные в виде объектных модулей,
Функции, указатели, ссылки 183 находятся в библиотеке компилятора, а описания функций в виде прототипов необходимо включать в программу дополнительно. Обычно это делают с помощью препроцессорной директивы #include <имя_заголовка> Здесь имя_заголовка определяет текстовый (заголовочный) файл, содержащий прототипы той или иной группы стандартных (библиотечных) функций. Например, в текстах практически всех написанных нами программ присутствует команда #include <iostream> Эта программа включает описания библиотечных классов и принадлежащих им функций для ввода и вывода данных. (Из всех средств мы до сих пор использовали только объекты потокового ввода-вывода cout, cin, соответствующие им операции », << и манипуляторы endl, hex.) Попробуйте удалить из любой работающей программы директиву #include <iostream> (если, конечно, она в ней есть) и посмотрите на возмущенные сообщения компилятора - он перестанет "узнавать" многие конструкции в тексте программы. Например, станет неизвестным объект с именем cout. Подобно тому, как это сделано в библиотеке стандартных функций компилятора, следует поступать и при разработке своих программ, состоящих из достаточно большого количества функций, размещенных в разных исходных файлах. Прототипы функций и описания внешних объектов (переменных, массивов и т.д.) помещают в отдельный файл, который препроцессорной командой #include "имя_файла" включают в начало каждого из исходных файлов программы. В отличие от библиотечных функций компилятора имя такого заголовочного файла в команде #include записывается не в угловых скобках О, а в кавычках Имя определенного программистом заголовочного файла обычно снабжают расширением .h. При этом не нужно беспокоиться об увеличении размеров создаваемой программы. Прототипы функций нужны только на этапе
компи184 Глава 6 ляции и не переносятся в объектный модуль, т.е. не увеличивают машинного кода. А прототипы тех функций, которые не вызываются в программе, вообще не используются компилятором. Например, для тех функций, которые мы определили выше, можно написать такой заголовочный файл: //example, h - прототипы функций из примеров: void printfchar * = "номер страницы", int к = 1); double minfdouble a, double b); double cubefdouble x= 1); int maxfint, int m = 0); void write(void); Как показано ранее, спецификация параметра может содержать его умалчиваемое значение. Это значение используется в том случае, если при обращении к функции соответствующий аргумент опущен. При задании начальных (умалчиваемых) значений должно соблюдаться следующее соглашение. Если параметр имеет умалчиваемое значение, то все параметры, специфицированные справа от него, также должны иметь начальные значения. Например, можно так определить функцию печати: void printfchar* name = "Номер дома: ", int value = 1) { cout« name « value « end!; В зависимости от количества и значений аргументов в вызовах функции она будет выводить такие сообщения: print(); //Выводит: Номер дома: Г print("Номер комнаты: "); //Выводит: Номер комнаты: Г printf, 15) // Ошибка - можно опускать только //параметры, начиная с конца их списка Вот еще один вариант функции вывода с умалчиваемыми значениями параметров: void display (int value = 1, char *name = "Номер дома:") { cout« name « value « endl;} Обращения к ней могут быть такими: displayf); //Выводит: Номер дома: V display(15); //Выводит: Номер дома: 15' displayf6,"Размерность: "); //Выводит: 'Размерность: 6'
Функции, указатели, ссылки 185 6.2. Функции с переменным количеством параметров (аргументов) В языках Си и Си++ допустимы функции, количество параметров у которых при компиляции определения функции не фиксировано. Кроме того, могут быть неизвестными и типы параметров. Количество и типы параметров становятся известными только в момент вызова функции, когда явно задан список аргументов. При определении и описании таких функций, имеющих списки параметров неопределенной длины, спецификация параметров заканчивается многоточием. Формат прототипа функции с переменным списком параметров: тип имя (спецификация_явных_параметров, ...); Здесь тип - тип возвращаемого функцией значения; имя - имя функции; спецификация_явныхпараметров - список спецификаций отдельных параметров, количество и типы которых фиксированы и известны в момент компиляции. Эти параметры можно назвать обязательными. После списка явных (обязательных) параметров ставится необязательная запятая, а затем многоточие, извещающее компилятор, что дальнейший контроль соответствия количества и типов параметров при обработке вызова функции проводить не нужно. Сложность в том, что у переменного списка параметров нет даже имени, поэтому непонятно, как найти его начало и где этот список заканчивается. Каждая функция с переменным списком параметров должна иметь механизм определения их количества и их типов. Принципиально различных подходов к созданию этого механизма всего два. Первый подход предполагает добавление в конец списка реально использованных необязательных аргументов специального аргумента-индикатора с уникальным значением, которое будет сигнализировать об окончании списка. В теле функции параметры последовательно перебираются, и их значения сравниваются с заранее известным концевым признаком. Второй подход предусматривает передачу в функцию значения реального количества аргументов. Значение реального количества используемых аргументов можно передавать в функцию с помощью одного из явно задаваемых (обязательных) параметров. В обоих подходах — и при задании концевого признака, и при указании числа реально
186 Глава 6 используемых аргументов - переход от одного аргумента к другому выполняется с помощью указателей, т.е. с использованием адресной арифметики. Непосредственное программирование этих переходов с помощью указателей имеет серьезный недостаток - зависимость от реализации. Для обеспечения мобильности программ с функциями, имеющими изменяемые списки параметров, в каждый соответствующий стандартам компилятор языка Си (и языка Си++) включается специальный набор макроопределений (макросов). Они становятся доступными при включении в текст программы заголовка <cstdarg>. Макросы, обеспечивающие стандартный (не зависящий от реализации) способ доступа к конкретным спискам аргументов переменной длины, имеют такие заголовки: void va_start(vajist рагат, последний_явный_параметр); type va_arg( vajist param, type); void va_end(vajist param); Кроме перечисленных макросов, в cstdarg определен специальный тип данных vajist, соответствующий потребностям обработки переменных списков параметров. Именно такого типа должны быть первые аргументы, используемые при обращении к макросам va_start(), va_arg(), vajendi). Объясним порядок использования перечисленных макроопределений в теле функции с переменным количеством параметров. Напомним, что каждая из функций с переменным количеством параметров должна иметь хотя бы один явно специфицированный параметр, за которым после необязательной запятой стоит многоточие. В теле функции обязательно определяется объект типа vajist. Например, так: vajist factor; Определенный таким образом объект factor обладает свойствами указателя. С помощью макроса va_start() объект factor связывается с первым необязательным параметром, т.е. с началом списка неизвестной длины. Для этого в качестве второго аргумента при обращении к макросу vajstart{) используется последний из явно специфицированных параметров функции (предшествующий многоточию): vajstart (factor, последний явный _параметр)\ Функции, указатели, ссылки
187 Указатель factor сначала "нацеливается" на адрес последнего явно специфицированного параметра, а затем перемещается на его длину и тем самым устанавливается на начало переменного списка параметров. Именно для этого функция с переменным списком параметров должна иметь хотя бы один явно специфицированный параметр. Теперь с помощью разыменования указателя factor мы можем получить значение первого аргумента из переменного списка. Однако нам неизвестен тип этого аргумента. Тип параметра (и будущего аргумента) нужно каким-то образом передать в функцию. Если это сделано, т.е. известен тип type очередного параметра, то значение аргумента доступно с помощью выражения: *( type *) factor Без этого явного разыменования с приведением типов можно обойтись, используя обращение к макросу vajarg (factor, type) Макрос возвращает значение очередного аргумента типа type, на который «смотрит» указатель. Вторая задача макрокоманды уэ_агд() - заменить значение указателя factor на адрес следующего аргумента в списке. Узнав каким-то образом тип, например type 1, этого следующего параметра (аргумента), можно вновь обратиться к макросу: vajarg (factor, typel) Это обращение позволяет получить значение следующего аргумента и переадресовать (настроить) указатель factor на аргумент, стоящий за ним в списке, и т. д. Макрос va_end() предназначен для организации корректного возврата к началу переменного списка параметров. Его единственным параметром должен быть указатель типа vajist, который использовался в функции для перебора параметров. Таким образом, для наших рассуждений вызов макроса должен иметь вид vajend (factor); Макрос va_end() обычно модифицирует свой аргумент (указатель типа vajist)у и поэтому factor нельзя будет повторно
ис188 Глава 6 пользовать для перебора аргументов без предварительного вызова макроса va_start{). Для иллюстрации особенностей использования описанных макросов рассмотрим несколько примеров. Следующая программа включает функцию с изменяемым списком параметров, первый из которых (единственный обязательный) определяет число действительно используемых при вызове необязательных аргументов. Функция вычисляет сумму целочисленных аргументов, количество которых определено значением первого аргумента. //Р06_02.срр - заданное количество необязательных параметров #include <iostream> #include <cstdarg> //Для макросредств перебора параметров using namespace std; // функция суммирует значения своих //параметров типа int long summafint k,...){ //k- число суммируемых параметров vajist pick; va_start(pick, k); long total = 0; for(; k; k~) total += va_arg(pick, int); return total; } int main() { // Главная функция cout« ”summa(2, 6, 4) = " << summa(2,6,4) « endl; cout« ,fsumma(6t 1, 2, 3, 4, 5, 6) = " << summa(6,1,2,3,4,5,6) « endl; return 0; } Результат выполнения программы: summa(2, 6,4)= 10 summa(6, 1, 2, 3, 4, 5, 6) = 21 Для доступа к списку параметров определен указатель pick. Макрос va_start(pick,k) «настраивает» этот указатель на первый необязательный параметр. Затем в цикле выполняется
обращеФункции, указатели, ссылки 189 ние к макросу va_arg(pick, int) и вычисление суммы в виде значения переменной total. Параметром цикла служит аргумент к, значение которого уменьшается на 1 после каждой итерации и, наконец, становится нулевым. Обратите внимание, что в функции summa аргументы "перебираются" в обратном порядке, по сравнению с их размещением в обращении к функции. Остальное очевидно из текста программы и результатов. Отметим, что переменный список аргументов "перебирается" в функции только один раз и нет необходимости обращаться к макросу vajend(). Следующая программа содержит функцию для вычисления произведения переменного количества аргументов. Признаком окончания списка служит аргумент с нулевым значением. //Р06_03.срр - Индексация конца списка параметров #include <iostream> #include <cstdarg> //для макросредств using namespace std; // Функция вычисляет произведение параметров: double productf double par, ...) { iff par == 0.0) return 0.0; double prod = par; // Формируемое произведение double arg; // Очередной (не первый) аргумент vajist prt; // Указатель для списка аргументов va_start(prt, par); // Настроились на второй параметр whilefarg = vajargfprt, double)) prod *= arg; return prod; } intmainf) { cout« "prod(2e0, 4e0, 3e0, OeO) = " << productf 2e0,4e0,3e0,OeO) « endl; cout« "productf 1.5, 2.0, 3.0, 0.0) = " << productf 1.5,2.0,3.0,0.0) « endl; cout« "productf 1.4, 3.0, 0.0, 16.0, 84.3, 0.0) = cout << productf 1.4,3.0,0.0,16.0,84.3,0.0) « endl; cout« "productfOeO) = "« productf OeO) « endl; return 0; }
190 Глава 6 Результат выполнения программы: prod(2eO, 4е0, ЗеО, ОеО) = 24 productf 1.5, 2.0, 3.0, 0.0) = 9 productf 1.4, 3.0, 0.0, 16.0, 84.3, 0.0) =4.2 product(OeO) = О В функции product() предполагается, что все аргументы имеют тип double. В вызовах функции проиллюстрированы некоторые варианты задания аргументов. Обратите внимание на вариант с нулевым значением аргумента в середине списка. Аргументы вслед за этим значением игнорируются. Чтобы функция с переменным количеством параметров могла воспринимать аргументы различных типов, необходимо в качестве исходных данных каким-то образом передавать ей информацию о типах аргументов. Для однотипных параметров возможно, например, такое решение - передавать с помощью дополнительного обязательного параметра признак типа аргументов. Если параметры функции изменяются и по количеству, и по типам, то признаки типов передавать в функцию достаточно сложно. Поучительным примером таких функций служат библиотечные функции форматного ввода-вывода языка Си: printffchar* format, ...); scanf(char* format, ...) В обеих функциях форматная строка (часто это строковая константа), связанная с указателем format, содержит спецификации преобразования (%d - для десятичных чисел, %е - для вещественных данных в форме с плавающей точкой, %f - для вещественных значений в форме с фиксированной точкой и т.д.). Кроме того, эта форматная строка в функции printf() может содержать произвольные символы, которые переносятся в выходной поток без какого-либо преобразования. Чтобы продемонстрировать особенности построения функций с переменным числом параметров, классики языка Си [28] рекомендуют самостоятельно написать функцию, подобную функции printf(). Последуем их совету, применяя простейшие средства вывода языка Си++. Ограничимся использованием только спецификаций преобразования "%сГ и ”%Г.
Функции, указатели, ссылки 191 //Р06_04.срр - Упрощенный аналог printff) //По мотивам K&R, [28], с. 152 #include <iostream> #include <cstdarg> //Для макросредств using namespace std; void miniprintfchar *format, ...) { vajist ар; // Указатель на необязательный параметр char *р; // Указатель для просмотра строки format int ii; // Целые параметры double dd; // Параметры типа double va_start(ap, format); // Настроились на второй параметр for(p = format; *р; р++) { if (*р !- '%'){ cout« *р; continue; } switch (*++р) { case ’сГ: И = va_arg(ap,int); cout«ii; break; case T: dd = va_arg(ap,double); cout« dd; break; default: cout« *p; } // Конец переключателя } // Конец цикла просмотра строки-формата va_end(ap); // Подготовка к завершению функции } intmainf) { int k = 154; double е = 2.718282; miniprintf "Integer k = %d,\tdouble e = %f", k, e); cout« endl; return 0; } Результат выполнения программы: Integer k= 154, double e = 2.71828
192 Глава 6 Особенностью предложенной функции miniprint() и ее серьезных прародителей - библиотечных функций языка Си printf() и scant() - является использование одного явного параметра и для задания типов последующих аргументов, и для определения их количества. Для этого в форматной строке (в строковой константе, определяющей формат вывода) записывается последовательность спецификаций, каждая из которых начинается символом '%'. Количество спецификаций должно быть в точности равно количеству аргументов в следующем за форматом списке. Конец обмена и перебора параметров определяется по достижению конца форматной строки. 6.3. Рекурсивные функции В классической работе Д. Баррона1, анализируя соотношение между рекурсией и итеративными методами, автор в шутливой форме утверждает, что самой страшной "ересью" в программировании считалась вера (или неверие) в рекурсию. Именно в связи с неоднозначным отношением к рекурсии средства для ее реализации либо вовсе не включались в создаваемые языки программирования, либо языки программирования не реализовывали самые очевидные итерационные методы. В настоящее время дискуссии о целесообразности рекурсии можно считать законченными. В публикациях Н.Вирта2 и в работах других авторов достаточно четко очерчены границы эффективности применения рекурсивного подхода. Его рекомендуют избегать в тех случаях, когда есть очевидное итерационное решение. Например, метод рекурсивного определения факториала удобен для объяснения понятия рекурсии, однако не дает никакого практического выигрыша в программной реализации. Рекурсивные алгоритмы эффективны в тех задачах, где рекурсия использована в определении обрабатываемых данных. Поэтому серьезное изучение рекурсивных методов нужно проводить, вводя такие динамические структуры данных, как стеки, деревья, списки, очереди и другие данные, структура которых может быть задана рекурсивно. Здесь 1 См.: Баррон Д. Рекурсивные методы в программировании.— М.: Мир, 1974: MACDONALD: LONDON, 1969. 2 См.: Вирт Н. Алгоритмы и структуры данных.— М.: Мир, 1989.
Функции, указатели, ссылки 193 же рассмотрим только принципиальные возможности, которые предоставляет язык Си++ для организации рекурсивных алгоритмов. Предварительно отметим, что различают прямую и косвенную рекурсии. Функция называется косвенно рекурсивной в том случае, если она содержит обращение к другой функции, содержащей прямой или косвенный вызов определяемой (первой) функции. В этом случае по тексту определения функции ее рекурсивность (косвенная) может быть не видна. Если в теле функции явно используется вызов именно этой функции, то имеет место прямая рекурсия, т.е. функция, по определению, рекурсивная (иначе — самовызываемая или самовызывающая: self-calling). Классический пример — функция для вычисления факториала неотрицательного целого числа: long factfint к) { if (к < 0) return 0; if (к == 0) return 1; return к * fact(k-1); } Для отрицательного аргумента результат, по определению факториала, не определен. В этом случае функция возвратит нулевое значение. Для нулевого параметра функция возвращает значение 1, так как, по определению, 0! равен 1. В противном случае вызывается та же функция с уменьшенным на 1 значением параметра, и результат умножается на текущее значение параметра. Тем самым для положительного значения параметра к организуется вычисление произведения к *(к-1) *(к-2) *... *3 *2 * 1 * 1 Обратите внимание, что последовательность рекурсивных обращений к функции fact прерывается только при вызове fact(0). Именно этот вызов приводит к последнему множителю (единичному) в произведении, так как последнее выражение, из которого вызывается функция, имеет вид: 1 * fact( 1 -1) Так как в языке Си++ отсутствует операция возведения в степень, то следующая рекурсивная функция вычисления целой степени вещественного ненулевого числа может оказаться полезной: 13- 2762
194 Глава 6 double ехро(double a, int п) { if(n == 0) return 1; if (a == 0) return 0; if(n > 0) return a * expofa, n-1); if(n < 0) return expofa, n+1)/a; } При обращении вида expo(2.0, 3) рекурсивно выполняются вызовы функции ехро() с изменяющимся вторым аргументом: ехро(2.0,3), ехро(2.0,2), ехро(2.0,1), ехро(2.0,0). При этих вызовах последовательно вычисляется произведение 2.0 *2.0 * 2.0 * 1 и формируется нужный результат. Вызов функции для отрицательного значения степени, например ехро(5.0,-2), эквивалентен вычислению выражения ехро(5.0,0)/5.0/5.0 Отметим некоторую математическую неточность. В функции ехро() для любого показателя при нулевом основании результат равен нулю, хотя возведение в нулевую степень нулевого основания должно приводить к неопределенной ситуации. В качестве еще одного примера рекурсии рассмотрим функцию определения с заданной точностью eps корня уравнения f(x) = 0 на отрезке [а, Ь]. Предположим для простоты, что исходные данные задаются без ошибок, т.е. eps > 0, b > а, и вопрос о возможности нескольких корней на отрезке [а, Ь] нас не интересует. Не очень эффективная рекурсивная функция для решения поставленной задачи содержится в следующей программе: //Р06_05.срр - Определение корня математической функции #include <iostream> #include <cmath> //Для библиотечных математических функций using namespace std; #include "cyrToDos.h” // Счетчик обращений к исследуемой функции: static int counter = 0; // Определение математической функции: double f( double х) {extern int counter;
Функции, указатели, ссылки 195 counter++; // Подсчет обращений return (2.0/х * cos(x/2.0)); } // Рекурсивная функция для поиска корня математической //функции Ц) методом деления пополам: double recRootf double a, double b, double eps) { double fa = f(a), fb = f(b), c, fc; if (fa *fb>0){ cout« "Неверен интервал локализации корня!"; exit(1); } с = (а + b)/2.0; // Середина отрезка fc = ffc); // Значение функции if(fc == 0.0 || b - а < eps) return с; return (fa * fc < 0.0) ? recRootf a, c, eps): recRootfc, b, eps); ) intmainf) { double root, a = 0.1, //Левая граница интервала b = 3.5, // Правая граница интервала eps = 5е-5; // Точность локализации корня root = recRootf a, b, eps); cout« cyrToDosf "Число обращений к тестовой функции = ") « counter « endl; cout« cyrToDosf "Корень = ") « root« endl; return 0; } Результат выполнения программы: Число обращений к тестовой функции = 54 Корень = 3.141601 В рассматриваемой программе использована библиотечная функция exit(). Функция exit() завершает выполнение программы и возвращает операционной системе значение своего параметра. Неэффективность предложенной программы связана, например, с излишним количеством обращений к программной реализации функции, для которой определяется корень. При каждом 13*
196 Глава 6 рекурсивном вызове recRoot() повторно вычисляется значение f(a), f{b), хотя они уже известны после предыдущего вызова. Предложите свой вариант исключения лишних обращений к f() при сохранении рекурсивности. 6.4. Подставляемые (inline-) функции Некоторые функции в языке Си++ можно определить с использованием специального служебного слова inline. Спецификатор inline позволяет определить функцию как встраиваемую, иначе говоря, подставляемую или "открыто подставляемую", или "инлайн-функцию”. Например, следующая функция определена как подставляемая: inline double modulef double x = 0, double y = 0) { return sqrtfx *x + y *y); } Функция module^) возвращает значение типа double, равное "расстоянию" от начала координат на плоскости до точки с координатами (х,у), определяемыми значениями аргументов. В теле функции вызывается библиотечная функция sqrt() для вычисления вещественного значения квадратного корня положительного аргумента. Так как подкоренное выражение в функции всегда неотрицательно, то специальных проверок не требуется. Обрабатывая каждый вызов встраиваемой функции, компилятор "пытается" подставить в текст программы код операторов ее тела. Спецификатор inline для функций, не принадлежащих классам (о последних будем говорить в связи с классами), определяет для функций внутреннее связывание. Во всех других отношениях подставляемая функция является обычной функцией, т.е. спецификатор inline в общем случае не влияет на результаты вызова функции, она имеет обычный синтаксис определения и описания, подчиняется всем правилам контроля типов и области действия. Однако вместо команд передачи управления единственному экземпляру тела функции компилятор в каждое место вызова функции помещает соответствующим образом настроенные команды кода операторов тела функции. Тем самым при многократных вызовах подставляемой функции размеры программы могут увеличиться,
Функции, указатели, ссылки 197 однако исключаются затраты на передачи управления к вызываемой функции и возвраты из нее. Кроме экономии времени при выполнении программы, подстановка функции позволяет проводить оптимизацию ее кода в контексте, окружающем вызов, что в ином случае невозможно [2]. Наиболее эффективно использовать подставляемые функции случаях, когда тело функции состоит всего из нескольких операторов. Идеальными претендентами на определение со спецификатором inline являются несложные короткие функции. Удобны для подстановки функции, основное назначение которых — вызов других функций либо выполнение преобразований типов. Так как компилятор встраивает код подставляемой функции вместо ее вызова, то определение функции со спецификатором inline должно находиться в том же исходном файле, что и обращение к ней, и размещаться до первого вызова. Синтаксис языка не гарантирует обязательной подстановки кода функции для каждого вызова функции со спецификатором inline. Определение допустимости открытой подстановки функции в общем случае невозможно [2]. Например, следующая функция, по-видимому, не может быть реализована как подставляемая даже в том случае, если она определена со спецификатором inline [2]: inline void Ц) { char ch = 0; if (cin » ch&&ch != 'q') f(); } Функция f() в зависимости от вводимого извне значения переменной ch либо просто возвращает управление, либо рекурсивно вызывает себя. Допустима ли для такой функции подстановка вместо стандартного механизма вызова? Это определяется реализацией... Следующий случай, когда подстановка для функции со спецификатором inline проблематична, — вызов этой функции с помощью указателя на нее (т.е. с помощью ее адреса, см., например, разд. 6.6). Реализация такого вызова, как правило, будет выполняться с помощью стандартного механизма обращения к функции. Следующие причины могут препятствовать реализации функции как подставляемой даже в том случае, когда функция определена со спецификатором inline:
198 Глава 6 • функция слишком велика, чтобы выполнить ее подстановку; • функция рекурсивна; • обращение к функции в программе размещено до ее определения; • функция вызывается более одного раза в выражении. Однако ограничения на выполнение подстановки в основном зависят от реализации. Если же для функции со спецификатором inline компилятор не может выполнить подстановку из-за контекста, в который помещено обращение к ней, то функция считается статической (static), и обычно выдается предупреждающее сообщение. Хотя теоретически подставляемая функция ничем не отличается по результатам от обычной функции, существует несколько особенностей, которые следует учитывать, "подсказывая" компилятору с помощью спецификатора inline в определении функции целесообразность подстановок. Так как порядок вычисления аргументов функций не определен синтаксисом языка Си++, то возможна различная реализация вычисления аргументов при обычном вызове функции и при ее подстановке. Возможна ситуация, когда функция со спецификатором inline в одной и той же программе имеет две формы реализации вызова — подстановкой и стандартным механизмом обращения к функции. При этом разные формы вызова могут реализовывать разный порядок вычисления аргументов, и два обращения к одной и той же функции с одинаковыми аргументами могут привести к различным результатам. Еще одна особенность подставляемых функций — невозможность их изменения без перекомпиляции всех частей программы, в которых эти функции вызываются. 6.5. Функции и массивы Массивы могут быть параметрами функций, и функции могут возвращать указатель на массив в качестве результата. Рассмотрим эти возможности. При передаче массивов через механизм параметров возникает задача определения в теле функции количества элементов мае-
Функции, указатели, ссылки 199 сива, использованного в качестве аргумента. При работе с массивами типа char[ ], представляющими строки в стиле Си, последний элемент имеет значение '\0\ и затруднений практически нет. Анализируется каждый элемент, пока не встретится символ '\0', и это считается концом строки-массива. В следующей программе введена функция len() для определения длины строки в стиле Си, передаваемой в функцию с помощью параметра: //Р06_06.срр - Массивы-строки в качестве параметров #include <iostream> using namespace std; #include "cyrToDos.h” int lenfchar e[ ]) { int m = 0; while (e[m++]); return m - 1; } intmainf) { char str[] = "Pro Tempore!";// "Своевременно"(лат.) cout« cyrToDosf "Длина строки \ "Pro Tempore!\"равна ") «len(str)« endl; return 0; } Результат выполнения программы: Длина строки "Pro Tempore!"равна 12 В теле функции len() строка-параметр воспринимается как массив, и обращение к его элементам выполняется с помощью явного индексирования. Если массив-параметр функции не есть строка в стиле Си, то нужно либо использовать только массивы фиксированного, заранее определенного размера, либо передавать значение размера массива в функцию явным образом. Часто это делается с помощью дополнительного параметра. Следующая программа иллюстрирует эту возможность на примере функции для вычисления косинуса угла между двумя многомерными векторами, каждый из которых представлен одномерным массивом-параметром:
200 Глава 6 //Р06_07.срр - Массивы в качестве параметров #include <iostream> #include <cmath> //Для математических функций using namespace std; double cosinusfint n, double x[ ], double y[ ]) { double a = 0, b = 0, c = 0; for (int i = 0; i < n; i++) { a += x[i] * y[i]; b +=x[i] * x[i]; c += y[i] * y[i]; } return a/sqrt(double(b * c)); } intmainf) { double vec1[] = { 1, 1, 1, 1, 1, 1, 1}; double vec2[] = {-1, -1, -1, -1, -1, - 7, -1}; cout« "Cos(vec1, vec2) = " << cosinus(7, vec1, vec2) « endl; return 0; } Результат выполнения программы: Cos(vec1, vec2) = -1 Так как имя массива есть указатель, адресующий начало массива, то любой массив, имя которого используется в качестве аргумента, может быть изменен за счет выполнения операторов тела функции. Например, в следующей программе функция max_vect() формирует массив, адресованный указателем z. Каждый элемент этого массива равен максимальному из значений соответствующих элементов двух других массивов, адресованных параметрами-указателями х и у: //Р06_08.срр - Указатели на массивы в качестве параметров ft include <iostream> using namespace std; void max_vect(int n, int *x, int *y, int *z) { for (int i = 0; i < n; /++; z[i] = x[i]>y[i]?x[i]:y[i]; }
Функции, указатели, ссылки 201 intmainf) { int а[ ] = { 1, 2, 3, 4, 5, 6, 7}; int b[ J= { 7, 6, 5, 4, 3, 2, 1}; int c[7]; max_vect(7,a,b,c); for (inti -0; i < 7; i++) cout« "\t" « c[i]; cout« end!; return 0; } Результат выполнения программы: 7 6 5 4 5 6 7 Параметр int п служит для определения размеров массивов, адресованных параметрами-указателями. В качестве функции, возвращающей указатель на массив, рассмотрим функцию, формирующую новый массив на основе двух целочисленных массивов, элементы в каждом из которых упорядочены по неубыванию. Новый массив должен включать все элементы двух исходных массивов таким образом, чтобы они оказались упорядоченными по неубыванию. //Р06_09.срр - Функция, возвращающая указатель на массив #include <iostream> using namespace std; ft include "cyrToDos.h" // функция "слияния"двух упорядоченных массивов int *fusion(int n, int* a, intm, int* b) { int *x = new int[n + m]; // Массив для размещения результатов int ia = 0, ib = 0, ix = 0; while (ia < n && ib < m) //До конца одного массива x[ix++] = (a[ia] > b[ib] ? b[ib++] : a[ia++]); if (ia >= n) // Переписан массив a[ ] while (ib < m) x[ix++] = b[ib++]; else // Переписан массив b[ ] while (ia < n) x[ix++] = a[ia++]; return x; }
202 Глава 6 int main(void) { int c[] = { 1, 3, 5, 7, 9}; int d[ ] = { 0, 2, 4, 5 }; int *h; // Указатель для массива результатов int kc = sizeof(c)/sizeof(c[0]); //Количество элементов int kd = sizeof(d)/sizeof(d[0]); // Количество элементов h = fusionfkc, c, kd, d); cout« cyrToDosf "Объединение массивов:") « endl; for (int i = 0; i < kc + kd; i++) cout << " "« h[i]; cout« endl; delete[] h; return 0; } Результат выполнения программы: Объединение массивов: 012345579 Особенность и в некотором смысле недостаток языка Си++ (и его предшественника языка Си) — несамоопределенность массивов, под которой понимается невозможность по имени массива (по указателю на массив) определять его размерность и размеры по каждому измерению. Несамоопределенность массивов затрудняет их использование в качестве параметров функций. Действительно, простейшая функция — транспонирование квадратной матрицы — требует, чтобы ей были известны не только имя массива, содержащего элементы матрицы, но и размеры этой матрицы. Если такая функция транспонирования матрицы для связи по данным использует аппарат параметров, то в число параметров должны войти указатель массива с элементами матрицы и целочисленный параметр, определяющий размеры матрицы. Однако здесь возникают затруднения, связанные с одним из принципов языков Си и Си++. По определению, многомерные массивы как таковые не существуют. Если мы описываем массив с несколькими индексами, например, так: double prim[ 6] [4] [2]; то мы описываем не трехмерный, а одномерный массив с именем prim, включающий шесть элементов, каждый из которых имеет
Функции, указатели, ссылки 203 тип double[4][2]. В свою очередь каждый из этих элементов есть одномерный массив из четырех элементов типа double[2]. Мы не случайно еще раз остановились на особенностях синтаксиса и представления многомерных массивов. Дело в том, что эти тонкости не бросаются в глаза при обычном определении массива, когда его размеры (и размерность) фиксированы и явно заданы в определении. Однако при необходимости передать с помощью параметра в функцию многомерный массив начинаются неудобства и неприятности. Вернемся к функции транспонирования матрицы. Наивное, очевидное, но неверное решение — определить заголовок функции таким образом: void transponir(double х[ ][ ], int п)... Здесь п - предполагаемый порядок квадратной матрицы; double х[ ][ ]; — неверная попытка специфицировать двумерный массив с заранее неизвестными размерами. На такую попытку транслятор отвечает гневным сообщением: ErrorSize of type is unknown or zero И он прав — при описании массива (и при спецификации массива-параметра) неопределенным может быть только первый (самый левый) размер. Вспомним — массив всегда одномерный, а его элементы должны иметь известную и фиксированную длину. В массиве х[ ][ ] не только неизвестно количество элементов одномерного массива, но и ничего не сказано о размерах этих элементов. Примитивнейшее разрешение проблемы спецификации двумерного массива иллюстрирует такой заголовок функции для транспонирования матриц: void transpfint п, double d[ ][3]) Примитивность и неэффективность продемонстрированного решения состоят в том, что в функции transpO массив-параметр специфицирован с фиксированным вторым размером, т.е. транспонируемая квадратная матрица может быть только с размерами 3x3. Указанные ограничения на возможность применения многомерных массивов в качестве параметров можно обойти несколькими путями. Первый путь — подмена многомерного массива
од204 Глава 6 номерным и имитация внутри функции доступа к многомерному массиву. (Здесь будут полезны макроопределения с индексами в качестве параметров.) Второй путь - использование вспомогательных массивов указателей на массивы. Третий путь предусматривает применение классов для представления многомерных массивов. О классах будет сказано позже. Подробно остановимся только на представлении и передаче матриц (двумерных массивов) с использованием вспомогательных массивов указателей на одномерные массивы. Одномерные массивы служат в этом случае для представления строк матриц. Так как и вспомогательный массив указателей, и массивы-строки матрицы являются одномерными, то их размеры могут быть опущены в соответствующих спецификациях параметров. Тем самым появляется возможность обработки в теле функции двумерных, а в более общем случае и многомерных массивов с изменяющимися размерами. Конкретные значения размеров должны передаваться в тело функции либо с помощью дополнительных параметров, либо с использованием глобальных (внешних) переменных. Следующая программа иллюстрирует один из способов передачи в функцию информации о двумерном массиве, размеры которого заранее неизвестны. Функция trans() выполняет транспонирование квадратной матрицы, определенной вне тела функции в виде двумерного массива. Параметры функции: intn - порядок матрицы; double *р[ ] - массив указателей на одномерные массивы элементов типа double. В теле функции обращение к элементам обрабатываемого массива осуществляется с помощью двойного индексирования. Здесь p[i] - указатель на одномерный массив (на строку матрицы с элементами типа double), p[i][j] - обращение к конкретному элементу двумерного массива. Текст программы: //Р06_10.срр - Массив-параметр указателей на массивы #include <iostream> using namespace std; // функция транспонирования квадратных матриц void transfintn, double *p[ ]) { double x; forfint i = 0; i < n - 1; i++)
Функции, указатели, ссылки 205 for(intj = / + 1; j < п; j++) { x = p[i][j]; p[i]{j]=PU][i]; рШШ=х; } } int main() {// Матрица для транспонирования: double А[4] [4] = { 11, 12, 13, 14, 21, 22, 23, 24, 31, 32, 33, 34, 41, 42, 43, 44 }; // Вспомогательный одномерный массив указателей: double *ptr[] = {(double *)&А[0], (double *)&А[ 1 ], (double *)&А[2], (double *)&А[3] }; int n - 4; transfn, ptr); //Печать результатов обработки матрицы: for (int i = 0; i < n; i++) // Перебор строк { cout« ”Line " «(i+1) « // Цикл печати элементов строки: for (int j = 0; j < n; j++) cout« "\f" << A[i][j]; cout« endl; } return 0; } Результаты выполнения программы: Line 1: 11 21 31 41 Line 2: 12 22 32 42 Line3: 13 23 33 43 Line 4: 14 24 34 44 В основной программе матрица представлена двумерным массивом с фиксированными размерами double >4[4][4]. Такой массив (его имя) нельзя непосредственно использовать в качестве аргумента вместо параметра со спецификацией double *р[ ]. Поэтому вводится дополнительный вспомогательный массив указателей double *ptr[ ]. В качестве значений элементам этого
206 Глава 6 массива присваиваются адреса строк матрицы, т.е. &Л[0], &А[ 1], &А[2], &А[3], преобразованные к типу double*. Дальнейшее очевидно из текста программы. В следующей программе матрица формируется в основной программе как совокупность одномерных динамических массивов - строк матрицы и динамического массива указателей на эти массивы-строки. Элементы массива указателей имеют тип mf*, с массивом указателей в целом связывается указатель int **pi. Для простоты опущены проверки правильности выделения памяти и выбрано фиксированное значение (т == 3) порядка матрицы. Функция fil\() присваивает элементам квадратной матрицы значения "подряд": а[0][0] = 0, а[0][1] = 1 ... и т.д. Таким образом, а[/] [/] = (/ * п) + у, где п - порядок матрицы. В иллюстративных целях указатель int** mat на массив указателей на строки матрицы специфицирован в заголовке без использования квадратных скобок. Первый из параметров функции fill() со спецификацией int п определяет размеры квадратной матрицы. Такие же параметры у функции matPrint(). Ее назначение — вывод по строкам значений элементов квадратной матрицы. Текст программы: //Р06_11.срр - Матрица как набор одномерных массивов #include <iostream> using namespace std; // функция, определяющая значения элементов матрицы void fillfint n, int** mat) { int k = 0; for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) mat[i][j] = /C++; // Функция печати квадратной матрицы: void matPrintf int n, int ** matr) { for (int i = 0; i < n; /++; (// Цикл перебора строк cout « "Line " << (i + 1) « // Цикл печати элементов строки: for (int j = 0; j < n; j++) cout« "\t"« matr[i][j]; cout« endl; }
Функции, указатели, ссылки 207 intmainf) { int **pi; // Указатель на массив указателей int m = 3; //Размеры массивов, т.е. порядок матрицы //Динамические массивы для представления матрицы: pi = new int* [m]; // Вспомогательный массив указателей for (int i = 0; i < m; i++) pi[i] = new int [m]; // Формируем строки (одномерные // массивы) fill(m, pi); // Заполнение матрицы matPrintfm, pi);//Печать матрицы for (int i = 0; i < m; i++) delete pi[i]; delete[ ] pi; return 0; ) Результаты выполнения программы: Line 1: 0 1 2 Line 2: 3 4 5 Line 3: 6 7 8 Так как матрица создается как набор динамических массивов, то в конце программы помещены операторы delete для освобождения памяти. Многомерный массив с переменными размерами, сформированный в функции, непосредственно невозможно вернуть в вызывающую программу как результат выполнения функции. Однако, возвращаемым функцией значением может быть указатель на одномерный массив указателей на одномерные массивы. В следующей программе функция single_matr() возвращает именно такой указатель типа int **. В тексте функции формируется набор одномерных массивов с элементами типа int и создается массив указателей на эти одномерные массивы. Количество одномерных массивов и их длины определяются параметром функции, специфицированном как int п. Совокупность создаваемых динамических массивов представляет квадратную матрицу порядка п. Диагональным элементам матрицы присваиваются единичные значения, остальным - нулевые, т.е. матрица
заполняет208 Глава 6 ся как единичная диагональная. Локализованный в функции single_matr() указатель mf** р "настраивается" на создаваемый динамический массив указателей и используется в операторе возврата из функции, т. е. возвращается его значение. В основной программе вводится с клавиатуры желаемое значение порядка матрицы (intn), а после ее формирования печатается результат. В программе используется функция matPrint(), определенная в предыдущей программе. Ее текст из файла matPrint.h включен в программу препроцессорной директивой #include "matPrint.h". Текст программы: //Р06_12.срр - Единичная матрица с изменяемым порядком #include <iostream> #include "cyrToDos.h" using namespace std; // функция, формирующая единичную матрицу: int ** single_matr(int n) // n - нужный размер матрицы {// Вспомогательный указатель на формируемую матрицу: int ** р; // Массив указателей на строки - одномерные массивы: р = new int* [n]; if (Р == 0) { cout << cyrToDosC'He создан динамический массив!"); exit( 1); } //Цикл создания одномерных массивов: for (int i = 0; i < n; i++) { // Формирование строки элементов типа int: p[i] = new int [n]; if (p[i] == 0) { cout« cyrToDos(''He создан динамический массив!"); exit( 1); } // Цикл заполнения строки: for (int j = 0; j < n; j++) P[i][j] = (j!=i?0: 1); } return p; }
Функции, указатели, ссылки 209 // функция печати квадратной матрицы: #include "matPrint.h" intmainC) { int n; // Порядок матрицы cout« cyrToDosCВведите порядок матрицы: "); cin » n; int ** matr; // Указатель для формируемой матрицы //Обращение к функции для создания единичной матрицы: matr = single_matr(n); // Обращение к функции для вывода матрицы: matPrintfn, matr); //Очистка памяти от динамических массивов: for (int i = 0; i < n; i++) delete [] matr[i]; delete [ ] matr; return 0; > Результат выполнения программы: Введите порядок матрицы: 4<ENTER> Line 1: 1 0 0 0 Line 2: 0 1 0 0 Line 3: 0 0 1 0 Line 4: 0 0 0 1 Обратите внимание на тот факт, что динамические массивы создаются в функции, вызванной из основной программы, а обращение к ним выполняется как из функции matPrmtQ, так и из текста основной программы. Здесь же (в функции main) память освобождается от динамических массивов перед окончанием программы. 6.6. Указатели на функции Прежде чем вводить указатель на функцию, напомним, что в понятие «тип функции» входят: тип возвращаемого функцией значения, спецификация параметров, cv-квалификаторы и спецификация исключений. Последние два элемента пока рассматривать рано. Обратите внимание, что имя функции никак не вли- ,4-2762
210 Глава 6 яет на ее тип. При использовании имени функции без последующих скобок и параметров имя функции выступает в качестве указателя на эту функцию, и его значением служит адрес размещения кода функции в памяти. Это значение адреса может быть присвоено другому указателю, и затем уже этот новый указатель можно применять для вызова функции. Однако в определении нового указателя должен быть тот же тип, что и тип функции. Указатель на функцию определяется следующим образом: тип (*имяуказателя)( спецификацияпараметров); Например: int(*func1 Ptr)(char)\ - определение указателя с именем funcIPtr на функцию с параметром типа char, возвращающую значение типа int. Если приведенную синтаксическую конструкцию записать без первых круглых скобок, т.е. в виде int * fun(char); то компилятор воспримет ее как прототип некой функции с именем fun и параметром типа char, возвращающей значение указателя типа int *. Второй пример: char *(*func2Ptr)(char *, int); - определение указателя с именем func2Ptr на функцию с двумя параметрами типа указатель на char и типа inf, возвращающую значение типа указатель на char. Определенному таким образом указателю можно с помощью операции присваивания либо инициализацией присваивать адреса функций, имеющих тот же тип, что и указатель. В качестве простейшей иллюстрации сказанного приведем программу с указателем на функцию: //Р06_13.срр - Указатели на функции #include <iostream> using namespace std; void f1() { // Определение f1 cout« "Call f1()"« endl; } void f2() { // Определение f2 cout« ”Call f2()"« endl; } int main() { void (*ptr)(void); //ptr - Указатель на функцию ptr = f2; // Присваивается адрес f2()
Функции, указатели, ссылки 211 (*ptr)(); //Вызов f2() по ее адресу ptr = f1; //Присваивается адрес f1() (*ptr)(); //Вызов f1() по ее адресу ptr( )i // Вызов эквивалентен (*ptr)(); return 0; } Результат выполнения программы: Call f2() Callf1() Call f1() В программе описан указатель ptr на функцию, и ему последовательно присваиваются адреса функций /2 и f1. Заслуживает внимания форма вызова функции с помощью указателя на функцию (*имя_указателя)(список_аргументов); Здесь значением имени указателя служит адрес функции, а с помощью операции разыменования * обеспечивается обращение по адресу к этой функции. Будет ошибкой записать вызов функции без скобок в виде *ptr(). Дело в том, что операция ( ) имеет более высокий приоритет, чем операция * обращения по адресу. Следовательно, в соответствии с синтаксисом будет вначале сделана попытка обратиться к функции ptr(), а к результату будет отнесена операция разыменования, что будет воспринято как синтаксическая ошибка. При определении указатель на функцию может быть инициализирован. В качестве инициализирующего значения должен использоваться адрес функции, тип которой соответствуют типу определяемого указателя. Присваивая значения указателям на функции, необходимо, как обычно, соблюдать соответствие типов правой и левой частей операции присваивания. То же справедливо и при последующем вызове функций с помощью указателей, т.е. типы и количество аргументов, используемых при обращении к функции по адресу, должны соответствовать параметрам вызываемой функции. Например, только некоторые из следующих операторов будут допустимы: 14*
212 Глава 6 char f1(char) {...} //Определение функции char f2(int) {...} //Определение функции void f3(double) (...) //Определение функции int* f4(char *){...} //Определение функции char (*pt 1 Hint); // Указатель на функцию char (*pt2)(int); // Указатель на функцию void (*ptr3)( double) = f3; // Инициализированный указатель pt1 = f1; pt2 = f3; ptl = f4; pt1 = f2; pt2 = pt1; char c=(*pt1)( 44); c=(*pt2)('\t'); //Ошибка - несоответствие параметров // Ошибка - несоответствие типов // Ошибка - несоответствие типов //Правильно // Правильно // Правильно // Ошибка - несоответствие параметров Следующая программа иллюстрирует некоторые возможности механизма вызовов функций с помощью указателей. //Р06_14.срр - Вызов функций по адресам через указатель #include <iostream> using namespace std; #include ,fcyrToDos.hff // Функции одного типа: int addfint n, intm) {return n + m;} intdivide(intn, intm) { return n/m; } int multfint n, int m) { return n * m; } int subtfint л, int m) { return n - m; } intmainf) { int (*par)(int, int); // Указатель на функцию int a = 6, b = 2; char ch = while (ch /= '') { cout« cyrToDosTАргументы: a = ") « a « ", b = ” << b; cout« cyrToDosC. Результат для ch = \ "') « ch « W cout« cyrToDosf" равен "); switch (ch) { case '+7 par = add; ch = /7 break; case ’-7 par = subt; ch = ' 7 break; case '*7 par = mult; ch = -7 break;
Функции, указатели, ссылки 213 case '/': par = divide; ch = '*'; break; } cout < < (a = (*par)(a, b)); // Вызов по адресу cout« endl; } return 0; } Результат выполнения программы: Аргументы: а = 6, b = 2. Результат для ch = '+' равен 8 Аргументы: а = 8, to = 2. Результат для ch = '/'равен 4 Аргументы: а = 4, Ь = 2. Результат для ch = равен 8 Аргументы: а = 8, b = 2. Результат для ch = '-'равен 6 Цикл продолжается, пока значением переменной ch не станет пробел. В каждой итерации указатель par получает адрес одной из функций, и изменяется значение ch. По результатам программы легко проследить порядок выполнения ее операторов. Указатели на функции могут быть объединены в массивы. Например, double (*ptrArray)(char)[4]\ - массив с именем ptrArray из четырех указателей на функции, каждая из которых имеет параметр типа char и возвращает значение типа double. Чтобы обратиться, например, к третьей из этих функций, потребуется такой оператор: double а =(*ptrArray[2])(Т); Как обычно, индексация массива начинается с 0, и поэтому третий элемент массива имеет индекс 2. Приведенное описание массива ptr Array указателей на функции не особенно наглядное и ясное. Для функций с большим количеством параметров и сложными типами возвращаемых значений оно может стать и вовсе непонятным. Ситуация особенно усложняется, когда такое описание типа должно входить в более сложные описания, например, когда указатели функций и их массивы используются в качестве параметров других функций. Для удобства последующих применений и сокращения производных описаний рекомендуется с помощью спецификатора typedef вводить имя типа указателя на функции. Примеры:
214 Глава 6 typedef double (*PTD)(double); typedefchar *(*PTC)(char); typedef void (+PTDUNC)(PTD, int, double); Здесь PTD - имя типа "указатель на функцию с параметром типа double, возвращающую значение типа double44. РТС - имя типа "указатель на функцию с параметром типа char, возвращающую указатель на тип char44. PTDUNC - имя типа "указатель на функцию, возвращающую пустое значение (типа void)44. Параметрами для этой функции служат: PTD - указатель на функцию типа double имя (double), значение типа int и значение типа double. (В определение имени типа PTDUNC вошел только что определенный тип с именем PTD.) Введя имена типов указателей на функции, проще описать соответствующие указатели, массивы и другие производные типы: PTD ptdouble 1, ptdouble2[5]; // Указатель и массив //указателей на функции типа double HMB(double) PTC ptchar; // Указатель на функцию типа char *(char) PTDUNC ptfunc[8]; // Массив указателей на функции Массивы указателей на функции удобно использовать при разработке программ, управление которыми выполняется с помощью меню. Для этого действия, предлагаемые на выбор будущему пользователю программы, оформляются в виде функций, адреса которых помещаются в массив указателей на функции. Пользователю предлагается выбрать из меню нужный ему пункт (в простейшем случае он вводит номер выбираемого пункта), и по номеру пункта, как по индексу, из массива выбирается соответствующий адрес функции. Обращение к функции по этому адресу обеспечивает выполнение требуемых действий. Самую общую схему реализации такого подхода иллюстрирует следующая программа для обработки файлов: //Р06_15.срр - Массив указателей на функции #include <iostream> using namespace std; / #include ,,cyrToDos.h,, //Определение функций для обработки меню: void act1(char* name)
Функции, указатели, ссылки 215 {cout« cyrToDosf']Действия по созданию файла ")«name«endl;} void act2(char* name) { cout « cyrtoDos("Действия по уничтожению файла ") <<name«endl;} void act3(char* name) {cout« cyrToDos(,fДействия по чтению файла ")«name«endl;} void act4(char* name) { cout « cyrToDosf "Действия по модификации файла ") <<name«endl;} void act5(char* name) {cout« cyrToDosf "Действия по закрытию файла.") « endl; exitfO); // Завершить программу } // Тип MENU указателей на функции типа void (char *): typedef void(*MENU)(char *); // Инициализация таблицы адресов функций меню: MENU MenuAct[5] = {act1, act2, act3, act4, act5}; int main() { int number; // Номер выбранного пункта меню char FileName[30]; // Строка для имени файла cout« cyrToDosf "СПИСОК ПУНКТОВ МЕНЮ: ") « endl; cout« cyrToDosf "1 - создание файла") « endl; cout« cyrToDosf "2 - уничтожение файла") « endl; cout« cyrToDosf "3 - чтение файла") « endl; cout« cyrToDosf "4 - модификация файла") « endl; cout« cyrToDosf "5 - выход из программы") « endl; while (1) // Бесконечный цикл {while (1) { //Цикл продолжается до ввода правильного номера cout« cyrToDosf "Введите номер пункта меню:"); cin » number; if (number >= 1 && number <= 5) break; cout« cyrToDosf"Нет такого пункта меню!") « endl; } if (number != 5) {cout« cyrToDosf "Введите имя файла: "); cin » FileName; // Читать имя файла } // Вызов функции по указателю на нее:
216 Глава 6 (*MenuAct[number-1 ])(FileName); } // Конец бесконечного цикла return 0; } При выполнении программы возможен, например, такой диалог: СПИСОК ПУНКТОВ МЕНЮ: 1 - создание файла 2 - уничтожение файла 3 - чтение файла 4 - модификация файла 5 - выход из программы Введите номер пункта меню: 1<ENTER> Введите имя файла: a.t<ENTER> Действия по созданию файла a.t Введите номер пункта меню: 2<ENTER> Введите имя файла: b.t<ENTER> Действия по уничтожению файла b. t Введите номер пункта меню: 3<ENTER> Введите имя файла: c.t<ENTER> Действия по чтению файла с. t Введите номер пункта меню: 4<ENTER> Введите имя файла: d.t<ENTER> Действия по модификации файла d.t Введите номер пункта меню: 5<ENTER> Действия по закрытию файла. Пункты меню повторяются, пока не будет введен номер 5 — выход из программы. Указатели на функции — незаменимое средство языков Си и Си++, когда объекты обработки должны быть заданы алгоритмически. Например, создавая процедуры для вычисления определенного интеграла задаваемой пользователем математической функции или для построения таблицы значений математической функции, нужно иметь возможность передавать в процедуру имя функции, реализующей нужный алгоритм (математическую функцию). Удобнее всего организовать связь между функцией, реализующей метод обработки (например, численный метод
инФункции, указатели, ссылки 217 тегрирования), и той функцией, для которой этот метод нужно применить, через аппарат параметров, в число которых входят указатели на функции. В качестве примера рассмотрим задачу вычисления корня математической функции f(x) на интервале локализации [А; В]. Численный метод (деление пополам интервала локализации) оформляется в виде функции со следующим заголовком: double гооЦуказатель_на_функцию, double A, double В, double EPS) Здесь Л — нижняя граница интервала локализации корня; В — верхняя граница того же интервала; EPS — требуемая точность определения корня. Для удобства введем обозначение типа "указатель на функцию": typedef doublet *pointFunc)(double); С использованием указателя pointFunc прототип функции root() можно определить так: double root(pointFunc F, double A, double B, double EPS); В следующей программе функция root() определена полностью и вычисляется корень функции у = х2 - 1 на интервале [0.5, 2.0]: //Р06_16.срр - Указатель функции как параметр функции #include <iostream> using namespace std; #include "cyrToDos.h" // Обозначение для типа указатель на функцию: typedef doublet *pointFunc)(double); // Определение функции для вычисления корня: double root(pointFunc F, double A, double B, double EPS) { double x, y, c, Fx, Fy, Fc; x-А; у - В; Fx = ( *F)(x); // Значение функции на левой границе Fy-( *F)(y); // Значение функции на правой границе if (Fx * Fy > 0.0 || х>у) { cout« cyrToDos( ”Неверен интервал локализации"); ехЩ 1); // Аварийное завершение программы }
218 Глава 6 do { с = (у + х)/2; // Центр интервала локализации Fc = (*F)(c); // Значение функции в с if(Fc * Fx > 0) { х = с; Fx = Fc; } else {у-c; Fy- Fc;} } while (Fc /= 0 && у - x > EPS); return c; } #include <cmath> // Определение тестовой функции у = х * х - 1: double testfuncf double х) { return х *х - 1; } intmainf) { double result; result = rootftestfunc, 0.5, 2.0, 1e-5); cout« cyrToDosf "Корень тестовой функции: ") « result« endl; return 0; } Результат выполнения программы: Корень тестовой функции: 0.999998 Опыт работы на языках Си и Си++ показал, что даже не новичок в области программирования испытывает серьезные неудобства, разбирая синтаксис конструкций, включающих указатели на функции. Например, не каждому сразу становится понятным такой прототип функции [6, 11]: void qsort(void *base, sizej nelem, size_t width, int (*fcmp)(const void *p1, const void *p2)); Это прототип функции быстрой сортировки, входящей в стандартную библиотеку функций. Здесь size_t — беззнаковый тип обозначения размеров. Этот тип определен в заголовке <cstddef>. Вместо него можно в ряде случаев использовать int. Прототип находится в заголовочном файле <cstdlib>. Разберем элементы прототипа и напишем программу, использующую указанную функцию. Функция qsort() сортирует содержимое таблицы однотипных элементов, постоянно вызывая функцию
сравнеФункции, указатели, ссылки 219 ния, подготовленную пользователем. Для вызова функции сравнения ее адрес должен заместить указатель fcmp, специфицированный как параметр. Для использования qsort() программист должен подготовить таблицу сортируемых элементов в виде одномерного массива и написать функцию, позволяющую сравнивать два любых элемента сортируемой таблицы. Остановимся на параметрах функции qsort(): base указатель на начало таблицы сортируемых элементов (адрес, например, нулевого элемента массива); nelem количество сортируемых элементов в таблице (целая величина, не большая размера массива); width размер элемента таблицы (целая величина, определяющая в байтах размер одного элемента массива); fcmp указатель на функцию сравнения, получающую в качестве параметров два указателя р 1, р2 на элементы таблицы и возвращающую в зависимости от результата сравнения целое число: если *р1 < *р2, fcmp возвращает целое < 0; если *р1 == *р2, fcmp возвращает 0; если *р1 > *р2, fcmp возвращает целое > 0. При сравнении символ "меньше чем" < означает, что после сортировки левый элемент отношения *р 1 должен оказаться в таблице перед правым элементом *р2, т.е. значение *р 1 должно иметь меньшее значение индекса в массиве, чем *р2. Аналогично (но обратно) определяется расположение элементов при выполнении соотношения "больше чем" >. В следующей программе функция qsort() используется для упорядочения массива указателей на строковые константы разной длины. Упорядочение должно быть выполнено таким образом, чтобы последовательный перебор массива указателей позволял получать строковые константы в алфавитном порядке. Сами строковые константы в процессе сортировки не меняют своих положений. Изменяются только значения указателей в массиве. //Р06_17.срр - Упорядочение с помощью функции qsort() #include <iostream> ^include <cstdlib> //Для функции qsortf) #include <cstring> //Для функции strcmpf) using namespace std;
220 Глава 6 #include "cyrToDos.h" // функция для сравнения строковых констант: int sravnif const void *a, const void *b) { unsigned tong *pa = (unsigned tong *)a, *pb = (unsigned long *)b; return strcmp((char *)*pa, (char *)*pb); } intmain() { char *pc[] = { ”Sine Cura - Синекура”, ”Pro Forma - Ради формы”, ”Differentia Specifica -” ”\n\t\t Отличительная особенность”, ”Alea Jacta Est! - Жребий брошен!”, ”ldem Per Idem "\n\t\t Определение через определяемое”, ”Fiat Lux! - Да будет свет!”, ”Multa Paucis - Многое в немногих словах” }; // Размер таблицы: int n = sizeof(pc)/sizeof(pc[0]); cout« cyrToDosf”До сортировки:”) « end!; for (int i = 0; i < n; i++) { cout« ”pc[”« i« ”]=”«(unsigned long)pc[i] « ” -> ”; cout« cyrToDos(pc[i])«endl; } //Вызов функции упорядочения: qsort((void *)pc, //Адрес начала сортируемой таблицы п, //Количество элементов сортируемой таблицы sizeof(pc[0]), // Размер одного элемента sravni // Имя функции сравнения (указатель) ); cout« cyrToDos(”\n После сортировки:”)«епб1; for (int i = 0; i < n; i++) { cout« ”pc[”« i« "]=”«(unsigned long)pc[i] « ” -> ”; cout« cyrToDos(pc[i])«endl; } return 0;
Функции, указатели, ссылки 221 Результат выполнения программы: До сортировки: рс[0]=5627 -> Sine Сига - Синекура рс[ 1]-5648 -> Pro Forma - Ради формы рс[2]=5680 -> Differentia Specifica - Отличительная особенность рс[3]=5744 -> Alea Jacta Est! - Жребий брошен! рс[4]=5808 -> Idem Per Idem - Определение через определяемое рс[5]=5859 -> Fiat Lux! - Да будет свет! рс[6]=5904 -> Multa Paucis - Многое в немногих словах После сортировки: рс[0]=5744 -> Alea Jacta Est! - Жребий брошен! рс[1]=5680 -> Differentia Specif ica - Отличительная особенность рс[2]=5859 -> Fiat Lux! -Да будет свет! рс[3]=5808 -> Idem Per Idem - Определение через определяемое рс[4]-5904 -> Multa Paucis - Многое в немногих словах рс[5]=5648 -> Pro Forma - Ради формы рс[6]=5627 -> Sine Cura - Синекура Обратите внимание на значения указателей pc[i] до и после сортировки. После упорядочения рс[0] получит исходное значение рс[3] и т.д. Для выполнения сравнения строковых констант (а не указателей - элементов массива рс[ ]) в функции sravni() использована библиотечная функция strcmp(), прототип которой имеет вид: int strcmpf const char *s1, const char *s2); Функция strcmpO выполняет беззнаковое сравнение строковых констант, адресованных указателями s^ и s2. Сравнение выполняется без учета регистров букв латинского алфавита. Функция выполняет сравнение посимвольно, начиная с начальных символов и до тех пор, пока не встретятся несовпадающие символы либо не закончится одна из строковых констант. Определение функции strcmpO требует, чтобы параметры имели тип const char *. Входные параметры функции sravniO имеют тип const
222 Глава 6 void *, как предусматривает определение функции qsort(). Необходимые преобразования для наглядности выполнены в два этапа. В теле функции sravni() определены два вспомогательных указателя типа unsigned long *, которым присваиваются значения адресов элементов сортируемой таблицы (элементов массива рс[ ]) указателей. В свою очередь, функция strcmp() получает разыменования этих указателей, т.е. адреса строковых констант. Таким образом, выполняется сравнение не элементов массива char* рс[ ], а тех констант, адреса которых являются значениями рс[/]. Однако функция qsort() работает с массивом рс[ ] и меняет местами только значения его элементов. Последовательный перебор массива рс[ ] позволяет в дальнейшем получить строковые константы в алфавитном порядке, что иллюстрирует результат выполнения программы. Так как pc[i] - указатель на некоторую строковую константу, то его разыменование в операции вывода << в поток cout выполняется автоматически. Если не использовать вспомогательных указателей ра, рЬ, то функцию сравнения строковых констант можно вызвать из тела функции sravni() таким оператором: return strcmp((char *)(*(unsigned long *)а), (char *)(*(unsigned long +)b)); Здесь каждый указатель void * вначале преобразуется к типу unsigned long *. Последующее разыменование "достает" значение соответствующего указателя рс[/], а уж затем преобразование (char *) формирует указатель на строковую константу, который нужен функции strcmp(). Если заменить традиционные операции приведения типов на рекомендованные Стандартом Си++, то оператор возврата примет вид: return strcmp(reinterpret_cast<char *> (*static_cast<unsigned long *>(a)), reinterpret_cast<char *> Cstatic_cast<unsigned long *>(b))); Отметим, что обойтись только одной формой операции приведения типов static_cast не удастся - компилятор не разрешит с ее помощью преобразовать значение unsigned long в указатель типа char *. Поэтому приходится использовать операцию
Функции, указатели, ссылки 223 reinterpret_cast, в отношении которой компилятор полностью доверяет программисту, точнее, перекладывает на него ответственность за результат преобразования типов. 6.7. Ссылки В языке Си++ ссылка определена как другое имя уже существующего объекта. Основные достоинства ссылок проявляются при работе с функциями, однако ссылки могут использоваться и безотносительно к функциям. Для определения ссылки используется символ &, если он употребляется в таком контексте: type & имя_ссылки инициализатор В соответствии с синтаксисом инициализатора, наличие которого обязательно, определение ссылки может быть таким: type & имя_ссылки = выражение; или type & имя_ссылки(выражение); Раз ссылка есть "другое имя уже существующего объекта", то в качестве инициализирующего выражения должно выступать имеющее значение леводопустимое выражение, т.е. имя некоторого объекта, уже размещенного в памяти. Значением ссылки после определения с инициализацией становится адрес этого объекта. Примеры определений ссылок: int L = 777; // Определена и инициализирована переменная L int &RL = L; // Значением ссылки RL является адрес //переменной L int & Rl(0); // Опасная инициализация - значением ссылки RI //становится адрес объекта, в котором // временно размещено нулевое целое значение В определении ссылки символ & не является частью типа, т.е. RL или RI имеют тип int и именно так должны восприниматься в программе. Итак, имя_ссылки определяет местоположение в памяти инициализирующего выражения, т.е. значением ссылки является адрес объекта, связанного с инициализирующим выражением.
224 Глава 6 Функционально ссылка ведет себя подобно обычной переменной того же, что и ссылка, типа. Для доступа к содержимому участка памяти, на который "смотрит" ссылка, нет необходимости явно выполнять разыменование, как это нужно для указателя. Если рассматривать переменную как пару "имя_переменной - значение_переменной", то инициализированная этой переменной ссылка может быть представлена парой "имя_ссылки - значение_переменной,г. Из этого становится понятной необходимость инициализации ссылок при их определении. Тут ссылки схожи с константами языка Си++. Раз ссылка есть имя, связанное со значением (объектом), уже размещенным в памяти, то, определяя ссылку, необходимо с помощью начального значения определить тот объект (тот участок памяти), на который указывает ссылка. После определения с инициализацией имя_ссылки становится еще одним именем (синонимом, псевдонимом, алиасом) уже существующего объекта. Таким образом, для нашего примера оператор RL -= 77; уменьшает на 77 значение переменной /_. Связав ссылку (RL) с переменной (/_), мы получаем две возможности изменять значение переменной: RL = 88; или L = 88; Здесь есть аналогия с указателями, однако отсутствует необходимость в явном разыменовании, что обязательно при обращении к значению переменной через указатель. Проиллюстрируем сказанное следующей простой программой: //Р06_18.срр - Обращение к значению переменной по ссылке #include <iostream> using namespace std; intmainf) { long z = 12345678L; // Выделяется память для переменной long& sz = z; // Определяется ссылка - синоним для z
Функции, указатели, ссылки 225 cout« "sz = " << sz « "\tz = " << z << endl; z = 87654321L; cout« "sz = "« sz « "\tz = " << z << endl; z = 0; cout« "sz = "« sz « "\t\tz =" «z « endl; return 0; } Результат выполнения программы: sz = 12345678 sz = 8765432 sz = 0 z = 12345678 z = 87654321 z-0 Как видно из результатов, имена переменной и ссылки, "настроенной” на эту переменную, полностью равноправны и соотносятся с одним и тем же участком памяти. Присваивание значения переменной z приводит к изменению значения, связанного со ссылкой sz, и, наоборот, присваивание значения ссылке sz меняет значение переменной z. Ссылки не есть полноправные объекты, подобные переменным либо указателям. После инициализации значение ссылки изменить нельзя, она всегда "смотрит" на тот участок памяти (на тот объект), с которым она связана инициализацией. Ни одна из операций не действует на ссылку, а относится к тому объекту, с которым она связана. Можно считать, что это основное свойство ссылки. Таким образом, ссылка полностью аналогична исходному имени объекта. Конкретизируем и поясним сказанное. Пусть определены: double а[] = { 10.0, 20.0, 30.0, 40.0 }; //а - массив double *ра = а; //ра - указатель на массив double& га = а[0];//га - ссылка на первый элемент массива double * & rpd = а; // Ссылка на указатель (на имя массива) Для ссылок и указателей из нашего примера соблюдаются равенства ра == &га, *ра == га, rpd == а, га == а[0]. Тогда sizeof(pa)==4 - размер указателя типа double *; size- of(ra)==8 - размер элемента массива double а[0], связанного со ссылкой га при инициализации; sizeof(rpc()==4 - размер указателя на массив с элементами double. 15-2762
226 Глава 6 Итак, результатом применения операции sizeof к ссылке является не ее размер, а размер именуемого ею объекта. Так как квадратные скобки [ ] есть обозначение бинарной операции, то в соответствии с основным свойством ссылки оператор rpd[0] += rpd[2] + rpd[ 3]; относится к элементам массива а[ ]. После его выполнения а[0] == 80.0 Применив к ссылке операцию получения адреса &, определим не адрес ссылки, а адрес того объекта, которым инициализирована ссылка. Можно рассмотреть и другие операции, но вывод один — каждая операция над ссылкой является операцией над тем объектом, с которым она связана. Так как ссылки не есть настоящие объекты, то существуют ограничения при определении и использовании ссылок. Во- первых, ссылка не может иметь тип void, т.е. определение void& имя-ссылки запрещено. Ссылку нельзя создать с помощью операции new, т.е. для ссылки нельзя выделить новый участок памяти. Не определены ссылки на другие ссылки. Нет указателей на ссылки и невозможно создать массив ссылок. Например, все следующие определения ссылок ошибочны: long v1 = 6, v2- 4, v3 = 8; double & ref; void & refvar = v1; long & refnew = newflong &); long & refarr[3] = { v1, v2, v3 }; long & & refref = v2; // Нет инициализации ссылки // void& недопустимо для ссылки // Ссылке не выделяют память // Массивов ссылок не бывает // Нет ссылок на ссылки Ссылка, определенная с типом type, должна быть инициализирована либо объектом того же типа type, либо с помощью объекта, который может быть по умолчанию приведен к этому типу. После определения ссылка не может быть никаким способом "оторвана" от объекта инициализации и связана с другим объектом, т.е. значение ссылки не может изменяться. Однако один объект может быть адресован любым числом ссылок и указателей: long v = 7; // Определена переменная v long & ref1 = v; //Ссылка ref1 связана c v
Функции, указатели, ссылки 227 long * ptr = &ref1; // Указатель ptr адресует v long & ref2 = re 1 f; // Ссылка ref2 указывает на v Теперь к значению переменной v можно добраться четырьмя способами: с помощью имени v\ ссылки ref1\ разыменования указателя *ptr и ссылки ref2. В определении типа для ссылки можно использовать модификатор const, т.е. допустимо определять ссылку на константу: const type& имя_ссылки инициализатор; В отличие от обычной ссылки (ссылки на переменную) ссылка на константу не позволяет изменить значение того объекта, с которым она связана. Например: double Euler = 2.718282;// Определили переменную const double & eRef = Euler; // eRef - ссылка на константу При таких определениях: Euler = 0.0; //Допустимый оператор eRef = 0.0; // Ошибка, так как ссылка объявлена на константу При определении ссылок на константы инициализирующее выражение может быть не только леводопустимым и даже может не иметь типа type. Например: const double & ref pi = 3.141593; // Ссылка типа double // На константу типа double const double & refO = 0; // Ссылка типа double // На константу 0 типа int В последнем определении инициализирующее выражение имеет тип int, отличный от типа double ссылки геЮ. Поэтому при инициализации выполняется автоматическое преобразование типов. Если при определении ссылки на константу в инициализаторе используется праводопустимое выражение, например константа, то создается временный объект, он инициализируется значением этого выражения, а ссылка становится его именем. В соответствии с общими принципами время жизни этого временного объекта определяется областью действия, в которой определяется ссылка. Итак, если инициализирующее выражение для ссылки на константу праводопустимое, то прежде всего вычисляется его 15*
228 Глава 6 значение. Следующий шаг — применяется необходимое преобразование типа полученного значения выражения к типу ссылки. Затем создается временная переменная нужного типа и туда заносится это значение. Адрес временной переменной используется в качестве значения инициализируемой константной ссылки. Иллюстрировать показанную цепочку действий можно таким образом. Пусть определение ссылки имеет вид: const type1& имя_ссылки = выражение_type2; Так как некоторым аналогом ссылки является указатель, то действия по инициализации ссылки с обозначением имя_ссыл- ки эквивалентны в некотором приближении следующей последовательности: type 1 temp; // Временная переменная temp = type 1(выражение^ре2); // Приведение типов и присваивание type 1 * const имя_указателя = &temp; // Имитация //инициализации указателя как аналога ссылки Пример (определяется ссылка на константу): const double & cdr = 11;// Справа константа типа int Интерпретация приведеннего определения с помощью указателя: double temp; // Временная переменная temp temp = doublef 11); //Присваивание значения переменной temp double * const cdrp = & temp; // Присваивание адреса // постоянному указателю Отметим, что такая интерпретация не совсем точна и отображает лишь последовательность действий. Полный аналог ссылки есть константный, т.е. неизменяемый указатель, который автоматически разыменовывается при каждом использовании. Таких константных указателей в Си++ нет - указатели автоматически разыменовываться «не умеют».
Функции, указатели, ссылки 229 При определении ссылок обязательна их инициализация. Однако в описаниях ссылок инициализация не обязана присутствовать. К таким описаниям ссылок относятся: • описания внешних ссылок (со спецификатором extern): doubleA ref; // Ошибка - нет инициализации extern double& ref2; //Допустимо - инициализируется // в другом блоке; • описания ссылок на компоненты класса; • спецификации параметров функций; • описание типа возвращаемого функцией значения. С классами и их компонентами мы встретимся позже, описания внешних ссылок ничем не отличаются от описания внешних переменных или массивов, а вот соотношение ссылок и функций нужно рассмотреть подробно. Действительно, в качестве основных причин включения ссылок в язык Си++ указывают необходимость повысить эффективность обмена с функциями через аппарат параметров и целесообразность возможности использовать вызов функции в качестве леводопустимого значения. При использовании ссылки в качестве параметра обеспечивается доступ из тела функции к соответствующему аргументу, т.е. к участку памяти, выделенному для аргумента. При этом параметр-ссылка обеспечивает те же самые возможности, что и параметр-указатель. Отличия состоят в том, что в теле функции для параметра- ссылки не нужно применять операцию разыменования *, и аргументом должен быть не адрес (как для параметра-указателя), а обычная переменная. Следующая программа иллюстрирует в сравнении доступ к объектам (переменным) из тела функции через указатели и ссылки: //Р06_19.срр - Ссылки и указатели в качестве параметров #include <iostream> using namespace std; // функции, меняющие значения аргументов: void changePtrf double *a, double *b) { double c = *a; *a = *b; *b = c; > void changeRef(double& x, double& y) {
230 Глава 6 double z = x; x = у; у = z; } intmainf) { double d= 1.23; double e = 4.56; changePtr(&d,&e); cout« "d ="«d « "\te = "« e « endl; changeRef(d,e); cout« "d = " << d « ”\te = ”<<e « endl; return 0; } Результат выполнения программы: d-4.56e = 1.23 d = 1.23 e = 4.56 В функции changePtr() параметры специфицированы как указатели. Поэтому в ее теле выполняется их разыменование, а при обращении к changePtr() в качестве аргументов используются адреса (&d, &е) тех переменных, значения которых нужно поменять местами. В функции changeRef() параметры специфицированы как ссылки. Ссылки обеспечивают доступ из тела функции к аргументам, в качестве которых используются обычные переменные, определенные в вызывающей программе. Подобно указателю на функцию определяется и ссылка на функцию: тип_функции (& имя_ссылки)(спецификацияпараметров) инициализирующее _выражение; Здесь тип_функции - это тип возвращаемого функцией значения, спецификация параметров определяет сигнатуру функций, допустимых для ссылки, инициализирующее_выражение - включает имя уже известной функции, имеющей тот же тип и ту же сигнатуру, что и определяемая ссылка. Например, int ifunc (double, int); // Прототип функции int(& iref)(double, int) = ifunc; // Определение ссылки iref - ссылка на функцию, возвращающую значение типа int и имеющую два параметра с типами double и int. Напомним, что
Функции, указатели, ссылки 231 использование имени функции без скобок (и без параметров) воспринимается как адрес функции. Ссылка на функцию обладает всеми правами основного имени функции, т.е. является его синонимом (псевдонимом). Изменить значение ссылки на функцию невозможно, поэтому указатели на функции имеют гораздо большую сферу применения, чем ссылки. Следующая программа иллюстрирует вызовы функции по основному имени, по указателю и по ссылке: //Р06_20.срр - Ссылка и указатель на функцию #include <iostream> using namespace std; void funcfchar с) { // Определение функции cout« c « end!; } intmainf) { void (*pf)(char); //pf- указатель на функцию void (&rf)(char) = func; // rf- ссылка на функцию func(!A'); // Вызов по имени pf = func; // Указателю присваивается адрес функции (*pf)( Ъ'); // Вызов по адресу с помощью указателя rf( С); // Вызов по ссылке return 0; } Результат выполнения программы: А В С Определение ссылки может содержать ее инициализацию и в круглых скобках. В нашем примере была бы допустима и такая конструкция: void (&rf)(char)( func); // rf - ссылка на функцию Так же можно инициализировать и указатель на функцию: void (*pf)(char)(func); //pf - указатель на функцию Рассмотрим следующую программу, в которой ссылка — возвращаемый результат выполнения функции:
232 Глава 6 //Р06_21.срр - Ссылка как возвращаемое функцией значение #include <iostream> using namespace std; // Функция возвращает ссылку на элемент массива // с максимальным значением: int& rmaxfint п, int d[ ]) { int im = 0; for (int i = 1; i < n; i++) im = d[im] > d[i] ? im : i; return d[im]; } intmainf) { int n = 4; intx[] = ( Ю, 20, 30, 14 }; cout« ”rmax(n,x) = " << rmax(n,x) « endl; rmax(n,x) = 0; for (int i = 0; i < n; i++) cout« ”x[”« /'« "] = M« x[i] « endl; cout« endl; return 0; } Результат выполнения программы: rmax(n,x) = 30 x[0]= 10 x[1] = 20 x[2] = 0 x[3] = 14 В программе дважды используется обращение к rmax(). Один из вызовов находится в левой части оператора присваивания. С его помощью заносится нулевое значение в элемент х[2], вначале равный 30. Возвращение функцией ссылки позволяет организовать многократное вложение обращений к нескольким функциям. В результате таких вложенных обращений один и тот же объект можно в одном выражении обрабатывать по разным законам. Пример: //Р06_22.срр - Вложенные вызовы функций, возвращающих ссылки ft include <iostream> using namespace std; // Возводит в куб аргумент и возвращает ссылку на него:
Функции, указатели, ссылки 233 double & rcube(double & z) { z = z * z * z; return z; } // Изменяет знак аргумента и возвращает ссылку на него: double & rinvertf double & d) { d = -d; return d; } // Возвращает ссылку на максимальный аргумент: double & rmaxfdouble & х, double & у) { return х> у? х: у; } // Печатает значения аргументов и возвращает ссылку // на последний из них: double & rprintfchar* name, double & e) { cout« name « e « end!; return e; } intmainf) { double a - 10.0, b = 8.0; rprintf "rcube (rinvert (rmax (a,b))) = ", rcube(rinvert(rmax(a,b)))) = 0.0; cout« "a = "« a « ",\tb = "«b « endl; rcube(rinvert(rmax(a,b))) = 0.0; cout« "a = " «a « ",\tb = M << b « endl; return 0; } Результат выполнения программы: rcube (rinvert (rmax (a,b))) = -1000 a = 0, b = 8 a = 0, b = 0 Присваивание rprint() = 0.0 позволяет по цепочке передать нулевое значение аргументам функций и изменяет значение того из аргументов (а) функции rmax(), который вначале был максимальным. Последующее присваивание rcube() = 0.0 обнуляет значение аргумента Ь.
234 Глава 6 В следующей программе введены две функции, возвращающие ссылки на элементы двумерных массивов с изменяемыми размерами. Функция elem() позволяет по заданным значениям индексов "добираться" до конкретного элемента. Особого выигрыша применение такой функции не дает, она еще раз иллюстрирует возможности функций, возвращающих значения ссылок. Функция maxelem() возвращает ссылку на максимальный элемент двумерного массива. Используя ее в левой части оператора присваивания, можно изменить значение максимального элемента. Именно это и выполняется в цикле основной программы. Результаты ее работы и комментарии в тексте поясняют сказанное. Текст программы: //Р06_23.срр - Ссылки на элементы "двумерных” массивов #include <iostream> using namespace std; //Доступ к пространству имен std // Возвращает ссылку на конкретный элемент матрицы: double & elem(double +*matr, intk, inti) { return matr[k][l]; } // функция заполняет матрицу значениями от 1 до 9: void matrixf int n, intm, double **pmatr) { int k = 0; for (int i = 0; i < n; i++) for (int j = 0; j < m; j++) elem(pmatr,i,j) = k++ %9+ 1; } // Возвращает ссылку на максимальный элемент матрицы: double& maxelemfintn, intm, double **pmatr) { int im = 0, jm = 0; for (int i = 0; i < n; i++) for (int j = 0; j < m; y++; if(pmatr[im][jm] < pmatr[i][j]) { im = /'; jm =j; } return pmatr[im][jm]; } // функция печатает матрицу по строкам: voidmatrjorintfintn, intm, double **pmatr) { for (int i = 0; i < n; i++) {//Цикл перебора строк
Функции, указатели, ссылки 235 cout« "Line " << (i + 1) « // Цикл печати элементов строки: forfintj = 0; у < т; /++у cout« "\Г << pmatr[i][j]; cout« endl; } } intmainf) { double z[3][4]; double *ptr[] = {(double *)&z[0], (double *)&z[1], (double *)&z[2] }; matrix(3,4,ptr); // Заполняем матрицу matr_print(3,4,ptr); // Печать исходной матрицы cout« endl; for (int i = 0; I < 4; i++) // Обнулим 4 максимальных maxelem(3,4,ptr) = 0.0; // элемента matr_print(3,4,ptr); //Печать измененной матрицы return 0; } Результат выполнения программы: Line 1: 1 2 3 4 Line 2: 5 6 7 8 Line 3: 9 1 2 3 Line 1: 1 2 3 4 Line 2:5 0 0 0 Line 3: 0 1 2 3 6.8. Перегрузка функций Цель перегрузки функций состоит в том, чтобы несколько функций могли иметь одинаковое имя, но различались по типам и количеству параметров. Например, могут потребоваться функции, каждая из которых возвращает максимальное из значений элементов одномерного массива, передаваемого ей в качестве аргумента. Массивы, использованные как аргументы, могут быть
236 Глава 6 + разных типов, но пользователь не должен беспокоиться о том, как называется каждая из функций — все функции имеют одно имя, но различаются спецификациями параметров. Обращение выполняется к той из одноименных функций, список аргументов которой соответствует списку параметров. Для достижения этого все одноименные функции должны иметь разные сигнатуры. Для примера предположим, что функция выбора максимального значения элементов из массива должна работать для массивов типа int, long, float, double. В этом случае придется написать четыре разных варианта функции с одним и тем же именем. В следующей программе эта задача решена: //Р06_24.срр - Перегрузка функций #include <iostream> using namespace std; #include "cyrToDos.h" long max_element(int n, int array[ ]) { // Функция для int value = array[0]; // массивов с элементами типа int for (inti = 1; i < n; i++) value = value > array[i] ? value : array[i]; cout « cyrToDosf "Для (int) : "); return long(value); } long max_element(int n, long array[ ]) { // Функция для long value = array[0]; // массивов с элементами типа long for (inti = 1; i < n; i++) value = value > array[i] ? value : array[i]; cout« cyrToDosf "Для (long) : "); return value; } double max_element(int n, float array[ ]) {// Функция для float value = array[0]; // массивов с элементами типа float for (inti = 1; i < n; i++) value = value > array[i] ? value : array[i]; cout« cyrToDosf "Для (float): "); return doublet value); } double max_element(int n, double array[ ]) {// Функция для double value = array[0]; //массивов с элементами типа double
Функции, указатели, ссылки 237 for (inti = 1; i < п; i++) value = value > array[i] ? value : array[i]; cout« cyrToDosf "Для (double) : "); return value; } intmainf) { intx[ ]={ 10, 20, 30, 40, 50, 60 }; long f[] = { 12L, 44L, 5L, 22L, 37L, 30L }; floaty[] = {0. If, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f}; doublez[] = { 0.01, 0.02, 0.03, 0.04, 0.05, 0.06}; cout« ,,max_elem(6,x) = " << max_element(6,x) « endl; cout« ,,max_elem(6,f) = " << max_element(6,f) « endl; cout« ,,max_elem(6,y) = ” << max_element(6,y) « endl; cout« ,,max_elem(6,z) = " << max_element(6,z) « endl; return 0; } Результат работы программы: Для (int) : max_elem(6,x) = 60 Для (long) : max_elem(6, f) = 44 Для (float) : max_elem(6,y) = 0.6 Для (double): max_elem(6,z) = 0.06 В программе для иллюстрации независимости сигнатур функций от типа возвращаемого значения две функции, обрабатывающие целые массивы (int [ ], 1опд[ ]), возвращают значение одного типа long, а функции, обрабатывающие вещественные массивы (double[ ], float[ ]), обе возвращают значение типа double. Распознавание перегруженных функций при вызове выполняется по сигнатурам, поэтому перегруженные функции должны иметь одинаковые имена, но спецификации их параметров должны различаться по количеству и(или) по расположению. При использовании перегруженных функций нужно с осторожностью задавать умалчиваемые значения их аргументов. Предположим, мы следующим образом определили перегруженную функцию умножения разного количества параметров: double multy(double х) { return х + х +х; } double multy(double х, double у) { return х + у + у;} double multy(doubleх, double у, doublez) { return x *y + z;}
238 Глава 6 Каждое из следующих обращений к функции multy() будет однозначно идентифицировано и правильно обработано: multy(0.4) multyf 4.О, 12.3) multyfO. 1е-6, 1.2е4, 6.4) Однако добавление в программу определения функции double multyfdouble а = 1.0, double b = 1.0, double с = 1.0, double d = 1.0); { return a*b + c*d;} навсегда запутает любой компилятор при попытках обработать, например, такой вызов: miv/fy(0.1e-6, 1.2е4)
239 Глава 7 ПРЕПРОЦЕССОРНЫЕ СРЕДСТВА 7.1. Стадии и команды препроцессорной обработки В компилятор языка как обязательный компонент входит препроцессор. Назначение препроцессора - обработка исходного текста программы до ее компиляции. Результат препроцессорной обработки — единица трансляции (translation unit). Препроцессорная обработка в соответствии с требованиями стандарта языка Си++ включает несколько стадий, выполняемых последовательно. Конкретная реализация транслятора может объединять несколько стадий, но результат должен быть таким, как если бы они выполнялись последовательно: • все системно-зависимые обозначения (например, системно-зависимый индикатор конца строки) перекодируются в стандартные коды; • каждая пара из символов \ и "конец строки" убираются, и тем самым следующая строка исходного файла присоединяется к строке, в которой находилась эта пара символов; • в тексте распознаются директивы препроцессора, а каждый комментарий заменяется одним символом пустого промежутка (пробела); • выполняются директивы препроцессора и производятся макроподстановки; • ескейп-последовательности в символьных и строковых константах, например \п\ заменяются на их эквиваленты (на соответствующие числовые коды); • смежные строковые константы конкатенируются, т.е. соединяются в одну строковую константу. Знакомство с перечисленными задачами препроцессорной обработки объясняет некоторые правила синтаксиса языка. Например, становится понятным смысл утверждений: "каждая
стро240 Глава 7 ковая константа может быть перенесена в файле па следующую строку, если использовать символ \" или "две строковые константы, записанные рядом, воспринимаются как одна строковая константа". Рассмотрим подробно стадию обработки директив препроцессора. При ее выполнении возможны следующие действия: • замена идентификаторов (обозначений) заранее подготовленными последовательностями символов; • включение в программу текстов из указанных файлов; • исключение из программы отдельных частей ее текста (условная компиляция); • макроподстановка, т.е. замена обозначения параметризованным текстом, формируемым препроцессором с учетом конкретных параметров (аргументов). Для управления препроцессором, т.е. для задания нужных действий, используются команды (директивы) препроцессора, каждая из которых помещается на отдельной строке и начинается с символа #. Определены следующие препроцессорные директивы: #define, ttinclude, ttundef, #if, #ifdef, #ifndef, #else, #endif, ttelif, #line, terror, #pragma, #. Завершается каждая препроцессорная директива кодом перехода на новую строку. Директива ttdefine имеет несколько модификаций. Они предусматривают определение макросов или препроцессорных идентификаторов, каждому из которых ставится в соответствие некоторая символьная последовательность. В последующем тексте программы препроцессорные идентификаторы заменяются на заранее запланированные в соответствующих директивах ttdefine последовательности символов. Директива ttinclude позволяет включать в текст программы текст из выбранного файла. Директива ttundef отменяет действие команды ttdefine, которая определила до этого имя препроцессорного идентификатора. Директива ttif и ее модификации ttifdef, ttifndef совместно с директивами #e/se, ttendif, ttelif позволяют организовать условную обработку текста программы. Условность состоит в том, что компилируется (переносится в единицу трансляции) не весь текст, а только те его части, которые по препроцессорным правилам выделены с помощью перечисленных директив.
Препроцессорные средства 241 Директива #Ппе позволяет управлять нумерацией строк в файле с программой. Имя файла и начальный номер последующей нумерации строк указываются непосредственно в директиве #/юе. Директива #еггог позволяет задать текст диагностического сообщения, которое выводится при возникновении ошибок. Директива #ргадта вызывает действия, зависящие от реализации. Директива # ничего не вызывает, так как является пустой, т.е. не дает никакого эффекта и всегда игнорируется. Рассмотрим возможности перечисленных команд при решении типичных задач, поручаемых препроцессору. 7.2. Замены в тексте Для замены идентификатора заранее подготовленной последовательностью символов используется директива (обратите внимание на пробелы): #define идентификатор строка_замещения Строка замещения - это последовательность символов, окончанием которой (и директивы в целом) служит код перехода на новую строку. Обратите внимание, что в отличие от строковых констант строка_замещения не ограничивается кавычками. Если строка_замещения оказывается слишком длинной, то, как любую строковую константу языка Си++, ее можно продолжить в следующей строке текста программы. Для этого в конце продолжаемой строки замещения помещается символ *\* (обратная наклонная черта). В ходе одной из стадий препроцессорной обработки этот символ вместе с последующим символом конца строки будет удален из текста программы. Директива #define может размещаться в любом месте обрабатываемого текста, а ее действие в обычном случае распространяется от точки размещения до конца текста. Директива, во-первых, определяет идентификатор как препроцессорный. В результате обработки все вхождения определенного командой #define идентификатора в текст программы заменяются строкой замещения. Символы пробелов, помещенные в начале и в конце строки замещения, в подстановке не используются. Пример: ,6-2762
242 Глава 7 Исходный текст Результат препроцессорной обработки #define begin { #define end } int main() begin операторы end int main() { операторы } В данном случае программист решил использовать для обозначения операторных скобок идентификаторы begin, end. До компиляции препроцессор заменяет все вхождения этих идентификаторов стандартными скобками { и }. Соответствующие указания программист дал препроцессору с помощью директив #define. Пример, в котором строкой замещения служит строковая константа, записанная в двух строках текста: #define STROKA "\n Multum, non multa - \ многое, но немногоГ cout« STROKA; Будет выведено: Multum, non multa - многое, но немного! С помощью команды #define удобно выполнять настройку программы. Например, если в программе требуется работать с массивами, то их размеры можно явно определять на этапе препроцессорной обработки: Исходный текст Результат препроцессорной обработки #define К 40 int main() { int М[К][К\\ double А[К], В[К][К]\ intmain() { /лШ[40][40]; double А[Л0], В[40][40];
Препроцессорные средства 243 При таком подходе очень легко изменять предельные размеры сразу всех массивов, изменив только одну константу в команде ftdefine. Отметим, что те же возможности в языке Си++ обеспечивают константы, определенные в тексте программы. Например, того же результата можно достичь, записав: intmain() { const int к = 40; int М[к][к]\ floatА[к], В[к][к]\ Именно в связи с расширением возможностей констант в языке Си++ по сравнению с языком Си команда ttdefine для объявления констант используется реже. Предусмотренные директивой #define препроцессорные замены не выполняются внутри строковых и символьных констант и комментариев, т.е. не распространяются на тексты, ограниченные кавычками ("), апострофами (’) и разделителями (/*, */)• В то же время строка замещения может содержать перечисленные ограничители, например, так, как это было в замене препроцес- сорного идентификатора STROKA. Если в программе нужно часто выводить значение какой-либо переменной и, кроме того, снабжать эту печать одним и тем же пояснительным текстом, то удобно ввести сокращенное обозначение оператора печати. Например, так: ttdefine РК cout« "\п Номер элемента N = ”« N « 7 После этой директивы использование в программе оператора РК; будет эквивалентно (по результату) оператору из строки замещения. Например, последовательность операторов int N = 4; РК; приведет к выводу такого текста: Номер элемента Л/ = 4. Если в строку замещения входит идентификатор, определенный как препроцессорный в другой команде ttdefine, то в стро- 16*
244 Глава 7 кс замещения выполняется замена этого идентификатора (т. е. выполняется цепочка подстановок). Замены в тексте можно отменять с помощью команды #undef идентификатор После выполнения такой директивы идентификатор для препроцессора становится неопределенным и его можно определять повторно с помощью другой команды #define. Проиллюстрировать цепочку подстановок с изменением значения препроцессорного идентификатора можно таким примером: //Р07_01.срр - Цепочки подстановок и директива #undef #include <iostream> using namespace std; intmainf) { #define К 50 #define PE cout« "K = " << К « endl; PE; #undef К 50 ttdefine К 30 PE; return 0; } Результаты выполнения программы: K = 50 К = 30 Обратите внимание, что идентификатор К внутри строки замещения, обрамленной кавычками ("), не заменен на 50 или на 30. Директиву #undef удобно использовать при разработке больших программ, когда они собираются из отдельных "кусков текста", написанных в разное время или разными программистами. В этом случае могут встретиться одинаковые обозначения разных объектов. Чтобы не изменять исходных файлов, включаемый текст можно "обрамлять" подходящими директивами #define - #undef и тем самым устранять возможные ошибки. Схема такая:
Препроцессорные средства 245 А = 10; //Основной текст #define А X А = 5; // Включенный текст #undefA В = А; // Основной текст При выполнении программы В примет значение 10, несмотря на наличие оператора присваивания А = 5; во включенном тексте. 7.3. Включение текстов из файлов Начиная работать с языком Си++, пользователь сразу же сталкивается с необходимостью использования в программах средств ввода-вывода. Для этого в начале текста программы помещают директиву: #include <iostream> Выполняя эту директиву, препроцессор включает в программу средства рвязи с библиотекой ввода-вывода. Поиск файла, соответствующего имени iostream, ведется в стандартных системных каталогах. Команда #include имеет две формы записи: #include <имя_заголовка> // Имя в угловых скобках #include "имя_файла" // Имя файла в кавычках Если используются угловые скобки, то препроцессор разыскивает файл в стандартных системных каталогах. Если имя_фай- ла заключено в кавычки, то вначале препроцессор просматривает текущий каталог пользователя и только затем обращается к просмотру системных каталогов. Кроме файлов, которые включаются в программу для применения в ней средств стандартной библиотеки, в заголовок
прог246 Глава 7 раммы могут быть включены любые другие файлы (стандартные или подготовленные специально). Заголовочные файлы оказываются довольно эффективным средством при модульной разработке крупных программ, когда связь между частями программы, размещаемыми в разных файлах, реализуется не только с помощью параметров, но и через внешние объекты, глобальные для нескольких или всех частей. Описания таких внешних объектов (переменных, массивов, структур, классов и т.п.) помещаются в одном файле, который с помощью директив #include включается во все части программы, где необходимы внешние объекты. В тот же файл можно включить и директиву подключения средств библиотеки ввода-вывода. Заголовочный файл может быть, например, таким: #include <iostream> // Включение средств обмена extern int ii, jj, II; // Целые внешние переменные extern float АА, ВВ; // Вещественные внешние переменные В практике программирования на Си++ обычна и в некотором смысле обратная ситуация. Если в программе используется несколько функций, то иногда удобно текст каждой из них хранить в отдельном файле. При подготовке программы пользователь включает в нее тексты используемых функций с помощью команд #include. В качестве примера укажем заголовочный файл cyr ToDos.h. 7.4. Условная компиляция Условная компиляция обеспечивается в языке Си++ набором команд, которые, по существу, управляют не компиляцией, а препроцессорной обработкой: #/7 константное_выражение #ifdef идентификатор #ifndef идентификатор #else #endif #elif
Препроцессорные средства 247 Первые три команды выполняют проверку условий, две следующие — позволяют определить диапазон действия проверяемого условия. Команду #e//f рассмотрим несколько позже. Общая структура применения директив условной компиляции такова: текст_1 #else текст_2 #endif Конструкция #e/se текст_2 не обязательна. Текст_1 включается в единицу трансляции только при истинности проверяемого условия. Если условие ложно, то при наличии директивы #e/se включается текст_2. Если директива #e/se отсутствует, то весь текст от #/f до #enc//f при ложном условии опускается. Различие между формами команд #/f состоит в следующем. В первой из перечисленных директив #/f проверяется значение константного целочисленного выражения. Если оно отлично от нуля, то считается, что проверяемое условие истинно. Например, в результате выполнения директив #/75+4 текст_1 ttendif текст_1 всегда будет включен в единицу трансляции. В директиве #/fdef проверяется, определен ли с помощью команды #define к текущему моменту идентификатор, помещенный после #ifdef. Если идентификатор определен как препроцессорный, то текст_1 будет включен в единицу трансляции. В директиве #ifndef проверяется обратное условие — истинным считается неопределенность идентификатора, т.е. тот случай, когда идентификатор не был использован в команде #define или его определение было отменено командой #undef. Условную компиляцию удобно применять при отладке программ для включения или исключения контрольных выводов информации (отладочных печатей). Например,
248 Глава 7 #define DE 1 mfdefDE cout« " Отладочная печать #endif Таких печатей, появляющихся в программе в зависимости от определенности идентификатора DE, может быть несколько и, убрав директиву #define DE 1, сразу же отключаем все отладочные печати. Файлы, предназначенные для препроцессорного включения в текст программы, обычно снабжают защитой от повторного включения. Такое повторное включение может произойти, если несколько текстовых файлов, в каждом из которых запланировано препроцессорное включение одного и того же файла, объединяются в общий текст программы. Например, такими средствами защиты снабжены все заголовочные файлы стандартной библиотеки. Схема защиты от повторного включения может быть такой: // Файл с именем filename ttifndef _FILE_NAME ... //Включаемый текст файл. • filename #define _FILE_NAME 1 Xendif Здесь _FILE_NAME - зарезервированный программистом для файла filename препроцессорный идентификатор, который не должен встречаться в других текстах программы. Для организации мультиветвлений во время обработки препроцессором исходного текста программы введена директива #e//f константное выражение Структура исходного текста с применением этой директивы такова: #/f... текстjxnBjf #eiif выражение_ 1 текст 1
Препроцессорные средства 249 #e//f выражение_2 текст_2 #e/se текст_для_случая_е18е Xendif Препроцессор проверяет вначале условие в директиве #/f, если оно ложно — вычисляет выражение_1, если выражение_1 равно 0, - вычисляется выражение_2 и т.д. Если все выражения ложны, то в компилируемый текст включается текст_для_слу- чая_е1ве. В противном случае, т.е. при появлении хотя бы одного истинного выражения (в #/f или в #e//f), начинает обрабатываться текст, расположенный непосредственно за этой директивой, а все остальные директивы не рассматриваются. Таким образом, препроцессор обрабатывает всегда только один из участков текста, выделенных командами условной компиляции. 7.5. Макроподстановки средствами препроцессора Макрос, по определению, есть средство замены одной последовательности символов другой. Для выполнения замен должны быть заданы соответствующие макроопределения. Простейшее макроопределение мы уже ввели, рассматривая директиву #define идентификатор строка_замещения Такая директива удобна, однако она имеет существенный недостаток — строка замещения фиксирована. Большими возможностями обладает макроопределение с параметрами #define имя(список_параметров) строка_замещения Здесь имя - имя макроса (идентификатор), список_пара- метров - список разделенных запятыми идентификаторов. Между именем макроса и списком параметров не должно быть пробелов. Классический пример макроопределения: #define max(a,b) (а< b? b : а)
250 Глава 7 позволяет формировать в программе выражение для вычисления максимального из двух значений аргументов. При таком определении вхождение в программу обращения тах(Х, Y) заменяется выражением (X < Y? Y: X), а использование обращения max(Z,4) приведет к формированию выражения (Z < 4 ? 4 : Z) В первом случае при истинном значении X < Y возвращается значение Y, иначе — значение X. Во втором примере Zсравнивается с константой 4 и выбирается большее из значений. Не менее часто используется определение ^define ABS(X) (X < 0 ? - (X): X) С его помощью можно вычислять абсолютное значение аргумента. Конструкция ABS(E-Z) заменяется выражением (E-Z<0?-(E-Z):E-Z), в результате вычисления которого определяется абсолютное значение выражения E-Z. Сравнивая макросы с функциями, наиболее часто отмечают, что в отличие от функции, определение которой всегда присутствует в одном экземпляре, коды, формируемые макросом, вставляются в программу столько раз, сколько раз используется макрос. В этом отношении макросы подобны встраиваемым (inline) функциям, но в отличие от встраиваемых функций подстановка для макроса выполняется всегда. Обратим внимание на еще одно отличие: функция определена для данных того типа, который
Препроцессорные средства 251 указан в спецификации ее параметров и возвращает значение только одного конкретного типа. Макрос пригоден для обработки аргументов любого типа, допустимых в выражениях, формируемых при обработке строки замещения. Тип получаемого значения зависит только от типов аргументов и от самих выражений. Таким образом, макрос может заменять несколько функций. Например, приведенные макросы тах() и ABS() верно работают для аргументов и с целыми, и с вещественными типами, а результат зависит только от типов параметров. Механизм перегрузки и шаблоны функций позволяют решать те же задачи, что и макросы. Именно поэтому в отличие от языка Си в программах на Си++ макросредства используются реже. Покажем на примере некоторые ограничения и возможности макроопределений с параметрами: //Р07_02.срр - Особенности макроопределений с параметрами #include <iostream> using namespace std; #define max(a,b) (a < b ? b : a) #define t(e) e*3 #define PRINTfc) {cout« #c << "is equalcout« c « endl; } #define Ex*x intmainf) { intx = 2; PRINT(max(++x, ++x)); PRINT(t(x)); PRINT( t(x + x)); PRINT(t(x + x)/3); PRiNT(E); } В результате выполнения программы получен следующий, объясняемый ниже, результат: max(++x, ++х) is equal 5 t(x) is equal 15 t(x + x) is equal 20 t(x + x)/3 is equal 10 E is equal 25 Обратите внимание на то, что в макроопределении PRINT строка замещения включает два оператора, разделенных точкой с
252 Глава 7 запятой, причем в строке замещения имеются пробелы. Напоминаем, что при подстановке аргументов макроса в строку замещения запрещены подстановки внутрь кавычек, апострофов или ограничителей комментариев. В случае необходимости подставляемое значение аргумента макроса можно заключить в строке замещения в кавычки Для этого используется специальная операция #, которая записывается непосредственно перед параметром в нужном месте строки замещения. В результате обработки макрообращения PRINT(E) будет сформирована последовательность операторов {cout« "Е" «"is equalcout« Е « endl;} в строке замещения препроцессорное выражение #с заменено на Последующая подстановка вместо Е аргумента х*х приведет к такому результату: { cout << ”\л" << "Е" « ” is equal cout« х*х;} Именно поэтому в результатах слева от слова "равно" ("is equal") выводится изображение аргумента, использованного в обращении к PRINT(). Рассмотрим другие результаты. При обработке оператора PRINT{max{++x, ++х)); выполняется цепочка подстановок. В качестве изображения аргумента выводится тах(++х, ++х), затем выполняется подстановка строки замещения для макроса тах(), и печатается значение выражения (++х < ++х ? ++х: ++х). При вычислении значения этого выражения х увеличивается на 3, т.е. становится равным 5. Обращение PRINT(t(x)) выводит значение выражения х*3. Обращение PRINT(t(x + х)) выводит значение выражения х + х*3. Обратим внимание, что макрос f(e) в этом случае не утраивает значения своего аргумента. Чтобы утраивалось значение любого аргумента макроса f(e), следует использовать скобки вокруг параметра в строке замещения, т.е. записать #deflne t{e) (е)*3.
Препроцессорные средства 253 При таком определении t(e) в операторах PRINT(t{x + х)); PRINT(t(x + х))/3; сформировались бы выражения (х + х)*3, равное 30 и (х + х)*3/3, равное 10. Приведенные примеры показывают, что для устранения неоднозначных или неверных использований макроподстановок, параметры в строке замещения и саму строку замещения по крайней мере полезно заключать в скобки. Продемонстрируем особенности и возможности более сложных макросов, которые в ряде случаев могут заменять традиционные функции языка Си++. Напишем макрос для вывода элементов массива в виде таблицы, точнее, по столбцам с размещением в каждой строке выбранного программистом количества элементов. Каждый элемент будем выводить в виде имя_масива[индекс]=значение Чтобы упростить решение, для оформления столбцов применим табуляцию. Кроме того, будем считать, что длина последовательности имя_масива[индекс]=значение не велика и "регулярность" таблицы не нарушается. В следующей программе определим и инициализируем массив, а затем с помощью макроса выведем его элементы. Текст программы: //Р07_03.срр - Макрос как аналог функции #include <iostream> using namespace std; //Макрос для печати массива в виде таблицы: #define ARRAY_PRINT(ARRAY, N) \ {int \ for(_i=0;_i<sizeof(ARRAY)/sizeof(ARRAY[0]); _i++) \ { cout« #ARRAY"["<<_/<<"]=" \ «ARRAY[J]«"\t"; \ if((J+1) %N==0) cout«endl; \ ; \ } intmainf) { intar[ ]={ 1,2,3,4,5,6,7,8,9,0};
254 Глава 7 ARRAY_PRINT(ar,4); //Обращение к макросу cout« end!; return 0; } У макроса ARRAY_PRINT(ARRAY, N) два параметра. Первый именует массив, второй определяет число столбцов в таблице при выводе элементов массива. Обратите внимание, что макрос вычисляет количество элементов в массиве-аргументе, применяя выражение с операцией sizeof. Тем самым для макроса не требуется в параметрах задавать число элементов массива. В программе определен и инициализирован массив из десяти элементов. Применение макроса приводит к следующим результатам выполнения программы: Результат выполнения программы: аг[0]=1 аг[ 1 ]=2 аг[2]=3 аг[3]=4 ar[4]=5 ar[5]=6 ar[6]=7 аг[7]=8 аг[8]=9 аг[9]=0 Задание. Для размещения элементов массива «по столбцам» в макросе использована табуляция (код, представляемый эскейп-последовательностьюУ). При увеличении длины изображения каждого элемента массива правильное размещение элементов в столбцах будет нарушено. Измените строку замещения таким образом, чтобы таблица правильно формировалась при большем количестве элементов у массива-аргумента. 7.6. Препроцессорные операции и дополнительные директивы Остановимся на некоторых особенностях препроцессора, его операций и директив. При препроцессорной обработке исходного текста программы каждая строка обрабатывается отдельно. (Напоминаем, что возможно "соединение" строк: если в конце строки стоит символ ’V, а за ним — неотображаемый символ перехода на новую строку \п\ то эта пара символов исключается и следующая строка
неПрепроцессорные средства 255 посредственно присоединяется к текущей строке.) Анализируя строки, препроцессор сначала распознает лексемы. Лексемами для препроцессора являются: • имена заголовков (заголовочных файлов); • идентификаторы; • числа; • символьные константы; • строковые константы; • препроцессорные операции и разделители; • отдельные, отличные от пробельных, символы, не относящиеся к вышеупомянутым. В последовательности лексем, образующей строку замещения, разрешается использование двух операций - # и ##, первая из которых помещается перед параметром, а вторая — между любыми двумя лексемами. Операция #, уже использованная выше, требует, чтобы текст, замещающий данный параметр в формируемой строке, заключался в двойные кавычки. Например, для определения #define sm(zip) cout« #zip обращение (макровызов) sm(сумма); приведет к формированию оператора cout« "сумма"', Операция ##, допускаемая только между лексемами строки замещения, позволяет выполнять конкатенацию лексем, включаемых в строку замещения. Определение #define abc(a,b,c,d) a##(##b##c##d) позволит сформировать выражение s/V?(x+y), если использовать макровызов abc(sinlxl,+,,y). Препроцессорная операция defined позволяет по-другому записать директивы условной компиляции: #/f defined есть аналог #ifdef, #if!defined есть аналог #ifndef. Для нумерации строк можно использовать директиву #//ле целочисленная_константа
256 Глава 7 которая указывает компилятору, что следующая ниже строка текста имеет номер, определяемый целой десятичной константой. Директива может определять номер строки и имя файла: #//ле целочисленная_константа "имя_файла" Директива terror последовательность_лексем приводит к выдаче диагностического сообщения в виде последовательности лексем. Естественно применение директивы #еггог совместно с условными препроцессорными командами. Например, определив некоторую препроцессорную переменную NAME, ttdefine NAME 5 в дальнейшем можно проверить ее значение и выдать сообщение, если у NAME окажется другое значение: #if (NAME != 5) terror NAME должно быть равно 5 ! Сообщение будет выглядеть так: fatal: имя_файла номер_строки tterror directive: NAME должно быть равно 5 ! Допустима пустая директива из одного символа: #, не вызывающая никаких действий. Команда #pragma последовательность_лексем определяет действия, зависящие от конкретной реализации компилятора. Например, используется вариант этой директивы для извещения компилятора о наличии в тексте программы команд ассемблера. 7.7. Встроенные (предопределенные) макроимена Существуют встроенные (заранее определенные, предопределенные) макроимена, доступные препроцессору во время обработки. Они позволяют получить следующую информацию:
Препроцессорные средства 257 LINE десятичная константа - номер текущей обрабатываемой строки файла с программой Си++. Принято, что номер первой строки исходного файла равен 1. FILE_ _ строка символов — имя компилируемого файла. Имя изменяется всякий раз, когда препроцессор встречает директиву #include. После окончания включения файла восстанавливается предыдущее значение макроимени __FILE__. _ _DATE_ _ строка символов в формате: "месяц число год" ("Мтт dd уууу"), определяющая дату начала обработки исходного файла. Названия месяцев {Мтт) те же, которые выдает функция asctime{) из стандартной библиотеки языка Си: "Jan”, "Feb", "Mar”, "Apr”, "May", "Jun”, "JuF, "Aug", "Sep”, "Okt”, "Nov”, "Dec". В последовательности символов dd первым символом является пробел, если значение даты меньше 10. Т1МЕ_ _ строка символов вида "часы:минуты:секунды" ("hh:mm:ss"), определяющая время начала трансляции текущего исходного файла. Формат hh:mm:ss соответствует результату, выдаваемому функцией asctimeQ. STDC константа, равная 1, если компилятор работает в соответствии с ANSI-стандартом. В противном случае значение макроимени STDC_ _ не определено. Стандарт Си++ предполагает, что наличие имени STDC_ _ определяется реализацией. В конкретных реализациях набор предопределенных макроимен может быть шире. Приведем иллюстративный пример использования некоторых директив и предопределенных макроимен. //Р07_04.срр - Макросы, предопределенные имена, директива #Нпе #include <iostream> using namespace std; #define INPUT(x) { cout«#x"="; cin»x; } #define OUTPUT(x) cout«#x"="«x«endl; , 7-27«
258 Глава 7 // Макрос ввода и суммирования чисел: #define CYCLE(SUMMA) { \ double _МЕМВ; \ SUMMA = 0.0; \ do { INPUT(_MEMB) \ SUMMA += JAEMB; } \ while (_МЕМВ);} intmain() { double result; OUTPUT(_ _FILE ); OUTPUT(_ _LINE_ _); CYCLE( result); #line 100 "filejantom.cpp" OUTPUT(_ _FILE_ _); OUTPUT(_ _LINE_ J; OUTPUTf result); return 0; } Результаты выполнения программы: _ _FILE_ _=P07_04.cpp LINE_ _=16 _MEMB=4<ENTER> _MEMB=42<ENTER> _MEMB=4<ENTER> _MEMB=0<ENTER> _ _FILE_ _-fileJantom. cpp __jUNE__=101 result=50 В программе три макроса и препроцессорная директива #//пе. Кроме того, использованы предопределенные препроцессорные имена _ _FILE_ _ и _ _LINE _ В строке замещения макроса CYCLEf) последовательность операторов, заключенная в операторные скобки, т. е. это блок, в котором определена вспомогательная переменная double_MEMB. Эта переменная использована в качестве аргумента при обращении к макросу INPUT(). Именно эта переменная получает вводимые с клавиатуры значения. Параметр
Препроцессорные средства 259 SUMMA в строке замещения используется слева в двух операторах присваивания. Следовательно, при обращении к макросу аргументом может быть только леводопустимое выражение. Обратим внимание на сложность макроса CYCLE(). В нем присутствует определение вспомогательной переменной double _МЕМВ и реализован цикл. В теле цикла выполняется обращение к макросу INPUT(_MEMB). При ошибках программу с таким макросом трудно отлаживать. Именно из-за сложностей при отладке макросы чаще всего используют в готовом виде, т. е. размещают в библиотеках и потом применяют в прикладных программах только макровызовы. Макровызов CYCLEf result) после препроцессорной обработки превращается в такую последовательность операторов: { double _МЕМВ; result = 0.0; do { { cout«"_MEMB""="; cin» _MEMB;} result += _MEMB; } while (_MEMB); } ; Обратите внимание, что в качестве «подсказки» пользователю выводится строка «JMEMB-», а сумма введенных чисел накапливается в аргументе макроса, т. е. в переменной result. В основной программе обращения к макросу OUTPUT(_ _FILE_ _); OUTPUT(_ _LINE_ _); позволяют вывести значения имени файла и номер строки. Обращения к ним до команды #//ле 100 "filejantom.cpp” сообщают реальные номер строки и имя файла. (Можете посчитать строки текста и убедиться, что номер строки выведен правильно. А имя файла при трансляции было именно таким.) В команде #Ппе изменен номер следующей за этой командой строки и введено новое имя файла. Поэтому результаты такие: _ _FILE_ _=file_fantom.cpp __LINE__=101 Остальные детали, по-видимому, не требуют пояснений.
260 Глава 8 СТРУКТУРЫ И ОБЪЕДИНЕНИЯ 8.1. Структура как совокупность данных Из основных типов языка Си++ пользователь может конструировать производные типы. Наиболее значимым из структурированных производных типов в языке Си++ является класс. Чтобы не рассматривать одновременно множество понятий, относящихся к классам, и все разнообразие средств классов, остановимся вначале на частных случаях класса — на структурах и объединениях. Однако предварительно введем простейший формат определения (спецификации) класса: ключ_класса имя_класса {компоненты класса}; Последовательность, заключенная в фигурные скобки {}, называется телом класса. Часть определения, предшествующая телу класса, называют его заголовком. Ключ_класса - это одно из служебных слов: c/ass, struct, union. Имя_ класса - это идентификатор, свободно выбираемый программистом. Компоненты класса - типизированные данные (поля данных) и принадлежащие классу компонентные функции (методы класса). Компоненты класса вводятся в соответствии с синтаксисом определений данных (объектов) и функций. Такое определение класса называют его спецификацией. Спецификация класса (в отличие от определений функций) завершается обязательным разделителем "точка с запятой". Если в классе отсутствуют явные определения методов, а в качестве ключа использовано служебное слово struct, то класс соответствует структурному типу с теми правами, которые имеют структурные типы в языке Си. Если в качестве ключа класса
исСтруктуры и объединения 261 пользовано служебное слово union и в теле класса нет определений методов, то создается объединяющий тип. Отметив "родственные отношения" понятий класс, структура и объединение, рассмотрим структуры и объединения в том виде, какой они унаследовали от языка Си. При этом не стоит забывать, что структуры и объединения есть только частные случаи классов. Изучение структур и объединений есть первый шаг в освоении общего механизма классов. Структура - это объединенное в единое целое множество поименованных элементов (полей данных) в общем случае разных типов. Сравнивая структуру с массивом, следует отметить, что массив — это совокупность однородных объектов, имеющая общее имя — идентификатор массива. Другими словами, все элементы массива являются объектами одного и того же типа. Это не всегда удобно. Пусть, например, библиотечная (библиографическая) карточка каталога должна включать сведения, которые приведены для книг в списке литературы, помещенном в конце нашей книги. Таким образом, для каждой книги будет указываться следующая информация: • фамилия и инициалы автора (строка символов); • заглавие книги (строка символов); • место издания (строка символов); • издательство (строка символов); • год издания (целое число); • количество страниц (целое число). Если к библиографической карточке каталога нужно обращаться как к единому целому, то воспользоваться массивом для представления всех ее данных довольно сложно. Все данные имеют разные длины и разные типы. Объединить такие разнородные данные удобно с помощью структуры. Каждая структура включает в себя один или несколько объектов (переменных, массивов, указателей, структур и т.д.), называемых элементами (полями данных) структуры. Сведения о данных, входящих в библиографическую карточку, с помощью структуры можно представить таким структурным типом (классом с ключом struct): struct card { char author [30]; // Ф. И. О. автора книги
262 Глава 8 char title [80]; char city [20] ; char firm [20]; int year; // Заголовок книги И Место издания Ц Издательство И Гэд издания И Количество страниц int pages', >; Определение вводит новый тип с конкретным именем card. Элементы структуры могут быть базовых или производных типов. В структурах типа card будут элементы базового типа int и производного типа char [ ]. Определив структурный тип, можно определять и описывать конкретные структуры, т.е. структурированные объекты, например, так: cardrec\, гес2, гесЗ; Здесь определены три структуры (три объекта) с именами red, гес2, гесЗ. Каждая из этих структур содержит в качестве элементов свои собственные данные (поля данных) char title [80]; char city [20]; набор которых определяет структурный тип с именем card. Если структура определяется однократно, т.е. нет необходимости в разных частях программы определять или описывать одинаковые по внутреннему составу структурированные объекты, то можно не вводить именованный структурный тип, а непосредственно определять структуры одновременно с определением их компонентного состава. Следующее определение вводит спецификацию безымянного структурного типа, две структуры с именами XX, УУ, массив структур с именем ЕЕ и указатель pst на структуру: struct { c/iar/V[12]; int value] } XX, YY, EE[8], *pst\ ВХХ,УУ и в каждый элемент массива ЕЕ[0], ..., ЕЕ[7] входят в качестве элементов массив char N[ 12] и целая переменная value. Имени у этого структурного типа нет.
Структуры и объединения 263 Для обращения к объектам, входящим в качестве элементов (полей данных) в конкретную структуру, чаще всего используются уточненные (квалифицированные) имена: имя_структуры.имя_элемента_структуры Например, для определенной выше структуры YY оператор YY. value = 86; присвоит полю данных value значение 86. Точно так же можно вывести в выходной поток cout значение поля данных любой структуры, например для ввода значения поля данных value структуры ££[4] можно использовать оператор cin » ЕЕ[4].value', Как понятно из этих примеров, элемент структуры (поле данных) обладает правами объекта того типа, который для поля указан в определении структурного типа. Например, для полей данных с именем value из структур £Е[0] ЕЕ[7], XX, YY определен тип int. Подчеркнем еще раз, что в языке Си++ структурный тип — это частный случай класса, а конкретная структура — объект этого класса. При определении структур (объектов) возможна их инициализация, т.е. задание начальных значений их элементов. Например, введя структурный тип card, можно следующим образом определить и инициализировать конкретную структуру: card dictionary = { "Hornby AS.", "Oxford students\ dictionary of Current English ", "Oxford", "Oxford University", 1984, 769}; Нужно еще раз обратить внимание на отличие имени объекта—конкретной структуры (в наших примерах dictionary, red, rec2, recZ, XX, YY, ££[0], ..., £E[7]) от имени структурного типа (в нашем случае card). С именем структурного типа не связан никакой конкретный объект. Определение структурного типа вводит только шаблон (формат, внутреннее строение) структур-объектов. Идентификатор card в нашем примере — это название структурного типа, т.е. "штамп" или шаблон структур-объектов,
264 Глава 8 которые будут определены в программе. Приведем пример программы со структурным типом и объектами-структурами: //Р08_01.срр - Структурный тип, структура и ее поля #include <iostream> using namespace std; ttdefine PRINT(c) cout « #c « " = "« c « endl; intmainf) { struct card { char author [30]; char title [80]; char city [20]; char firm [20]; int year; int pages; }; //Ф. И. О. автора книги // Заголовок книги // Место издания //Издательство //Год издания // Количество страниц card dictionary = {"Hornby A.S.", "Oxford students" " dictionary of Current English", "Oxford", "Oxford University", 1984, 769}; PRINTfdictionary.author); PRINT( dictionary, title); PRINT( dictionary, city); PRINT( dictionary, firm); PR IN T( dictionary, year); PRINT(dictionary.pages); return 0; ) Результаты выполнения программы: dictionary.author = Hornby A. S. dictionary, title = Oxford students dictionary of Current English dictionary, city = Oxford dictionary.firm = Oxford University dictionary.year = 1984 dictionary, pages = 769 Определение структурного типа может быть совмещено с определением конкретных структур этого типа: struct PRIM {char * name; long sum;} А, В, C;
Структуры и объединения 265 Здесь определен структурный тип с именем PRIM и три структуры А, 6, С, имеющие одинаковое внутреннее строение. Повторяем, что будет ошибкой использовать имя структурного типа для именования элемента структуры: PRIM, sum = QOOL; // Ошибочная конструкция С. sum = 800L; // Верная конструкция Так как имя структурного типа обладает правами имен типов, то разрешено определять указатели на структуры: имя_структурного_типа * имя_указателя_на_структуру; Как обычно, определяемый указатель может быть инициализирован. Значением каждого указателя на структуру может быть адрес структуры того же типа, т.е.адрес байта, начиная с которого структура размещается в памяти. Структурный тип задает ее размеры и тем самым определяет, на какую величину (на сколько байтов) изменится значение указателя на структуру, если к нему прибавить 1 (или из него вычесть 1). Например, после наших определений структурного типа card и структуры гес2 можно так записать определение указателя на структуру типа card: card * ptrcard = &rec2; Здесь определен указатель ptrcard и ему с помощью инициализации присвоено значение адреса одной из конкретных структур (одного из объектов) типа card. После определения и инициализации такого указателя появляется еще одна возможность доступа к элементам структуры гес2. Ее обеспечивает операция -> доступа к элементу структуры, с которой в этот момент связан указатель. Формат соответствующего выражения таков: имя_указателя -> имя_элемента_структуры Например, количество страниц книги, информация о которой хранится в структуре гес2, будет значением выражения ptrcard -> pages Вторая возможность обращения к элементу структуры с помощью адресующего ее указателя — это разыменование указателя и формирование уточненного имени такого вида: (*имя_указателя). имя_элемента_структуры
266 Глава 8 Обратите внимание на круглые скобки. Операция разыменования должна относиться только к указателю, а не к элементу структуры. Таким образом, следующие три выражения эквивалентны: (*ptrcard). pages ptrcard->pages rec2.pages Все они именуют один и тот же элемент (поле данных) int pages конкретной структуры гес2, имеющей тип card. Для структур могут быть определены ссылки: имя_структурного_типа& имя_ссылки_на_структуру инициализатор; Например, для введенного выше структурного типа PRIM можно таким образом ввести ссылки на структуры (объекты) Л, В: PRIM & ref А = А; PRIM & refB(B); Для разнообразия ссылки ref А и refB инициализированы в примере по-разному. После таких определений ref А есть синоним имени структуры Л, refB есть другое имя для структуры В. Теперь возможны, например, такие обращения: • A. sum эквивалентно ref A. sum; • * В. name эквивалентно * refB.name; • A. name эквивалентно ref A. name. Изучая структуры, имеет смысл обратить внимание на их представление в памяти ЭВМ. В следующей программе определена структура STR и выведены значения адресов ее элементов: //Р08_02.срр - Размещение в памяти элементов структуры #include <iostream> using namespace std; intmain() { struct {long L; char C[3]; double D;
Структуры и объединения 267 } STR = {10L,'аЪ\'с',20.3}; cout« "sizeoff STR) = " << sizeoff STR) « endl; cout« "sizeoff STR.C) = "« sizeoff STR.C) « endl; cout« "&S.TR.L = \Г « &STR.L « endl; cout« "&STR.C = \t"« &STR.C « endl; cout« "&STR.D = \t" « &STR.D « endl; return 0; } Результаты выполнения программы (DJGPP): sizeof(STR) = 16 sizeoffSTR.C) = 3 &STR.L = 0xa9598 &STR.C = 0xa959c &STR.D = 0xa95a0 Результаты выполнения программы и соответствующая им схема на рис. 8.1 иллюстрируют основные соглашения о структурах. Сумма размеров элементов структуры (4+3+8=15) меньше, чем отведено структуре в целом (16). Дело в том, что размер массива char С[3] не кратен длине слова компьютера, и перед следующим элементом double D для выравнивания вставлен неиспользуемый (пустой) байт. Байты: 98 99 9а 9Ь 9с 9d 9е 9f аО а1 а2 аЗ а4 а5 аб а7 ... ... X X X X а b с XXX X X X X X , long L ( char СГ31 ◄ ^ double D < » Рис. 8.1. Размещение в памяти конкретной структуры из программы Р8_02.срр Чтобы продемонстрировать зависимость результатов (размещения в памяти данных) при работе со структурами, приведем результаты выполнения той же программы, откомпилированной другим транслятором.
268 Глава 8 Результаты выполнения программы в Visual Studio .NET 2005: sizeoffSTR) = 16 sizeoffSTR.С) = 3 & STR.L = 0012FF50 &STR.C = 0012FF54 &STR.D = 0012FF58 Размещение элементов структуры в памяти может в некоторых пределах регулироваться с помощью опций компилятора. Например, в ряде случаев имеется возможность "плотной упаковки" структур, при которой выравнивание по границам слов не выполняется. В отношении элементов структур существует практически только одно существенное ограничение - элемент структуры не может иметь тот же самый тип, что и определяемый структурный тип. Таким образом, следующее определение структурного типа ошибочно: struct mistake {mistake s; int m\}; // Ошибка! В то же время элементом определяемой структуры может быть указатель на структуру определяемого типа: struct correct { correct * pc; long f\}; // Правильно! Элементом определяемой структуры может быть другая структура, тип которой уже определен: struct begin {intk; char * h\) strbeg\ struct next {begin beg\ float d\}; Если в структурном типе нужно в качестве элемента использовать указатель на структуру другого типа, в которой, в свою очередь, используется указатель на данную (первую) структуру, то разрешена такая последовательность: struct А\ И Неполное определение структурного типа struct В { struct А * ра;}; struct А { struct В * pb\}; Неполное определение структурного типа А допустимо использовать в определении структурного типа В, так как определение указателя ра на структуру типа А не требует сведений о
разСтруктуры и объединения 269 мере структуры типа Л. Последующее определение в той же программе структурного типа А обязательно. Использование в структурах типа А указателей на структуры уже введенного типа В не требует пояснений. Рассматривая "взаимоотношение" структур и функций, видим две возможности: возвращаемое функцией значение и параметры функции. Функция может возвращать структуру как результат. Пример: struct help { char * name; int number;}; help func1(); // Прототип функции Функция может возвращать указатель на структуру (на объект): help * func2(); // Прототип функции Функция может возвращать ссылку на структуру (на объект): help & func3(); 11 Прототип функции Через аппарат параметров информация о структуре может передаваться в функцию либо непосредственно (по значению), либо через указатель, либо с помощью ссылки: void func4 (help str); // Передача по значению void func5 (help * pst); // С помощью указателя void func6 (help & rst); 11С помощью ссылки Напомним, что применение ссылки или указателя на объект в качестве параметра позволяет избежать дублирования объекта в памяти. Операндом для операции new может быть структурный тип. В этом случае выделяется память для структуры использованного типа, и выражение с операцией new возвращает указатель на выделенную память. В начале главы был введен структурный тип "библиографическая карточка". Безымянный объект этого структурного типа можно так разместить в динамической памяти: card * pointer; pointer = new card; Память может быть выделена и для массива структур, например, так:
270 Глава 8 card * list; list = new card [9]; В этом случае выражение с операцией new возвращает указатель на начало массива. Дальнейшие действия с элементами массива структур подчиняются уже знакомым правилам индексации и доступа к элементам структур. Например, разрешен такой оператор: list[0].year = 1906; 8.2. Объединения разнотипных данных Со структурами "в близком родстве" находятся объединения (т. е. классы, вводимые ключом union), которые вводятся с помощью служебного слова union. Чтобы пояснить "степень родства" объединений со структурами, рассмотрим приведенное выше в программе Р7_02.срр определение структуры STR: struct {long L; char C[3], double D; } STR; Размещение этой структуры в памяти схематично изображено на рис.8.1. Важно то, что каждый элемент структуры имеет свое собственное место в памяти и размещаются эти элементы последовательно. Определим внешне очень похожее на структуру STR объединение UNI: union {long L; char C[3]; double D; } UNI; Количество элементов в объединении с именем UNI и их типы совпадают с количеством и типами элементов в структуре STR. Но существует одно очень важное отличие (которое иллюстрирует рис. 8.2) — все элементы объединения имеют один и тот же начальный адрес.
Структуры и объединения 271 Участок памяти, выделенной объединению Рис. 8.2. Следующая программа подтверждает сказанное: //Р08_03.срр - Размещение в памяти объединения #include <iostream> using namespace std; int main() { union { long L; char C[3]; double D; } UNI = { 10L }; cout« "sizeoff UNI) = " << sizeoff UNI) « endl; cout« "sizeoff UN I.L) = " << sizeoff UNI.L) « endl; cout« "sizeoff UNI. C) = " << sizeoff UNI.C) « endl; cout« "sizeoffUNI.D) = " << sizeoffUNI.D) « endl; cout« "&UNI = \t"« &UNI« endl; cout« "&UNI.L = \Г « &UNI.L « endl; cout« "&UNI.C = \t"« &UNI.C « endl; cout« "&UNI.D = \t"« &UNI.D « endl; return 0; } Результаты выполнения программы: sizeoff UNI) = 8 sizeoff UNI.L) = 4 sizeoff UNI. C) = 3 sizeoffUNI.D) = 8 &UNI = 0xa95a0 &UNI.L = 0xa95a0 &UNI.C = 0xa95a0 &UNI.D = 0xa95a0 a95a0 байт байт байт байт байт байт байт байт char С[3] long L double D Схема размещения в памяти объединения UNI
272 Глава 8 Как подтверждают приведенные результаты выполнения программы, все элементы объединения UNI имеют один начальный адрес. Размеры элементов соответствуют их типам, а размер объединения определяется максимальным из размеров его элементов. Итак, объединение можно рассматривать как структуру, все элементы которой при размещении в памяти имеют нулевое смещение от начала. Тем самым все элементы объединения размещаются в одном и том же участке памяти. Размер участка памяти, выделяемого для объединения, определяется максимальным из длин его элементов (см. рис. 8.2). Если для структур, рассматриваемых в этой главе, вместо термина "класс" мы использовали термин "структурный тип", то для объединений можно говорить об объединяющем типе: union имяобъединяющего_типа {элементы объединения}; Пример определения объединяющего типа (не забывайте, что это частный случай класса): union mixture { double d; long E[ 2]; int K[ 4]; Введя объединяющий тип, можно определять конкретные объекты-объединения, их массивы, а также указатели и ссылки на объединения: mixture mA, mB[4]; Ц Объединение и массив объединений mixture * pmix; // Указатель на объединение mixture & rmix = mA; // Ссылка на объединение Для обращения к элементу объединения можно использовать либо уточненное имя: имя объединения, имяолемента либо конструкции, включающие указатель: указатель на объединение-> имяолемента
Структуры и объединения 273 (*указатель_на_объединение). имя_элемента либо конструкцию, включающую ссылку: ссылка_наобъединение, имя_элемента Примеры выражений с обращениями к полям данных объектов-объединений: mA.d = 64.8; тВ[2].Е[1]= 10L; pmix = & тВ[0]; pmix->E[0] = 66; cin »(*pmix). К[ 1 ]; cin » rmix.E[0]; Заносить значения в участок памяти, выделенный для объекта-объединения, можно с помощью любого из его элементов. То же самое справедливо и относительно чтения содержимого участка памяти, выделенного для объединения. Если бы элементы объединения имели одинаковую длину и одинаковый тип, а отличались только именами, то использование объединения было бы подобно применению ссылок. Просто один участок памяти в этом случае имел бы несколько различных имен: union {int //, int jj} unij; unij.ii = 15; // Изменяем содержимое cout« unij.jj; 11 Выводим to же самое содержимое Основное достоинство объединений — возможность разных трактовок одного и того же содержимого (битового представления) участка памяти. Например, введя объединение union { float F\ unsigned long K\ } FK\ можно занести в участок памяти, выделенный для объединения FK, вещественное число FK.F- 3.141593f; а затем рассматривать код его внутреннего представления как некоторое беззнаковое длинное целое: cout« FK.K\ (В данном случае будет выведено 1078530012.) IS'2762
274 Глава 8 Итак, основное назначение объединений - обеспечить возможность доступа к одному и тому же участку памяти с помощью объектов разных типов. Это позволяет трактовать содержимое одного и того же участка памяти как значения данных разных типов. Необходимость в таком механизме возникает, например, для выделения из внутреннего представления (из кода) объекта определенной части. Если включить в объединение символьный массив такой же длины, что и другие элементы объединения, то получим возможность доступа к отдельным байтам внутреннего представления объединения. Например, определим объединение (рис. 8.3): union { float F; unsigned long L\ char H[4];} FLH; Занеся в участок памяти, выделенный для объединения FLH, вещественное число, например, так: FLH.F= 2.718282f; можем получить значение кода его внутреннего представления с помощью уточненного имени FLH.L и (или) значения кодов, находящихся в отдельных байтах: FLH.H[0], FLH.H[ 1], FLH.H[2], FLH.H[ 3]. Участок памяти, выделенной объединению Массив символов char Н[4] Н[3] Н[2] H[1] H[0] —► байт байт байт байт unsigned long L 4- float F —► Рис. 8.3. Размещение в памяти объединения FLH Как массивы, так и структуры могут быть элементами объединений, причем здесь возможны весьма разнообразные сочетания. При определении конкретных объединений разрешена их инициализация, причем инициализируется только первый элемент объединения (остальные получат значения по коду первого). Например:
Структуры и объединения 275 union compound { long LONG; float FLO; char CHAR[ 4]; }; compound mix 1 = {11111111/.}; // Правильно compound mix2 = { 'a', 'b\ 'c\ ’d'}\// Ошибка Разрешено формировать массивы объединений и инициализировать их: compoundmixture[ ] = {lLf2L, 3L, 4L }; Здесь для каждого элемента mixture[f] введенного массива из четырех объединений типа compound инициализация выполнена для первого компонента объединения, т.е. начальное значение явно получил каждый элемент mixture[i].LONG. Доступ к внутренним кодам этих значений возможен также через элементы mixture[i].INT\j] и mixture[i].CHAR[k], 8.3. Битовые поля структур и объединений В классах (мы рассматриваем пока их частные случаи — структуры и объединения) в качестве полей данных (элементов) могут использоваться битовые поля. Каждое битовое поле представляет целое или беззнаковое целое значение, занимающее в памяти фиксированное число битов (например, 1 или 16 бит). Битовые поля могут быть только элементами классов, т.е. не могут появляться как самостоятельные объекты программ. Битовые поля не имеют адресов, т.е. для них не определена операция &, нет указателей и ссылок на битовые поля. Они не могут объединяться в массивы. Назначение битовых полей — обеспечить удобный доступ к отдельным битам данных. С помощью битовых полей можно формировать объекты с длиной внутреннего представления, не кратной байту. Это позволяет плотно "упаковывать" информацию и тем самым экономить память, например, при работе с однобитовыми "флажками". 18*
276 Глава 8 Определение структуры с битовыми полями имеет такой формат: struct имя_структурного_типа { тип_поля имя_поля: ширина_поля; тип_поля имя_поля: ширина_поля; } имя_структуры\ Здесь: тип_поля - перечисление или один из базовых целых типов int, char, short, long и их знаковые и беззнаковые варианты. имяполя - идентификатор, выбираемый пользователем; ширина_поля - целое неотрицательное десятичное число, значение которого обычно не должно превышать длины слова конкретной ЭВМ. Таким образом, диапазон возможных значений ширины_поля существенно зависит от реализации. Однако стандарт языка Си++ не запрещает вводить битовые поля, длина которых превышает принятую максимальную длину битового поля. В этом случае лишние биты служат для выравнивания размещения кодов в памяти и не учитываются в значении, представляемом битовым полем. Внутреннее представление битовых полей в памяти существенно зависит от реализации. Например, младшие биты могут быть как справа, так и слева. Пример определения структурного типа с битовыми полями: struct { Int а: 10; int b: 14; } хх, *рх; Здесь же определены структура-объект хх и указатель рх на структуру. Для обращения к битовым полям используются те же конструкции, что и для обращения к обычным элементам структур: имя_структуры. имя_поля указатель_на_структуру- >имя_поля ссылка _на_структуру. имя_поля (*указательнаструктуру). имя_поля
Структуры и объединения 277 Например, для структуры хх и указателя рх допустимы такие операторы: хх.а = 1; рх = &хх; рх->Ь = 48; Мы уже отметили, что от реализации зависит порядок размещения полей структуры в памяти ЭВМ. Биты полей могут размещаться как справа налево, так и слева направо. Кроме того, реализация определяет, как размещаются в памяти битовые поля, длина которых не кратна длине слова и (или) длине байта (рис. 8.4). Для компиляторов, работающих на IBM PC, поля, размещенные в начале описания структуры, имеют младшие адреса. Такое размещение изображено на рис. 8.4. 76543210 76543210 765432 1 0 76543210 ZZZZZZ ZZZZZZZZ ZZ ZZZZZZZZ f Int Ь:14 р 4 Int а: 10 р 76543210 76543210 765432 1 0 76543210 ZZZZZZZZ Z ZZZZ Z XX X XXXX X XX , Int b:14 „ 4 Int a: 10 „ Рис. 8.4. Варианты размещения в памяти битовых полей структуры В компиляторах часто имеется возможность изменять размещение битовых полей, выравнивая их по границам слов или выполняя плотную упаковку. Некоторые возможности влиять на размещение битовых полей в памяти имеются и на уровне синтаксиса самого языка Си++. Во-первых, при определении битового поля разрешается не указывать его имя. В этом случае (когда указаны только двоеточие и ширина поля) в структуру вводятся неиспользуемые (недоступные) биты, формирующие промежуток между значимыми полями. Например:
278 Глава 8 struct { int а: 10; int :6; int P:14; }yy; В структуре yy между полем int a: 10 и полем int b: 14 размещаются 6 бит, не доступных для использования. Их назначение — выравнивание полей по плану программиста (рис. 8.5). 76543210 7 6 5 4 3 2 1 0 765432 1 0 76543210 ZZZZZZ ZZZZZZZZ У УУУУ У ZZ ZZZZZZZZ 4 int Ь:14 ^ < М 6 ► 4 lnta-ЛО ¥ Рис. 8.5. Структура с безымянным битовым полем Битовые поля в объединениях используются для доступа к нужным битам того или иного объекта, входящего в объединение. Например, следующее объединение позволяет замысловатым способом сформировать код символа 'D' (равный десятичному числу 68): union { char simb; struct {intx:5; inty:3\ }hh; ) cod; cod.hh.x = 4; cod.hh.y = 2; cout« cod. simb; 11 Выведет на экран символ V Рис. 8.6 иллюстрирует формирование кода 68, соответствующего символу 'D'. Номера битов: Битовые поля: Элемент char: 01 1 I 0 о 1 о 1 , 1 0 1 0 1111 ^ cod.hh.v ^ cod.hh.x Ч w Л Щ w char simb Рис. 8.6. Объединение со структурой из битовых полей
Структуры и объединения 279 Для иллюстрации особенностей объединений и структур с битовыми полями рассмотрим следующую программу: //Р08_04.срр - Битовые поля, структуры, объединения #include <iostream> using namespace std; #include "cyrToDos.h" // функция упаковывает в один байт остатки отделения //на 16 двух целых чисел-параметров: unsigned char codfint a,int b) { union {unsigned char z; struct {unsigned int x:4; // Младшие биты unsigned int y:4; // Старшие биты } hh; } un; un.hh.x = a % 16; un.hh.y = b % 16; return un.z; } // Функция изображает на экране двоичное представление // байта-параметра: void binar( unsigned char ch) { union {unsigned char ss; struct { unsigned aO: 1; unsigned a1:1; unsigned a2:1; unsigned a3:1; unsigned a4:1; unsigned a5:1; unsigned a6:1; unsigned a7:1; } byte; } cod; cod.ss = ch; // Занести значение параметра в объединение // Выводим биты внутреннего кода значения параметра: cout« cyrToDosf "\п НОМЕРА БИТОВ :\t 7 6 543 2 10"); cout« сугТоОоз("\пЗначения битов:"); cout« "\t"« cod.byte.а7 « ""« cod.byte.аб; cout« ”"« cod. byte.a5 « ""« cod. byte. a4;
280 Глава 8 cout« ""« cod.byte.аЗ «””« cod.byte.a2; cout« ”"« cod.byte.a1 « " ” << cod.byte.aO; cout« ”\n"; } intmainf) { int k; int m, n; cout« ”\nm = cin » m; cout« "n = cin » n; к = cod(m,n); cout« "cod = "« k; binar(k); return 0; } Возможный результат выполнения программы: т = 1 <Enter> п = 3 <Enter> cod = 49 НОМЕРА БИТОВ: 76 5432 10 Значения битов: 0 0 1 10 00 1 Результат еще одного выполнения программы: m = 0 <Enter> п = 1 <Enter> cod= 16 НОМЕРА БИТОВ: 765432 10 Значения битов: 0 0 0 10 0 0 0 Комментарии в тексте программы и приведенные результаты объясняют особенности программы. В функциях cod() и Ыпаг() использованы объединения, включающие структуры с битовыми полями. В функции cod() запись данных выполняется в битовые поля структуры hht входящей в объединение ип, а результат выбирается из того же объединения ип, но как числовое значение байта. В функции Ыпаг() обратное преобразование — в нее как значение параметра передается байт, содержимое которого
побиСтруктуры и объединения 281 тово "расшифровывается" за счет обращения к отдельным полям структуры byte, входящей в объединение cod. Одно из принципиальных отличий языка Си++ от языка Си — возможность включения в структуры и объединения не только данных, но и функций. В этом случае мы уже не будем использовать термины "структурный тип" и "структура", "объединение" и "объединяющий тип", а будем говорить о классах и их объектах. Подробно о классах речь пойдет в следующей главе.
282 Глава 9 КЛАСС КАК АБСТРАКТНЫЙ ТИП 9.1. Класс как расширение понятия структуры Вначале остановимся на понятии "тип". Тип в языках программирования — понятие первичное, как понятие множества в математике, и поэтому оно не определяется через более простые понятия. Как мы уже показывали, тип существует и для функции, и для объекта. Тип объекта задает множество возможных значений, принимаемых объектом, и совокупность операций, выполняемых над этими значениями. Совокупность принципов проектирования, разработки и реализации программ, которая базируется на абстракции данных, предусматривает создание новых типов данных, с наибольшей полнотой отображающих особенности решаемой задачи. Создаваемые пользователем абстрактные типы данных могут обеспечить представление понятий предметной области решаемой задачи. В языке Си++ программист имеет возможность вводить собственные типы данных и определять операции над ними с помощью классов. Для каждого типа определяется множество значений и вводится набор операций, пригодных и удобных для обработки этих значений. Класс — это производный структурированный тип, введенный программистом на основе уже существующих типов. Механизм классов позволяет создавать типы в полном соответствии с принципами абстракции данных, т.е. класс задает некоторую структурированную совокупность типизированных данных и позволяет определить набор операций над этими данными. Как уже говорилось в гл. 8, простейшим образом класс можно определить с помощью конструкции, называемой спецификацией класса:
Класс как абстрактный тип 283 кпюч_класса имя_класса {поля_данных_и_методы_класса}; где ключ ^класса - одно из служебных слов class, struct, union; имя_класса - произвольно выбираемый идентификатор; поля_данных - определения и описания типизированных данных; методы_класса - определения и прототипы принадлежащих классу функций. В предыдущей главе введены структурный и объединяющий типы. В отличие от классов в них не было явно определенных функций (методов). Однако в этой и последующих главах мы уже не будем вспоминать структуры и объединения как таковые, а соответствующие им типы будем рассматривать как классы. Заключенный в фигурные скобки список полей данных и методов класса называют телом класса. Телу класса предшествует заголовок. В простейшем случае заголовок класса включает ключ класса и его имя. Определение (спецификация) класса всегда заканчивается точкой с запятой. В теле спецификации класса могут быть данные, функции (методы), классы, перечисления, битовые поля, дружественные функции, дружественные классы. Вначале для простоты будем считать, что в спецификации класса присутствуют только типизированные данные (базовые и производные) и функции (методы). Отметим терминологические трудности, связанные с классами. Все компоненты класса языка Си++ в английском языке обозначаются термином member (член, элемент, часть, компонент). Функции, принадлежащие классу, обозначают термином member functions, а данные класса имеют название data members. В работах, посвященных объектно-ориентированному программированию с использованием других языков (Java, С#, SmallTalk, Oberon, Eiffel и т.д.), чаще всего принадлежащие классу функции, называют методами или функциями класса. Члены данных называют свойствами, данными, элементами данных, полями данных и т.п. Будем использовать равноправно термины метод, принадлежащая классу функция, компонентная функция. Члены данных (класса или его объекта) будем называть полями данных, или компонентными данными. В качестве кпюча_класса можно использовать служебное слово struct, но класс отличается от обычного структурного типа (унаследованного из языка Си), по крайней мере, явным
при284 Глава 9 сутствием в классе методов (функций). Например, следующая спецификация простейшим образом вводит класс "комплексное число": struct complex 1 { //Вариант класса "комплексное число" double real; // Вещественная часть double imag; // Мнимая часть // Определить значение комплексного числа: void define (double re = 0.0, double im = 0.0) { real = re; imag = im; } //Вывести значение комплексного числа: void display(void) { cout« "real ="« real; cout« ", \timag = " « imag « endl; > >; В отличие от традиционного структурного типа в класс complex1, кроме полей данных (real, imag), включены две функции (два метода) define() и display(). Недостатков в нашем простейшем определении класса комплексных чисел несколько. Однако отложим их объяснение и устранение, а обратим еще раз внимание на тот факт, что класс (как и его частный случай — структурный тип), введенный пользователем, обладает правами типа. Следовательно, можно определять и описывать объекты класса и создавать производные типы: complex 1XI, Х2, D; // Три объекта класса complexl complex 1 * point = &D; // Указатель на объект класса complexl dim[8] //Массив объектов класса complexl complexl & Name = Х2; // Ссылка на объект класса complexl и т.д. (Класс "комплексное число" очень полезен в прикладных программах, и поэтому такой класс входит в стандартную библиотеку. Библиотечный класс complex становится доступным при включении в программу заголовка <complex>.) Итак, класс - это тип, введенный программистом. Каждый тип служит для определения объектов. Для определения объекта класса используется, например, такая конструкция: имякласса имя_объекга\ Класс как абстрактный тип
285 В каждый определяемый объект (в экземпляр класса) входят данные, соответствующие полям данных класса. Методы класса позволяют обрабатывать данные конкретных объектов, но в отличие от полей данных методы не тиражируются при создании конкретных объектов класса. Если перейти на уровень реализации, то место в памяти выделяется именно для полей данных каждого объекта класса. Определение объекта класса предусматривает выделение участка памяти и деление этого участка на фрагменты, каждый из которых соответствует отдельному полю данных класса. Таким образом, и в объект Х\, и в объект dim[3] класса complex 1 входит по два поля данных типа double, представляющих вещественные и мнимые части комплексных чисел. Как только объект класса определен, появляется возможность обращаться к его данным, с помощью "квалифицированных" (уточненных) имен, каждое из которых имеет формат: имя_объекта.имя-класса::имя_поля_данных Имя класса с операцией :: обычно может быть опущено, и чаще всего для доступа к данным конкретного объекта уже известного класса (как и в случае структур языка Си) используется уточненное имя: имя_объекта. имя_ поля_данных При этом возможности те же, что и при работе с элементами структур. Например, можно явно присвоить значения полям данных объектов класса complex 1: X1. real = dim[3]. real = 1.24; Xl.lmag = 2.3; dim[3].imag = 0.0; Уточненное (квалифицированное) имя метода класса имя_объекта.имя-класса::имя_метода обеспечивает вызов метода для обработки данных именно того объекта, который указан в уточненном имени. Например, можно таким образом задать значения полей данных для определенных выше объектов класса complex 1: Х1. complexl::define(); //По умолчанию real==0.0, imag==0.0 X2.define(4.3,20.0); //Комплексное число 4.3 +1*20.0
286 Глава 9 С помощью метода display() можно вывести значения полей данных любого из объектов класса complex 1. (Разумно выполнять вывод только для объектов, которым уже присвоены осмысленные значения.) Например, следующее обращение X2.display(); приведет к выводу такого результата: real = 4.3, imag = 20.0. Другой способ доступа к данным объекта предусматривает явное использование указателя на объект и операции косвенного выбора ->: указатель_на_объект_класса -> имя_класса::имя_поля данных либо указатель_на_объект_класса -> имя_поля_данных Определив, как сделано выше, указатель po/nf, адресующий объект D класса complex1, можно следующим образом присвоить значения данным объекта D: point -> real = 24.5; point -> complex1::imag = 19.5; Указатель на объект класса позволяет вызывать методы класса для обработки данных того объекта, который адресуется указателем. Форматы обращения к методу: указатель_на_объект класса -> имя_класса::имя_метода(аргументы) указатель_на_объект класса -> имя_метода(аргументы) Например, вызвать метод display() для данных объекта D позволяет выражение point->display(); В качестве второго иллюстративного примера рассмотрим класс, описывающий товары на складе магазина. Поля данных, вводимые классом: • название товара;
Класс как абстрактный тип 287 • оптовая (закупочная) цена; • торговая наценка (в процентах); Метод — функция печати (вывода) сведений о товаре с указанием розничной цены. Предположим, что для любых товаров, представляемых объектами класса, торговая наценка одинаковая. В этом случае ее можно представить в классе одним (единственным) полем данных, общим для всех объектов класса. Такие поля данных называются статическими. Приведем определение (спецификацию) класса: #include "cyrToDos.h' struct goods {char name[40]; double price; static int percent; //Определение класса "товары" // Наименование товара // Оптовая (закупочная) цена // Торговая наценка, в % //Метод для объектов класса: void Displayf) { // Вывод данных о продаваемом товаре cout« cyrToDosfname); cout« cyrToDosf"- розничная цена: "); cout« long(price *(1.0 + goodsr.percent * 0.01)) « endl; } }; Торговая наценка (percent) определена как статическое поле данных класса. Статические поля данных не "дублируются" при создании объектов класса, т.е. каждое статическое поле данных существует в единственном экземпляре. Доступ к статическому полю данных возможен только после его инициализации. Для инициализации используется конструкция: тип_поля имя_кпасса::имя_поля_данных инициализатор; В нашем примере может быть такой вариант: int goodsr.percent = 12; Это предложение должно быть размещено в глобальной области (global scope) после определения класса, но до обращений к percent. Только при инициализации статическое поле данных
288 Глава 9 класса получает память и становится доступным. Для обращения к статическому полю данных применимы три способа. Во-первых, используется квалифицированное имя: имя_кпасса::имя_поля _данных Во-вторых, статическое поле данных доступно "через" имя конкретного объекта: имя_объекта. имя_класса::имя_поля _данных имя-объекта, имя_поля _данных Третий способ обеспечивает указатель, адресующий какой- либо из уже существующих объектов класса: указатель_на_объект_класса -> имя_класса::имя_поля_данных указатель_на_объект_класса -> имя_поля_данных В следующей программе иллюстрируются перечисленные возможности и особенности классов со статическими полями данных, а также используется массив объектов. Предполагается, что текст спецификации класса "товары" размещен в файле goods, h и включается в программу препроцессорной директивой. //Р09_01.срр - Массив объектов класса goods #include <iostream> using namespace std; #include "cyrToDos.h" #include "goods, h"// Текст определения класса //Инициализация статического компонента: int goods: .percent = 12; intmainf) { goods wares[] = { { "Мужской костюм", 2300 }, { "Косметический набор", 276}, { "Компьютер", 27000 }, { "Очки", 280}, { "Кроссовки", 2040 } }; int k = sizeoff wares) / sizeof(wares[0]); cout« cyrToDosf "Список товаров при наценке ") « wares[0].percent« "%:"« endl; for (int i = 0; i < k; i++) wares[i]. Display();
Класс как абстрактный тип 289 //Изменение статического компонента: goods::percent = 10; cout« cyrToDosf "\пСписок товаров при наценке ") « wares[0].goods::percent« « end!; goods *pGoods = wares; for (int i = 0; i < k; i++) pGoods++->Display(); return 0; } Результат выполнения программы: Список товаров при наценке 12%: Мужской костюм - розничная цена: 2576 Косметический набор - розничная цена: 309 Компьютер - розничная цена: 30240 Очки - розничная цена: 313 Кроссовки - розничная цена: 2284 Список товаров при наценке 10%: Мужской костюм - розничная цена: 2530 Косметический набор - розничная цена: 303 Компьютер - розничная цена: 29700 Очки - розничная цена: 308 Кроссовки - розничная цена: 2244 Статическое поле данных goods:.percent инициализировано до функции main(). Его значение не изменяется при инициализации элементов массива wares[ ]. В списках значений объектов класса goods не отражено существование статического поля данных. Оно одно для всех объектов класса! При обращении к нему использованы имена: goods: .percent, wares[0]. percent, wares[0].goods::percent. Для иллюстрации разных способов доступа к полям данных классов определен указатель pGoods на объекты класса goods. Он инициализирован значением адреса первого элемента массива объектов &wares[0]. В цикле указатель pGoods с операцией -> используется для вызова метода display(). После каждого вызова указатель с помощью операции постинкремента изменяется — настраивается на следующий элемент массива, т.е. на очередной объект класса goods. 19“2762.
290 Глава 9 9.2. Конструкторы, деструкторы и статусы доступа Конструкторы. В определениях класса сотр/ех1 и класса goods есть недостатки. Первый из них — отсутствие автоматической инициализации создаваемых объектов. Для каждого вновь создаваемого объекта класса complex1 необходимо вызвать метод define() либо явным образом с помощью уточненных имен присваивать значения данным объекта, т.е. переменным real и imag. В предыдущей программе объекты класса goods получили начальные значения при инициализации массива, которая выполняется по правилам, относящимся к структурам и массивам. Для инициализации объектов класса в его определение (спецификацию) можно явно включать специальный метод, называемый конструктором. Формат конструктора может быть таким: имя_класса(список параметров) инициализатор_конструктора { операторы_тела_конструктора } Имя конструктора по правилам языка Си++ должно совпадать с именем класса. Конструктор явно или неявно вызывается при определении (или размещении в памяти с помощью операции new) каждого объекта класса. Основное назначение конструктора — превращение участка памяти в объект класса, т. е. инициализация объектов (их полей данных). Существуют два способа инициализации полей данных создаваемого объекта. Во- первых, можно в теле конструктора присваивать значения полям данных объекта. Эти значения обычно вычисляются с учетом параметров конструктора. Инициализатор конструктора, помещаемый между списком параметров и телом конструктора, в этом случае опускается. Таким способом для класса complex1 можно ввести конструктор, эквивалентный функции cfef/ne(), но отличающийся от нее только названием: complex 1( double re = 0.0, double im = 0.0) { real = re; imag = im; } Второй способ предусматривает применение инициализатора конструктора (в стандарте языка употребляется термин "ctor-ini-
Класс как абстрактный тип 291 tializer’). Инициализатор конструктора представляет собой списокинициализаторов_полей^данных, отделенный от скобки, закрывающей список параметров, разделителем : (двоеточие). Каждый инициализатор списка относится к конкретному (не статическому) полю данных, присутствующему в классе, и имеет вид: имя_поля _данных (список_выражений) Инициализаторы полей данных отделяются в списке друг от друга запятыми. Для полей данных базовых типов в списке выражений используется одно выражение, в которое могут входить константы, параметры конструктора и имена уже инициализированных полей данных. Например, конструктор для класса complex1 можно определить так: complex1(double re = 0.0, double im = 0.0) : real (re), imag(im) П В качестве второго примера, где используется конструктор с инициализатором, рассмотрим следующий класс: struct example { int ii; double dd; double rr; example (int in, double dn, double rn) : ii(5), rr(ii * dn + in - dd), dd(rn+ii) {} }; В классе example три поля данных разных базовых типов. Конструктор содержит инициализатор, определяющий значение поля данных /V константой 5, а в выражения для инициализации dd и гг входят и параметры конструктора, и уже определенные значения других полей. Следует обратить внимание, что инициализация полей выполняется в порядке их размещения в определении класса. Именно поэтому в выражение /7 * dn + in - dd инициализации поля гг входит имя поля dd, хотя в списке инициализатор dd(rn+ii) размещен последним. При создании нового объекта с привлечением конструктора с инициализатором последовательность действий такова:
292 Глава 9 1. Выделяется память для объекта. 2. Инициализируются не статические поля данных объекта с помощью инициализатора конструктора. (В списке инициализаторов могут присутствовать не все поля данных класса.) 3. Исполняются операторы тела конструктора, причем эти операторы могут изменить значения полей данных, полученные за счет инициализации, и значения статических полей класса. Перейдем к другим особенностям конструкторов. В соответствии с синтаксисом языка для конструктора не определяется тип возвращаемого значения, даже тип void недопустим. С помощью параметров конструктору могут быть переданы любые данные, необходимые для создания и инициализации объектов класса. В конструктор complex 1 () передаются значения для полей данных объекта "комплексное число”. По умолчанию (за счет умалчиваемых значений параметров) формируется комплексное число с нулевыми мнимой и вещественной частями. В конструкторе класса example отсутствуют умалчиваемые значения параметров, поэтому при вызове конструктора нужно всегда указать три аргумента. В общем случае конструктор может быть сколь угодно сложным. Например, в классе "матрицы" конструктор может выделять память для массивов, с помощью которых представляется каждая матрица — объект данного класса, а затем инициализировать эти массивы. Размеры матриц и начальные значения их элементов такой конструктор может получать через параметры. Для класса "товары на складе магазина" конструктор можно определить следующим образом: goods(char *new_name, double new_price) : name (new_name), price (new_price) П В конструкторе класса goods можно было бы изменять по какому-либо закону и значение заранее инициализированного статического поля данных percent, однако в рассматриваемом примере этого не делается. Кроме отсутствия конструкторов в классах complex 1 и goods, введенных с помощью служебного слова struct, есть второй недостаток — это общедоступность (открытость) всех полей данных класса (и его объектов). В любом месте программы, где
Класс как абстрактный тип 293 определен объект класса, можно с помощью уточненных имен (например, имя_объекта.real или имя_объектаатад) или с по- мощью указателя на объект и операции косвенного выбора -> получить доступ к полям данных этого объекта. Тем самым не выполняется основной принцип абстракции данных — инкапсуляция (сокрытие) данных внутри объектов. В соответствии с правилами языка Си++ все поля данных и все методы класса, введенного с помощью ключа класса struct, являются открытыми (public). Для изменения видимости полей данных и методов в определении класса можно использовать спецификаторы доступа. Спецификатор доступа — это одно из трех служебных слов: private (собственный или закрытый), public (общедоступный или открытый), protected (защищенный), за которым помещается двоеточие. Появление любого из спецификаторов доступа в тексте определения класса означает, что до конца определения класса либо до другого спецификатора доступа все поля и методы класса имеют указанный статус. Защищенные (protected) поля и методы в классах нужны только в случае построения иерархии классов, т. е. при наследовании. При использовании классов без наследования, т. е. без порождения на основе одних классов других (производных), применение спецификатора protected эквивалентно использованию спецификатора private. Применение в качестве ключа класса служебного слова union приводит к созданию классов с несколько необычными свойствами, которые нужны для специфических ситуаций. В каждый момент времени исполнения программы объект-объединение проявляет себя только как одно из полей данных, присутствующих в классе, определенном с помощью union. Тем самым один и тот же участок памяти, точнее, один и тот же код этого участка, может рассматриваться как значения разных типов. Все поля данных и методы union-класса являются общедоступными (открытыми), но доступ может быть изменен с помощью спецификаторов доступа protected, private, public. Все поля и методы класса, определение которого начинается со служебного слова class, являются закрытыми, иначе собственными (private), т.е. недоступными для внешних обращений. Так как класс, все поля и методы которого недоступны
294 Глава 9 вне его определения, редко может оказаться полезным, то обычно некоторые из методов специфицируются как открытые public (общедоступные) и защищенные protected. Итак, для сокрытия данных внутри объектов класса, определенного с применением ключа struct, достаточно перед их появлением в спецификации класса поместить спецификатор private: При этом обычно требуется, чтобы некоторые или все принадлежащие классу функции остались доступными извне, что позволило бы манипулировать с данными объектов класса. Этим требованиям будет соответствовать следующее определение класса "комплексное число" (не забывайте, что в стандартной библиотеке есть класс complex, и наш класс - только иллюстрация): #include <iostream> using namespace std; struct complex2 { // Определение класса "комплексное число" //Методы класса (все открытые - public): //Конструктор объектов класса: complex2( double re = 1.0, double im = 0.0) : real (re), imag (im) П // Вывести значение комплексного числа: void displayf ) { cout« "real = "« real; cout« ", \timag = "« imag « endl; } // Получить доступ к вещественной части числа: double &ге() { return real; } // Получить доступ к мнимой части числа: double &im() { return imag; } //Данные класса (скрыты от прямых внешних обращений): private: //Изменить статус доступа на "собственный" double real; // Вещественная часть double imag; //Мнимая часть };
Класс как абстрактный тип 295 По сравнению с классом сотр!ехЛ в новый класс complex2, кроме конструктора, дополнительно введены методы ге() и /7т7(), с помощью которых можно получать доступ к данным объектов. Они возвращают ссылки соответственно на вещественную и мнимую части того объекта, для которого они будут вызваны. Напомним, что для конструктора не задается тип возвращаемого значения. Существуют особенности и в вызове конструктора. Но прежде чем объяснять их и приводить примеры создания объектов класса complex2, рассмотрим, какие конструкторы вообще могут быть в классе. Вот их названия: конструктор копирования (единственный), конструкторы приведения типов, конструктор без параметров (единственный, но не обязательный) и конструкторы общего вида. Еще придется ввести термин конструктор умолчания. Итак, "конструктор превращает фрагмент памяти в объект, для которого выполнены правила системы типов" [3], т.е. в объект того типа, который предусмотрен определением класса. Конструктор (и не один) существует для любого класса, причем он может быть создан без явных указаний программиста. Если в классе программист не определил ни одного конструктора, то по умолчанию формируются конструктор без параметров и конструктор копирования с прототипами соответственно: Т::Т(); T::T(const Т&); где Т- имя класса. Эти конструкторы по умолчанию создаются как открытые (public). Указав на автоматическое (неявное) появление в классах конструкторов, вспомним рассмотренные в предыдущей главе частные случаи классов — структурные и объединяющие типы и правила создания их объектов — структур и объединений. Не приводя примеров, напомним форматы определений: имя_структурного_типа имя_структуры; имяобъединяющего_типа имя объединения; Именно конструкторы без параметров, неявно присутствующие в этих типах, вызываются, когда компилятор обрабатывает определения приведенного формата.
296 Глава 9 В классе программист может явно определять конструкторы. Но если он этого не сделал, то конструктор без параметров и конструктор копирования создаются автоматически (их добавляет компилятор). Существует одна важная особенность: если в классе явно определен хотя бы один конструктор, то конструктор без параметров автоматически не создается. Атакой конструктор часто бывает просто необходим. Заменить конструктор без параметров может конструктор, все параметры которого имеют умалчиваемые значения. В определении класса одновременно не могут присутствовать и конструктор без параметров, и конструктор, все параметры которого имеют умалчиваемые значения (нарушаются правила перегрузки имен функций). Конструктор, для обращения к которому нет необходимости указывать аргументы, называют конструктором умолчания. Конструктор умолчания используется в тех случаях, когда для создания объектов или массивов объектов используются такие определения: имя_класса имя_объекта; имя_класса имя_массива_объектов [размер]; указатель_на_объекты_кпасса = new имя_класса; указатель_на_объекты_класса = new имя_класса [размер]; Здесь размер - выражение, определяющее количество элементов в создаваемом массиве объектов. В любом из таких определений объектов и массивов всегда вызывается конструктор умолчания. В классе complex2 конструктор без параметров отсутствует, а роль конструктора умолчания играет конструктор общего вида с прототипом: complex2::complex2 (double re = 1.0, double im = 0.0); (Конструктором умолчания он стал из-за наличия умалчиваемых значений параметров. К нему можно обращаться, не используя аргументов.) Пример: complex2 СС; В данном случае определен объект СС класса complex2. Для задания значений полей данных объекта СС использованы умалчиваемые значения параметров конструктора. Если вывести
знаКласс как абстрактный тип 297 чение выражения СС.ге(), то получим 1.0. Обращение к методу СС.//т?() вернет ссылку на CC.imag, и это поле данных для объекта СС будет иметь значение 0.0. Если в классе явно определен конструктор общего вида, то с его помощью можно создавать объекты с нужными значениями полей данных. Форматы определений: имя_класса имя_объекта(аргументы_конструктора); указатель_на_объекты_класса = new имя_класса(аргументы_конструктора); Примеры: complex2 SS( 10.3,0.22); //SS.real== 10.3; SS.imag == 0.22 complex2 EE(2.345); // EE. real == 2.345; EE. imag == 0.0 complex2 * par; // Указатель на объекты класса par = new complex2(2.2, 4.4); par -> displayf); Результат обращения к методу display(): real = 2.2, imag = 4.4 Мы рассмотрели и проиллюстрировали примерами только один вид конструкторов - конструктор общего вида. Он одновременно сыграл роль конструктора умолчания. Кроме того, мы привели формат прототипа конструктора копирования. Другие конструкторы некоторое время нам не понадобятся, хотя их значение очень велико в соответствующих задачах. Подчеркнем еще раз, что в каждом классе всегда присутствует конструктор копирования. По умолчанию конструктор копирования создается общедоступным. Он используется при передаче параметра по значению и при возврате функцией результата по значению. Очень часто определяемый по умолчанию конструктор копирования соответствует целям решаемой задачи, и определять его явным образом не нужно. Но в ряде случаев это необходимо. Именно дойдя до рассмотрения этих случаев, мы вернемся к конструкторам копирования. Итак, обобщим сказанное. • В классе может быть несколько конструкторов (перегрузка), но только один с умалчиваемыми значениями параметров либо с пустым списком параметров. • Конструктор не возвращает никакого значения.
298 Глава 9 • Нельзя получить адрес конструктора. • Параметром конструктора не может быть его собственный объект, но может быть ссылка на него, как у конструктора копирования. • Конструктор нельзя вызывать как обычный метод класса. Для явного вызова конструктора можно использовать две разные синтаксические формы: имя_класса имя_объекта(аргументы_конструктора); имя_класса( аргументы_конструктора); Первая форма допускается только при непустом списке аргументов. Она предусматривает вызов конструктора при определении именованного объекта данного класса. Примеры мы уже приводили. Однако обратим внимание на обязательность аргументов. Если их не будет, то компилятор решит, что это прототип функции, возвращающей значение объекта класса и имеющей имя, которым мы решили назвать объект. Вторая форма явного вызова конструктора приводит к созданию объекта, не имеющего имени. Созданный таким вызовом безымянный объект может использоваться в тех выражениях, где допустимо использование объекта данного класса. Например: complex 2 ZZ=complex2(4.0,5.0); Этим определением создается объект ZZ, которому присваивается значение безымянного объекта (с элементами real == 4.0, imag == 5.0), созданного за счет явного вызова конструктора. Еще один пример — создание объекта в динамической памяти с использованием операции new - приведен выше: par = newcomplex2(2.2t 4.4);. Деструкторы. В языке Си++ управление размещением объектов в памяти и их удалением из памяти, когда надобность в них исчезает, полностью во власти программиста. Как мы показали, объекты с нужными свойствами создаются с помощью конструкторов. Для удаления объектов (точнее, для выполнения действий, которые сопровождают удаление объекта) в каждом классе явно или неявно определяется специальный метод, называемый деструктором. Имя деструктора — это имя класса, перед которым помещен символ "тильда” (~). Форма определения деструктора:
Класс как абстрактный тип 299 -имя класса () { операторы деструктора ) Обратите внимание, что у деструктора нет возвращаемого значения и отсутствуют параметры. Если в классе программист явно не определил деструктор, то компилятор встроит его определение в класс автоматически. Статус доступа у такого деструктора public (открытый). Назначение деструктора - выполнение всех действий, сопровождающих удаление объекта. Наиболее важное - освобождение всех ресурсов, включенных в объект при его создании или выполнении действий над объектом. Такими ресурсами могут быть: участки памяти, динамически выделенной для полей объекта, файлы, открытые при создании объекта и связанные с ним, и т. п. Однако деструкторы могут быть нужны и при уничтожении объектов, не "захвативших" никаких ресурсов. Например, предположим, что перед удалением каждого объекта требуется сохранить в каком-то виде сведения об его свойствах, и это делать в программе нужно многократно и во многих местах. В этом случае удобно иметь средство, которое выполнит эту работу автоматически. Таким средством может служить специально подготовленный деструктор. Деструктор может вызываться явно, как и другие методы класса, однако подчеркнем, что вызов деструктора не уничтожает тот объект, для которого деструктор вызван. Форматы вызова деструктора: имя_объекта. ~имя_класса(); имяуказателя на_объект_класса ->~имя_класса(); Деструктор вызывается без вмешательства программиста (автоматически, неявно), когда программа "покидает" блок, в котором объект класса определен как объект автоматической памяти. Кроме того, деструктор неявно вызывается в тех случаях, когда объект, размещенный в динамической памяти, удаляется с помощью операции delete. В качестве примера рассмотрим программу, в которой определен класс symbol. Назовем этот класс "символьный элемент". В каждый объект этого класса входит поле данных int Number, определяющее порядковый номер объекта-элемента, и поле char
300 Глава 9 Meaning, в котором содержится некоторый символ - значение элемента. Кроме того, включим в класс статическое поле данных int static Amount, для подсчета общего количества присутствующих (созданных и не уничтоженных) в программе объектов класса symbol. Текст определения класса: // Класс "символьный элемент" // Общее количество элементов // Счетчик всех созданных элементов // Порядковый номер элемента // Значение элемента class symbol { int static Amount; int static Counter; int Number; char Meaning; public: //Конструктор: symbol!char value = ’a’): Meaning! value) { // Увеличиваем счетчик и количество элементов: Amount++; Counter++; Number = Counter; } //Деструктор: -symbol!) { Amount—; cout« ”Destructor! "; display!); } //Вывод сведений об объекте и общем количестве элементов: void display!) { cout« ”Number = "« Number; cout« ”,\tMeaning = "« Meaning; cout « "f\tAmount = " << Amount« endl; } }; В классе определено статическое поле данных int static Counter. Это счетчик всех созданных объектов класса (в том числе и уничтоженных). Его значение позволяет присваивать объектам их порядковые номера. В теле конструктора умолчания (он же конструктор общего вида) увеличиваются значения статических полей Counter и Amount. Поле Meaning получает значение в инициализаторе конструктора, а в поле Number заносится
текуКласс как абстрактный тип 301 щее значение счетчика объектов Counter. Прежде чем рассматривать деструктор, обратите внимание на метод display(). Он позволяет вывести сведения о данных объекта и общее количество объектов, присутствующих в программе. Задача деструктора в этом классе — учитывать уничтожение каждого объекта. Делает он это очень просто — при каждом выполнении уменьшает значение поля Amount. Для наглядности результатов работы с классом мы добавили в деструктор два оператора: cout« "Destructor! display(); Первый из них комментариев не требует, а назначение второго - вывести сведения именно о том объекте, для которого выполняется деструктор. "Сообщать" деструктору, для какого именно объекта нужно выполнить метод display(), не требуется. Поместим определение класса в текстовый файл symbol.h и используем класс в следующей программе: //Р09_02.срр - Статические поля данных и деструктор #include <iostream> using namespace std; #include "symbol.h"//Класс "символьный элемент" // Инициализация статических полей данных: int symboir.Amount = 0; int symbol ..Counter = 0; intmainf) { symbol A; symbol B('b'); A.displayf); //Number == 1, Amount == 2 { // Вложенный блок, symbol C( 'c'); symbol *ptr = new symbolf ptr -> displayf); // Number == 4, Amount == 4 delete ptr; // Неявно вызван деструктор ptr- >~symbol() } // Неявно вызван деструктор С. ~symbo!() symbol D('d'); D. displayf); // Number == 5, Amount == 3 return 0; // Неявно вызван деструктор для D, В, А }
302 Глава 9 Статические поля данных инициализированы нулевыми значениями после определения класса, но до функции main(). В теле функции main() есть вложенный блок. Объекты класса созданы до (A 6), внутри и после (D) вложенного блока. Внутри блока определены: объект С и безымянный объект, размещаемый в динамической памяти. Последний адресован указателем symbol * ptr. Рекомендуем по результатам выполнения программы проследить за вызовами деструктора и изменением счетчиков. Результат выполнения программы: Number = 1, Meaning = a, Amount = 2 Number = 4, Meaning = *, Amount = 4 Destructor! Number = 4, Meaning = *, Amount = 3 Destructor! Number = 3, Meaning = c, Amount = 2 Number = 5, Meaning = d, Amount = 3 Destructor! Number = 5, Meaning = d, Amount = 2 Destructor! Number = 2, Meaning = b, Amount = 1 Destructor! Number = 1, Meaning = a, Amount = 0 Обратите внимание, что деструктор неявно вызван при выполнении оператора delete ptr; при выходе из внутреннего блока и при завершении программы. Следует еще раз отметить, что деструктор не уничтожает объектов класса. Он выполняет действия, запрограммированные в его теле, но не удаляет объекта из программы. Деструктор можно вызвать как обычный метод класса. Во внутреннем блоке нашей программы (до уничтожения безымянного объекта) синтаксически допустимы такие операторы: В. ~symbolf); ptr -> -symbolf); Однако применение этих явных обращений к деструктору нарушит подсчет количества объектов. Значение Amount будет уменьшено, а реальное количество объектов в программе не изменится. Читатель может это проверить. 9.3. Поля данных и методы класса При определении класса в теле его спецификации вводятся данные (поля данных) и принадлежащие классу функции (методы).
Класс как абстрактный тип 303 Поля данных. Определение полей данных класса внешне аналогично обычному описанию объектов базовых и производных типов. Класс в этом отношении полностью сохраняет все особенности структурных типов. Именно поэтому поля данных класса могут быть названы его элементами. Элементы класса могут быть как базовых, так и производных типов, т.е. полями данных служат переменные, массивы, указатели и т.д. Как обычно, описания элементов одного типа могут быть размещены в одном предложении. Например: class point { double х, у, z; long a, b, c; }; В отличие от обычного определения данных при описании элементов класса невозможна их инициализация. Это естественное свойство класса, так как при его определении еще не существует участков памяти, соответствующих его полям данных. Напоминаем, что память выделяется не для класса, а только для объектов класса. Для инициализации полей данных объектов должен использоваться автоматический или явно вызываемый конструктор соответствующего класса. Существуют различия между обращениями к полям данных (и методам) класса из принадлежащих ему функций (из методов класса) и из других частей программы. Как уже показано на примерах, принадлежащие классу функции имеют полный доступ к его данным (и методам), т.е. для обращения к элементу класса из тела метода достаточно использовать только имя поля данных. Например, в деструкторе класса symbol выполняется обращение к методу класса display{), и этот метод выводит значения полей данных именно того объекта, для которого выполняется деструктор. В конструкторе класса symbol использован оператор Number = Counter;, который изменяет поле данных Number именно того объекта, который инициализируется конструктором. За простотой таких обращений из методов класса скрывается механизм неявного отождествления имен с элементами именно того объекта, для которого вызывается метод. Для доступа к полям данных (и методам) класса из операторов, выполняемых вне его определения, непосредственное
ис304 Глава 9 пользование имен элементов недопустимо. Смысл такого запрета определяется упомянутым механизмом "привязки" данных класса к конкретным объектам. Напомним, что определение класса не вводит его данных, а в нем только обозначается возможность их формирования при создании конкретных объектов класса. Явно размещается в памяти не класс, а конкретный объект класса. В отведенной для объекта области памяти выделяются участки, соответствующие полям данных. Для обращения к полю данных объекта извне, как мы уже говорили, нужно использовать операции выбора (. или ->). Первая из них позволяет сформировать уточненное имя по известному имени объекта: имя_объекта. имя_элемента Вторая операция обеспечивает обращение к полям данных объекта по указателю на объект: указатель_на_объект-> имя_элемента Подытожим сказанное. Хотя внешне поля данных класса могут быть подобны данным, определенным в блоке или в теле функции, но существуют некоторые значительные отличия. Данные класса не обязательно должны быть определены или описаны до (выше) их первого использования в принадлежащих классу функциях. То же самое справедливо и для методов класса, т.е. обратиться из одной функции класса к другой можно до ее определения внутри тела класса. Все поля данных и методы класса "видны" во всех операторах тела любой из функций класса. Именно поэтому, кроме областей видимости "файл", "блок", "функция", в Си++ введена особая область видимости "класс". Статические поля данных и статические методы класса. Статические поля данных классов уже использовались в классах goods и symbol, где ко всем объектам классов относилась в первом случае переменная static int percent - "торговая наценка" (см. Р09_01.срр) и во втором случае — "счетчики" int static Amount и int static Counter (см. P09_02.cpp). Рассмотрим эту возможность подробнее. Итак, каждый объект одного и того же класса имеет собственную копию нестатических полей данных класса. Можно сказать, что нестатические поля данных тиражируются при каждом определении объекта класса. Отличаются нестатические данные друг от друга именно
Класс как абстрактный тип 305 по "привязке" к тому или иному объекту. Это не всегда соответствует требованиям решаемой задачи. Например, если объекты создаются и при этом сцепляются, образуя связный список, то для просмотра всего списка удобно иметь единственный указатель на начало списка. Для добавления нового объекта в конец такого списка нужен указатель на последний элемент списка (на последний объект, включенный в список). Такие указатели на первый и последний объекты списка, а также уже упомянутый счетчик объектов можно сделать полями данных класса, но иметь их нужно только в единственном числе (каждый). Из примеров (классы goods и symbol) мы знаем, что поле данных создается в единственном экземпляре и не тиражируется при создании каждого нового объекта класса, если оно было определено в классе как статическое, т.е. имеет атрибут static. Статические данные класса после инициализации можно использовать в программе еще до определения объектов данного класса. Такую возможность для открытых (public) данных предоставляет квалифицированное имя имя_класса::имя_поля_данных Когда определен хотя бы один объект класса, к статическим данным этого класса можно обращаться как к обычным полям данных, т.е. с помощью операций выбора полей данных объекта (. и ->). Здесь возникает одно затруднение. На статические данные класса распространяются правила статуса доступа. Если статические данные имеют статус private или protected, то к ним извне можно обращаться только через открытые (public) методы класса. К моменту обращения к статическим данным класса объекты класса могут быть еще не определены. Без имени объекта (или его адреса) обычный метод класса вызвать нельзя в соответствии с требованиями синтаксиса. Хотелось бы иметь возможность обойтись без имени конкретного объекта при обращении к статическим данным класса. Такую возможность обеспечивают статические методы класса. Статический метод сохраняет все основные особенности обычных (нестатических) методов класса. К нему можно обращаться, используя имя уже существующего объекта класса либо указатель на такой объект. Дополнительно статическую функцию класса можно вызвать, используя квалифицированное имя го2762
306 Глава 9 имя_класса::имя_статического_метода С помощью квалифицированного имени статические функции можно вызывать еще до определения конкретных объектов. В следующей программе класс points определяет точку в трехмерном пространстве и одновременно содержит статический счетчик N таких точек. Обращение к счетчику обеспечивает статический метод count(). //Р09_03.срр - Статические поля и методы класса #include <iostream> using namespace std; #include "cyrToDos.h" class point3 // Точка в трехмерном пространстве { double х, у, z; //Координаты точки static int N; // Количество точек (счетчик) public: // Конструктор: point3(double xn = 0.0, double yn = 0.0, double zn = 0.0): x(xn), y(yn), z(zn) {N++;} //Добавлен объект // Обращение (доступ) к счетчику: static int & count() {return N;} }; // Внешнее определение и инициализация статического элемента: int point3::N = 0; int main(void) ( cout« "sizeof(point3) = " << sizeof(point3) « endl; point3 A(0.0,1.0,2.0); cout« "sizeof(A) = " << sizeoffA) « endl; point3 B(3.0,4.0,5.0); cout« cyrToDosf "Количество точек:") « point3::count() << endl; point3 C(6.0,7.0,8.0); cout« cyrToDosf "Количество точек:") « B.count() « endl; return 0; } Результат выполнения программы: sizeof(point3) = 24 sizeoffA) = 24 Количество точек: 2 Количество точек: 3
Класс как абстрактный тип 307 Обратите внимание, что размер класса point3 как типа равен размеру одного объекта этого класса. Память выделена для трех элементов типа double (каждый по 4 байта), и никак не учтено наличие в классе статического поля данных int N. Как уже говорилось, в отличие от обычных полей данных статические поля необходимо дополнительно описывать и инициализировать вне определения класса как глобальные переменные. Именно таким образом в программе Р9_03.срр получает начальное значение статическое поле point3\\N. Так как N - закрытое (private) поле, то последующие обращения к нему возможны только с помощью дополнительной открытой (public) функции. В данном примере это статическая функция countQ. Попытка извне класса обратиться к N с помощью квалифицированного имени point3::N будет воспринята как ошибка. Нестатическое поле данных класса может быть указателем или ссылкой на объект того же класса, но не может быть его собственным объектом. Статическим полем класса может быть указатель на объект класса и даже объект этого же класса. Указатели на поля данных и на методы класса. Две специфичные операции языка Си++ .* и ->* предназначены для работы с указателями на поля данных и методы класса. Прежде чем объяснить их особенности, отметим, что указатель на поле данных или на метод класса не является обычным указателем, унаследованным языком Си++ от языка Си. Обыкновенный указатель предназначен для адресации того или иного объекта (участка памяти). Указатель на поле данных класса не может адресовать никакого участка памяти, так как память выделяется не классу, а объектам этого класса при их создании. Таким образом, указатель на поле данных класса при определении не адресует никакого конкретного фрагмента памяти. Каким же образом определяются (описываются) такие указатели? Как эти указатели получают значения? Чем являются эти значения указателей? Какими возможностями обладают эти указатели? Для каких целей они используются? Почему и как с этими указателями используются две операции разыменования (.* и *->*)? Компоненты (члены) класса, как уже многократно повторялось, делятся на две группы — поля данных и методы класса. Для полей данных и функций (методов) класса указатели определяются по-разному. Начнем с указателей на принадлежащие 20*
308 Глава 9 классу функции (на методы). Их определение (без инициализации) имеет следующий формат: тип_возвращаемого_методом_значения (имя_класса::*имя_указателя_на_метод) (спецификацияпараметров_метода); Например, выше в классе complex2 ("комплексное число") определены методы double & ге(), double & im(). Вне класса можно следующим образом определить указатель ptCom: double & (comp!ex2::*ptCom)(); Указателю ptCom на методы класса complex2, можно почти обычным образом присвоить значение (это можно сделать и с помощью инициализации): ptCom = & complex2::re; // "Настройка"указателя Теперь для любого объекта А класса complex2 complex2 А( 10.0,2.4); // Определение объекта А можно с помощью указателя ptCom вызвать принадлежащую классу функцию ге(): (A. *ptCom)() = 11.1;//Изменится вещественная часть А cout«(A. *ptCom)(); // Вывод на печать A. real Изменив значение указателя ptCom = &complex2::im; // "Настройка"указателя можно с его помощью вызывать другую функцию того же класса: cout«(A. *ptCom)(); // Вывод значения мнимой части А complex2 В = А; // Определение нового объекта В (В. *ptCom)() += 3.0; // Изменение значения мнимой части В В данных примерах определен и использован указатель ptCom на метод без параметров, возвращающий значение типа double & и принадлежащий классу complex2. Его не удастся настроить на принадлежащие классу complex2 функции с другой сигнатурой и другим типом возвращаемого значения. Для
обраКласс как абстрактный тип 309 щения к методу display() указатель ptDisp нужно ввести следующим образом: void (comp!ex2::*ptDisp)(); Настроив указатель ptDisp на вещественную функцию display() класса сотр!ех2, можно вызвать эту функцию для любого объекта этого класса: ptDisp = &complex2::display; // "Настройка"указателя (В. *ptDisp)(); // Вызов функции displayf) для объекта В Формат определения указателя на поле данных класса: тип _данных имя_класса::*имя_указателя; В определение указателя можно включить его явную инициализацию, используя адрес конкретного поля данных из соответствующего класса: &имя_класса::имя_поля _данных При этом поле данных класса должно быть открытым (public). В определенном выше простейшем классе complex 1 есть открытые поля данных типа double, поэтому следующее определение (с инициализацией) указателя pdat будет вполне правильным: double complex1::*pdat = &complex1::imag; Теперь определим объект класса и присвоим его полям данных конкретные значения: complex 1 comp; comp, real-16.0; comp.imag=33.4; Так как указатель pdat "настроен" на конкретное поле данных imag класса complex^, то с его помощью можно обращаться к этому полю разных объектов класса complex^. Например, можно получить значение этого поля для объекта сотр: cout« "\ncomp.imag = " << comp. *pdat; Указатель pdat не константный, и его можно "настраивать" на любые поля класса complex1, имеющие тип double: pdat = &complex1::real; // Перестройка указателя cout« "comp, real ="« comp. *pdat« endl;
310 Глава 9 Результат выполнения приведенных операторов: comp, image = 33.4 comp, real = 16 Указатель на поле данных класса можно использовать в качестве аргумента при вызове функции. В приведенных примерах мы использовали операцию выполняющую разыменования указателей на поля данных и методы класса. Форматы выражений с этой операцией: имя_объекта. *указатель_на_поле _данных имя_объекта. *указатель_на_метод( аргументы) Слева от операции .* кроме имени конкретного объекта может помещаться ссылка на объект. Если определен указатель на объект класса и введены указатели на поля и методы того же класса, то доступ к данным конкретного объекта либо к методу, обрабатывающему данный объект, можно получить с помощью бинарной операции ->*: указатель_на_объект_класса-> *указатель_на_поле _данных указатель_на_объект_класса-> *указатель_на_метод( аргументы) Первым (левым) операндом должен быть указатель на объект класса, значение которого — адрес объекта класса. Второй (правый) операнд — указатель на поле данных или метод класса. Если второй операнд — леводопустимый, то и результат применения операции ->* (как и операции .*) есть леводопустимое выражение. Например, определим и инициализируем указатель рст1 на поле данных класса complexA: double complex1::*pcm1 = &complex1::real; Определим и инициализируем указатель pcomplexA на объекты класса complexА: complex 1 CM; CM. real = 10.2; CM.imag = -6.4; complex 1 *pcomplex1 =& CM; Теперь, применяя операцию ->*, получим леводопустимое выражение, которое можно использовать так: pcomplex 1 - > *pcm 1 = 22.2;
Класс как абстрактный тип 311 Приведенный оператор присваивания изменяет значение вещественной части комплексного числа, представленного объектом СМ класса complex1. Если справа от операции ->* находится инициализированный указатель на метод класса, то выполнится обращение к соответствующему методу: complex2 S(22.2,33.3); // Объект класса complex2 *pComplex = &S; // Указатель на объект класса void (complex2:: *pdisplay)(); // Указатель на метод pdisplay = &complex2: .display; //"Настройка”указателя (pComplex-> *pdisplay)(); // Вызов метода через указатель на объект класса и указатель на метод В данном примере на экран выводится сообщение: real = 22.2, imag = 33.3 Внешнее определение методов класса. Метод должен быть обязательно определен или по крайней мере описан в теле (в спецификации) класса. В отличие от обычных (глобальных) функций метод имеет доступ ко всем полям данных и методам класса (независимо от их статуса доступа). Функция-метод имеет в качестве области видимости класс, к которому она относится. Как уже говорилось в главе, посвященной функциям, в языке Си++ программист может влиять на компилятор, предлагая ему оформить ту или иную функцию как подставляемую (встраиваемую). Для этих целей используется служебное слово (спецификатор) inline. При определении классов их методы также могут быть специфицированы как подставляемые. Кроме явного применения служебного слова inline для этого используется следующее соглашение. Если определение (не прототип) принадлежащей классу функции полностью размещено в классе (в теле класса), то эта функция по умолчанию считается подставляемой. Именно таким образом определены методы классов сошр/ех1, goods, complex2, symbol, использованных ранее в качестве примеров. Все функции перечисленных классов воспринимаются компьютером как подставляемые, т.е. при каждом вызове этих функций их код "встраивается" непосредственно в точку вызова. Существует второй способ определения принадлежащих классу
312 Глава 9 функций. Он состоит в том, что внутри тела класса помещается только прототип метода, а его определение — вне класса, как определение любой другой функции, входящей в программу. При таком внешнем определении метода он также может быть снабжен спецификатором inline. Во внешнем определении метода программист "должен сообщить" компилятору, к какому именно классу он относится. Для этого используется бинарная форма операции :: (указание области видимости). Формат ее использования в этом случае таков: имя_класса::имя_метода Приведенная конструкция, называемая квалифицированным именем метода, означает, что функция есть метод класса и лежит в его области видимости. Именно такое определение "привязывает" функцию к классу и позволяет в ее теле непосредственно использовать любые поля данных класса (его объектов) и любые принадлежащие классу функции. Итак, при внешнем определении метода класса в его теле класса помещается прототип: тип_возвращаемого значения имя_метода (спецификация_и_инициализация_параметров); Вне тела класса метод определяется таким образом: тип_возвращаемого значения имя_класса::имя_метода (спецификация_ параметров) {определения_ и _операторы метода } После скобки, закрывающей список параметров, может быть помещено в виде суффикса служебное слово const. Его присутствие указывает, что с помощью действий метода невозможно изменить поля данных того объекта, для которого метод вызывается. Проиллюстрируем внешнее определение методов класса, особенности обращения к методам из других методов одного класса и применение методов к объектам класса на примере. Определим класс «рациональная дробь». Напомним, что рациональной дробью называют упорядоченную пару целых чисел (а;Ь), где b — натуральное число. Традиционное обозначение рациональной дроби: а/Ь, где а — числитель, b — знаменатель дроби. Знак дроби определяется знаком числителя, так как знаменатель всегда положителен (Ь>0).
Класс как абстрактный тип 313 Спецификация класса (назовем его fraction — дробь) может быть такой: class fraction { int а; // Числитель int b; // Знаменатель public: fractionfint ai-1, int bi= 1); // Прототип конструктора fractionf const fraction & fra): a(fra.a), b(fra.b) { cout«"Copy Constructor! "; printf); } fraction add(const fraction & ); void printf); }; (Еще раз обратите внимание на обязательность точки с запятой в конце спецификации класса.) Целочисленные поля данных а и b определяют числитель и знаменатель будущих объектов-дробей. Они закрыты (private) от внешнего доступа. Все методы класса открыты (public). Конструктор общего вида (он же конструктор приведения типов и конструктор умолчания) специфицирован прототипом, где заданы умалчиваемые значения параметров. Конструктор копирования в спецификации класса fraction определен полностью. Методы add() - сложение дробей и print() - вывод сведений о дроби специфицированы прототипами. Деструктор по умолчанию определен автоматически (добавлен компилятором). Приведем внешние определения методов класса fraction. Конструктор умолчания (умалчиваемые значения параметров заданы в прототипе) и одновременно общего вида: fraction:.fractionfint ai, int bi): afai), bfbi) { if (b <= 0) { cout« "Error!" <<endl; printf); // обращение к методу класса exitfl); } } Метод для сложения дробей:
314 Глава 9 fraction fraction::add(const fraction & fra) { fraction temp; temp. a=a *fra. b+fra.a *b; temp.b=b*fra.b; return temp; } Обратите внимание, что параметром функции служит ссылка fra на неизменяемый (константный) объект класса fraction. В теле функции с помощью конструктора умолчания создается вспомогательный объект temp этого класса, его полям данных присваиваются вычисляемые значения числителя (temp.а) и знаменателя (temp.b). В соответствующих выражениях используются поля данных объекта-аргумента (fra.a, fra.b) и поля данных (а,Ь) того объекта, для которого будет выполняться метод add(). Значение объекта temp служит возвращаемым значением метода. При возврате выполняется конструктор копирования. При выходе из тела функции заканчивается время жизни локализованного в ней объекта temp, и вызывается деструктор, автоматически включенный в класс компилятором. Метод вывода сведений об объекте, по-видимому, в комментариях не нуждается: void fraction::print() { cout< <"Numerator = "< <a; cout«"\tdenominator = "«b«endl; } Иллюстрация работы с объектами класса (при условии, что его спецификация и внешнее определение методов размещены в файле fraction J1): //Р09_04.срр - Рациональная дробь - применение методов, ttinclude <iostream> using namespace std; ttinclude "fraction.h" intmainf) { fraction one( 1,2), two( 1,3), sum; sum=one.add(two); sum.print(); fraction error( 12,0); return 0; }
Класс как абстрактный тип 315 Результаты выполнения программы: Copy Constructor! Numerator = 5 denominator = 6 Numerator = 5 denominator = 6 Error! Numerator = 12 denominator = 0 Обратите внимание, что в функции add{) передача параметра по ссылке не потребовала копирования значения аргумента two во временный объект. Поэтому при использовании метода add() конструктор копирования вызывается только один раз для обеспечения возврата результата из метода add(). Попытка в основной программе создать объект error (дробь с нулевым знаменателем) «пресечена» - выдано сообщение и завершена работа программы. 9.4. Указатель this Когда нестатическая функция, принадлежащая классу (его метод), вызывается для обработки данных конкретного объекта, этой функции автоматически и неявно передается указатель на тот объект, для которого функция вызвана. Этот указатель имеет фиксированное имя this и незаметно для программиста ("тайно") определен в каждом нестатическом методе класса следующим образом: имя класса * const this = адресобрабатываемого_объекта; Имя this является служебным (ключевым) словом. Явно описать или определить указатель this нельзя. В соответствии с неявным определением this является константным указателем, т.е. изменять его нельзя, однако в каждом методе класса он указывает именно на тот объект, для которого метод вызывается. Говорят, что указатель this является дополнительным (скрытым) параметром каждого нестатического метода. Другими словами, при входе в тело метода указатель this инициализируется значением адреса того объекта, для которого метод вызван. Объект, который адресуется указателем this, становится доступным внутри метода именно с помощью этого указателя. При работе с методом класса внутри тела метода можно было бы везде использовать этот
316 Глава 9 указатель. Например, совершенно правильным будет такое определение класса: struct ss {int si; char sc; ss(intin, charcn)//Конструктор { this->si = in; this->sc = cn; } void print( ) // Функция вывода сведений об объекте {cout« "\п si = " << this->si; cout« "\n sc = "« this->sc; } }; В таком использовании указателя this нет никаких преимуществ, так как данные конкретных объектов доступны в методах класса и с помощью имен полей данных класса. Снятие неоднозначности в теле принадлежащей классу функции между одинаковыми именами параметра и поля данных класса можно осуществить и без использования указателя this. Гораздо чаще для этой цели применяют операцию изменения видимости, т.е. используют выражение имя_класса::имя_поля _данных Почти незаменимым и очень удобным указатель this становится в тех случаях, когда в теле принадлежащей классу функции нужно явно задать адрес того объекта, для которого она вызвана. Например, если в классе нужна функция, помещающая адрес выбранного объекта класса в массив или{ включающая конкретный объект класса в список, то такую функцию сложно написать без применения указателя this. Действительно при организации связных списков, звеньями которых должны быть объекты класса, необходимо включать в связи звеньев указатель именно на тот объект, который в данный момент обрабатывается. Это включение должна выполнить некоторая функция — метод класса. Однако конкретное имя включаемого объекта в момент написания этой принадлежащей классу функции недоступно, так как его гораздо позже произвольно выбирает программист, используя класс как тип данных. Можно передавать такой функции ссылку или указатель на нужный объект, но гораздо проще использовать указатель this. Итак, повторим, когда указатель this использован
Класс как абстрактный тип 317 в функции, принадлежащей классу, например, с именем ZOS, то он имеет по умолчанию тип ZOB * const и всегда равен адресу того объекта, для которого вызван метод. Если в программе для некоторого класса X определить объект X factor(5); то при выполнении конструктора класса X, создающего объект factor, значением указателя this будет &factor. Для пояснения возможностей указателя this рассмотрим в качестве примера класс, объекты которого формируют (образуют) двусвязный список. Спецификация (не полное определение) класса в файле member, h: #ifndef _MEMBER_ #define _MEMBER_ //member, h — "элементы двусвязного списка" - спецификация класса, class member { //Адрес последнего элемента списка: static member *last_memb; member *prev; // На предыдущий элемент списка member *next; // На следующий элемент списка char letter; // Содержимое (значение) элемента списка public: // Прототипы функций для работы со списком: member (char сс) {letter = сс;} // Конструктор void add(void); //Добавление элемента в конец списка //Вывод содержимого списка: static void reprintf); }; #endif Из объектов класса member, как из звеньев, может формироваться двусвязный список. Схема построения списка показана на рис. 9.1. В классе member имеется статическое поле данных - указатель lastjmemb на последний объект, уже включенный в список. Когда список пуст, значение lastjmemb должно быть равно нулевому указателю. Связь между объектами как звеньями списка организуется с помощью указателей next и prev. Выполняет "подключение" объекта к списку метод add(). Статическая
318 Глава 9 функция reprint() позволяет "перебрать" звенья списка (объекта класса member) в порядке от конца к началу и вывести символы ("содержания"). Конструктор инициализирует поле данных char letter для каждого создаваемого объекта. Iast_memb = О Р N 0 letter 0 < A i. p N F ■this last memb N letter letter —z— Пустой список Один элемент this Два элемента last memb Рис. 9.1. Последовательность формирования списка из объектов класса member (Р - previous - предыдущий, N - next - следующий) Определим в файле membClass.h методы класса member: //membClass.h - Определения функций класса member #ifndef _MEMB_CLASS_ #define _MEMB_CLASS_ #include <iostream> #include "cyrToDos.h" //Включаем спецификацию класса: #include "member, h" //Добавление элемента в конец списка: void member::add() {if(last_memb == 0) this -> prev = 0; else {lastjnemb -> next = this; this -> prev = last_memb;} lastmemb = this; this->next = 0; } //Вывод содержимого списка: void member::reprint() { member *uk; // Вспомогательный указатель
Класс как абстрактный тип 319 ик = lastjnemb; if(uk == 0) { cout« cyrToDosf "Список пуст!') « endl; return; } cout« cyrToDos("Содержимое списка:") « endl; // Печать в обратном порядке значений элементов списка: while (uk /= 0) { cout« uk->letter « '\f; uk = uk->prev; } cout« endl; } #endif Вне класса указатель last_memb до включения в список первого элемента инициализируется нулевым значением. Поэтому первым шагом выполнения функции add будет проверка значения last_memb. Если он равен нулю, то в список включается первый элемент (объект), для которого указатель prev на предшествующий элемент должен быть нулевым. Для подключения объекта к уже существующему списку необходимо указателю next последнего в списке объекта присвоить значение указателя this (адрес добавляемого объекта). В качестве указателя на своего предшественника (prev) подключаемый объект получает значение last_memb. Затем последним становится обрабатываемый (только что подключенный) объект (lastjnemb = this;) и обнуляется его указатель next на последующий объект в списке. Метод reprint() описан в классе как статическая функция. Это никак не сказывается на ее определении. Первое действие функции — "настройка" вспомогательного указателя uk на последний включенный в список объект. Его адрес всегда является значением указателя lastjnemb. Если список пуст, то на этом выполнение функции завершается. В противном случае в цикле печатаются значения uk->letter и указатель "перемещается" к предыдущему звену списка. В следующей программе инициализирован статический указатель lastjnemb, создаются объекты класса member, объединяются методом add() в двусвязный список, и этот список
выво320 Глава 9 дится на экран дисплея с помощью статической функции reprint(). //Р09_05.срр - Связный список - указатель this #include <iostream> using namespace std; #include "membClass.h"// Определение класса member //Инициализация статического поля данных (указателя): member *member::last_memb = 0; int main() { // Формирование объектов класса member: member А( ’а'); member В( Ъ ’); member С(’с'); member D('d'); // Вызов статического метода: member::reprint(); // Включение созданных объектов в двусвязный список: A.add(); B.add(); C.add(); D.add(); // Печать в обратном порядке значений элементов списка: member::reprint(); return 0; } Результат выполнения программы: Список пуст! Содержимое списка: d с b а Обратите внимание, что все поля данных класса member имеют статус закрытых (private) и недоступны из других частей программы. Доступ обеспечивают только методы, имеющие статус public. 9.5. «Друзья» классов Как уже было сказано, механизм управления доступом позволяет выделять общедоступные (public - открытые), защищенные (protected) и собственные (private) методы и поля данных классов. Защищенные компоненты доступны внутри класса и в производных классах. Собственные компоненты (закрытые) —
Класс как абстрактный тип 321 локализованы в классе и недоступны извне. С помощью открытых полей данных и методов реализуется взаимодействие класса с любыми частями программы. Однако имеется еще одна возможность расширить интерфейс класса. Ее обеспечивают дружественные функции. Дружественной функцией класса называется функция, которая, не являясь его методом, имеет доступ к его защищенным и закрытым полям данных и методам. Функция не может стать другом класса "без его согласия". Для получения прав друга функция должна быть описана в теле спецификации класса со спецификатором friend. Именно при наличии такого описания класс предоставляет функции права доступа к защищенным и закрытым компонентам. Пример класса с дружественной функцией: //Р09_06.срр - Класс с дружественной функцией #include <iostream> using namespace std; // Класс - "символ в заданной позиции экрана class charlocus { int х, у; // Координаты знакоместа на экране дисплея char сс; // Значение символа // Прототип дружественной функции для замены символа: friend void friend_put(charlocus *, char); public: charlocus(intxi=1, intyi=1, charci='a’)//Конструктор •• x(xi), y(yi), cc(ci) {} void display () {// Вывести данные об объекте cout« "х=”« х « "\ty="« у « M\fcc=" << сс « endl; } }; //Дружественная функция замены символа в объекте: void friend_put(charlocus *p, char c) { p->cc = c; } int main () { charlocus D(20,4, d'); // Создать объект charlocus S; // Создать объект 2 j-2762
322 Глава 9 D.displayf); cin.getf); S.display(); cin.get(); friend_put(&D,'*'); D.display(); cin.getf); friend_put(&S, ’ft'); S.display!); с'т.деЦ); return 0; } Результат выполнения программы: х=20 у=4 cc=d <ENTER> х=1 у-1 сс=а <ENTER> х-20 у=4 сс=* <ENTER> и X cc=# <ENTER> Программа последовательно выводит сведения о символах и их координатах. После вывода каждой строки программа останавливается и ожидает сигнала от пользователя. Этот режим обеспечивает использование в программе обращений к методу get{) класса istream: cin. get(); Объект cin представляет стандартный входной поток. По умолчанию он "настроен" на чтение данных от клавиатуры. Метод get!) прочитывает из входного потока код одного символа. В данном примере пользователь нажимал клавишу ENTER, соответствующий код считывался методом get!) и служил сигналом для продолжения диалога. Функция friend_put!) описана в классе charlocus как дружественная и определена вне класса как обычная глобальная функция (без указания имени класса, без операции :: и без спецификатора friend). Как дружественная она получает доступ к собственным данным класса и изменяет значение символа того объекта, адрес которого будет передан ей как значение первого параметра. Выполнение основной программы очевидно. Создаются два объекта О и S, для которых определяются координаты мест на эк-
Класс как абстрактный тип 323 ране и соответствующие символы ('сГ, ’а'). Затем открытая функция класса charlocus:: display{) выводит сведения об объектах. Функция friend_put заменяет симролы объектов, что демонстрирует повторный вывод на экран. Отметим особенности дружественных функций. Дружественная функция при вызове не получает указателя this. Объекты классов (или ссылки, или указатели на объекты) должны передаваться дружественной функции только явно, например через аппарат параметров. При вызове дружественной функции нельзя использовать операции выбора .(точка), т.е. недопустимы выражения: имя_объекта. имя_функции и указатель_на_объект -> имя_функции Все это связано с тем фактом, что дружественная функция не является методом класса. Именно поэтому на дружественную функцию не распространяется и действие спецификаторов доступа (public, protected, private). Место размещения прототипа дружественной функции внутри определения класса безразлично. Права доступа дружественной функции не изменяются и не зависят от спецификаторов доступа. В приведенном примере описание функции friendjout() помещено в разделе, который по умолчанию имеет статус доступа private. Дружественная функция имеет следующие особенности: • не может быть методом того класса, по отношению к которому определяется как дружественная; • может быть глобальной функцией (как в предыдущей программе): class CL { friendintf1(...);... }; intf1(...) { тело_функции } • может быть методом другого определенного класса: class CLASS { ... charf2(...);... }; class CL { ... friend char CLASS::f2(...);... }; В примере класс CLASS с помощью своего метода f2() получает доступ к данным и методам класса CL. 21*
324 Глава 9 • может быть дружественной по отношению к нескольким классам: class CL2; // Предварительное неполное описание класса class CL 1 V friend void ff(CL 1,CL2); class CL2 { friend void ff(CL 1,CI2);... }; voidfff...) { тело_функции } Использование механизма дружественных функций позволяет упростить интерфейс между классами. Например, дружественная функция позволит получить доступ к собственным или защищенным данным и методам сразу нескольких классов. Тем самым из классов можно иногда убрать методы, предназначенные только для доступа к этим "скрытым" полям данных и методам. В качестве примера рассмотрим дружественную функцию классов "точка на плоскости" и "прямая на плоскости". Класс "точка на плоскости" включает поля данных для координат (х, у) точки. Полями данных класса "прямая на плоскост^" будут коэффициенты Л, 6, С общего уравнения прямой А*х+В*у+С= 0. Дружественная функция определяет уклонение заданной точки от заданной прямой. Если (а, Ь) - координаты конкретной точки, то для прямой, в уравнение которой входят коэффициенты Д В, С, уклонение вычисляется как значение выражения Л *а + 6*Ь + С. В следующей программе определены классы с общей дружественной функцией, в основной программе введены объекты этих классов и вычислено уклонение от точки до прямой: //Р09_07.срр - Классы с общей дружественной функцией #include <iostream> using namespace std; #include "cyrToDos.h" class Iine2; // Предварительное описание //Класс "точка на плоскости": class point2 { double х, у; // Координаты точки public: point2(double xn = 0, double yn = 0): x(xn), y(yn) {} friend double deviationfpoint2,line2); };
Класс как абстрактный тип 325 //Класс "прямая на плоскости class Нпе2 { double А, В, С; // Параметры прямой public: Iine2(double а-1, double b-2, double с=3): А(а), В(Ь), С(с) П friend double deviationpoint2,line2); }; // Внешнее определение дружественной функции double deviationfpoint2 p,line2 line) { return line.A * p.x + line. В * p.у + tine.C; } int main() {point2 P( 16.0,12.3); //Определение точки P Hne2 Ц 10.0, -42.3,24.0); // Определение прямой L cout« cyrToDosl'ymiOHeHHe точки P от прямой L: ”) « deviationf P,L); return 0; } Результат выполнения программы: Уклонение точки Р от прямой L: - 336.29 В качестве упражнения можно вместо дружественной функции deviation() определить глобальную функцию с теми же параметрами. При этом в классы point2 и line2 придется ввести дополнительные методы для доступа к собственным (закрытым) данным. Класс может быть дружественным другому классу. Это означает, что все методы класса являются дружественными для другого класса. Дружественный класс должен быть определен вне тела класса, "предоставляющего дружбу". Все данные и методы класса, "предоставляющего дружбу", доступны в дружественном классе. Дружественный класс может быть определен позже (ниже), чем описан как дружественный. Например, так: class Х2 { friend class Х1;... }; class Х1 {...// Определение дружественного класса voidflf...); void f2(...); };
326 Глава 9 В данном примере функции П() и f2() из классаХ1 являются друзьями класса Х2, хотя они в классе Х2 не описаны со спецификатором friend. В качестве примера "дружбы" между классами рассмотрим класс point - "точка на плоскости" (поля данных — координаты точки) и дружественный ему класс vector - "вектор на плоскости" (поля данных — координаты начала и конца и длина вектора). Все поля данных точки закрытые, и доступ к ним в классе vector возможен только за счет дружеских отношений. Конструктор класса vector формирует объект "вектор" по двум объектам класса point, которые задают начало и конец вектора. Кроме того, в теле конструктора вычисляется длина вектора. Для получения значения длины вектора в классе vector определен открытый метод. В основной программе определены две точки и по ним построен вектор. Приведем текст программы. //Р09_08. срр -Дружественные классы #include <iostream> using namespace std; //Класс "точка на плоскости": class point { double x, у; // Координаты точки // Описание дружественного класса: friend class vector; public: pointf double xi=0.0, double yi=0.0) • x(xi), y(yi) {} }; //Класс "вектор": class vector { double xBeg, yBeg; // Начало вектора double xEnd, yEnd; // Конец вектора double norma; //Длина вектора public: vectorfpoint,point);//Конструктор "векторов" double norm() { //Длина вектора return norma; } };
Класс как абстрактный тип 327 //Определение конструктора класса "вектор vector::vector(point beg, point end) : xBeg(beg.x), yBeg(beg.y), xEnd(end.x), yEnd(end.y) { double dx, dy; dx = xEnd - xBeg; dy = yEnd - yBeg; norma = sqrt(dx*dx + dy*dy); } #include "cyrToDos.h" int main() { point A( 2.0,4.0); point B( 12.0,14.0); vector V(A,B); cout« cyrToDosCДлина вектора: ") « V.normf); return 0; } Результат выполнения программы: Длина вектора: 14.1421 Обратите внимание, что за счет дружественного отношения между классами конструктор класса vector напрямую с помощью уточненных имен обращается к данным класса point.
Глава 10 , *ЛЛ s БИБЛИОТЕЧНЫЙ КЛАСС string 10.1. Строки в языках Си и Си++ В стандартную библиотеку языка Си++ входит большое количество классов, один из которых, а именно класс string, целесообразно рассмотреть как можно раньше. Символьные константы (литералы) язык Си++ унаследовал от языка Си. В языке Си отсутствует строковый тип, т. е. строковые переменные отсутствуют. Вместо них используются массивы с элементами типа char, всегда содержащие в конце последовательности значащих символов код '\0* (концевой символ строки, иначе — ее терминальный символ). В языке Си++ сохранены те же возможности и разрешено использовать все библиотечные функции языка Си для работы со строками в виде названных массивов символов. Для подключения этих библиотечных средств к программам на Си++ используется заголовок <cstring>, заменивший <string.h> языка Си. Будем говорить о таком представлении строк, как о строках в стиле Си (с_строки). Применение строк в стиле Си (с_строк) в ряде случаев неудобно. Во-первых, с_строка не может динамически изменять размеры — она всегда ограничена тем массивом с элементами типа char, который выделен для ее представления. Во-вторых, в функциях для работы с с_строками нет контроля за их границами. В стандартную библиотеку языка Си++ введено несколько классов, позволяющих в виде объектов определять строковые переменные и выполнять над ними ряд операций. Мы начнем изучение их средств с класса string. Для справки отметим, что класс string является только одним из частных случаев библиотечных классов, предназначенных для представления и обработки последовательностей символов. Кроме класса string в библиотеку входит класс wstring , предназначенный для строк, символы которых
Библиотечный класс string 329 представлены многобайтовыми кодами. Примером такого кода является Unicode — код для представления символов алфавитов разных языков мира. В классе string предполагается, что отдельные символы строк имеют тип char. Для работы с этим классом, т. е. со строками в стиле Си++, в программу должен быть включен заголовок <string>. Основное отличие и преимущество объектов класса string от с_строк состоит в том, что программист избавлен от необходимости следить за их размерами. Длина строки в объекте класса string изменяется автоматически в соответствии с количеством тех данных (символов), которые хранятся в объекте. Объект класса string содержит нуль или более символов. Прежде чем рассматривать возможности и методы класса string, нужно отметить, что в большинстве из его методов используются параметры целочисленного беззнакового типа sizejype. Этот тип не является базовым типом языка, а определен в стандартной библиотеке таким образом, чтобы обеспечить независимость объектов и методов класса string от конкретной реализации библиотеки. Поисковые методы класса string возвращают значение типа sizejype. При неудачном поиске эти методы возвращают специальное значение npos, которое определено в классе string как статический компонент типа string:.sizejype. При обращениях к методам класса string вместо параметров типа sizejype почти всегда можно подставлять аргументы, приводимые к этому типу, например unsigned long, а результат выполнения поискового метода всегда следует присваивать переменной (объекту) типа string:.sizejype. Значение только такой переменной можно сравнивать со значением string::npost и в этом случае можно не опасаться ошибок из-за различий в реализациях библиотек и компиляторов. Приведенные сведения станут более понятными чуть позже, когда мы на примерах рассмотрим методы класса string. 10.2. Конструкторы класса string Для создания строк-объектов (далее — строк) используются конструкторы класса string. Чаще всего используются следующие конструкторы:
330 Глава 10 explicit stringO; - формирует пустую строку в стиле Си++ (пустой объект). Модификатор explicit требует, чтобы конструктор вызывался только явно. Его (модификатора) наличие гарантирует отсутствие непредусмотренных (неявных) преобразований за счет непреднамеренного неявного вызова этого конструктора; string(const char *, sizejtype л); - формирует объект-строку в стиле Си++ и помещает в нее символы из строки в стиле Си (из с_строки), указатель на которую использован в качестве первого аргумента конструктора. При отсутствии второго аргумента из массива char[ ] в создаваемый объект копируются все символы, за исключением терминального. Кроме указателя на строку в стиле Си в качестве первого параметра можно использовать строковую константу (литерал), т.е. последовательность символов, заключенную в кавычки. С помощью второго аргумента (sizejtype п) можно ограничить количество символов, копируемых из первого параметра; string(const string &); - конструктор копирования — формирует объект класса string - новую строку в стиле Си++, копируя в нее содержимое строки-аргумента; string(const string &, sizejype pos-0, sizejtype nPos=npos)m, - конструктор копирования части строки, заданной первым параметром, sizejtype pos - начало копируемой подстроки; sizejtype nPos - количество копируемых символов; string(size_type п, charch); - конструктор, который в создаваемую строку п раз помещает символ ch. 10.3. Операции над строками Для строк - объектов класса string определен следующий набор операций: = присваивание; результат - строка в стиле Си++. Операция определена для таких операндов: строка = строка строка = с_строка строка = символ
Библиотечный класс string 331 + конкатенация; результат - строка в стиле Си++. Операция определена для таких операндов: строка + строка строка + с_строка строка + символ с_строка + строка символ + строка += конкатенация с присваиванием; результат - строка в стиле Си++. Операция определена для таких операндов: строка += строка строка +=с_строка строка += символ Операции сравнения. Результат — логическое значение (типа bool). == сравнение на равенство != сравнение на неравенство < сравнение "меньше" <= сравнение "меньше или равно" > сравнение "больше" >= сравнение "больше или равно" Для каждой из операций сравнения определены три варианта, предусматривающих сравнение: двух строк в стиле Си++, строки в стиле Си++ со строкой в стиле Си и наоборот — строки в стиле Си со строкой в стиле Си++. Приведем форматы соответствующих выражений только для одной из операций сравнения: строка == строка строка == с_ строка с_ строка -- строка [ ] индексирование - доступ к символу строки по его индексу. Операция не обеспечивает проверку правильности задания индекса, т.е. допускает использование индекса, выходящего за пределы последовательности, представленной в строке. Результат при этом непредсказуем.
332 Глава 10 « - вывод всех символов строки в стандартный выходной поток. >> - чтение символов в строку из стандартного входного потока. Ввод начинается с первого отличного от пробела символа и продолжается до первого обобщенного пробельного символа. Начальные пробельные символы из входного потока прочитываются, но в строку не заносятся. Таким образом, если среди символов входного потока имеются пробелы, то вводится только первое «слово» (последовательность символов без пробелов). Ввод данных с пробелами осуществляется функцией getline(), описываемой ниже. Пример использования некоторых средств класса string: //P10JD1.cpp - Работа со строками в стиле Си++ #include <iostream> #include <string> using namespace std; intmainf) { string name("Enter your name: ”); string surname='Enter your surname: string result; // Конструктор умолчания cout«name; //Выводим подсказку cin > >name; // Читаем имя cout< <surname; // Выводим подсказку cin > >surname; // Читаем фамилию result ="Hellof " + name + " ” + surname + 7”; cout«result; return 0; } В программе тремя способами определены объекты класса string (строки). В первые два из них с помощью инициализации занесены тексты обращений к пользователю ("Enter your name — Введите ваше имя"; "Enter your surname — Введите вашу фамилию"). После вывода каждой из них в тот же объект (например, в name) считывается введенная пользователем информация (последовательность символов без пробелов). Остальное очевидно из результатов выполнения программы:
Библиотечный класс string 333 Enter your name: Tom<ENTER> Enter your surname: Sawyer<ENTER> Hello, Tom Sawyer! 10.4. Методы класса string Каждый нестатический метод класса всегда выполняется для некоторого объекта этого класса. Методы класса string всегда выполняют обработку той или иной строки, которую будем называть вызывающей (метод) строкой. Такое терминологическое соглашение необходимо, чтобы отличать вызывающую строку (объект, для которого выполняется метод) от строки, использованной в качестве аргумента метода. Вначале рассмотрим методы, расположив их по группам. В каждую группу включим методы, близкие по функциональности. Для ряда методов обращение допускает применение нескольких вариантов параметров (аргументов). Например, метод assign() позволяет присвоить вызывающей строке: • другую строку; • часть строки; • отдельный символ; • строку в стиле Си (с_строгу)\ • часть с_строки. Для таких методов сейчас не будем перечислять все допустимые варианты параметров, а отошлем читателя к Приложению 5, где приведены прототипы методов класса string в алфавитном порядке. 10.4.1. Доступ к символу, конкатенация, присваивание Обращение к отдельному символу строки по его номеру: char & at(size_type k)\ подобно операции индексирования [ ], метод at() обеспечивает доступ к символу вызывающей строки, индексированному значением параметра /с. Нумерация символов строки начинается с нуля. В отличие от операции [ ] метод at{) обеспечивает контроль за диапазоном аргумента. При
обраще334 Глава 10 нии за пределы вызывающей строки генерируется исключение out_of_range. (Исключениям посвящена глава 12.) Для правильного значения индекса / выражения Hne[f\ и line.at(i) эквивалентны, если line — объект класса string. Приписывание строк и символов в конец строки (конкатенация): string & аррепс!(параметры); - метод приписывает символы в конец последовательности символов вызывающей строки. Параметры определяют, откуда брать добавляемые символы (строка в стиле Си++, с_строка, отдельный символ) и сколько их. Если line 1 и line2 - объекты класса string, то следующие выражения эквивалентны: line 1 + line 2 line 1. append(Hne2) Присваивание: string & ass/gfn(napaMeTpbi); - метод заменяет содержимое вызывающей строки на последовательность символов, определяемую параметрами. Параметры определяют, откуда брать замещающие символы (строка в стиле Си++, с_строка, отдельный символ) и сколько их. Для объектов класса string следующие операторы эквивалентны: line 1 = Нпе2; line 1. assignf Hne2); 10.4.2. Размеры строк Определение размеров строк: sizejype size() const; sizejtype lengthf) const; Методы являются синонимами и выполняют одно и то же — вычисляют длину вызывающей строки — реальное количество символов, размещенных в ней. Действительно, в стандарте языка Си++ указано, что единственное действие функции length ( ) - это вызов функции size(). Наличие двух методов, эквивалентных по результату, объясняется тем, что стандартная библиотека C++
Библиотечный класс string 335 создавалась как объединение уже существующих библиотек и унаследовала разные их возможности. Напомним, что суффикс const после списка параметров указывает на невозможность с помощью действий метода изменить состояние (данные) того объекта, для которого этот метод вызывается. sizejype capacity) const; Метод определяет текущую "емкость" строки — количество символов, которые можно поместить в строку без увеличения выделенной ей памяти. Поясним появление этого метода в классе string. Возможность динамически, в процессе исполнения программы, изменять размеры объектов класса string обеспечена (на уровне реализации) тем, что строке обычно выделяется память "с запасом". Если при обработке строка должна стать больше, чем выделенный ей участок памяти, то автоматически выполняется перераспределение памяти (например, ранее выделенный участок памяти увеличивается вдвое). Таким образом, если при выполнении программы для некоторой строки s выполняется соотношение s.size() == s.capacity(), то следующее увеличение строки (например, за счет вставки в нее) приведет к автоматическому перераспределению памяти. Всегда выполняется условие: s.sizeQ <= s.capacity(). sizejype max_size() const; Метод вычисляет предельную максимальную емкость строки, т.е. максимальное количество символов, которые исполняющая программу система позволит разместить в строке. (Стандарт не ограничивает длину строк, и максимальная емкость определяется реализацией.) Изменение размеров строк: void resize(size_type п, char ch);- выполнение метода зависит от соотношения параметра п и размера вызывающей строки s. Если п < s.size(), то вызывающая строка уменьшается до размера л, и в ней остаются только п первых символов. Если п > s.size{), то вызывающая строка увеличивается до размера л, и в ее конец дописываются л - s.size() штук символов ch. Если будет задано л, превышающее s.max_size{), то посылается исключение length_error.
336 Глава 10 void resize(size_type n)\ - метод выполняется как и предыдущий, но в качестве параметра-заполнителя (char ch) берется умалчиваемое значение символьной переменной, принятое авторами стандартной библиотеки классов. Пример использования методов класса string. Задание: ввести из входного потока (с клавиатуры) строку символов и поместить после каждого символа пробел. Вводить строку будем с помощью операции >>. Тем самым в прочитанной строке не окажется пробелов. Используем конструктор умолчания и рассмотренные методы af(), assign(), append(), length(). //P10_02.cpp - Работа со строками в стиле Си++ #include <iostream> #include <string> using namespace std; intmain() { string line, str, result; cout«"Enter string: cin > > line; // Цикл по всем символам: for(inti=0; Kline.lengthf); i++) { str.assignf 1,line.at(i)); resultappend(str+' ’); } cout«result; return 0; } В программе определены три строки: line - для входных данных, result - для результата, str - вспомогательная — для формирования строки из очередного символа введенной строки и пробела. Результаты выполнения программы: Enter string: 12345<ENTER> 12345 В данной программе обращение к методу assign() корректнее записать с явным приведением константы 1 к типу size_type. Оператор примет такой вид: str.assignf string: :size_type(1), line. at(i)); // Конструктор умолчания // Выводим подсказку // Читаем данные // Присваивание символа //Добавление пробела // Вывод результата
Библиотечный класс string 337 10.4.3. Вставки, удаления, замены частей строк Вставки в строки: string & insert(параметры); - метод изменяет (рис. ЮЛ) вызывающую строку, вставляя в нее последовательность символов. Параметры определяют позицию вставки, откуда брать замещающие символы (строка в стиле Си++, с_строка, отдельный символ) и сколько их. Обратите внимание, что вызывающая строка при вставках обычно увеличивается. far Вызывающая строка до исполнения функции pos nPos А В С D Е ^ j line (аргумент) ZT s \ а Ь с А В С D Е d е f Я h Рис. 10.1. Схема для метода insert(): tar — позиция в строке для вставки; line — ссылка на строку, из которой берутся вставляемые символы; pos — начало последовательности выбираемых символов; nPos — количество выбираемых символов Удаление части строки: string & erase(int tar= 0, intnTar=npos)\ - начиная с позиции tar из вызывающей строки удаляются пТаг символов. Замена символов в строке: string & гер!асе{параметры)\ - метод изменяет (рис. 10.2) вызывающую строку, заменяя в ней последовательность символов, определяемую параметрами. Параметры определяют: заменяемую часть строки; откуда брать замещающие символы (строка в стиле Си++ или с_строка или отдельный символ) и сколько символов. При заменах в строках может изменяться количество символов, т.е. замена может сопровождаться как вставкой, так и удалением символов. Другими словами, заменяемая последователь- 22-2762
338 Глава 10 Рис. 10.2. Схема для метода replace() ность по длине может быть не равна заменяющей последовательности. Иллюстративный пример использования некоторых методов класса string. Задание. Представить в виде строки предложение, в котором слова отделены друг от друга пробелами. Удалить пробелы в начале строки и выделить круглыми скобками первое слово предложения. В программе используем конструктор общего вида, методы af(), erase(), insert(), s/ze(), capacityO, length(). Предложение представим объектом класса string (строкой) и инициализируем эту строку некоторым текстом. //Р10_03.срр - Работа со строками в стиле Си++ It include <iostream> ttinclude <string> using namespace std; #define PRINT(x) cout«#x'-"«x«endl; intmainf) { string line(” Cum grano sal is''); // В начале 11 пробелов PRINTf line. size( )); PRINT( line. capacity()); int i; // Ищем первый непробельный символ: for(i=0; i<line.length(); /++) iff line.at(i) != 9') break; iff Kline.lengthf)) // Удаляем пробелы в начале строки: line = line.erase(0,i); line.insertfstring::sizeJype(0), 1,'(');
Библиотечный класс string 339 int к; // Ищем конец первого слова: for{k=0; k<line.length(); k++) if(line.at(k) == '') break; line.insertfk, 1LU, PRINTfline); PRINTf line. size( )); PRINT( line, capacity)); return 0; } В программе определена и инициализирована строка с именем line. Перед первым словом предложения имеются пробелы. Комментарии в тексте программы поясняют этапы обработки. Обратите внимание на тип первого аргумента в обращении к методу line.insert(string::size_type(0)J ,'(')• В ряде реализаций класса string тип первого параметра метода insert() определен как unsigned long. Но это можно узнать только из заголовка этого метода в конкретной библиотеке. Чтобы исключить зависимость программы от реализации, в программе вместо аргумента типа unsigned long использовано приведение к типу size_type из класса string. Та же проблема может возникнуть при обращении к другим методам класса string. Поэтому было бы надежнее для всех параметров типа size_type (прототипы методов класса string приведены в приложении) выполнять для аргументов приведение типа string::size_type(). Результаты выполнения программы: line.size()=26 line, capacity )=32 line=( Cum) grano sal is line.size()=17 line, capacity )-32 В результатах обратите внимание на значения длины строки line и ее «емкости» до и после обработки. 10.4.4. Поиск в строке и извлечение подстрок Поиск в строке подстроки или символа Поиск в строке (в объекте класса string) последовательности, совпадающей с другой последовательностью (из объекта класса string или из строки в стиле Си), иллюстрирует рис. Ю.З. 22*
340 Глава 10 Вызывающая строка Аргумент (line) Рис. 10.3. Схема для метода findQ с аргументом-строкой sizejype find(параметры) const, - метод возвращает позицию (индекс) в вызывающей строке начала первого вхождения искомой подстроки или заданного символа. Параметры определяют, что и где разыскивается в вызывающей строке. Разыскиваемая последовательность может быть отдельным символом, строкой, частью с_строки или строки в стиле Си++. Поиск может начинаться с любой (заданной) позиции вызывающей строки. Вызывающая строка не изменяется. Если подстрока (или символ) не найдена(ен), то метод возвращает значение string::npos. Поэтому результат поиска нужно присваивать переменной типа string: :size_type и сравнивать полученное значение со stringr.npos. Поиск от конца строки (самого правого символа): size_type rfindf параметры) const; - метод возвращает позицию (индекс) в вызывающей строке начала последнего вхождения искомой подстроки или заданного символа. Параметры определяют, что и где разыскивается в вызывающей строке. Разыскиваемая последовательность может быть отдельным символом, строкой, частью с_строки или строки в стиле Си++. Поиск может начинаться с любой (заданной) позиции вызывающей строки и выполняется к началу строки. Вызывающая строка не изменяется. Если подстрока (или символ) не найдена(ен), то метод возвращает значение stringr.npos. Поэтому результат поиска нужно присваивать переменной типа string::size_type и сравнивать полученное значение со stringr.npos. Поиск первого (левого) вхождения любого символа заданной последовательности. sizejtype find_first_of(параметры) const; - метод возвращает позицию (индекс) в вызывающей строке первого (самого
левоБиблиотечный класс string 341 го) вхождения любого символа из последовательности. Параметры определяют: последовательность — образец поиска и где в вызывающей строке выполняется поиск. Последовательность-образец может быть отдельным символом, строкой, частью с_стро- ки или строки в стиле Си++. Поиск может начинаться с любой (заданной) позиции вызывающей строки и выполняется от начала к концу строки. Вызывающая строка не изменяется. Если никакой подходящий символ не найден, то метод возвращает значение stringr.npos. Поэтому результат поиска нужно присваивать переменной типа string::size_type и сравнивать полученное значение со stringr.npos. Поиск последнего (правого) вхождения любого символа заданной последовательности: sizejype find_last_of(параметры) const; - метод возвращает позицию (индекс) в вызывающей строке последнего (самого правого) вхождения любого символа из последовательности. Параметры определяют последовательность — образец поиска и где в вызывающей строке выполняется поиск. Последовательность- образец может быть отдельным символом, строкой, частью с_строки или строки в стиле Си++. Поиск может начинаться с любой (заданной) позиции вызывающей строки и выполняется от конца к началу строки. Вызывающая строка не изменяется. Если никакой подходящий символ не найден, то метод возвращает значение stringr.npos. Поэтому результат поиска нужно присваивать переменной типа string::size_type и сравнивать полученное значение со stringr.npos. Поиск первого (левого) вхождения символа, который не входит в заданную последовательность: sizejype findJirst_not_of(параметры) const; - метод возвращает позицию (индекс) в вызывающей строке первого (самого левого) вхождения любого символа, отсутствующего в последовательности. Параметры определяют последовательность — образец поиска и где в вызывающей строке выполняется поиск. Последовательность-образец может быть отдельным символом, строкой, частью с_строки или строки в стиле Си++. Поиск может начинаться с любой (заданной) позиции вызывающей строки и
вы342 Глава 10 полняется от начала к концу строки. Вызывающая строка не изменяется. Если отсутствующий в аргументе символ не найден, то метод возвращает значение stringr.npos. Поэтому результат поиска нужно присваивать переменной типа string:.sizeJtype и сравнивать полученное значение со stringr.npos. Поиск последнего (правого) вхождения символа, который не входит в заданную последовательность: sizejype find_last_not_of(параметры) const; - метод возвращает позицию (индекс) в вызывающей строке последнего (самого правого) вхождения любого символа, отсутствующего в последовательности. Параметры определяют последовательность — образец поиска и где в вызывающей строке выполняется поиск. Последовательность-образец может быть отдельным символом, строкой, частью с_строки или строки в стиле Си++. Поиск может начинаться с любой (заданной) позиции вызывающей строки и выполняется от ее конца к ее началу. Вызывающая строка не изменяется. Если отсутствующий в аргументе символ не найден, то метод возвращает значение stringr.npos. Поэтому результат поиска нужно присваивать переменной типа string:.size_type и сравнивать полученное значение со stringr.npos. Выделение подстроки: string substr(sizejype tar-O, sizejype nTar=npos) const; - метод, начиная с позиции tar (рис. 10.4), выделяет из вызывающей строки пТаг символов и формирует из них строку длиной min{nTar, s.size()-tar}, где s - вызывающая строка (рис. 10.5). При неверном задании аргументов, когда пТаг > s.sizef), генерируется исключение out_ofjange. Строка-объект, к которой применен метод, не изменяется. Рассмотрим пример использования некоторых методов класса string. Задание. Представить в виде строки предложение, в котором слова отделены произвольными символами, отличными от прописных латинских букв. Выбрать из этого предложения самое короткое слово.
Библиотечный класс string 343 Вызывающая строка Строка-результат Рис. 10.4. Схема для метода substr(), когда пТаг< s.size()-tar Строка-результат Рис. 10.5. Схема для метода substr(), когда пТаг> s.size()-tar Для решения задачи напишем функцию, которая выбирает из строки-параметра самое короткое слово и возвращает его в виде строки как результат. //Р10_04.срр - Работа с методами класса string #include <iostream> #include <string> using namespace std; //Функция выбирает из строки самое короткое слово: string minWordfstring line) { string::size_type wordBeg=0, wordEnd=0, resBeg-0, resEnd=line. lengthf); string model("ABCDEFGHIJKLMNOPQRSTUVWXYZ"); while((wordBeg=line.findJirst_of(model,wordBeg))!= string::npos) { wordEnd = line.find_first_not_of( model,wordBeg); if (word End == string: :npos) wordEnd = line.length(); if (wordEnd-wordBeg < resEnd-resBeg) { resEnd = wordEnd; resBeg = wordBeg; } wordBeg = wordEnd; }
344 Глава 10 return line.substr( resBeg,resEnd-resBeg); } intmainf) { string phrasef "QUOD LICET JOVI, NON LICET BOVI. "); cout< <minWord(phrase)«endl; return 0; } Результаты выполнения программы: NON В функции minWord() определены: model - строка-образец допустимых симеолов для поиска в исследуемой строке, заданной параметром string line; переменные wordBeg - начало очередного слова, wordEnd - конец очередного слова, resBeg - начало короткого слова, resEnd - конец короткого слова. В цикле от wordBeg==0 до достижения конца строки (до word6egf==string::npos) в исследуемой строке последовательно разыскиваются символ, принадлежащий образцу (model)t и символ, отсутствующий в образце. Поиск выполняется с помощью обращений к методам класса string: wordBeg-line. find_first_of( model, wordBeg)) и wordEnd = line. find_first_not_of( model, wordBeg) После определения границ очередного слова его длина wordEnd - wordBeg сравнивается с длиной (resEnd - resBeg) слова-претендента на роль самого короткого. Остальное в поиске, по-видимому, не требует пояснений. Найдя границы (resBeg, resEnd) самого короткого слова, функция в операторе return «вырезает» его из исследуемой строки (из аргумента) с помощью вызова метода line.substrf resBeg, resEnd-resBeg). В основной программе определен объект-строка с латинским предложением: string phrase( "QUOD LICET JOVI, NON LICET BOVI."). Обращение к функции minWordfphrase) позволяет получить из строки самое короткое слово.
Библиотечный класс string 345 10.4.5. Сравнение строк и их частей Сравнение строк: int сотраге(параметры) const; - метод выполняет лексикографическое сравнение вызывающей строки или ее части с последовательностью символов. Параметры определяют последовательность символов и какая часть вызывающей строки сравнивается с нею. Метод возвращает: нулевое значение, если последовательности символов совпадают; положительное значение, если последовательность из вызывающей строки лексикографически находится после последовательности, определенной параметрами; отрицательное - в противном случае. Рассмотрим пример использования некоторых методов класса string. Задание. Представить в виде строки предложение, в котором слова отделены произвольными символами, отличными от прописных и строчных латинских букв. Выбирая слова из этого предложения, разместить их в другой строке в алфавитном порядке. Для решения задачи напишем функцию firstWord(), которая находит в строке-параметре лексикографически самое «младшее» слово и возвращает его начало и длину. Функцию напишем на основе функции minWord() из предыдущего примера. Теперь сравнивать придется не длины слов, а их алфавитное упорядочение. Кроме уже знакомых поисковых методов, используем метод compare() для сравнения последовательностей (слов). Найденные значения начала слова и его длины вернем в точку вызова через параметры-ссылки. В основной программе будем последовательно выбирать из строки слова, записывать их в строку-результат и после этого удалять обработанное слово из исходной строки. Обработка будет завершена, когда в строке не останется слов, т. е. когда длина очередного слова будет нулевой. Для выполнения названных действий со строками используем методы append() и erase(). Текст программы: //Р10_05.срр - Работа с методами строк в стиле Си++ // Упорядочить по алфавиту слова в строке.
346 Глава 10 #include <iostream> #include <string> using namespace std; //Начало и длина расположенного раньше по алфавиту слова: void firstWordf string line, int & beg, int & len) { string::size_type wordBeg=0, wordEnd=0, resBeg=0, resEnd=line.length(); string modelf "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz"); // Выделяем и анализируем первое слово: wordBeg = line.find_first_of( model,wordBeg); if (wordBeg == stringr.npos) // нет слов в строке {beg = len = 0; return;} wordEnd = line.find_first_not_of( model,wordBeg); if(wordEnd == stringr.npos) // в строке только одно слово {beg = wordBeg; len = line, lengthf)-wordBeg; return; } resBeg = wordBeg; resEnd = wordEnd; wordBeg = wordEnd; //В цикле сравниваем по алфавиту слова (со второго по последнее): while((wordBeg=line.findJirst_of(model,wordBeg))!= stringr.npos) { wordEnd = line. find_first_not_of( model, wordBeg); if (wordEnd == stringr.npos) wordEnd = line.lengthf); if (line. substr(resBeg, resEnd-resBeg) compare(line.substr(wordBeg,wordEnd-wordBeg))>0) { resEnd = wordEnd; resBeg = wordBeg; } wordBeg = wordEnd; } beg = resBeg; len = resEnd-resBeg; return; } intmainf) { string phrase("one two three four five six seven eight nine ten"); string result; int beg, len=phrase.length(); whileflen != 0) { firstWordf phrase, beg, len); result, appendfphrase. substr(beg, len)+"");
Библиотечный класс string 347 phrase, erasefbeg, len); > cout« result« endl; return 0; Результаты выполнения программы: eight five four nine one seven six ten three two В основной программе определена строка phrase, инициализированная названиями цифр. Эти названия через пробелы последовательно переписываются в строку результатов result. 10.4.6. Обращение к данным объекта класса string В ряде случаев представление символьной информации с помощью объекта класса string оказывается непригодным для решения конкретной задачи. Например, чтобы открыть файл с помощью стандартных функций языка Си, необходимо задавать имя файла в виде с_строки. Поэтому нужны средства для представления содержимого объекта класса string в виде строки в стиле Си. Доступ к последовательности символов объекта класса string с представлением данных (символов) в виде с_строки обеспечивает метод const char * c_str() const; Возвращаемое значение — указатель на последовательность символов, входящих в тот объект класса string, для которого вызван метод. Эта последовательность (массив) принадлежит классу string (размещена в его буфере), и массив недоступен изменениям. В конец этой последовательности метод c_str() помещает терминальный символ '\0', превращающий массив в правильную с_строку. const char * data() const; Второй метод подобен c_str{), но не добавляет в конец последовательности символов строки терминальный символ '\0'.
348 Глава 10 Оба метода возвращают указатель на неизменяемый (const) массив, поэтому результат нельзя присвоить неконстантному указателю: string s="Hello, World!"; char * point = s. data(); // Ошибка!!! const char * pointer = s.dataf); //Правильно std::cout < < pointer[7]; // выведем W std::cout << pointer; // Ошибка - в последовательности нет '\0 Следующая попытка исправления предыдущей ошибки также неверна: pointers.Iength()+1] = \0'; // Указатель на константу!!! Правильное решение: const char * line = s.c_str(); // std::cout«line;//все верно, есть терминальный символ ’\0’. Методы c_str() и data() позволяют получить доступ к символам, сохраняемым в объекте класса string, но не позволяют “оторвать” данные от объекта-строки. Копирование символов строки в с_строку Для формирования из объекта класса string последовательности символов (массива типа char), доступной изменениям и не зависящей от объекта класса string, применяют метод sizejype copyfchar * str, sizejype nTar, sizejtype tar-O) const; Из вызывающей строки, начиная с позиции far, выбираются nTar символов и помещаются в символьный массив, адресованный указателем str. Если значение параметра nTar превышает длину вызывающей строки (больше числа символов в ней), то из вызывающей строки выбираются все символы. Строка, для которой вызывается метод, не изменяется. Метод сору() не добавляет в массив терминальный символ, поэтому для получения правильной строки в стиле Си его приходится помещать в конец последовательности явным образом. Пример: string s(“тестовая строка"); char *ptr = newchar[s.length()+1 ];
Библиотечный класс string 349 s.copy(ptr, s.length()); ptr[s.length()] = '\0'; std::cout< <ptr< <std::endl; 10.4.7. Массивы строк и обмены значениями строк Строки, подобно другим объектам, можно объединять в массивы. При определении массива строк его размер и значения элементов могут определяться инициализацией. Формат определения такого массива строк: string имя_массива [ ] = {“символы", “символы", “символы’7)] Прежде чем привести пример программы с массивом строк, обратим внимание на еще один метод класса string. void swap(string & line)] - обменивает значения (символы) вызывающей строки и строки, заданной параметром. Рассмотрим пример использования некоторых методов класса string. Задание. Определить и инициализировать массив строк с названиями городов и упорядочить эти названия по возрастанию их длин (по количеству букв в названии). //Р10_06.срр - Массив строк, обмены значений строк. #include <iostream> #include <string> using namespace std; intmainf) { string array[] = ("Warsaw", "Buenos Aires", "Venice", "Bonn", "Accra"}; intlen = sizeoff array)/sizeof( array[0]); forfint j-0; j<len-1; j++) forfint k=j+1; k<len; k++) iff array01- lengthf) > array[k]. lengthf)) array[j].swap( array[k]); forfint i=0; i<len; i++) cout< <array[i]< < endl; return 0; }
350 Глава 10 В программе определен массив строк аггау[ ]. Количество элементов в нем определено списком инициализации. Для удобства эта величина вычисляется и присвоена переменной 1еп. Для сортировки применен простой алгоритм перебора. Во вложенных циклах сравниваются длины строк array[f] и аггау[к]. В случае необходимости вызывается метод swap(), выполнение которого помещает короткую строку перед более длинной. Результаты выполнения программы: Bonn Accra Warsaw Venice Buenos Aires 10.5. Консольный ввод-вывод строк и обмены с файлами Библиотечная функция для ввода строк. С помощью операции >> нельзя прочитать в строку несколько разделенных пробелами слов (словом будем считать группу символов без пробелов). Эту задачу решает следующая функция из стандартной библиотеки, не принадлежащая классу string: istream & getlinefistream & is, string & line, chardelim='\n'); Функция считывает в объект line (строка в стиле Си++) данные из входного потока is до появления кода ограничителя delim, который также считывается, но не заносится в формируемую строку. По умолчанию в качестве ограничителя используется '\п’ - признак конца строки (формируемый при нажатии клавиши ENTER). Функция возвращает ссылку на входной поток. В качестве первого аргумента при чтении данных, вводимых с клавиатуры, используется объект cin. Функцию getline() можно использовать не только для ввода информации из стандартного входного потока cin, но и для чтения данных из текстового файла. Прежде чем привести примеры, напомним, что система ввода-вывода языка Си++ построена на основе потоков. Поток — это логическое устройство, из которого
Библиотечный класс string 351 исполняемая программа может получать (считывать) информацию и/или в которое программа может записывать данные. Каждый поток настраивается на конкретное физическое устройство ввода-вывода. После того как указанная настройка осуществлена, программе безразлично, с каким физическим устройством выполняется обмен информацией. Потоки действуют одинаково независимо от тех физических устройств ввода-вывода, с которыми они связаны. В работающей программе автоматически создаются и настраиваются на устройства обмена данными с внешним миром четыре потока, представляемых объектами tin (стандартный ввод), cout (стандартный вывод), сегг (стандартный вывод для сообщений об ошибках), clog (буферизированный вывод для сообщений об ошибках). Для работы с файлами нужно создавать явным образом другие потоки. Файловые потоки и текстовые обмены с ними. Существуют три типа потоков: для ввода (для чтения), для вывода (для записи) и для двунаправленного обмена (для ввода и вывода). Для работы с потоками, отличными от консольных потоков, в программе необходимо определить объекты следующих библиотечных классов: ifstream - для потоков ввода: ofstream - для потоков вывода; fstream - для потоков двунаправленного обмена. Названные классы становятся доступными в программе после подключения заголовка <fstream>. Подробному рассмотрению режимов и особенностей работы с потоками (с файлами) посвящена гл. 19. Для наших целей, т. е. для записи в файлы строк и для чтения строк из файлов, пока достаточно знать: как в программе создать объект потока; как связать этот объект с конкретным файлом; как затем выполнить чтение текстовых данных из файла или как выполнить запись текста в файл. Создать объект, представляющий поток, и одновременно связать его с конкретным файлом можно с помощью конструктора соответствующего класса. Для потоков чтения вызов конструктора может быть таким: ifstream имя_потока(имясуществующего_файла)\ 352
Глава 10 Имя_потока - выбираемый программистом идентификатор; имя_существующего_файла - строковая константа или с_строка. Вместо него в качестве аргумента нельзя использовать строку в стиле Си++, т. е. объект класса string. Значением аргумента при обращении к конструктору должен быть "адрес" файла. Адрес может быть либо полным указанием пути к файлу, либо сокращенным обозначением местоположения файла. Выбор зависит от взаимного расположения исполнимого модуля программы и самого файла. Если файл по указанному адресу не будет найден, то объект для представления потока не будет создан. Соответствующую проверку следует всегда выполнять. Пример. Создать объект с именем in для представления в программе потока, связанного с файлом test.txt, и проверить успешность выполнения: if stream in( "test, txt"); if (tin) { ceerr « "Нет файла!"« endl; exit( 1); } Создать объект для представления выходного потока и "настроить" его на еще не существующий или заменяемый файл позволит такой вызов конструктора: of stream имя_потока(имя_файла)\ В этом случае требования к имени файла те же - оно должно задаваться строковой константой или строкой в стиле Си. Файл с заданным именем (полным или не полным) создается каждый раз заново, и необходимо проверять успешность его создания. Проверка выполняется аналогично, т. е. проверяется существование объекта, представляющего поток. Рассмотрим пример использования некоторых методов класса string. Задание. Вывести в текстовый файл строки "исследуемого" файла, содержащие заданную последовательность символов (предложение или несколько символов). Результаты (найденные строки) будем помещать в файл, имя которого совпадает с именем исследуемого (входного) файла, но имеет расширение ".res”.
Библиотечный класс string 353 Составим программу, которая запрашивает у пользователя имя файла (string source), затем предложение (string sentence), которое нужно разыскивать в его строках. Затем программа открывает файл для чтения, т. е. создает объект inFile класса ifstream. При успешном создании объекта формируется имя файла для результатов (string result) и открывается с этим именем файл для вывода. Для чтения из входного потока, а затем из файла применим функцию getline(). При создании файлового входного потока (ifstream inFile) «достанем» из строки source название файла с помощью метода c_str(). Напомним, что этот метод позволяет получить содержимое объекта класса string в виде с_строки. Так же поступим и при открытии выходного потока. Текст программы: //Р10_07.срр - Вывести на экран строки файла, содержащие //заданную последовательность символов (или предложение) #include <iostream> #include <string> #include <fstream> // Средства для работы с файлами, using namespace std; intmainf) { string sentence; // Искомое предложение string source; // Имя файла cout« "Enter file name:"; getlinef cin, source); // Открыть файл source и связать его с потоком inFile: ifstream inFilef source.c_str()); if (!inFile) // Ошибка при открытии файла {cerr« "\nErrorfile name: "«source; cin.getf); exit( 1); } // Сформировать имя выходного файла: string result; intpos; pos = source. find('); result = source.erase( pos) + ".res"; 23 "2762
354 Глава 10 // Открыть выходной файл и связать его с потоком outFile: of stream outFile( result c_str()); if(!outFile) // Ошибка при открытии файла { cerr« "\nError file name: "«result; cin.geU); exit( 1); } cout« "Enter sentence: getlinefcin, sentence); string line; // Строка, читаемая из файла while( 1) { // Цикл чтения строк из файла getlinefinFile, line); if (inFile. еоЦ)) break; // Конец файла string::size Jype res; // Позиция в строке res = line.find( sentence); if (res == stringr.npos) continue; // Нет в строке outFile << line << "\n"; //Запись строки в файл } cout << "See file \""+result+"\"l" << endl; return 0; } Результаты диалога с программой: Enter file name: P10_06.cpp<ENTER> Enter sentence: string<ENTER> See file "P10_06.res"! Результаты выполнения в файле P10_06.res: ft include <string> string array[] = {"Warsaw", "Buenos Aires", В диалоге с программой пользователь ввел имя файла, содержащего исходный текст этой же программы Р10_06.срр. Затем введена последовательность символов string. Строки программы, в которых встречается эта последовательность, записаны в файл результатов. Обратимся к тексту программы и рассмотрим цикл с заголовком wfi//e(1). В теле цикла в объект line последовательно
прочиБиблиотечный класс string 355 тываются строки файла. После каждого обращения к функции getUne() в условном операторе проверяется значение выражения inFile.eof(). Оно примет значение true при достижении конца файла. В этом случае выполнение цикла и программы будут завершены. В противном случае в строке line с помощью метода find() ищется последовательность символов из строки sentence. При ее отсутствии метод вернет значение stringr.npos и выполнится переход к следующей итерации. В противном случае оператор outFile « line « "\п"; выводит в файл, представленный объектом outFile, выбранную строку. Остальное, по-видимому, не требует пояснений.
Глава 11 ПЕРЕГРУЗКА ОПЕРАЦИЙ И КЛАССЫ РЕСУРСОЕМКИХ ОБЪЕКТОВ 11.1. Расширение действия (перегрузка) стандартных операций Одной из привлекательных особенностей языка Си++ является возможность распространения действия стандартных операций на операнды, для которых эти операции первоначально в языке не предполагались. Мы уже знакомы с такой особенностью языка Си++. Например, операция поразрядного сдвига << использованная в выражениях, левый операнд которых представляет собой объект (cout) класса выходных потоков, играет роль операции вывода данных из исполняемой программы. Если S1 и S2 - символьные строки в стиле языка Си++ (объекты класса string), то их конкатенацию (соединение) можно выполнить с помощью выражения S1 + S2. Названные возможности операций обеспечены не базовыми механизмами языка Си++ (возможности операций не встроены в язык), а особенностями тех классов, для объектов которых эти операции применяются. Язык Си++ позволяет распространить действие любой стандартной операции на новые типы данных, вводимые пользователем с помощью классов. Распространить операцию на новые типы данных позволяет механизм перегрузки стандартных операций. Чтобы появилась возможность использовать стандартную для языка Си++ операцию (например, + или *) с необычными для нее данными, необходимо специальным образом определить ее новое поведение. Это возможно, если хотя бы один из операндов является объектом некоторого класса, т.е. введенного пользователем типа. В этом случае применяется механизм, схожий с механизмом определения функций. Для распространения действия операции на новые пользовательские типы данных программист
Перегрузка операций и классы ресурсоемких объектов 357 определяет специальную функцию, называемую "операция- функция" (operator function). Формат определения операции- функции: тип_возвращаемого_значения operator знак_операции (спецификация_параметров_операции-функции) {операторы _тела_операции-функции} При необходимости может применяться и прототип операции-функции: тип_возвращаемого_значения operator знак_операции (спецификация_параметров_операции-функции); В прототипе и в заголовке определения операции-функции используется ключевое слово operator, за ним помещен знак операции. Если принять во внимание, что конструкция operator знакоперации есть имя некоторой функции, то определение и прототип операции-функции подобны определению и прототипу обычной функции языка Си++. Определенная таким образом операция называется перегруженной (по-английски — overloaded), а сам механизм — перегрузкой, или расширением действия стандартных операций языка Си++. Количество параметров у операции-функции зависит от арности операции и от способа определения функции. Операция-функция определяет алгоритм выполнения перегруженной операции, когда эта операция применяется к объектам класса, для которого операция-функция введена. Чтобы явная связь с классом была обеспечена, операция-функция должна быть либо методом класса, либо внешней (возможно дружественной) функцией, у которой хотя бы один параметр имеет тип класса (или ссылки на объект класса). Например, для распространения действия бинарной операции * на объекты класса Т может быть введена внешняя функция с таким заголовком: Г operator *(Т х, Т у) Если после этого для класса Т определены два объекта А к В, то выражение А * В интерпретируется как вызов функции operator *(А,В).
358 Глава 11 Если операция-функция для той же бинарной операции определяется как метод класса Т, то заголовок у нее будет таким: Тoperator *(Тz) В этом случае выражение А* В интерпретируется как следующее обращение к методу: A.operator *(В). Для унарных операций (например, для операции изменения знака заголовки операций-функций будут такими: Тoperator-(Тz) - для внешней функции, Т operator-() - для метода класса Т. Если А - объект класса Т, то выражению А будут соответствовать обращения: operator -(А) - к внешней функции, A.operator-{ ) - к методу класса Т. Если операция-функция определяется как внешняя, то у нее должен быть доступ к методам и к полям данных объектов класса, выступающим в качестве операндов. Это возможно в двух случаях: когда операция-функция является дружественной классу либо в классе достаточно средств для обращения к полям данных объектов и методам класса. В качестве содержательного примера определим класс комплексных чисел с четырьмя операциями: • изменение знака - (унарный минус); •сложение двух комплексных чисел + (бинарный плюс); • ввод данных объекта (ввод значения комплексного числа — операция >>); • вывод сведений о комплексном числе (операция <<). Для операции — определим метод класса, операции-функции для ввода и вывода определим как дружественные, а для операции + введем внешнюю функцию с параметрами—объектами класса. Такое распределение операций по видам операций-функций не случайное. Необходимые объяснения будут приведены по мере рассмотрения примера. Спецификация класса: class myComplex { double re, im; public: myComplex (double xre = 0.0, double xlm = 0.0) : re(xre), im(xim)
Перегрузка операций и классы ресурсоемких объектов 359 {} double геаЦ) { return re;} double imag() {return Im;} myComplex operator - () //Перегрузка операции { return myComplex (-re, -im);} friend ostream & operator «(ostream &, const myComplex &); friend istream & operator »(istream &, myComplex &); }; Поля данных double re, im в классе myComplex определены как закрытые. Все методы класса открытые: конструктор (одновременно общего вида, умолчания и приведения типов); методы геаЦ ) и imag() для получения значений вещественной и мнимой частей комплексного числа, operator - () - для перегрузки операции В классе описаны как дружественные функции для перегрузки операций » и <<. Вне спецификации класса myComplex определим дружественные и внешнюю функции. Дружественная операция-функция для вывода: ostreamA operator «( ostream A output, const myComplex & cc) { output« "real ="« cc.re « ", \tlmage = "« cc.im « endI; return output; } Для этой функции возвращаемым значением и первым параметром служит ссылка на объект класса выходных потоков ostream. Вторым параметром является ссылка на объект класса myComplex. Обратите внимание, что имя output первого параметра представляет в теле функции выходной поток, использованный при обращении к операции-функции. Если obj — объект класса myComplex, то выражение cout« obj — это вызов операции-функции operator « (cout, obj), и функция вернет ссылку на cout. Так как функция возвращает ссылку, то обращение к ней является леводопустимым и можно составлять "цепочки" вида cout« pr « at« ke, где рг, at, ke могут быть как объектами класса myComplex, так и данными других типов.
360 Глава 11 Дружественная операция-функция для ввода: istream & operator »(istream & in, myComplex & cc) { cout« "real = in » cc.re; cout« "image = ” in » cc.im; return in; } Обратите внимание, что второй параметр — не константная ссылка на объект класса. Константная ссылка недопустима — функция должна изменять значение объекта, использованного в качестве второго аргумента. Функция возвращает ссылку на первый аргумент — входной поток, из которого будет выполняться чтение данных. Тем самым обеспечена возможность цепочек ввода значений вида aa»dd»ff»rr} где аа, с/с/, ff} гг - как объекты класса myComplex, так и переменные других типов. Внешняя операция-функция: myComplex operator + (myComplex Z, myComplex E) { return myComplex (Z.realf) + E.realf), Z.imagf) + E.imagO); } Функция получает в качестве параметров два объекта класса myComplex и возвращает значение объекта, сформированного в операторе return как безымянный объект класса myComplex. Для получения значений закрытых полей данных объектов-пара- метров используются методы геа/() и imag(). Разместив спецификацию класса и определения внешних функций в файле myComplex.h, напишем программу для иллюстрации возможностей введенных в классе myComp/ex операций. //Р11_01.срр - Перегрузка операций в классе комплексных чисел ttinclude <iostream> It include "myComplex.h" using namespace std; int main() { myComplex beta(4.3, -6.1); myComplex gamma; myComplex alfa; cin » gamma; // operator»(cin, gamma);
Перегрузка операций и классы ресурсоемких объектов 361 alfa = beta + gamma; // operator+(beta, gamma); cout« "alfa:\t”« alfa; //operator«(cout, alfa); alfa = - alfa; //alfa.operator-0; cout« alfa « beta « gamma; // "цепочка" return 0; } Результат выполнения программы: real = 0.4<ENTER> image = -6.1<ENTER> alfa: real = 4.7, image = -12.2 real = -4.7, image =12.2 real = 4.3, image = -6.1 real = 0.4, image = -6.1 Как и ранее, <ENTER> - условное обозначение нажатия клавиши ENTER. Еще раз обратим внимание, что, определив операцию-функцию, можно обратиться к ней не только неявно, используя знак операции, но и явно: operator знак_операции( аргументы) Примеры таких явных обращений приведены в качестве комментариев в тексте программы рядом с соответствующими неявными (более удобными) вызовами тех же операций-функций. Если операция-функция определена как метод класса, то вызвать ее явно можно с использованием имени объекта или указателя на объект и операций доступа (->, .)• Другими словами, в этом случае вызов операции-функции подобен вызову обычного метода класса. Никаких достоинств в таких обращениях нет, и их мы используем только в учебных иллюстративных целях. Итак, механизм классов дает возможность программисту определять новые типы данных, отображающие понятия решаемой задачи. Перегрузка стандартных операций языка Си++ позволяет сделать операции над объектами новых классов удобными и общепонятными. Перегрузка операций позволяет встроить вводимый программистом класс в систему базовых типов языка Си++. Но возникают два вопроса: «Можно ли вводить собственные обозначения для операций, не совпадающие со
стандартны362 Глава 11 ми операциями языка Си++? И все ли операции языка Си++ могут быть перегружены?» — К сожалению (или как констатация факта), вводить операции с совершенно новыми обозначениями язык Си++ не позволяет. Ответ на второй вопрос также отрицателен — существует несколько операций, не допускающих перегрузки. Приводим их список: . прямой выбор метода или поля данных структурированного объекта; .* обращение к методу или полю данных через указатель на него; ?: условная операция (тернарная); :: операция указания области видимости; sizeof операция вычисления размера (в байтах); # препроцессорная операция; ## препроцессорная операция. Рассмотрим еще несколько важных особенностей механизма перегрузки (расширения действия) стандартных операций языка Си++. При расширении действия (при перегрузке) стандартных операций нельзя и нет возможности изменять их приоритеты (иначе компилятор окончательно запутается). Нельзя изменить для перегруженных операций синтаксис выражений, т.е. невозможно ввести унарную операцию = или бинарную операцию ++. Нельзя вводить новые лексические обозначения операций, даже формируя их из допустимых символов. Например, операцию возведения в степень ** из языка Фортран нельзя ввести в языке Си++. Любая бинарная операция @ определяется для объектов некоторого класса двумя существенно разными способами: либо как метод с одним параметром, либо как внешняя (возможно дружественная) функция с двумя параметрами. В первом случае х @ у означает вызов х. operator @(у), во втором случае х@ у означает вызов operator @(х,у). Если в выражении с бинарной операцией оба операнда являются объектами класса, то в общем случае операция-функция может быть как методом этого класса, так и внешней функцией.
Перегрузка операций и классы ресурсоемких объектов 363 В соответствии с семантикой бинарных операций =,[],-> операции-функции с названиями operator =, operator [ ], operator -> не могут быть внешними функциями, а должны быть нестатическими методами того класса, для которого они определены. Если в выражение с бинарной операцией объект класса должен входить только как правый операнд, то операция-функция не может быть методом класса. В качестве примера может служить операция-функция вывода operator « (). Для нее левый операнд — всегда объект того класса, который представляет выходной поток, а объект вводимого программистом класса — всегда операнд правый. Поэтому в приведенном выше примере класса myComplex операция-функция вывода operator « () определена как дружественная. Любая унарная операция '$' определяется для объектов некоторого класса также двумя способами: либо как метод без параметров, либо как внешняя (возможно дружественная) функция с одним параметром. Для префиксной операции $ выражение $z означает вызов метода z.operator $() или вызов внешней функции operator $(z). Для постфиксной операции выражение z$ означает либо вызов метода z.operator $(int), либо вызов внешней функции operator $(z, int). Синтаксис языка Си++ определяет некоторые встроенные операции над операндами базовых типов как комбинации других встроенных операций над теми же операндами. Например, для переменной long т = 0; выражение ++т означает т += 1, что, в свою очередь, означает выполнение выражения т = т + 1. Такие автоматические замены выражений не реализуются и не справедливы для перегруженных операций. Например, в общем случае определение operator *=() нельзя вывести из определений operator *() и operator =(). Нельзя изменить смысл выражения, если в него не входит объект класса, введенного пользователем. В частности, нельзя определить операцию-функцию для операндов-указателей. (Напомним, что указатель не есть самостоятельный тип, а определяется, подобно массиву, как производный для существующего типа.) Невозможно для операнда т типа int изменить смысл выражения 2 + т и т.п.
364 Глава 11 Операция-функция, первым параметром которой предполагается базовый (стандартный) тип, не может быть введена как метод класса. Для объяснения этого ограничения предположим, что аа - объект некоторого класса и для него расширено действие операции +. При разборе выражения аа + 2 компилятором выполняется вызов операции-функции аа.operator +(2) или operator +(аа, 2). При разборе 2 + аа допустим вызов operator +(2,аа), но ошибочен 2.operator +(аа). Таким образом, расширение действия операции + на выражение стандартный_тип + объект класса допустимо только с помощью внешних операций-функций. Следующая таблица обобщает все варианты форматов обращений к операциям-функциям, вводимым для некоторой операции, условно обозначаемой знаком @, а также для операций =, [ ], ->. Таблица 11.1 Форматы операций-функций Выражение Метод класса Внешняя функция @ а a.operator @() operator @ (а) а@ b a. operator @(b) operator @ (а, Ь) а @ a.operator @ (0) operator @ (а, 0) а = b a. operator =(b) нет а[Ь] a.operator [ ] (b) нет 0) 1 V Сг a.operator -> (b) нет В таблице а — объект класса, для которого перегружены операции, b может быть объектом другого типа. Обратите внимание на префиксную @а и постфиксную а@ унарные операции. Унарные постфиксные операции в соответствии со Стандартом должны иметь дополнительный параметр, который всегда равен нулю. Распространение действия операций на объекты вводимого программистом класса служит средством для встраивания (агрегации) класса в систему типов, уже существующих в языке. Задача агрегации требует, чтобы при расширении действия операций были предусмотрены всевозможные сочетания типов операндов.
Перегрузка операций и классы ресурсоемких объектов 365 Например, определяя операцию сложения + для комплексных чисел, приходится учитывать сложение комплексного числа с вещественным и вещественного с комплексным, комплексного с целым и целого с комплексным и т.д. Если учесть, что вещественные числа представлены несколькими типами (float, double, long double) и целые числа имеют разные типы (short, int, long, char), то оказывается необходимым ввести большое количество операций-функций. К счастью, при вызове операций- функций для ее параметров действуют все соглашения о преобразованиях типов, и нет необходимости учитывать сочетания всех типов параметров. В ряде случаев для бинарной операции достаточно определить только три варианта сочетаний операндов: • стандартный_тип, класс • класс, стандартный_тип • класс, класс. Например, для рассмотренного класса myComplex введена операция-функция с прототипом myComplex operator + (myComplexх, myComplex у). Для агрегации в систему типов языка Си++, казалось бы, не хватает еще двух операций-функций: myComplex operator + (double х, myComplex у) {return(myComp!ex(x + y.real, y.imag));} myComplex operator + (myComplex x, double y) {return(myComplex(x.real + y, x.imag));} Обратим внимание на конструктор с прототипом: myComplex (double xre = 0.0, double xim = 0.0); Наличие в конструкторе умалчиваемого значения второго параметра позволяет обращаться к нему с одним параметром. Тем самым этот конструктор выступает в роли конструктора приведения типов. Поэтому становятся допустимыми выражения в следующих операторах: myComplex СС( 1.0,2.0); // Объект класса myComplex myComplex ЕЕ; // Объект класса myComplex ЕЕ = 4.0 + СС; // double + myComplex ЕЕ = ЕЕ + 2.0; // myComplex + double
366 Глава 11 ЕЕ = СС + ЕЕ; ЕЕ = СС + 20; СС = ЕЕ + 'е'; //myComplex + myComplex //По умолчанию приведение int к double // По умолчанию приведение char к double Выражение 4.0+СС в соответствии с правилами приведения типов преобразуется к виду туСотр1ех(Л.О) + СС, т. е. из вещественного числа 4.0 формируется комплексное число с нулевой мнимой частью. Затем действует механизм перегрузки операций и выполняется обращение к операции-функции, оба параметра которой имеют тип myComplex: operator +(туСотр!ех(4.0), СС). Для выражения СС + 20 цепочка преобразований несколько длиннее: вначале int преобразуется к типу double, затем конструктор приведения типов формирует объект класса myComplex и только затем выполняется операция-функция: operator +(СС, myComplex(double(20))). Таким образом, каждое выражение с операцией +, в которое входит, кроме объекта класса myComplex, операнд одного из стандартных типов, обрабатывается совершенно верно. Принятое в нашем классе умалчивание параметров конструктора является частным решением и не для всех классов пригодно. Можно было бы в качестве умалчиваемого значения мнимой части взять и число, отличное от нуля, но поведение объектов класса myComplex при сложении с данными стандартных типов оказалось бы при этом довольно загадочным. Например, введя конструктор с прототипом myComplexf double re, double im = 10.0); при выполнении myComplex LL = myComplexf 1.0,2.0); LL = LL + 4 + 5; получим LL == (10.0, 22.0), так как два неявных обращения к конструктору в выражении LL + 4 + 5 приводят к двум imag =10. В отличие от всех других унарных операций операции ++ и -- имеют, кроме префиксной формы, еще и постфиксную. Это привело к уже отмеченным особенностям при их перегрузке.
РасПерегрузка операций и классы ресурсоемких объектов 367 смотрим пример. Определим некий условный класс "пара чисел". В нем определим как дружественную операцию-функцию для перегрузки операции вывода "сведений" об объекте класса и, самое главное, введем четыре операции-функции для перегрузки постфиксных и префиксных операций ++ и —. Текст определения класса и дружественных функций (в файле myPair.h): #include <iostream> using namespace std; //Доступ к пространству имен std class myPair { //Класс "пара чисел" int n; // Целое число double х; // Вещественное число //Дружественная функция для префиксной операции ++: friend myPair & operator ++(myPair &); //Дружественная функция для постфиксной операции --: friend myPair & operator —(myPair &, int); //Дружественная функция для операции вывода: friend ostream & operator «( ostream&, myPair); public: // Конструктор: myPair (int ni=0, double xi=0.0): n(ni), x(xi) {} myPair & operator ++(int i) { // Метод для постфиксной операции ++ л+= 10; х+= 10.0; return *this; } myPair & operator - -() { // Метод для префиксной операции - - п -= 1; х -= 1.0; return *this; } }; ostream & operator «(ostream & out, myPair p) { out« "\tn = ”« p.n « "\tx = " << p.x « endt; return out; } myPair & operator ++(myPair & p) { // Префиксная операция ++ p.n += 7;p.x+= 1.0; return p; } myPair & operator--(myPair & p, int i) { //Постфиксная операция —
368 Глава 11 р.п -= 10; р.х-= 10.0; return р; } В классе myPair лва поля данных: intn и double х. Префиксные операции увеличивают (++) и уменьшают (--) на 1 значения каждого из этих полей. Чтобы продемонстрировать полную независимость смысла перегруженной операции от ее традиционного поведения, при определении перегруженных постфиксных операций принято необычное решение — каждая из них изменяет соответствующие поля на величину 10. Тем самым по результатам выполнения операции можно судить, какая из форм операции- функции была использована. В функции main() создадим объект set класса myPair и к нему применим неявно и явно все четыре операции-функции (в файле myPair.h): //Р11_02.срр - Неявные и явные вызовы операций-функций для ++ и —. ttinclude <iostream> using namespace std; It include "myPair.h" intmainf) { myPair set( 3,3.0); cout« "set: " «set; cout« "++set: "« ++set; cout« "sef++: " << sef++; cout« "—set: "« —set; cout« "set--: "« set--; cout« ”operator++(set): " cout« ”set.operator++(0). cout« "set.operator—(): ' cout« "operator—(set,0): return 0; // operator++( set) // set. operator++( 0) //set. operator—() // operator—(set, 0) « operator++(set); « set.operator++(0); << set. operator—(); << operator—(set,0); } Результаты выполнения программы: set: n-3 x = 3 ++set: n = 4 x = 4 set++: n = 14 x - 14
Перегрузка операций и классы ресурсоемких объектов 369 —set: set-: operator++(set): set.operator++(0): set.operator—(): operator—(set,0): л = 13 x= 13 n = 3 x-3 n-4 x-4 n = 14 x- 14 n= 13 x= 13 n=3 x=3 По результатам выполнения программы можно проследить за соответствующими изменениями полей объекта. Обратите внимание, что не соблюдается классическое (унаследованное из языка Си) правило выполнения постфиксной операции: "получи значение операнда, а затем измени его". 11.2. Изменение интерфейса существующего класса Класс myComplex очень "беден" - для его объектов определены только четыре операции (сложения, изменения знака, ввода и вывода). Имея в своем распоряжении исходный текст (код на языке Си++) определения класса, можно включить в него все нужные операции и тем самым расширить интерфейс класса. Но в ряде случаев возможность изменения уже существующего текста определения класса по каким-то причинам отсутствует. В этом случае программист может расширить возможности объектов класса, включив в свою программу исходный код определения класса, а затем написав определения дополнительных операций- функций. Мы уже знаем, что операция-функция может быть либо методом класса, либо его дружественной функцией, либо внешней по отношению к классу свободной функцией. (Функцию называют свободной, если она не входит в качестве метода ни в какой класс и не является дружественной ни для какого класса.) Если программист не может изменять исходный код определения класса, то у него для изменения его интерфейса остается только одна возможность — разрабатывать операции-функции как функции свободные. Но здесь должно быть выполнено одно обязательное условие — исходный класс должен обеспечивать доступ ко всем методам и полям данных, которые потребуются для новых операций-функций. 24- 2762
370 Глава 11 Продолжим наш пример с классом myComplex и дополним его операциями вычитания умножения * и сравнения на равенство ==. Исходное определение класса myComplex находится в заголовочном файле myComplex.h. Создадим еще один заголовочный файл newComplex.h, включив в него с помощью препроцес- сорных средств код исходного определения класса myComplex. Вслед за директивой включения текста из файла myComple.h разместим определения перечисленных новых операций-функций. При оформлении заголовочных файлов всегда целесообразно использовать препроцессорные средства организации условной компиляции (см. гл. 7). Их назначение — защитить текст от ошибочных повторных включений в одну и ту же программу. Общая схема нового заголовочного файла будет такой: #ifndef NEWJCOMPLEX #define NEWJCOMPLEX ^include "myComplex.h" определения внешних операций-функций #endif Здесь #ifndef — препроцессорная директива условной компиляции, встретив которую, препроцессор выполняет проверку "неопределенности" использованного в директиве идентификатора NEWJOOMPLEX. Для определения идентификатора как препроцессорного используется директива #define, после обработки которой идентификатор становится "известным" для препроцессора. Конец диапазона действия условной препроцессор- ной директивы определяется директивой #endif. Идентификатор NEWjCOMPLEX выбирается таким образом, чтобы вероятность его случайного использования в той программе, куда будет подключаться заголовочный файл newComplex.h, была минимальной. Текст заголовочного файла с определениями новых операций-функций для объектов класса myComplex: //newComplex.h - Расширенние класса комплексных чисел Uifndef NEWJCOMPLEX // Защита от повторных включений #define NEWJCOMPLEX
Перегрузка операций и классы ресурсоемких объектов 371 #include <iostream> using namespace std; //Доступ к пространству имен std #include "myComplex. h"// Существующее определение класса // Перегрузка операции вычитания: myComplex operator - (myComplex one, myComplex two) { return myComplex(one.real() - two.realf), one.imagf) - two.imagf)); } // Перегрузка операции умножения (*): myComplex operator^myComplex p, myComplex z) { return myComplex(p.real( )*z.real() - p.imagf )*z.imag(), p.realf )*z.imag() +z.real( )*p.imagf)); } //Бинарная операция сравнения объектов: bool operator^myComplex c 1, myComplex c2) { return (c1. rea!()--c2. real( )&&c1. imag()~c2. imag()); } #endif Использование класса с расширенным набором операций: //Р11_03.срр - Класс комплексных чисел с новыми операциями #include <iostream> using namespace std; #include "newComplex.h" intmainf) { //Определение объектов-комплексных чисел: myComplexbeta( 10,-10);//re-10.0, im=-10.0 myComplex gamma(-10,10); //re=-10.0, im=10.0 cout« "beta*gamma: "<< beta*gamma; cout« ’beta-gamma: ”« beta-gamma; cout« ”beta-gamma:" << (beta-gamma) « endl; cout« "-(beta-gamma):"« -(beta-gamma); return 0; } Результаты исполнения программы: beta *gamma: real = 0, image = 200 beta-gamma: real = 20, image = -20 24*
372 Глава 11 beta==gamma: О -(beta-gamma): real = -20, image = 20 В нашем примере существующий класс myComplex дополнен тремя операциями, ни одна из которых не изменяет значений операндов. Для обращения к закрытым полям данных объектов из тел соответствующих операций-функций применяются открытые методы real() и imag(). Эти методы возвращают значения соответствующих полей данных, но не позволяют изменять значения этих полей. Тем самым для объектов именно этого класса нельзя определить как свободные или дружественные операции- функции декремента и инкремента, т. е. для такого класса операции, изменяющие состояние того объекта, к которому они применены, нужно определять как методы класса. Пример такой операции-функции — метод operator-0, изменяющий знак комплексного числа. 11.3. Классы ресурсоемких объектов До сих пор мы принимали на веру, что для объектов одного класса выражение объект 1 = объект 2 выполняется правильно независимо от количества и типов полей объектов. На самом деле поддержка присваивания обеспечивается в каждом классе за счет присутствия в его определении соответствующей операции-функции с наименованием operator=. Такая операция-функция либо явно определена в классе программистом, либо при отсутствии определения неявно добавляется компилятором. То же самое справедливо и по отношению к конструктору копирования — его компилятор добавляет в класс автоматически, если программист не определил его явно. В том, что для операции присваивания компилятор автоматически включает в определение класса операцию-функцию с наименованием operator= и что в классе всегда есть конструктор копирования, можно «экспериментально» убедиться на следующем примере:
Перегрузка операций и классы ресурсоемких объектов 373 //Р11_04.срр - Операция присваивания и конструктор копирования #include <iostream> using namespace std; class triangle {//Класс "треугольник" double a, b, с; //Длины сторон треугольника public: trianglef double an=0.0, double bn=0.0, double cn=0.0) : a(an), b(bn), c(cn) { } friend ostream& operator «(ostream &, triangle); }; ostream & operator «(ostream & out, triangle tr) { cout< < "a-"< <tr.a« "\ tb=”< <tr.b« "\ tc="< <tr.c« endl; } intmainf){ triangle one, two(3.0, 4.0, 5.0); cout« one; one.operator=(two); //Эквивалент: one=two cout« one; triangle resftwo); // Конструктор копирования cout« res; return 0; } Результаты выполнения программы: a=0 b=0 c=0 a=3 b-4 c=5 a=3 b-4 c=5 В программе есть класс triangle, позволяющий определять объекты с полями данных double а, Ь, с. Это могут быть длины сторон треугольника. Но смысл их нам сейчас не важен. В классе явно определен конструктор общего вида (он же конструктор умолчания) и дружественная функция operator « () для перегрузки операции вывода сведений об объекте. Нет явно определенного конструктора копирования и нет операции-функции для присваивания. В функции main() определены два объекта класса triangle и с помощью вызова one.operator=(two) неявно определенной функции operator=() объекту one присвоено значение объекта two. Оператор cout « one позволяет убедиться, что все
374 Глава 11 правильно - результат именно такой, который ожидалось получить после выполнения "обыкновенного” оператора присваивания one=two; В нашем примере в классе triangle отсутствует не только явно определенная операция-функция присваивания, но и явное определение конструктора копирования. Чтобы убедиться в его наличии, в основную программу включены следующие операторы: triangle resftwo); cout« res; Результат соответствует нашим ожиданиям: а=3.0 в-4.0 с-5.0 Итак, конструктор копирования, который используется для формирования объекта res по объекту two, также существует и добавлен в класс автоматически без явных указаний программиста. Всегда ли можно доверять компилятору автоматическое построение операции-функции присваивания и создание конструктора копирования? — Далеко не всегда. Поясним это утверждение, сначала рассматривая конструкторы копирования. Как мы уже заметили, конструкторы инициализируют объект, т. е. формируют среду, в которой выполняются методы, относящиеся к объекту. Под средой, формируемой конструктором, понимаются не только участки памяти, выделяемые для объекта. При создании среды выполнения методов может требоваться выделение и других ресурсов, например, файлов, входящих в объект, динамически выделяемого участка памяти и т.д. Объекты, для которых требуется выделение ресурсов, отличных от участков статической и автоматической памяти, будем называть ресурсоемкими. Для иллюстрации их особенностей нам будет достаточно, если при создании среды объект, формируемый конструктором, получает, кроме участков автоматической и статической памяти, и динамически выделяемую память. Пусть имеется такое (ошибочное) определение класса, описывающего точку в многомерном пространстве: class badPoint { // НЕВЕРНОЕ определение класса int size; // Размерность пространства double * coord; // Указатель на массив координат точки
Перегрузка операций и классы ресурсоемких объектов 375 public: badPoint(int п=1, double z=0.0) //Конструктор : sizefri) { coord = new double [size]; for (int i=0;i<size;i++) coord[l] = z; ) friend ostream & operator«(ostream & out, badPointp); ~badPoint() { //Деструктор delete [ ] coord; } }; // Определение дружественной функции: ostream & operator«[ostream & out, badPointp) { out< < "size="< <p.size; for(inti=0; Kp.size; i++) out< < "\ t["< </< < 7=”< <p. coord[i]; out«endl; return out; } При создании объекта класса badPoint выделяются два участка памяти — один для полей данных size и coord там (в той памяти), где создается объект, второй — обязательно в динамической памяти. Второй участок выделяется за счет выполнения операции new в конструкторе. Схема распределения памяти для объекта приведена на рис. 11.1. Поля данных объекта size coord Динамическая память ♦ Рис. 11.1. Память ресурсоемкого объекта Деструктор при уничтожении объекта явно освобождает динамическую память операцией delete[ ]. В классе badPoint конструктор копирования и операция- функция присваивания явно не определены. И это совершенно неверно. Дело в том, что компилятор не может знать правил копирования конкретных ресурсоемких объектов. Он выполняет
376 Глава 11 только побитовое (поразрядное, иначе - поверхностное) копирование объектов. Чтобы пояснить сказанное, рассмотрим инициализацию объектов разных типов - стандартного и нашего ресурсоемкого: double d1 = 3.1415; double d2 = d1; badPoint p1(3, 1.2); bad Point p2 = p1; Инициализация объекта p2 класса badPoint и объекта d2 типа double внешне похожа, но семантически должна отличаться друг от друга. Для типа double выполняется только побитовое (поразрядное, поверхностное) копирование, т.е. участок памяти копируется по схеме, приведенной на рис. 11.2. сМ d2 Рис. 11.2. Схема поразрядного копирования Для ресурсоемких объектов класса badPoint необходимо так называемое глубокое копирование. Должны копироваться в этом случае не только данные объекта из основной памяти, но и те ресурсы, которые выделены объекту. Правильная инициализация объекта р2 значением объекта р 1 показана на рис. 11.3. Динамическая память Р1 Р2 Рис. 11.3. Глубокое копирование ресурсоемкого объекта
Перегрузка операций и классы ресурсоемких объектов 377 Однако компилятор может автоматически создать только конструктор поразрядного копирования, действие которого можно представить рис. 11.4. Динамическая память Р1 Р2 Рис. 11.4. Поразрядное (поверхностное) копирование ресурсоемкого объекта Основной недостаток поразрядного (поверхностного) копирования - наличие только одного ресурса для двух или более объектов, получивших значения посредством копирования. В дальнейшем такие объекты будут независимо друг от друга обращаться к одному ресурсу, что не всегда соответствует целям программиста. Уничтожение одного из объектов скажется на другом. У оставшегося объекта может появиться так называемая "висячая ссылка" - он сохранит адрес ресурса, который уже не принадлежит программе. В следующей программе это демонстрируется для объектов класса bad Point. //PI 1_05.cpp - "Трудности"ресурсоемких объектов (копирование) #include <iostream> using namespace std; #include "badPoint.h" intmainf) { badPoint temp(3,0.33); badPoint real=badPoint(temp); //Конструктор копирования cout«"real: \t"«real; temp. ~badPoint(); // Явный вызов деструктора cout«"real: \ t"«real; return 0; }
378 Глава 11 Результаты выполнения программы: real: size=3 [0]=0.33 [1]=0.33 [2]=0.33 real: size=3 [0]=6.36633е-314 [1]=0.33 [2]=0.33 В классе badPoint конструктор копирования определен неявно. В основной программе объект real получает значение за счет поразрядного (поверхностного) копирования значения объекта temp, а затем для объекта temp явно вызван деструктор. После этих действий значение динамической памяти объекта real изменилось. (Предупреждение: эта ошибка может иногда не проявиться при условиях, не зависящих от программиста, что еще более ухудшает дело.) Все сказанное относительно копирующего конструктора относится и к операции присваивания, когда она автоматически добавляется компилятором в определение класса. Следующая программа иллюстрирует сказанное: //Р11_06.срр - ''Трудности”ресурсоемких объектов (присваивание) ttinclude <iostream> using namespace std; //Доступ к пространству имен std #include "badPoint. h" int main() { badPoint one(2,12.3); // Определяем объект { // Вложенный блок badPoint two; // Определяем объект two = one; //Присваивание cout< < ”two:\t”< < two; } // Неявный вызов деструктора cout« "one: \ t"«one; return 0; } Результаты выполнения программы: two: size=2 [0]=12.3 [1]=12.3 one: size=2 [0]=6.36633e-314 [1]=12.3 В программе использован вложенный блок, где определен объект badPoint two, которому присвоено значения объекта one из охватывающего блока. При выходе из внутреннего блока автоматически уничтожается объект two. Но при этом после
уничтоПерегрузка операций и классы ресурсоемких объектов 379 жения объекта two! "испортилось" значение, на которое "смотрит" указатель coord в объекте one! Для устранения проиллюстрированного эффекта в классах ресурсоемких объектов необходимо явно определять конструктор копирования и операцию-функцию присваивания. Они должны обеспечивать присваивание значений не только полям данных объектов, но и выполнять копирование тех ресурсов, которые выделены объектам. В этом случае, как уже сказано, говорят о так называемом глубоком копировании. Определим на основе класса badPoint более корректно класс "точка многомерного пространства" с конструктором копирования и операцией-функцией присваивания. Назовем новый класс nPoint. Конструктор глубокого копирования класса nPoint можно определить так: nPointf const nPoint & point): size( point, size) { coord = new double [size]; for(inti=0; i<size; /'++) coord[i] = point.coord[i]; } От конструктора копирования, который мог построить компилятор, этот конструктор отличается явным выделением ресурса (динамической памяти для массива координат) и наличием средств для задания состояния выделенного ресурса (цикл присваивания значений элементам динамического массива). Теперь рассмотрим присваивание ресурсоемких объектов. Пусть obj 1 и оЬ/2 - объекты класса nPoint. Для правильного выполнения операции obj 1 = obj2 должна вызываться операция- функция присваивания с глубоким копированием. Ее можно определить так: nPoint & operator^const nPoint & point) { iff this != &point) { delete [ ] coord; coord = new double [size = point size]; for (int i=0;i<size;i++) coord[i] = point.coord[i]; } return *this; }
380 Глава 11 Если рассматривать выполнение операции obj 1 = obj2, то в теле операции-функции size и coord поля данных объекта obj 1, a this - указатель, его адресующий. Присваивание с глубоким копированием существенно отличается от конструктора глубокого копирования. Отличия связаны с разными задачами, возложенными на эти методы класса. Назначение конструктора копирования — превратить новый участок памяти в объект класса. Задача присваивания — занести данные в уже существующий объект класса. При выполнении присваивания: • необходимо удалять старые ресурсы, ранее принадлежащие объекту. Например, если объекту — точке восьмимерного пространства — присваивать значение объекта трехмерного пространства, то часть памяти, ранее выделенной для восьми координат, становится лишней; • если присваивание выполняется тому же самому объекту, то удалять старый ресурс недопустимо — нечего будет присваивать; • нужно обеспечить возможность "цепочек" присваиваний a=b=c=z, где a,b,c,z - объекты класса nPoint. В примере удаление старых ресурсов выполняет оператор delete [ ] coord; Защита от присваивания значения самому себе: if (this != & point) {...} Обеспечение цепочек присваиваний: return *this, Вопрос: «Почему в нашем примере будет ошибочной проверка if (*this \= point) {...} и при каком изменении класса она будет правомерна?» (Ответ: «Для использования такой проверки в классе nPoint должна быть определена операция сравнения на неравенство !=».) Объяснив роль конструктора копирования и назначение операции-функции присваивания для ресурсоемких объектов, приведем текст "правильного" определения класса "точка многомерного пространства" (в файле nPoint.h):
Перегрузка операций и классы ресурсоемких объектов 381 #include <iostream> using namespace std; class nPoint { // ПРАВИЛЬНОЕ определение класса int size; // Размерность пространства double * coord; // Указатель на массив координат точки public: nPointfint п-1, double z-0.0) //Конструктор общего вида : size(n) { coord = new double [size]; for (int i=0;i<size;i++) coord[i] = z; ; friend ostream & operator«(ostream & out, nPoint p); ~nPoint() { //Деструктор delete [ ] coord; } nPointf const nPoint & point) // Конструктор копирования : size(point.size) { coord = new double [size]; for (int i=0; i<size; i++) coord[i] = point.coord[i]; } nPoint & operator^const nPoint & point) {// Операция присваивания iff this != &point) { delete [ ] coord; coord = new double [size = point.size]; for (int i=0;i<size;i++) coord[i] = point coord[i]; } return *this; } }; // Определение дружественной функции: ostream & operator«(ostream & out, nPoint p) { out< < "size="< <p. size; for(inti=0; Kp.size; i++) out< < "\ t["< <i< < "]= "< <p. coord [i]; out«endl; return out;
382 Глава 11 Чтобы убедиться в корректном "поведении" объектов предлагаемого класса, выполним для него те же "эксперименты", которые проведены в программах Р11_05.срр и Р11_06.срр для класса badPoint. Разместим определение класса nPoint в файле nPoint.h и напишем следующую программу: //Р11_07.срр - Копирование и присваивание ресурсоемких объектов. #include <iostream> using namespace std; //Доступ к пространству имен std #include "nPoint.h"//Включение описания класса int main() { nPoint temp(3,0.33); nPoint real=nPoint(temp); //Конструктор копирования cout«"real: \t"«real; temp. ~nPoint(); //Явный вызов деструктора cout«"real: \t"«real; nPoint one(2,12.3); // Определяем объект { // Вложенный блок nPoint two; // Определяем объект two = one; // Присваивание cout< < "two: \t"« two; } // Неявный вызов деструктора cout< < "one: \ t"«one; return 0; } Результаты выполнения программы: real: size=3 [0]=0.33 [1]=0.33 [2]=0.33 real: size=3 [0]=0.33 [1]=0.33 [2]=0.33 two: size-2 [0]=12.3 [1]= 12.3 one: size-2 [0]=12.3 [1]=12.3 Сравнивая полученные результаты с результатами программ Р11_05.срр и Р11_06.срр, убеждаемся, что и операция присваивания и конструктор копирования "ведут себя" правильно - нет взаимного влияния объектов друг на друга.
ИСКЛЮЧЕНИЯ 12.1. Общие сведения об исключениях Механизм обработки особых ситуаций присутствовал в разных языках программирования до появления Си++. Один из примеров — язык ПЛ/1, в котором программисты могли работать как со встроенными (заранее запланированными) ситуациями, так и с ситуациями, создаваемыми (формируемыми) по указанию программиста при наступлении того или иного события. Типичные встроенные ситуации: “деление на нуль”, “достижение конца файла”, “переполнение в арифметических операциях” и т.п. В языке Си++ почти любое состояние, достигнутое в процессе выполнения программы, можно заранее определить как особую ситуацию и предусмотреть действия, которые нужно выполнить при ее возникновении. Делается это с помощью исключений. Прежде чем вводить конструкции, обеспечивающие программиста средствами для работы с исключениями, объясним уникальность этого механизма и его отличия от уже существовавших, например в языке Си, возможностей. При разработке программ, которыми в дальнейшем должны пользоваться другие программисты (например, при создании библиотек), часто возникает положение, когда автор знает, как выявлять ошибочные ситуации, но не знает, как действовать при их появлении. В свою очередь, программист—потребитель программ знает, как нужно поступать при возникновении ошибочных ситуаций, но не знает, как их обнаружить, не "влезая" в текст или код используемых (чужих) программ. Простейшие примеры таких ситуаций: “деление на нуль” или “достижение конца файла”. Как пишет Б. Страуструп [1,2), именно для выхода из таких тупиковых положений служит механизм исключений — средство, позволяющее отделить выявление особой ситуации от обработки информации о ней.
384 Глава 12 Для реализации механизма обработки исключений в язык Си++ введены следующие служебные слова: try (контролировать, пытаться), catch (ловить, перехватывать) и throw (бросать, генерировать, посылать). Общая схема посылки и обработки исключений выглядит так: try { операторы throw выражение^; операторы throw выражение_2; операторы } catch (спецификацияисключения_ 1) { операторыобработкиисключения_ 1} catch (спецификацияисключения_2) ( операторыобработкиисключения _2} Служебное слово try позволяет выделить в любом месте программы блок контроля за исключениями try {операторы} Среди операторов, заключенных в фигурные скобки, могут быть: любые операторы языка; описания объектов и функций; определения локальных объектов. Кроме того, в блок контроля за исключениями помещают специальные операторы, генерирующие (посылающие) исключения. Формат такого оператора: throw выражение; С помощью выражения, использованного после служебного слова throw, формируется специальный объект, называемый исключением. Все исключения создаются как временные объекты, а тип и значение каждого исключения определяется формирующим его выражением. Оператор throw выполняет посылку исключения, т. е. передает управление и пересылает исключение непосредственно за блок контроля (за фигурную скобку, закрывающую блок). В этом месте обязательно размещается так называемая ловушка, или обработчик исключений (ловушек может быть несколько). Важно отметить, что исключения могут посылаться из разных мест одного блока контроля; они могут быть как
Исключения 385 одинакового типа, так и разных типов. Каждый обработчик, как было сказано, имеет следующий формат: catch (спецификацияисключения) { операторы_обработкиисключения } Спецификация исключения в заголовке обработчика подобна спецификации параметра функции и может иметь одну из трех форм: тип_исключения имя тип исключения многоточие Внешне и функционально обработчик исключений похож на определение функции с одним параметром, не возвращающей никакого значения. Когда вслед за блоком контроля размещены несколько ловушек, то они должны отличаться друг от друга типами принимаемых исключений. Посланное из блока контроля исключение ловится соответствующим ему обработчиком исключений и после выполнения операторов его тела управление передается за последний обработчик. Отметим следующее. Английский термин "try” переводится как "попытка", "пытаться", "пробовать". Эти слова очень точно передают назначение блока try. Заключенный в него код нужно попытаться выполнить. Если не возникает исключения — все в порядке, продолжайте выполнение программы. Если послано исключение - переходите к его идентификации и обработке в соответствующей ловушке catch. Еще раз обратим внимание, что механизм обработки исключений является общим средством управления программой. С его помощью можно обрабатывать не только аварийные, но и любые другие ситуации, возникшие в процессе выполнения программы. Как пишет Б.Страуструп, механизм исключений в C++ в основном является средством передачи управления специальному коду в вызывающем модуле (см. [1], с. 232). В качестве примера рассмотрим применение механизма исключений при определении наибольшего общего делителя (НОД) двух целых чисел х и у. Напомним классический алгоритм Евклида: 25~2762
386 Глава 12 • если х == у, то ответ найден, НОД-х; • если х < у, то у заменяется значением у - х; • если х > у, тох заменяется значением х-у. Алгоритм Евклида применим при соблюдении двух условий: • оба числа неотрицательны; • оба числа отличны от нуля. Нарушение каждого из названных условий будем считать особой ситуацией и посылать исключения. В первом случае исключением будет объект библиотечного класса string. Во втором случае, когда появляется нулевой параметр, в качестве исключения пошлем значение второго, возможно ненулевого, параметра типа int. Оформим алгоритм вычисления НОД в виде функции с заголовком int GCD (intx, int у), в тексте которой поместим операторы генерации исключений. В функции mainQ поместим блок контроля за исключениями и обработчики исключений двух типов — string и int. Текст программы: //Р12_01.срр - Исключения типа int и string it include <iostream> #include <string> using namespace std; int GCD (intx, inty) { if(x == 0) throw y; iffy == 0) throwx; if(x < 0) throwstring(,f\nNegative parameter V); iffy < 0) throw stringf "\nNegative parameter 2”); while (x != y) { iffx > y) x = x - y; else у-y -x; } return x; } int main () { try { cout« "GCD (66, 44) = " « GCD (66, 44) « endl; cout« "GCD (0, 7) = "« GCD (0, 7) « endl; cout« "GCD (-6, 4) = " « GCD (-6, 4) « endl; )
Исключения 387 catch (const string report) { cout« report« endl; } catch (const int ex) { cout« "One parameter is ZERO! cout« "Another equals "«ex; } return 0; } Результат выполнения программы: GCD(66, 44) =22 One parameter is ZERO! Another equals 7 В теле функции GCD( ) цикл, реализующий алгоритм Евклида, и перед циклом — четыре оператора посылки исключений, которые выполняются при нарушении условий применимости алгоритма Евклида. Первые два оператора throw посылают исключения типа int, два следующих - объекты типа string. В основной программе в блоке fry три обращения к функции GCD (), причем только первое обращение не содержит ошибочных параметров. Первый обработчик исключений "ловит" исключения типа string, второй предназначен для обработки исключений типа int. При обращении к функции GCD ( ) с нулевым параметром (см. результаты выполнения программы) посылается исключение типа inf, и после его обработки выполнение программы завершается. При этом третий вызов функции (с отрицательным параметром) не выполняется, т. е. управление не возвращается из обработчика в блок try. В нашем примере в качестве исключений использованы уже присутствующие в языке и его библиотеке типы (string, int). В ряде случаев этого недостаточно и желательно в исключении передавать больше информации. Такую возможность дает применение в качестве исключений объектов пользовательских классов. Модифицируем наш пример, введя специальный класс для объектов-исключений и написав новую функцию для вычисления НОД двух чисел. Программа может быть такой: //Р12_02.СРР - Исключения пользовательского типа ^include <iostream> 25*
388 Глава 12 #include <string> using namespace std; struct DATA {// Класс объектов-исключений int n, m; string s; DATAfint x, int y, string str): // Конструктор n(x), mfy), s(str) { } }; int GCD_ONE(intx, inty) {//Определение функции if(x == 0 \ \ y== 0) throw DATA(x,y, "ZERO!”); if(x < 0) throw DATA(x,y, "Negative parameter 1."); iffy < 0) throw DATA(x,y, "Negative parameter 2."); while (x != y) { iffx > y) x = x - y; else у-y - x; } return x; } int mainf) { try { cout« "GCD_ONE(66,44) = " « GCD_ONE(66,44) « endl; cout« "GCDjONEf 0,7) = " « GCD_ONE(0,7) « endl; cout« "GCDjONEf -12,8) = " « GCD_ONE(-12,8) « endl; ) catch (DATA d) { cerr « d.s « "x-"« d.n « ", y="« d.m;} return 0; } Результат выполнения программы: GCD_ONE(66,44) = 22 ZERO! x=0, y=7 Обратите внимание, что обращение GCD_ONE(-12,8) не выполнено. Исключения в этой программе формируются как объекты специального созданного для решения этой задачи класса DATA. В каждый объект исключения входит строка сообщения и два числовых значения (присвоим им значения параметров, при которых
Исключения 389 возникла особая ситуация). Класс DATA определен как структура, что делает его поля данных открытыми (общедоступными). Нужно обратить внимание, что объекты класса DATA формируются внутри одной функции, но доступны в другой функции. Это особое свойство исключений. Они создаются как временные статические объекты в одном блоке, но доступны в другом. Что касается определения класса объектов-исключений, то следует отметить, что оно не обязательно размещается в глобальном пространстве имен, как класс DATA из приведенной программы. Следовательно, при спецификации "параметра" ловушки (обработчика) исключений для обозначения типа "параметра" в общем случае должна использоваться форма имя_пространства_имен::имя_класса 12.2. Синтаксис и семантика механизма исключений Как объект исключение создается выражением из оператора throw в контролируемом блоке, за счет выполнения оператора throw пересылается за пределы этого блока, обрабатывается в ловушке исключений и, наконец, исчезает после окончания обработки. Достоинство механизма исключений состоит в том, что можно сделать временной разрыв между разработкой функций, генерирующих исключения, и определением алгоритмов, обрабатывающих исключения. Механизм обработки исключений позволяет переносить обработку ситуации из той точки, где она возникла, в точку, где программист запланировал ее обработку. Исключение позволяет передавать любое количество информации. Следует обратить особое внимание на то, что после обработки исключения нет возможности вернуться в то место, откуда исключение было послано. Одна из особенностей: механизм исключений предназначен только для синхронных событий, т. е. для таких событий, которые произошли в процессе выполнения программы, а не привнесены внешними воздействиями. Например, отключение питания компьютера не удается обработать как исключение.
390 Глава 12 Схема передач управления при обработке исключений приведена на рис. 12.1. Существуют, как уже указано, три формы спецификации исключений, т. е. три вида обработчиков: 1) catch (тип имя) {операторы} 2) catch (тип) (операторы) 3) catch (...) (операторы) В первом варианте спецификация включает имя, которое как параметр функции можно использовать в операторах обработки исключения. Второй вариант не предполагает использования значения исключения, важен только тип исключения. В третьем случае обработчик реагирует на любые исключения независимо от их типа.
Исключения 391 Так как после выхода из блока контроля за исключениями "просмотр" обработчиков исключений выполняется последовательно, то обработчик без параметра (с многоточием вместо него) нужно размещать в конце последовательности обработчиков. (После него никакой обработчик не будет выполняться.) В предыдущих примерах использовались обработчики исключений, в которые передавались не только тип, но и значение исключений. Продемонстрируем использование обработчиков исключений второго и третьего видов в следующей программе, где ловушки исключений не получают данных (передаются только типы исключений): //Р12_03.срр - Исключения без передачи данных #include <iostream> using namespace sth; class zeroDivide {}; //Класс объектов-исключений class overflow { }; //Класс объектов-исключений //Функция для деления значений параметров: double div (double n, double d) { if (d == 0.0 &&n == 0.0) throw 0.0; if (d == 0.0) throwzeroDividef ); double b -n/d; if( b > 1e+30) throw overflowf); return b; ) double x = 1e-20, z= 1e+20, w = 0.0; // Глобальные данные void RR(void) { //Применение функции деления div() try { w = div(4.0, w); z = div(z, x); w = div(0.0, 0.0); } //end of try catch (overflow) { cerr « "overflow"« endl; z= 1e+30; x= 1.0; } catch (zeroDivide) { cerr « "zeroDivide ”« endl; w= 1.0; )
392 Глава 12 catch (...) { се гг « "Indeterminacy!"« endl; } } // end of RR int main () { RR(); RR(); RR(); return 0; } Результаты выполнения программы: zeroDivide overFlow Indeterminacy! Обратите внимание на то, что в теле функции RR( ) "запланировано" троекратное обращение к функции div(), но реально при трех последовательных обращениях к RR() функция div() вначале вызывается один раз, затем два раза и только потом три раза. В каждом случае при обработке соответствующего прерывания изменяются значения переменных, служащих аргументами div() в теле функции RR(). Поясним, как выполняется "сравнение" посланного исключения с параметрами (со спецификациями параметров) обработчиков (ловушек). Предположим, что обработчик исключений имеет следующий вид: catch ( Т х ) {действия_обработчика } где Т — некоторый тип. Обработчик «ловит» исключения типов 7, const 7, const 7 & и 7&. Исключение перехватывается обработчиком и в том случае, если тип исключения может быть стандартным образом приведен к типу параметра обработчика. Если исключение послано, но соответствующий ему обработчик не найден, то вызывается специальная библиотечная функция terminateQ. Обращение к этой функции завершает выполнение программы. Оператор, формирующий исключение, может иметь две формы: throw выражение; throw;
Исключения 393 В первом случае исключение формируется как статический объект, значение и тип которого определяются выражением генерации. Копия этого объекта передается за пределы блока контроля за исключениями. Эта копия существует до тех пор, пока исключение не будет полностью обработано. Оператор throw; без выражения может использоваться только внутри обработчика исключений, и его применение разумно в том случае, когда существует вложение блоков контроля за исключениями. Задача этого оператора — ретрансляция полученного им исключения в блок следующего (более высокого уровня). Другими словами, если обработчик "не настроен" на обработку полученного им исключения, он может оператором throw; направить его дальше (Б. Страуструп [2]). В заголовке функции можно указывать, какие исключения эта функция порождает или ретранслирует. Для этого используется такая синтаксическая конструкция: тип имя_функции (спецификацияпараметров) throw (список_типов) {...операторы тела функции...} Конструкция throw(cnncoK_TnnoB) называется спецификацией исключений конкретной функции. Список_типов в спецификации исключений может быть пустым. Суффикс throw() с пустым списком имен типов указывает, что за пределы функции не должны передаваться никакие исключения. Если в заголовке функции нет суффикса throw, то эта функция может посылать любые исключения. Примеры заголовков функций, для которых указаны спецификации исключений: void myFund ( ) throw (А, В) { } void myFunc2() throw ( ) { ) С помощью суффикса throw указано, что функция myFuncM) может порождать исключения типов/) и В, а функция myFunc2() не может порождать вообще никаких исключений. Если внутри функции myFunc2() порождается исключение, то оно должно быть обработано внутри нее.
394 Глава 12 Если функция создает исключения, отличные от тех, которые указаны в ее спецификации исключений, то управление передается специальной функции unexpected(), которая вызывает функцию terminate(), и тем самым завершается выполнение программы. Если функция myFunc2() из нашего примера генерирует исключения, то управление также передается функции unexpected(). Суффикс throw не входит в тип функции. Пример: void 12 () throw (); void 13 () throw ( BETA); void ( *1ptr)(); fptr = 12;... Iptr = 13; Здесь указателю fptr совершенно правильно присвоены адреса двух функций одного типа, потенциально генерирующих разные исключения. Проиллюстрируем ретрансляцию исключений и применение спецификаций исключений следующим примером: //Р12_04.срр - Ретрансляция и суффиксы исключений #include <iostream> using namespace sth; void compare (int k) throwfint, const char *) { i1( k%2 != 0) throw k; // Исключение int - "нечетный" else throw "even"; //Исключение char * - "четный" } void gg (int j) throwf) { // Вызывающая функция try { try {compare (j);} catch (int) { cout« "Odd"« endI; //"Нечетный" throw; // Ретрансляция исключения } catch (const char *) { cout« "Even"« endl; } }
Исключения 395 catch (int i) { cout« "Result ="<</« endl; } } int main () (99(4); 99(7); return 0; Результат выполнения программы: Even Odd Result = 7 Функция compare() в зависимости от четности параметра посылает исключения разных типов (int либо const char *). При нечетном значении аргумента в качестве исключения посылается его значение типа int. При четном значении аргумента исключением служит строка в стиле Си, точнее, строковая константа ”even". В теле функции дд() два вложенных блока контроля за исключениями. Из одного обработчика исключений внутреннего блока исключения типа int ретранслируются и ловятся во внешнем блоке. Суффикс функции дд() указывает, что все исключения, возникающие при ее исполнении, будут обработаны в ее теле. Если оператор throw, использовать вне блока контроля за исключениями, то вызывается специальная функция terminate(), которая завершает выполнение программы. То же самое происходит в том случае, если среди обработчиков исключений не будет найден подходящий. Аналогичная ситуация может возникнуть и при ретрансляции исключения в тот блок, в котором нет подходящего для него обработчика. Напомним, что каждый обработчик исключений с непустой спецификацией исключения настроен только на исключения своего типа и может за один раз обработать только одно исключение заданного типа или преобразуемого к заданному.
396 Глава 12 12.3. Исключения в конструкторах Механизм исключений позволяет передавать информацию из конструкторов при возникновении в них особых (запланированных программистом) ситуаций. Обрабатывать эти исключения можно двумя способами. Во-первых, можно, как обычно, создать блок контроля за исключениями и набор обработчиков в том месте, где вызывается конструктор, т.е. создаются объекты класса. Кроме того, Стандартом введена специальная форма генерации и обработки исключений непосредственно в конструкторах. Внешнее определение конструктора может иметь такой вид: имя_класса::имя_класса (спецификацияпараметров) try: список_инициализаторов {операторы_тела_конструктора} последовательность обработчиков исключений Обработчики перехватывают исключения, возникающие при инициализации и при выполнении операторов тела конструктора. В следующей программе используем два способа перехвата исключений, посылаемых конструктором. Определим класс точек на плоскости со счетчиком их количества. //Р12_05.срр - Исключения в конструкторе #include <iostream> #include <string> using namespace std; #define PRiNT(X) cout«#X"="«X«endl class point2 { double x, y; static int N; // Статическое поле данных public: point2(double xn. = 0.0, double yn = 0.0); static int& countf) // Статическая функция {return N;} }; intpoint2:: N = 0;//Инициализация //Определение конструктора: point2::point2 (double xn, double yn) try: x(xn), y(yn) {
Исключения 397 N++; if(N == 1) throw "The begin!"; if (N>2) throw string! "The end!"); } catch(const char * ch) {cout«ch«endl;} int main () { try { PRINT(point2::count( )); point2A(0.0, 1.0); PRINT(A. countf)); point2 B; PRINT(point2::count()); point2 C; PRINT(point2::count()); point2 D( 1.0,2.0); PRINTf D. count( )); } catch( string str) {cout«str;} return 0; } Результаты выполнения программы: point2::count( )=0 The begin! A.count()=1 point2::count()=2 The end! Исключение в виде строковой константы "The begin!"послано при первом обращении к конструктору. Оно обработано в конструкторе, откуда выводится сообщение "The begin!". Исключение string("The end!") посылается, когда число созданных объектов класса point2 превысит 2. Оно перехватывается не в конструкторе, а в основной программе, после чего нет возврата в контролируемый блок, и программа завершена без создания последних объектов класса points.
Глава 13 ВКЛЮЧЕНИЕ И НАСЛЕДОВАНИЕ КЛАССОВ 13.1. Отношение включения классов Если в одном проекте определены и используются несколько классов, то между их объектами могут быть различные отношения. Самое простое — независимость объектов, т.е. независимость порождающих их классов. Более сложное — отношение включения. Следующее более интересное отношение - отношение наследования. Об отношении включения говорят, используя выражение "включает как часть" ("has а" - владеет, содержит в себе, ему принадлежит). Об отношении наследования можно сказать, используя выражение "является частным случаем" ("is а" - является). Например, самолет является частным случаем транспортного средства, здесь — наследование. Самолет имеет крылья и мотор, здесь — включение. В учебных пособиях по программированию на языке Си++ иногда не делается четкого разграничения этих двух отношений между классами и соответственно между объектами этих классов. Объясняется такая небрежность большими выразительными возможностями языка Си++, который позволяет не задумываться над семантикой отношений между классами при реализации как схемы наследования, так и схемы включения. Например, класс "прямоугольник" можно построить как класс, включающий две точки - вершины, определяющие его размеры и размещение. В то же время прямоугольник можно считать наследником класса "точка", которой добавлены размеры. Обе указанные схемы очень просто реализуются разными средствами языка Си++. Рассмотрим отношение включения, которое характеризуется выражением "включает как часть". Введем класс "точка на плоскости":
Включение и наследование классов 399 class point { double х; double у; public: pointf double xi=0.0, double yi=0.0): *(xi), У(У0 { } friend ostream & operator«(ostream & out, point p); }; ostream & operator«(ostream & out, point p) { out< < "x= "< <p.x< < "\ ty= ”< <p. y; return out; } В классе point явно определен конструктор общего вида, играющий роли и конструктора приведения типов, и конструктора умолчания. Выполнена перегрузка операции вывода <<. (Конструктор копирования и операцию присваивания компилятор добавил автоматически.) Определим класс «окружность», включающий в качестве поля данных (центра окружности) объект класса point: class circle { point center; double radius; public: circle (point c, double r): // Конструктор centerfc), radiusfr) {} circle (double xi,double yi, double r): // Конструктор center(xi, yi), radiusfr) {} friend ostream & operator«(ostream & out, circle cir); }; ostream & operator«(ostream & out, circle cir) { out«"Circle center: "<<cir.center; out«"\tCircle radius: ,,«cir.radius«endl; return out; } В классе circle перегружена операция вывода << и определены два конструктора общего вида: с двумя параметрами (типов
400 Глава 13 point и double) и с тремя параметрами типа double. Обратите внимание на списки инициализаторов. В них используются вызовы конструкторов класса point. В первом выражение center(c) - это вызов конструктора копирования, во втором center(xi, у/) - обращение к конструктору общего вида. В классе point явно определен только второй из них (он же конструктор умолчания и конструктор приведения типов). Конструктор копирования класса point создан компилятором автоматически без явного указания программиста. В основной программе (см. Р13_01.срр) создадим объекты обоих классов и выведем данные об объектах-окружностях: int main() { point tip; circle ovalftip, 12.0); cout«oval; circle obodf 10,20,2.0); cout«obod; cout< < ,,sizeof(tip)="< <sizeof( tip)«endl; cout«”sizeof(oval)="«sizeof(oval)«endl; return 0; } В программе объект tip класса point создается конструктором умолчания, а для объектов obod, oval класса circle вызываются разные конструкторы общего вида. Важно понимать, в каком порядке выполняются конструкторы классов circle и point. Рассмотрим создание объекта: circle obod( 10,20,2.0); Вначале вызывается конструктор класса circle. В его списке инициализаторов выражением centerfxi, yi) вызывается конструктор класса point. Затем инициализируется поле данных double radius и только после этого выполняется тело конструктора класса circle. В нашем примере оно не содержит операторов. Результаты выполнения программы: Circle center: х=0 у=0 Circle radius: 12 Circle center: x= 10 y=20 Circle radius: 2 sizeof(tip)=16 sizeof(oval)=24
Включение и наследование классов 401 В результатах обратим внимание на размеры участков памяти, выделенных для объектов классов point и circle. Размер tip определяют только его поля данных double х, double у. В объект oval дополнительно включено поле данных double radius. 13.2. Общие сведения о наследовании в Си++ При наследовании один класс — базовый — представляет объекты общего вида, другой — производный — описывает более конкретные объекты, которые являются разновидностью (частным случаем) объектов базового класса. В теории объектно-ориентированного программирования класс называется производным или наследником базового класса, если он описывает специфическое подмножество тех объектов, которые определяются базовым классом. Для такого отношения наследования в теории объектно-ориентированного программирования (см. [18, 19]) существует правило (принцип Лискова) подстановки (substitution rule). Формулировка правила подстановки: любой объект производного класса должен быть пригоден к употреблению в каждом месте, где применим объект базового класса. Так ли это в практике программирования на языке Си++? И да, и нет. Механизмы наследования, предоставляемые языком Си++, не требуют при наследовании строгого продвижения от общего к частному. Они позволяют не только конкретизировать более общее понятие (базовый класс), дополняя его при наследовании специфическими свойствами, но и сужать базовое понятие, исключая из рассмотрения некоторые из его свойств. В качестве примера возьмем геометрические понятия: точку и эллипс. Если принять за базовое понятие точку, то эллипс как производное понятие можно представить как точку (центр эллипса), дополненную линейными размерами — полуосями эллипса. Если базовым понятием считать эллипс, то производное понятие точка — это эллипс с нулевыми линейными размерами. Обе названные схемы взаимоотношений этих понятий могут быть реализованы с помощью механизма наследования языка Си++. Они позволят учесть и такие различия между точкой и эл- 26-2762
402 Глава 13 липсом, как наличие у эллипса площади, расположение (поворот) эллипса относительно координатных осей и др. Рассмотрим в качестве второго примера взаимоотношения понятий «эллипс» и «окружность». Если базовое понятие «эллипс», то окружность — это эллипс с равными полуосями, каждая из которых - радиус окружности. Если базовое понятие «окружность», то эллипс можно представить как дополнение свойств окружности (центр, радиус) и еще одним свойством — длиной второй полуоси. (В качестве первой полуоси выступит свойство базового понятия — радиус окружности.) От ошибок, связанных с подменой включения наследованием, предостерегает Б. Страуструп ([1J, с. 817): «Новичкам часто приходит в голову сделать класс «аэроплан» производным от класса «двигатель». Это — плохая идея, поскольку самолет не является двигателем, он имеет двигатель ... Несмотря на удобство и краткость записи, которые обеспечивает наследование, его следует использовать почти исключительно для выражения отношений, четко определенных в проекте». Если В — базовый класс, а С — производный, то их отношение можно представить графически (рис. 13.1). На схеме наследования производный класс помещают ниже базового, и классы соединяют стрелкой, направленной (нацеленной) на базовый класс. Наследование можно определить как процесс создания новых (производных) классов-наследников на основе уже существующих классов, называемых базовыми. При наследовании базовые классы не изменяются и остаются доступными. Производный класс получает поля данных и методы Рис. 13.1. Отношение базового класса и может быть по усмотре- наследования классов нию автора (программиста) дополнен новыми данными и методами. Что же можно унаследовать из базового класса, и как "распоряжается наследством" производный класс? Рассмотрим, какие возможности представляет язык Си++ для реализации наследования классов. Вначале приведем пример — эллипс как наследник класса "точка". Определение класса "точка” имеет обычный синтаксис и может быть таким: В С
Включение и наследование классов 403 class point { double х, у; // Координаты точки public: pointfdouble xi=0.0, double yi=0.0): x(xi), y(yi){ } void move(double dx, double dy) {// Перемещение точки x+=dx; y+=dy; > void displayf) {// Вывод значений координат cout< < "x= "< <x< < "\ ty= "< <y< < endl; } }; В классе point координаты точки представлены закрытыми полями данных (double х, у). Открытые методы формируют интерфейс класса. На основе класса point создадим производный класс. Для этого используем простейший синтаксис, при котором спецификация производного класса имеет следующий вид: class имя_производного_кпасса: public имя_базового класса {поля_данных_и_методы_производного_класса}; Имя базового класса у нас point, а в качестве имени производного класса выберем идентификатор ellipse. Тогда определение производного класса можно сделать таким: class ellipse: public point { double dmin, dmax; public: ellipse (double xi, double yi, double din, double dax): pointfxi, yi), dmin(din), dmax(dax) { } void displayf) { cout«"Center:\t"; point: :display(); cout« "dmin- ”< < dmin < < ”\ tdmax= ”< < dmax< < endl; } double squaref) { return 3.14159*dmin*dmax; } }; 26*
404 Глава 13 Рис. 13.2. К примеру наследования классов В классе, определяющем объекты-эллипсы, унаследованы данные и методы класса point. Кроме того, определены dmin и dmax - полуоси эллипса. Таким образом, в каждый объект класса ellipse входят четыре поля данных: х, у, dmin, dmax. Если оценить память, выделяемую для объектов классов, то получим: sizeof(point)=16, sizeof(ellipse)=32. ellipse< double x double у double dmin double dmax point Рис. 13.3. Схема распределения памяти для объекта производного класса Конструктор класса ellipse из списка инициализаторов непосредственно обращается к конструктору базового класса point. Класс ellipse наследует метод point::move() без изменений. Кроме того, он наследует и заменяет метод point::display() методом ellipse::display(). В теле метода ellipse::display() для вывода координат центра вызывается метод point::display(). Обратите внимание на необходимость префикса (квалификатора point::). Метод squaref) отсутствовал в классе point. С его помощью вычисляется площадь эллипса. Продемонстрируем возможности производного класса, следующим образом определив основную программу (Р13_02.срр): int main() { ellipse oval( 10,20,5.0,12.0); oval, displayf); // Метод класса ellipse oval. move( -10.0, -5); // Метод класса point oval.displayf); cout«"square="«oval.square(); return 0; }
Включение и наследование классов 405 Результат выполнения программы: Center: x=JO у=20 dmin=5 dmax= 12 Center: х=0 y=15 dmin=5 dmax= 12 square= 188.495 Обращение oval.square(), т.е. вызов для объекта класса ellipse его метода нас уже не удивит. Но обратите внимание, что для объекта oval производного класса непосредственно вызывается метод move() базового класса point, т.е. метод move() наследуется и доступен для объектов производного класса ellipse. В то же время следующая попытка обратиться к унаследованным из класса point полям данных х и у будет недопустима: cout«oval.x< < "\ t"«oval. у< < endI; // ошибка! Дело в том, что поля данных point::x, point::y имеют в базовом классе статус private. Они не доступны не только вне классов, но и внутри определения производного класса. Именно поэтому в методе ellipse::display() использовано обращение к открытому методу базового класса point::display(), и нет непосредственного обращения к координатам центра. Правила определения статуса доступа компонентов базового класса при наследовании достаточно сложные и будут подробно рассмотрены чуть позже. Итак, при наследовании: • производный класс беспрепятственно обращается к доступным для него полям данных и методам базового класса; • базовый класс не имеет доступа к полям данных и методам производного класса; • в объект производного класса включаются поля данных базового класса, т.е. можно считать, что в объект производного класса входит экземпляр объекта базового класса; • если в производном классе имя поля данных или метода базового класса использовано для других целей, то соответствующий компонент базового класса доступен в производном классе с помощью операции указания области видимости: имя базы:: имя компонента
406 Глава 13 Это правило иллюстрирует обращение point::display() в рассмотренном примере; • в производном классе у наследуемой функции базового класса может быть "разная судьба". Поясним последнее утверждение. Пусть в классе В определен метод f() и класс С является производным от В (рис. 13.4). Возможны три варианта. 1. Производный класс получает функцию B::f() без каких-либо изменений (функция point::move() в нашем примере). 2. Производный класс может заменить унаследованную функцию B::f() своей функцией C::f(). У функции класса С та же спецификация параметров, сигнатуры различаются только квалификаторами, например display!). В этом случае говорят, В: ■П) j i С Рис. 13.4. Наследование классов с методом в базовом классе что метод производного класса "экранирует" одноименный метод базового класса. 3. Производный класс может определить функцию с тем же именем, но с другой спецификацией параметров — имеет место перегрузка. В случаях, указанных в пунктах 2 и 3, алгоритм функции C::f() может быть абсолютно независим от алгоритма функции базового класса. Однако функция C::f() может дополнить действия функции B::f(). Для этого в теле функции С::f() вызывается функция B::f(). Именно так написана функция ellipse::display(), в теле которой вызывается функция point::display(). 13.3. Синтаксис наследования и доступность компонентов Сокращенный формат спецификации (определения) производного класса (полный синтаксис требует знакомства с шаблонами классов, которые мы еще не вводили, см. главу 16): ключ_класса имя_класса: список_спецификаторов_баз {поля_данных_и_методы_производного_класса}; где ключ_класса - одно из служебных слов: struct, class (обратите внимание на отсутствие union).
Включение и наследование классов 407 Спецификатор базы (в списке они разделены запятыми) может быть таким: 1. спецификатор_доступа полное_имя_кпасса 2. virtual спецификатор_доступа полное_имя_класса 3. спецификатор_доступа virtual полное_имя_класса В качестве необязательного спецификатора доступа используются: public, protected, private. полное _имя_класса - это или собственно имя базового класса, или имя шаблона классов. В обоих случаях класс или шаблон должен быть ранее полностью определен. О шаблонах классов речь пойдет позже, а пока будем в качестве базовых использовать обычные классы. Производный класс (еще раз обратите внимание, что он не может быть объединением union) "получает в наследство" поля данных и методы базового класса. Отметим, кстати, что производный класс не перемещает к себе наследуемые компоненты, они остаются в базовом классе. Как мы уже упомянули, можно считать, что в каждый объект производного класса входит безымянный объект базового класса со всеми своими полями данных и методами. При наследовании важную роль играет статус доступа компонентов базового класса и использованный в определении производного класса спецификатор. Прежде чем рассматривать правила определения доступности методов и полей данных производного класса, напомним, какую роль доступность компонентов играет в обычном определении класса. Если компоненты закрыты (private), то к ним можно обратиться только из определения класса или из дружественных ему функций и дружественных классов. Когда компоненты общедоступны* иначе говоря открыты (public), к ним можно дополнительно обратиться и через объекты класса, используемые вне его определения. Специально для наследования введен статус защищенных (protected) компонентов. Вне механизма наследования свойства защищенных компонентов совпадают со свойствами закрытых (private). Доступность компонентов класса для его методов и объектов можно проиллюстрировать схемой (см. Лафоре Р. [13], с. 367), приведенной на рис. 13.5.
408 Глава 13 Рис. 13.5. Разнодоступные компоненты класса. Стрелками показана доступность компонентов для объектов и методов класса На схеме соединительными линиями показана доступность компонентов внутри класса и извне для объекта класса. По существу это иллюстрация уже известного нам правила. Внутри класса (для его методов и дружественных функций) все компоненты доступны. Извне класса доступ разрешен только к открытым компонентам. Теперь рассмотрим правила, действующие при наследовании. На доступность унаследованного компонента влияют: статус доступа компонента в базовом классе, то, каким ключом класса (class или struct) вводится производный класс и какой спецификатор размещен перед базовым классом. Все правила доступа обобщает помещенная ниже таблица доступности (табл. 13.1) унаследованных компонентов базового класса. Таблица 13.1 Доступность унаследованных компонентов Доступность в базовом классе Спецификатор struct (ключ класса) class (ключ класса) public protected private отсутствует public protected недоступны 0 т ’ к Р private private недоступны 3 а ' к Р
Включение и наследование классов 409 Продолжение Доступность в базовом классе Спецификатор struct (ключ класса) class (ключ класса) public protected private public public protected недоступны 0 T " к p public protected недоступны 0 T > к p public protected private protected protected protected недоступны 3 a Щ protected protected недоступны 3 k a Щ public protected private private private private недоступны 3 a ' к P private private недоступны 3 a к P В таблице "недоступны" означает отсутствие доступа из методов производного класса к полям и методам базового и тем более их (компонентов базового класса) недоступность для объектов производного класса. Иногда ошибочно говорят, что эти компоненты не наследуются. Но наследование выполняется, ведь поля данных базового класса всегда входят в объекты производного класса. Пример к таблице: class В { private: long row; protected: int t; public: char u; }; class E: В {...};//t, и - private (закрытое наследование); struct S: В {...};// u - public; t - protected(открытое наследование); class F: protected В {...}; //t, u - protected, (защищенное наследование); class P: public В {...}; //1 - protected, u - public (открытое наследование); class 0: private B{...}; //t, u - private (закрытое наследование). В комментариях отмечено, какой статус доступа получают в производном классе поля int t и char и. Поле данных row недоступно во всех производных классах. Спецификатор доступа перед именем базового класса и ключ производного класса (c/ass, struct) совместно определяют открытое (откр.), закрытое (закр.) и защищенное (защ.) наследования (см. табл. 13.1). При отсутствии спецификатора для ключа
410 Глава 13 class и при наследовании со спецификатором private выполняется закрытое наследование. Спецификатор public и ключ struct при отсутствии спецификатора обеспечивают открытое наследование. Наличие спецификатора protected перед именем базового класса приводит к защищенному наследованию. В нашем примере класс ellipse создается с помощью открытого наследования класса point. Это обеспечено наличием спецификатора доступа public перед именем базового класса в определении (в спецификаторе базы public point) производного класса. Если убрать перед именем базового класса спецификатор доступа public, то даже открытые поля данных и методы базового класса после наследования получат статус private. Это, однако, не помешает обращению к ним непосредственно в определении производного класса. Например, для класса ellipse останутся верными инициализатор pointfxi, yi) в конструкторе и обращение point::display() в методе ellipse::display(). Но в основной программе обращение oval.movef 10.0,-5.0) станет ошибочным. Сообщение компилятора при такой ошибке: In function 'int main()':'void point::move(double, double)' is inaccessible within this context Закрытые (private) компоненты базового класса всегда остаются недоступными, как в определении производного класса, так и для его объектов. Например, как при наличии спецификатора public перед именем базового класса, так и при его отсутствии попытка заменить в методе ellipse::display() обращение point::display(); на оператор cout« "х= "< <х< < "\ ty= "< <у« endl; или оператор cout«"x="« point::х <<"\ty="« point::y«endl; приведет к таким сообщениям компилятора: In method void ellipse::display!)': 'double point::x' is private within this con- text double pointy is private within this context Прежде чем закончить разбор этого примера, проиллюстрируем возможности защищенных полей данных и методов классов. Если в определении класса point перед полями данных
Включение и наследование классов 411 double х и double у поместить спецификатор protected:, то эти компоненты получат статус защищенных и будут доступны даже при закрытом наследовании в любом производном классе. В этом случае появление оператора cout«"x="«x«"\ty="«y«endl; в теле метода ellipse::display() будет допустимо. (Попытки обратиться к защищенным компонентам х и у вне определения производного класса недопустимы, они всегда наследуются как закрытые или защищенные.) Лафоре (см. [13], с. 385) приводит следующую схему (рис. 13.6), иллюстрирующую различие между открытым и закрытым наследованиями. class А Рис. 13.6. Доступность компонентов при наследовании классов При открытом наследовании объект (objB на рис. 13.6) производного класса имеет доступ к открытым полям данных и методам как своего, так и базового классов. При закрытом наследовании (objC на рис. 13.6) объекту доступны только открытые компоненты производного класса. В обоих случаях из методов
каж412 Глава 13 дого производного класса доступны открытые и защищенные компоненты базового класса. При закрытом или защищенном наследовании любые открытые методы и поля данных базового класса можно выборочно сделать открытыми и в производном классе (для его объектов). Для этого в открытой секции производного класса нужно явно следующим образом указать, что соответствующие имена (компонентов) относятся к пространству имен базового класса: public: using имя_базы::имя_компонента; В нашем примере такое объявление в классе ellipse: public: usingpointr.move; позволит использовать метод point::move() с объектами производного класса ellipse при закрытом наследовании (например, при отсутствии спецификатора public перед именем базового класса). Так как каждый класс может иметь производный класс, и этот класс, в свою очередь, может порождать другой производный класс, то формируются цепочки наследования или иерархии классов. Иерархию наследования изображают с помощью ациклического графа, причем каждый производный класс принято помещать ниже базовых. В примере на рис. 13.7 класс С — базовый для D и производный от В. Для класса D базовыми являются и класс С, и класс В. Класс С есть прямой базовый класс для D, а класс В называют косвенным, или не прямым (indirect) базовым Рис. 13.7. Иерархия наследования
Включение и наследование классов 413 классом для D. Класс называется прямым базовым классом, если он входит в список базовых при определении класса. Тексты базовых- классов размещаются в программе раньше, чем тексты производных классов. Для приведенной на рис. 13.7 схемы последовательность спецификаций классов должна быть такой: class В class С :public class D:public С 13.4. Множественное наследование и виртуальные классы На рис. 13.7 изображено линейное, или простое, наследование, когда для каждого производного класса есть только один базовый. Но в языке Си++ класс может иметь несколько прямых базовых классов. Наличие нескольких прямых базовых классов называют множественным наследованием. Пример (рис. 13.8): class Х1 { }; class Х2 { }; class ХЗ { }; class Y1: public Х1, public Х2, public ХЗ { }; Рис. 13.8. Схема множественного наследования При множественном наследовании никакой класс не может больше одного раза использоваться в качестве прямого базового. Однако класс может больше одного раза быть непрямым базовым классом. Пример такого дублирования базового класса приведен на рис. 13.9:
414 Глава 13 classX {...long double ax;...}; class Y: public X {double ay;}; class Z: public X {...intaz;}; class D: public Y, public Z {...}; Рис. 13.9. Дублирование непрямого базового класса Размеры объектов классов при дублировании базового: sizeoffX) = 12 (long double) sizeoffY) = 20 (long doublet-double) sizeof(Z)=16(long double+int) sizeof(D)=36(long double+long double+double+int) Размеры объектов подтверждают двукратное вхождение поля данных класса X в объект класса D. Если мы хотим обратиться к полю данных ах класса X из D, то должны указать, "по какому пути" это сделать, используя полностью квалифицированные имена: D::Y::X::ax или D::Z::X::ax В данном примере поля данных класса X дважды входят в каждый объект производного класса D. Это не всегда нужно. Чтобы устранить такое "удваивание" полей данных базового класса в объектах производного, достаточно указать, что класс наследуется как виртуальный. Для этого служит служебное слово virtual, помещаемое в спецификатор базы. Пример (соответствующий ациклический граф приведен на рис 13.10):
Включение и наследование классов 415 class X {long double ах;}; class Y: virtual public X {double ay;}; class Z': virtual public X {int az}; class D: public Y, public Z { }; Рис. 13.10. Виртуальное наследование класса A" Для реализации виртуального наследования в производный класс компилятор незаметно для программиста включает в качестве поля данных указатель на виртуальный базовый класс. Это можно обнаружить, если определить размеры участков памяти, выделяемых для объектов классов с виртуальным базовым. В нашем примере: sizeof(X)= 12 (long double) sizeof(Y)=24(long double + double + x*) slzeof(Z)=20(long double + int + x*) sizeoff D)=32(long double + double + int + x* +x*J Итак, класс, производный от виртуального, включает (рис. 13.1): • объект (данные) базового класса; • указатель на объект базового класса; • данные производного класса Виртуальность класса является не свойством класса, а результатом организации наследования. Один и тот же непрямой базовый класс может быть включен в иерархию наследования одновременно и как виртуальный, и как не виртуальный класс.
416 Глава 13 Без виртуальности При виртуальности класса X класса X long double ax long double ax double ay X* long double ax double ay int az X* i i i i i i i i i Int az Поля данных Поля данных объекта класса D объекта класса D Рис. 13.11. Схемы памяти, выделенной для объектов производных классов D Рассмотрим следующий пример с множественным наследованием (рис. 13.12): classX {...long double ах;}; class A: virtual public X {double aa;}; class В: virtual public X {int ab;}; class C: public X {long double ac;}; class F: public A, public B, public C { }; Рис. 13.12. Виртуальное и не виртуальное наследования одного класса Размеры объектов классов: sizeof(X)=12 sizeof(A)=24 (12+8+4) sizeof(B)=20( 12+4+4)
Включение и наследование классов 417 sizeof(C)=24(12+12) sizeof(F)=56 (12+4+8+4+4+12+12) Здесь F - производный класс от трех базовых. Класс X многократно наследуется. Однако при формировании объекта класса F в него входят не три, а два экземпляра полей данных класса X, так как для классов А и В класс X является виртуальным базовым. ах (12 байт) х* (4 байта) аа (4 байта) х* (4 байта) аЬ (4 байта) ах (12 байт) ас (12 байт) Рис. 13.13. Схема участка памяти, выделенного для объекта класса F 13.5. Локальные классы Локальные классы — это классы, определенные внутри блока (например, внутри функции, внутри другого класса). Локализация класса предполагает недоступность его компонентов вне области определения. В локальном классе разрешено использовать статические данные из охватывающего класс блока и внешние для этого блока: данные и функции элементы перечислений. Перечислим ограничения и особенности локальных классов. • Локальный класс не может иметь статических компонентов. В нем запрещено использовать переменные (объекты) автоматической памяти, определенные в объемлющем (охватывающем класс) блоке. Методы локального класса могут быть определены только внутри его спецификации. Тем самым они создаются только как подставляемые, встроенные (inline) функции. • Функция, включающая определение класса, не имеет особых прав доступа к его компонентам. Здесь действуют общие правила локализации объектов. 27 -2762
418 Глава 13 Для иллюстрации правил использования в локальном классе внешних для него объектов и функций приведем программу: //Р13_03.срр. - Локализация класса #include <iostream> using namespace std; int g() { return 22;} int x; void f() { static int s = 37; intx = 0; struct local { //intg() { return x; } int g() { return s; } int h() { return s; } int k() { return ::x; } int l() { return g(); } int e() { return ::g(); } }; // Внешняя функция // Глобальная переменная //Локальная статическая переменная //Локальная автоматическая переменная //Локальный класс // Ошибка: х в автоматической памяти // ОК - метод класса // ОК - обращение к статической s // ОК - обращение к глобальной х // ОК - обращение к методу // ОК - обращение к внешней функции local array[8]; OK - массив объектов локального класса cout< <аггау[2]. e()«'\t'< <аггау[2]. !()< <endl; /А- } //local* р = 0; // Ошибка: local вне видимости int main() { Ц); return 0; } Результаты выполнения программы: 22 37 Программа составлена на основе примера из Стандарта языка Си++ [4] и является сугубо иллюстративной. Ее назначение - показать правильные и ошибочные обращения к компонентам локального класса и из его методов — к внешним объектам.
Глава 14 СПЕЦИАЛЬНЫЕ МЕТОДЫ КЛАССОВ И ПЕРЕГРУЗКА ОПЕРАЦИЙ ПРИ НАСЛЕДОВАНИИ 14.1. Методы при наследовании классов Задумаемся над тем, как при наследовании в производном классе "появляются" методы. С данными проще — поля данных базового класса входят в каждый объект производного класса. Их доступность для объектов и методов производного класса определяется довольно сложными правилами, которые уже рассмотрены выше. В свою очередь, методы производного класса могут быть: • унаследованы; • написаны программистом; • созданы компилятором независимо от действий программиста. Как и для данных базового класса, для методов действуют правила их доступности в производном классе, и можно считать, что эти правила мы понимаем. Когда в производный класс программист добавляет методы, то они могут быть совершенно независимы от методов базового класса, могут их экранировать, могут перегружать методы базового класса, что уже нами обсуждалось. В зависимости от того, какие методы явно определил в производном классе программист, компилятор может добавить (или не добавить) в определение класса тот или иной метод, отнесенный синтаксисом языка к специальным. Такое неявное добавление методов существенно влияет на функциональность производного класса и возможности его взаимодействия с объектами и методами его базового класса. Рассмотрим эти вопросы подробнее. Стандарт языка Си++ особо выделяет и относит к специальным методам класса: 27*
420 Глава 14 • конструктор умолчания; • конструктор копирования; • деструктор; • операцию-функцию присваивания. Если программист явно не определяет эти функции, то компилятор создает их автоматически. (О затруднениях, которые при этом возникают для классов ресурсоемких объектов, уже говорилось.) Специальные методы классов влияют на создание, копирование и уничтожение объектов. Очень часто указанные функции создаются, наследуются и вызываются неявно, но при наследовании в производном классе в ряде случаев необходимо обращаться явно даже к неявно определенным специальным методам базового класса. Прежде чем разбирать особенности оформления и применения в производных классах специальных методов, отметим, что конструкторы и операция присваивания не наследуются. Другими словами, если в производном классе программистом не определена операция присваивания, то компилятор создаст ее автоматически (по своему усмотрению). Если нет конструкторов, то конструктор умолчания и конструктор копирования в производном классе будут определены компилятором. 14.2. Присваивание при наследовании Приведем совершенно безобидный пример. Пусть следующим образом определены базовый и производный классы: struct BAS {int b; }; struct DIR: BAS { double d;}; В обоих классах присутствуют неявно определенные конструкторы, деструкторы и операции-функции operator=(). В основной программе определим объекты производного класса и выполним несколько действий (Р14_01.срр): int main() { DIR one, two; // Конструктор умолчания DIR() one.b = 12; one.d = 8.8; two = one; // Выполняется DIR::operator=()
Специальные методы классов и перегрузка операций 421 cout«"two.b = ''«two.b«"\ttwo.d = "<<two.d«endl; return 0; } Результаты выполнения программы: two.b = 12 two.d = 8.8 Для создания объектов one, two производного класса DIR использованы конструкторы умолчания обоих классов. (Из конструктора производного класса выполняется обращение к конструктору базового класса.) При выполнении оператора two = one; вызваны операции-функции присваивания, неявно определенные в обоих классах. В данном примере все корректно и никаких трудностей не возникает. При выполнении присваивания из метода DIR::operator=() был неявно вызван метод BAS::operator=(), из конструктора DIR() осуществлено обращение к конструктору BAS(). Эти действия компилятор запланировал без участия программиста. Особенности, а иногда и затруднения появляются в тех случаях, когда в производном классе нужно явно определить те или иные специальные методы. (Например, это необходимо в тех случаях, когда производный класс определяет ресурсоемкие объекты либо когда значения полей данных производного класса зависят от значений полей данных базового класса.) Дело в том, что при явном определении метода производного класса нельзя надеяться, что компилятор автоматически и корректно выполнит обращение к соответствующему методу базового класса. Нужный метод базового класса (даже при отсутствии его явного определения) необходимо явно вызывать из соответствующего метода производного класса. В производном классе нашего примера явное определение операции-функции для перегрузки операции присваивания может быть таким (Р14_02.срр): DIR <5 operator^const DIR & х) { if (this == &x) return *this; BAS::operator=(dynamic_cast<const BAS &>(x)); this ->d = x.d; return *this;} };
422 Глава 14 Параметр операции-функции — константная ссылка на объект производного класса. В случае присваивания «самому себе» никаких действий не требуется, возвращается ссылка на объект- операнд (* this). В противном случае выполняется явное обращение к операции-функции присваивания базового класса BAS::operator=(). Аргументом служит параметр операции- функции производного класса, приведенный к значению ссылки на объект базового класса. Обратите внимание, что выполнено обращение к функции, которая в базовом классе явно не определена. Если в функции DIR::operator=() не будет явного обращения к BAS::operator=(), получим неверный результат (14_02.срр) приблизительно (в зависимости от реализации) такого вида: two.b = 20757 two.d = 8.8. (В этом примере обращение к функции BAS::operator=() можно заменить в функции DIR::operator=() оператором присваивания Ь=х.Ь; но в более сложных случаях при перегрузке операции присваивания производного класса без явного вызова операции- функции присваивания базового класса не обойтись.) В рассмотренном примере оператор two = one; присваивает объекту производного класса значение другого объекта того же класса. Рассмотрим особенности присваивания разнотипных объектов, находящихся в отношении наследования. Разумно и безопасно применять операцию присваивания в том случае, когда значение правого операнда без потерь приводится к типу левого операнда. Когда же это требование удовлетворяется и что нужно сделать, чтобы удовлетворить ему? Присваивание объекту базового класса значения объекта производного класса допустимо, но опасно. Неявно созданная компилятором в базовом классе операция присваивания «ничего не знает» о том, что нужно делать с полями данных, добавленными в объект производного класса. Например, в функцию mainQ приведенного выше примера можно добавить такие строки: В AS bas; // Конструктор умолчания BAS() bas = two; //Можно, но зачем? После такого присваивания (независимо от наличия или отсутствия функции DIR::operator=()) поле данных bas.b получит
Специальные методы классов и перегрузка операций 423 значение two.b. Что делать со значением поля данных two.d, может быть известно только программисту, применившему такой оператор. Если он это планирует, то ему необходимо в базовом классе явно определить метод BAS::operator=(const DIR &), который правильно "распорядится" с данными производного класса. Нам нет смысла это рассматривать. Присваивание объекту производного класса значения объекта базового класса допустимо только в том случае, если в производном классе программист явно определил подходящие средства. Если этого нет, то компилятор не может найти соответствующую операцию-функцию для выполнения присваивания и выдает сообщение об ошибке. Если ничего не изменять в определениях наших классов, то ошибочен оператор one = bas. Рассмотрим средства, которые можно использовать, чтобы разрешить присваивание объекту производного класса значения объекта базового класса. Проблема состоит в том, что в объекте производного класса (в общем случае) есть поля данных, которые отсутствуют в объекте базового класса. Какие значения должны получить эти поля данных, знает только прогоаммист-разра- ботчик. Он может добавить в производный класс такой метод (Р14_03.срр): DIR & operator=(const BAS & х) { BAS::operator=(x); this ->d = 3.14159; return *this; } В этом случае после присваивания one = bas; поле данных one.b получит значение bas.b, a one.d будет присвоено значение 3.14159. Вторая возможность сделать присваивания one = bas допустимым связана с применением операции приведения типов. Однако и в этом случае придется вносить изменения в производный класс. Дело в том, что если просто использовать любую операцию приведения типов, то ни одна из них по умолчанию "не знает" какие значения нужно присваивать полям данных производного класса. Например, при выполнении оператора one = static_cast<const DIR &> (bas);
424 Глава 14 не известно, что присвоит d. Получим, например, такие значения: опе.Ь = 12 one.d = 1.4447е-309. Выполнив классическое приведение типов (с помощью операции, унаследованной из языка Си) one = (DIR)bas; получим сообщение об ошибке: по matching function for call to DIR::DIR(BAS &) Компилятор в этом случае не нашел конструктора приведения типов. Чтобы "работало” приведение типов, необходимо добавить в производный класс конструктор с заголовком DIR::DIR (BAS &). В этом конструкторе программист может указать, как определить значения полей данных, добавленных в производный класс при наследовании. Рассмотрим роль конструкторов подробнее. 14.3. Конструкторы при наследовании При создании объекта производного класса из его конструктора всегда явно или неявно вызывается конструктор базового класса. При этом соблюдается одно условие — конструктор базового класса должен быть вызван и выполнен до исполнения операторов тела конструктора производного класса. Конструктору базового класса должны при необходимости передаваться параметры. В приведенном в предыдущей главе примере конструктор класса «эллипс» обращается из списка инициализации к конструктору класса «точка»: ellipse (double xi, double yi, double din, double dax): pointfxi, yi), dmin(din), dmax(dax) {} В этом примере параметры, необходимые конструктору базового класса, получены из списка параметров производного класса. При такой явной схеме вызова конструктора базового класса наглядно видно, что объект базового класса создается как компонент производного класса. Входящий в производный класс неименованный объект класса point при инициализации
«равнопраСпециальные методы классов и перегрузка операций 425 вен» с явно введенными в класс ellipse компонентами drnin, dmax. Конструктор производного класса не имеет права непосредственно инициализировать поля данных базового класса даже в тех случаях, когда они унаследованы как открытые. Таким образом, для рассмотренных выше классов DIR и BAS недопустимо такое определение конструктора: struct DIR: BAS { double d; DIR(intbif double di):b(bi), d(di) {} //ошибка - явно обратились }; Правильный вариант конструктора: DIRfintbi, double di):BAS(bi), d(di) {} //верно - вызвали конструктор Приведем последовательность действий при создании объекта производного класса. 1. Выделяется участок памяти для объекта производного класса, т.е. достаточный для размещения полей данных и базового и производного классов. 2. Выполняется конструктор базового класса. Тем самым часть выделенной памяти превращается в объект базового класса, и этот объект инициализируется. 3. Конструктор производного класса инициализирует фрагмент памяти, отведенный для полей данных, добавленных производным классом. Как всегда, и в базовом, и в производном классах присутствуют по несколько конструкторов (некоторые определены неявно). Выбор конструктора производного класса определяется контекстом, в котором создается объект производного класса. Какой при этом из конструкторов базового класса будет вызван, зависит от того, как построен используемый конструктор производного класса. Если в инициализаторе вызванного конструктора производного класса нет явного обращения к конструктору базового класса, то неявно вызывается конструктор умолчания базового класса. (Если конструктора умолчания в базовом классе нет, то компиляция завершается аварийно.) Приведем еще раз формат конструктора и обсудим особенности его элементов при наследовании:
426 Глава 14 имя_класса (спецификация параметров): инициализатор 1,.... инициализаторК {операторы тела конструктора} Напомним, что размещенный после списка параметров список инициализаторов называется инициализатором конструктора. Его задача — инициализация прямых и виртуальных базовых объектов и нестатических полей данных класса. Стандартом определена следующая последовательность выполнения конструктора производного класса: 1. Инициализируются виртуальные базовые классы (их объекты) в том порядке, как они размещены в списке спецификаторов баз производного класса (а не в инициализаторе конструктора). 2. Инициализируются прямые базовые классы (их объекты) в том порядке, как они размещены в списке спецификаторов баз. 3. Инициализируются нестатические поля данных производного класса в том порядке, как они размещены в тексте определения класса (а не в инициализаторе конструктора). 4. В заключение выполняется тело конструктора производного класса. Итак, в инициализаторе конструктора производного класс необходимо предусмотреть вызов конструктора базового класса. Если этого не предусмотреть, то по умолчанию будет вызван конструктор умолчания базового класса. (Напоминаем: такой конструктор создается автоматически только в том случае, если в классе НЕТ ДРУГИХ КОНСТРУКТОРОВ!) Следует помнить, что даже в том случае, когда в теле конструктора производного класса есть обращение (по любому поводу) к конструктору базового класса, сначала будет выполнено явно заданное или явно не заданное обращение к конструктору базового класса из инициализатора конструктора производного класса. Конструктор любого класса по умолчанию создается как открытый (public), именно таким его следует делать и при явном определении. Для полноты картины, но не в качестве рекомендации, отметим, что конструктор базового класса можно сделать защищенным (protected). Но при этом к нему можно будет обращаться извне только из инициализатора конструктора производного класса.
Специальные методы классов и перегрузка операций 427 В цепочках наследования конструкторы выполняются, начиная от базового класса. Отметим, что хотя конструктор базового класса выполняется раньше окончания выполнения конструктора производного класса, но аргументы он получает из вызова конструктора производного класса. О правильности задания аргументов должен позаботиться программист. Вернемся к нашему примеру с классами базовым BAS и производным DIR. Покажем, что с помощью конструктора приведения типов можно обеспечить нужное программисту присваивание объекту производного класса значения объекта базового класса. Текст программы: //Р14_04.срр - Присваивание и конструкторы при наследовании #include <iostream> using namespace std; struct BAS{ int b; }; struct DIR: BAS { double d; DIR(const BAS & bi): BAS(bi), d(2.71828) {} }; int main() { BAS bas; // Конструктор умолчания BAS() DIR onefbas); // Конструктор приведения типов bas.b = 10; one.d=0.0; one = bas; // Правильно при наличии DIR(const BAS & bi) cout«f,one.b - ,'«one.b«,,\tone.d = "<<one.d«endl; return 0; } Результаты выполнения программы: one.b= 10 one. d = 2.71828 В классе DIR явно определен конструктор приведения типов: DIR(const BAS & bi): BAS(bi), d(2.71828) { } Его применение для определения в основной программе объекта DIR one(bas) не представляет интереса (нет ничего нового). Важно то, что этот конструктор неявно вызывается при
выполне428 Глава 14 нии операции присваивания one = bas. Здесь слева объект класса DIR, а справа — объект класса BAS. Именно поэтому поле one.d принимает значение 2.71828, хотя до этого ему явно был присвоен нуль. 14.4. Деструкторы при наследовании Недопустимо, чтобы деструктор базового класса был закрытым. Он может быть только открытым или защищенным. Деструктор производного класса выполняется раньше деструктора базового класса и автоматически вызывает деструктор базового класса. Таким образом, уничтожение объектов происходит в обратном порядке по сравнению с их созданием. Сначала уничтожаются компоненты, добавленные при наследовании, затем включенный объект базового класса. Таким образом, деструкторы в цепочке наследования выполняются, начиная от последнего производного класса по направлению к базовому. При множественном наследовании деструкторы базовых классов вызываются в обратном порядке по отношению к перечислению базовых классов в списке спецификаторов баз производного класса. 14.5. Перегрузка операций при наследовании Как мы уже говорили, перегрузку операций можно выполнять с помощью методов класса, с помощью вспомогательных (свободных) функций и с помощью дружественных классу функций. Рассмотрим, какие особенности появляются при перегрузке операций для классов, находящихся в отношении наследования. Известно, что перегрузку операций ввода-вывода нельзя выполнить с помощью методов класса. Часто эти операции-функции вводятся как дружественные. Проиллюстрируем примером (Р14_05.срр) особенности обращений из дружественных функций производного класса к дружественным функциям базового класса (рис. 14.1).
Специальные методы классов и перегрузка операций 429 Наследование классов Объект производного класса BAS DIR Intk double z Рис. 14.1. Схемы для примера с перегрузкой операций ввода-вывода Базовый класс: class BAS { protected: int к; friend istream & operator »(istream & in, BAS & b); friend ostream & operator «(ostream & ou, const BAS & b); }; Дружественные для класса BAS функции ввода-вывода: istream & operator »(istream & in, BAS & b) { cout« "k = "; in » b.k; return in; } ostream & operator « (ostream & ou, const BAS & b) { ou « "k = ” << b.k « endl; return ou; } Производный класс: class DIR: BAS { double z; friend istream & operator » (istream & in, DIR & d); friend ostream & operator «(ostream & ou, const DIR & d); }; Дружественные для класса DIR функции ввода-вывода: istream & operator »(istream & in, DIR &d) { cout«" BAS: "; operator»(in, dynamic_cast<BAS&>(d));
430 Глава 14 cout« "D/Я;: z = in » d.z; return in; } ostream & operator «(ostream & ou, const DIR & d) { о и << "BAS:: ou«dynamic_cast<const BAS &>(d); ou « "DIR:: z = "<< d.z « endl; return ou; } Прежде чем комментировать особенности операций-функций базового и производного классов, приведем программу, в которой они используются, и результаты ее выполнения: int main() { DIR one, two; cin»one; two = one; cout«two; return 0; } Результаты выполнения программы: BAS: к = 95<ENTER> DIR: z-3.14159<ENTER> BAS:: к = 95 DIR:: z = 3.14159 Пользователь вводит значения полей данных объекта one производного класса, и после присваивания two = one выводятся значения полей данных объекта two. И ввод, и вывод не вызывают ни разочарования, ни удивления. В обеих операциях-функциях производного класса выполняется обращение к соответствующим операциям-функциям базового класса. В теле операции-функции operator>>() производного класса DIR обращение operator»(in, dynamic_cast<BAS &>(d)); обеспечивает ввод значений полей данных только базовой части объекта DIR d. Затем выполняется ввод значения поля данных z, принадлежащего объекту производного класса. Приведение
Специальные методы классов и перегрузка операций 431 dynamic_cast<BAS &>(d) выделяет из объекта производного класса базовую часть. Очень важна роль приведения именно ссылки на объект производного класса DIR & к ссылке на объект базового класса BAS&. Попытка приводить сами объекты приведет к неудаче. Объяснение следующее. Если использовать выражение (BAS)d, то это эквивалентно обращению к конструктору копирования BAS(d). Его выполнение приведет к созданию копии объекта d, которая станет аргументом функции operator>>(). Затем в теле этой операции присваивание будет выполнено полю данных int к, принадлежащему копии, что никак не изменит базовую часть производного объекта. В теле операции-функции operator«() производного класса DIR оператор ou«dynamic_cast<const BAS &>(d); обеспечивает вывод только базовой части производного объекта. Вывод значения поля double z производного класса выполняется явным образом. Как показывают примеры, при наследовании приведение типов играет особую роль. Поясним возможности разных способов приведения типов при наследовании. Для явного приведения типов можно использовать, во-первых, операцию, унаследованную из языка Си. Ее применение часто называют прямым приведением типов (имя_типа) выражение Второй вариант — явное обращение к конструктору приведения типов имя_класса (выражение) Обе операции эквивалентны — в обоих случаях выполняется обращение к конструктору приведения типов того класса, имя которого используется. Для приведения указателей и ссылок лучше использовать специальные операции языка Си++: static_cast< целевой тип > (выражение) dynamic_cast < целевой тип > (выражение), где выражение — это ссылка или указатель. Отметим, что операция static_cast эквивалентна операции прямого приведения типов из языка Си. Обе операции
приведе432 Глава 14 ния не анализирует приводимый объект, за корректностью преобразования должен следить программист. Б. Страуструп [2] отмечает, что при использовании static_cast не выполняется никаких проверок во время исполнения программы и тем самым возможны неверные преобразования. Однако static_cast всегда сформирует новое (возможно, неверное) значение. Операция dynamic_cast объединяет приведение типа и проверку допустимости этого преобразования во время исполнения. При приведении указателей выражение dynamic_cast <Т*> (р) преобразует операнд р к нужному типу Т*, если только это возможно. В противном случае будет возвращено значение 0. При приведении ссылок выражение dynamic_cast <Г&> (г) операнд г будет приведен к типу Г&. Если приведение к этому ссылочному типу заканчивается неудачно, возникает исключение bad_cast. В языке Си++ есть еще две операции приведения типов: const_cast <целевой тип> (выражение) reinterpret_cast <целевой тип> (выражение) Первая из них выполняет для значения выражения "отмену" таких атрибутов, как const и volatile. Тип, к которому выполняется приведение, должен быть таким же, как и тип выражения, за исключением отсутствия отменяемого атрибута. Следует отметить, что const нельзя снять никакой из операций dynamic_cast, static_cast, reinterpret_cast. Поэтому операция constjcast чаще всего применяется именно для отмены константности выражения. Операция reinterpretjcast позволяет выполнить преобразование для типов, несовместимых по наследованию. 14.6. Принцип подстановки и его реализация на языке Си++ В теории объектно-ориентированного подхода наследование иногда определяют как возможность доступа представителей производного класса к данным и методам базового класса. В языках программирования наследование означает, что поведение (методы) и состояние (данные) объектов производного класса являются расширением свойств (методов и данных) ба-
Специальные методы классов и перегрузка операций 433 зового класса. Производный класс имеет свойства базового класса и, кроме того, при наследовании получает дополнительные свойства. В то же время, поскольку производный класс является более специализированной формой (частным случаем) базового класса, то он в некотором смысле является сужением базового класса. Эта двойственность отношений между базовым и производным классами (расширение и сужение одновременно) должна аккуратно учитываться при рассмотрении проблем наследования. Вторая трудность понимания и применения наследования связана с тем, что производные классы могут переопределять поведение, унаследованное от базового класса. Идеализированное описание наследования в языках программирования предполагает справедливость следующих утверждений: • объекты производного класса должны включать все данные базового класса; • объекты производного класса должны обладать поведением объектов базового класса (если нет прямого переопределения методов базового класса); • объект производного класса должен быть неотличим от объекта базового класса в сходных ситуациях. Формализация приведенного описания наследования привела к сформулированному Лисковым (см. [19]) принципу подстановки. В соответствии с ним каждый объект производного класса может быть использован в любой ситуации вместо объекта базового класса. Принцип подстановки и перечисленные утверждения, относящиеся к описанию наследования, в конкретных реализациях наследования верны далеко не всегда. Формы наследования (с одной из их классификаций можно познакомиться по работе Тимоти Бадда [19]) весьма разнообразны, и только в очень частных случаях наследование соответствует идеальному определению. При программировании с применением наследования на языке Си++ чаще всего различают только две формы наследования. Обозначаются они терминами "является" (is а) и "содержит” или "реализован посредством" (has а). (Заметим, что в начале этой главы мы уже использовали это разграничение отношений между классами. Там отношение "содержит" было реализовано как явное включение объектов одного класса в объекты другого класса. От- 2g-2762
434 Глава 14 ношение "является" мы отнесли тогда к собственно наследованию. Там же было отмечено, что язык Си++ позволяет использовать механизм наследования классов для представления любого из этих отношений.) Для формы "является" в Си++ используется открытое наследование, для формы "содержит" — закрытое. При открытом наследовании иногда можно добиться выполнения принципа подстановки. При закрытом наследовании он обычно нарушен полностью. Рассмотрим примеры, на которых продемонстрируем нарушение и выполнение принципа подстановки. Введем базовый класс "окружность на плоскости" с полями данных: радиус (double rad), координаты центра (Int х, у), длина окружности (double ten). Методы: конструкторы, move( ) — для перемещения (смещения) центра, compressf ) — для изменения радиуса в заданное число раз. Как дружественную определим операцию-функцию operator«( ) для вывода сведений об окружности. Определение (спецификация) класса: class circle { protected: double rad, len; intxc, yc; public: circle (double ri=0.0,int xi=0, int yi=0) : rad(ri), xc(xi), yc(yi), len(2*3.14159*ri) {} void movefint dx, int dy) { xc+=dx; yc+=dy;) circle compressf double k){ rad *= k; len *= k; return *this; } friend ostream & operator «(ostream & out, const circle & cir); }; Внешнее определение операции-функции для перегрузки операции вывода: ostream & operator «(ostream & out, const circle & cir) { out« "rad =”« cir. rad « ", len = "« cir. len «", xc = "<<cir.xc«", yc = "<<cir.yc« endl; return out; }
Специальные методы классов и перегрузка операций 435 Так как мы заранее планируем использовать класс circle в качестве базового, то его поля данных определены как защищенные (protected), а методы, конечно, открытые. Метод compressf) возвращает значение объекта класса circle. На базе класса circle определим производный класс "круг". Новые компоненты: площадь круга (double sqr) и дружественная функция для перегрузки операции вывода сведений о круге. Попытаемся так определить производный класс, чтобы добиться выполнения принципа подстановки, т. е. требуется, чтобы к объектам производного класса можно было бы применять методы базового класса, и результат их применения был адекватен решаемой каждым методом задаче. Метод move( ) изменяет положение центра окружности, и с его помощью можно безболезненно перемещать центр круга — проблем не видно. Сложнее с методом compressf). Дело в том, что в производном классе имеется поле данных (площадь круга), значение которого зависит от радиуса круга. Радиус круг наследует от окружности. Если применить к объекту класса "круг" метод circle::compress(), то этот метод изменит радиус окружности (и круга), но никак не повлияет на площадь круга. Это совершенно естественно, ведь автор класса circle ничего не знает о том, какие классы будут построены на основе circle. Можно построить прямой цилиндр, конус, в основании которого окружность, и т.д. Этот пример иллюстрирует возможность зависимости данных производного класса от данных базового класса. В таких случаях необходимо при обращении объекта производного класса к методу базового класса изменять данные производного класса. Это сделать метод базового класса не может. Следовательно, необходимо, чтобы в производном классе метод был определен по-своему и перегрузил метод базового класса. С учетом сказанного так определим производный класс "круг": class disk: public circle { double sqr; public: disk (double ri-O, intxi=0, intyi=0) : circle(ri,xi,yi), sqr(3.14159*ri*ri) {} disk (const circle & cir): circle(cir) { sqr = 3.14159*rad*rad; } 28*
436 Глава 14 disk compressf double к) { circle::compress(k); sqr *= k*k; return * this; } friend ostream & operator «(ostream & out, const disk & dis); Внешнее определение операции-функции для перегрузки операции вывода: ostream & operator «(ostream & out, const disk & dis) { out« dynamic_cast<const circle &>(dis); out «''sqr = ,,«dis.sqr«endl; return out; } В производном классе явно определены два конструктора, и конструктор копирования добавлен компилятором. Конструктор общего вида в комментариях не нуждается. Конструктор приведения типов disk(const circle & cir) обеспечивает присваивание объекту производного класса значения объекта базового класса. В теле конструктора явно вычисляется площадь круга по значению радиуса окружности, представленной параметром. Метод disk::compress(double к) явно вызывает метод circle::compress(k). Тем самым изменяется значение радиуса в базовом объекте. Затем явным оператором присваивания изменяется значение поля данных sqr производного класса. Возвращаемое значение метода - значение объекта производного класса. Особенности перегрузки операций вывода (<<) при наследовании мы уже обсуждали выше. Применение введенных классов иллюстрирует следующая программа (Р14_06.срр): intmain(){ disk опе(0.1), two; cout« "one: "« one; one.move(10,5); cout«"one.move( 10,5): "«one;
Специальные методы классов и перегрузка операций 437 two = one.compressf 100); cout« "two:"« two; cout« "one.compressf 10):"« one.compress( 10); return 0; } В программе создаются два объекта one, two производного класса. Оператор one.move( 10,5); для объекта производного класса вызывает метод базового класса. Затем для объектов one и two последовательно вызывается метод производного класса compressf). Внешне это неотличимо от обращения к соответствующему методу базового класса, так что принцип подстановки соблюден. Результаты выполнения программы: one: rad = 0.1, len = 0.628318, хс = 0, ус = О sqr = 0.0314159 one.movef 10,5): rad = 0.1, len = 0.628318, хс = 10, ус = 5 sqr = 0.0314159 two: rad = 10, len = 62.8318, xc= 10,yc = 5 sqr = 314.159 one.compressf 10): rad = 100, len = 628.318, xc = 10, yc = 5 sqr = 31415.9 14.7. Наследование и ресурсоемкие классы Рассмотрим теперь особенности закрытого наследования. При нем каждый производный класс использует поля данных базового класса, но переопределяет или заменяет его методы. Напомним еще раз, что если в производном классе явно определяется операция-функция присваивания (или конструктор копирования), то нужно не забывать о необходимости выполнить присваивание (или копирование) полей данных базового класса. Проще всего при этом явно обращаться к операции-функции присваивания базового класса (вызывать конструктор копирования базового класса). Делать это можно и в тех случаях, когда соответствующие методы определены в классе неявно (по умолчанию). Без этого никак не обойтись, когда поля данных базового класса закрытые.
438 Глава 14 Некоторые особенности возникают, если базовый и/или производный класс определяет ресурсоемкие объекты. Рассмотрим эту проблему на следующем примере. Известно, что в языке Си++ индексы элементов массивов могут принимать значения, только начиная с нуля. Пусть нужно ввести класс для представления массивов переменных размеров с произвольными границами, например, как это было в языке Алгол 60, т.е. для каждого объекта класса (для конкретного массива) необходимо задавать предельные значения его индекса [min, max], и иметь возможность обращаться к элементам, указывая значение индекса из этих пределов. Рассмотрим два варианта решения этой задачи. В первом случае базовый класс будет определять ресурсоемкие объекты. Во втором варианте их будет определять производный класс. Вначале спроектируем нужный нам класс как производный от класса "массив изменяемой длины". Базовый класс (Р14_07.срр): class base { //Базовый класс - массив изменяемой длины protected: int sizeB; // Размер массива double * data; // Указатель на элементы массива public: // Конструктор умолчания и общего вида: basefint size= 1): sizeB(size) {data = new double[sizeB];} // Конструктор копирования: base(const base & b): sizeBfb. sizeB) { data = new doublefsizeB]; for(int i=0; i < sizeB; i++) data[i] = b.data[i]; } // Операция присваивания: base & operator=(const base & b) { if (this == & b) //Сравнение адресов!!! return *this; delete [] data; data = new double[sizeB=b.sizeB]; for(inti=0; i<sizeB; i++)
Специальные методы классов и перегрузка операций 439 data[i] = b.data[i]; return *this; } ~base() //Деструктор {delete [ ] data;} }; Так как класс требует для своих объектов выделения динамической памяти, то в нем явно определены конструктор копирования, деструктор и операция-функция присваивания. Производный класс (массив с произвольными границами): class array: private base {//Закрытое наследование int beg, end; public: arrayfint iBeg=0, int iEnd= 1): // Конструктор умолчания и общего вида beg(iBeg>iEnd?iEnd:iBeg), end(iBeg>iEnd?iBeg:iEnd), base(end-beg+1) {} fpend ostream & operator« (ostream & out, const array & ar); double & operator[ ](int n) { return data[n-beg]; } }; Производный класс array вводит два целочисленных поля данных beg, end, определяющих пределы изменения индекса массива, который будет представлять объект этого производного класса. Конструктор производного класса с помощью параметров задает значения полей данных int beg, end, затем выполняет обращение к конструктору базового класса, где для массива будет выделена память. Обратите внимание, что при инициализации полей, определяющих пределы изменения индекса массива, выполняется проверка значений аргументов, чтобы соблюдалось требование: beg<= end. Метод operator[]( ) служит для перегрузки операции индексирования. С помощью дружественной функции перегружена операция вывода: ostream & operator«(ostream & out, const array & ar) { for(inti=ar.beg; i<=ar.end; I++)
440 Глава 14 out< < </< < "]-'«const_cast<array & >(ar)[i]< < ”\ Г; out«endl; return out; } Основная программа может быть такой (Р14_07.срр): int main() { array s( 6,9); // Конструктор общего вида forfint k=6;k< 10;k++) s[k]=2*k; cout«s; { // Вложенный блок array z(s); // Конструктор копирования cout«z; } array r( 12,99); r=s; cout«r; return 0; } Результаты выполнения программы: [6]=12 [7]=14 [8]=16 [9]=18 [6]=12 [7]=14 [8]=16 [9]=18 [6]=12 [7]=14 [8]=16 [9]=18 В результатах обратите внимание на значение объекта г. Этот объект-массив создается с предельными значениями индексов 12, 99, а после выполнения присваивания r=s; в нем оказывается только четыре элемента с индексами от 6 до 9. При построении на основе ресурсоемкого класса производных, не требующих для своих объектов выделения новых ресурсов, нет необходимости в производном классе явно определять специальные методы (конструкторы умолчания, копирования, деструктор и операцию-функцию присваивания). Компилятор создаст их без затруднений автоматически. Особенности построения ресурсоемкого производного класса рассмотрим на той же задаче, но классы определим так (Р14_08.срр):
Специальные методы классов и перегрузка операций 441 class indexSegment {//Базовый класс - интервал изменения индекса protected: int beg,* end; public: //Конструктор умолчания и общего вида: indexSegmentfint iBeg-1, int iEnd=1): beg(iBeg>iEnd?iEnd:iBeg), end(iBeg>iEnd?iBeg:iEnd) {} }; Конструктор, анализируя параметры, так присваивает полям данных int beg, end значения, чтобы всегда соблюдалось требование beg<=end. Тем самым задаются предельные значения индекса массива, который будет определяться производным классом. Отметим, что в базовым классе indexSegment деструктор, конструктор копирования и операция присваивания будут добавлены компилятором. Производный класс (массив с произвольными границами индекса): class newArray: private indexSegment { double * data; public: //Конструктор общего вида: indexSegmentfiBeg, iFnd) { newArrayfint iBeg=1, intiEnd=1): data = new double[end-beg+1 ]; } // Конструктор копирования: newArrayf const newArray & a): indexSegmentf a.beg, a.end) { data = new double[end-beg+1 ]; for (int i=0; i < end-beg+1; i++) datafi] = a.datafi]; } // Операция присваивания: newArray & operator^const newArray & a) { if (this == & a) return *this;//Сравнение адресов!!! delete [ ] data; beg = a. beg; end = a.end; data = new double[end-beg+1 ];
442 Глава 14 for(inti=0; i<end-beg+1; i++) data[i] = a.datafi]; return *this; } ~newArray() //Деструктор { delete [] data;} friend ostream & operator« (ostream & out, const newArray & ar); double & operator[](intn) {//перегрузка операции индексирования return data[n-beg]; } Внешнее определение операции-функции operator«() отли^- чается от такой же функции в классе array только типом второго параметра. Так как производный класс требует для своих объектов выделения ресурсов (динамически распределяемой памяти), то в нем явно определены конструктор копирования, операция присваивания и ,деструктор. Для иллюстрации применения классов можно использовать практически ту же самую основную программу: int main() { newArray s( 6,9); forfint k=6;k< 10;k++) s[k]=2*k; cout«s; { // Вложенный блок newArray z(s); cout«z; } newArray r( 12,99); r=s; cout«r; return 0; } Результаты выполнения совпадают с результатами выполнения предыдущей программы.
Глава 15 ВИРТУАЛЬНЫЕ ФУНКЦИИ И АБСТРАКТНЫЕ КЛАССЫ 15.1. Виртуальные функции Виртуальность функций никак не связана с виртуальностью классов. Изобретатель языка Си++ объясняет включение в него виртуальных функций необходимостью обеспечить один из видов полиморфизма. Термин полиморфизм (poly - много, тог- phos - форма, греч.) дословно означает "много форм". В языках программирования полиморфный объект — это сущность (переменная, аргумент функции и др.), хранящая в разные моменты выполнения программы значения разных типов (см., например, Т. Бадд [19]). Одну из форм полиморфизма мы уже рассмотрели достаточно подробно. Речь идет о перегрузке функций. Смысл ее состоит в том, что одно имя функции в зависимости от типов, количества или последовательности аргументов позволяет обратиться к разным кодам, предназначенным для выполнения действий. В этом случае имя функции становится многозначным — ему соответствуют разные алгоритмы. Такой вид полиморфизма называют процедурным полиморфизмом. Еще один вид полиморфизма связан с перегрузкой операций. Так как для перегрузки операций вводятся специальные функции (операции-функции), то можно считать, что тут также имеет место процедурный полиморфизм. Второй вид полиморфизма в языке Си++ связан с механизмом наследования и реализуется с помощью виртуальных функций. Он основан на различии между статическим типом переменной и динамическим (динамически изменяемым) типом того значения, которое хранится в этой переменной. Переменной такого вида в Си++ является указатель базового класса, который может содержать как адреса объектов своего класса, так и объектов производных классов. Однако для достижения
полиморфиз444 Глава 15 ма без механизма виртуальных функций названной возможности указателя базового класса адресовать объекты производных классов недостаточно. Именно применение виртуальных функций позволяет с помощью одного указателя со статическим типом базового класса обращаться к виртуальным методам (функциям) разных производных классов. Рассмотрим, как это происходит и как эту возможность можно использовать. Нам уже известно, что каждый метод класса - это функция, принадлежащая этому классу. Если в определение нестатического метода добавить спецификатор virtual, то метод становится виртуальной функцией этого класса. Виртуальными могут быть не любые функции, а только нестатические методы классов, и механизм их действует только при наследовании. Если функция определена как виртуальная в базовом классе, то ее повторное определение (даже без спецификатора virtual) в производном классе также создает виртуальную функцию. Для этого в базовом и производном классах необходимо совпадение имен функций и спецификаций их параметров. Возвращаемые этими функциями значения могут быть разных типов. Подчеркнем, что речь идет не о перегрузке, т. е. не о процедурном полиморфизме. Сигнатуры функций различаются только их принадлежностью разным классам. Легче всего понять назначение виртуальных функций, рассмотрев вначале пример без виртуальных функций. Пусть следующим образом определены базовый и производный классы: struct base { void fun (int i) { cout« “base: “« i«endl; } }; struct dir: base { void fun (int i) { cout« "dir: “«i*i«endl; } }; В базовом и в производном классах определены функции (не статические методы) с одинаковыми именами и типами параметров. Если обращаться к этим функциям, используя объект имя_объекта.Юп(аргумент),
Виртуальные функции и абстрактные классы 445 то никакой неоднозначности не возникает — вызывается функция того класса, которому принадлежит объект. Рассмотрим вызов функции с использованием адреса объекта, присвоенного указателю указатель_на_объект -> fun( аргумент). Какая функция и в каком случае будет вызываться? Это зависит от типа указателя и типа того объекта, адрес которого ему присвоен. Если типы указателя и адресуемого им объекта совпадают — никакой неоднозначности нет, — вызывается функция того класса, которому принадлежит объект (и тип которого имеет указатель). Однако указателю на объекты базового класса можно присваивать адреса объектов производных классов. Таким образом, в приведенной конструкции указатель может иметь тип базового класса, но его значением может быть адрес объекта производного класса. В этом случае для невиртуальных функций всегда вызывается функция базового класса. Проиллюстрируем сказанное (Р15_01.срр): int main ( ) { base В, *bp = & В; dir D, *dp = & D; base *pbd = &D; bp -> fun(1); dp -> fun (5); pbd -> fun (4); return 0; ) // Указатель на объекты базового класса // Указатель на объекты производного класса // Указатель типа base *, адресует объект производного класса // Печатает: base: 1 // Печатает: dir: 25 // Печатает: base: 4 Как показывают результаты, “настроив” указатель (pbd) базового класса на объект производного класса (D), не удается вызвать обычную (не виртуальную) функцию производного класса. Но если в нашем примере сделать функцию base::fun (int i) базового класса виртуальной, т.е. вставить ь ее заголовок слово virtual: virtual void fun (Int i),
446 Глава 15 то вызов pbd -> fun (4) приведет к такому выводу в выходной поток: dir: 16. (Выполнится метод dirr.void fun (int i) производного класса!) Таким образом, в случае виртуальности методов указатель на объекты базового класса, которому присвоен адрес объекта производного класса, обеспечивает доступ к методу производного класса. (Напоминаем, что повторное определение унаследованной виртуальной функции в производном классе делает эту функцию производного класса виртуальной.) Наследование, как пишет Мейерс ([7], с. 18), привело к необходимости различать для указателей и ссылок статические и динамические типы. Ссылка (и указатель) получает статический тип при определении. В нашем примере указатели bp, pbd имеют статический тип base *, указатель dp - статический тип dir *. Тип статической ссылки (и указателя) задан в ее (его) определении слева от имени. Инициализатор или правая часть операции присваивания задают динамический тип указателя. Динамический тип ссылки определяет ее инициализатор. В нашем примере за счет инициализации указатель Ьр принимает динамический тип base *, а указатели dp, pbd - динамический тип dir*. При обращении к виртуальной функции с помощью указателя базового класса решающим является динамический тип указателя. Если указатель адресует объект базового класса — вызывается виртуальная функция базового класса. Если указатель адресует объект производного класса, то вызывается виртуальная функция производного класса. Для невиртуальных функций определяющим является статический тип указателя. 15.2. Присваивания при наследовании Прежде чем продолжить изучение виртуальных функций, рассмотрим еще некоторые затруднения, которые возникают при их отсутствии. Не задумываясь о последствиях, определим два класса: базовый и производный (Р15_02.срр):
Виртуальные функции и абстрактные классы 447 class BAS { int к; public: BASfint ki=0): k(ki){ } friend ostream & operator «(ostream & ou, const BAS & b); }; class DIR: public BAS { double z; public: DIR(intki=0, double zi=0.0): BAS(ki), z(zi){ } friend ostream & operator «(ostream & ou, const DIR & d); }; Правильно определим дружественные функции вывода: ostream & operator «(ostream & ou, const BAS & b) { ou « "k = ” << b.k « endl; return ou; } ostream & operator «(ostream & ou, const DIR & d) { ou« "BAS::"; ou«dynamic_cast<const BAS & >(d); // Повышающее приведение ou « ”DIR::z = "<< d.z « endl; return ou; } В теле второй операции-функции вывода (для объектов производного класса) ссылка на объект производного класса с помощью операции dynamicjcast явно приводится к ссылке на объект базового класса. В иерархии наследования базовый класс принято размещать над производным. Поэтому такое приведение типов называют повышающим. Используя классы, напишем такую программу (Р15_02.срр): int main() { DIR one( 15,4.0), two; BAS * p1 = &one; BAS * p2 = & two; *p2 =*p1; // Присваивание только полей базового класса!!!
448 Глава 15 cout< < two« endl; two=one; // Все правильно! cout«two«endl; return 0; } Результаты выполнения программы: BAS::k = 15 DIR::z = 0 BAS::k = 15 DIR::z = 4 В программе присваивание объекту two производного класса значения другого объекта one производного класса выполняется дважды. Первый раз при помощи разыменования указателей базового класса (в операторе *р2 = *р 1;). В этом случае, как показывают результаты, выполняется только частичное присваивание - только данные из базового класса получили новые значения. Присваивание самих объектов (two = one;) выполнено правильно - получили BAS::k= 15 и DIR::z = 4. Объяснение состоит в следующем. Дело в том, что в классах DIR и BAS отсутствуют явные определения операций-функций для перегрузки операции присваивания. Эти операции-функции как невиртуальные методы добавляют в классы компилятор и делают это по своим правилам. После этого оператору two = one; соответствует обращение two.operator=(one) к методу с прототипом DIR:: DIR & operator^ const DIR &); Так как статический тип указателей р 1 и р2 определен как BAS*, то оператору *р2=*р1; соответствует вызов (*р2).орега- tor=(*p 1) функции с прототипом BAS::BAS & operator^const BAS &); Эта функция "ничего не знает" о том, как присваивать данные производного класса. Как же добиться того, чтобы с помощью указателей базового класса р1, р2 можно было бы обратиться к методу присваивания
Виртуальные функции и абстрактные классы 449 производного класса DIR? Из предыдущего примера мы видели, что при использовании указателей на базовый класс для обращения к методам производного класса особую роль играют виртуальные функции. Нам уже известно, что для обращения через указатель базового класса к нестатическому методу производного класса необходимо, чтобы 1) указатель имел динамический тип производного класса и 2) метод был виртуальной функцией. Оба указателя р 1, р2 имеют нужный динамический тип DIR *. Но функции присваивания в явном виде не определены в классах и сами по себе они не могут стать виртуальными. Кроме того, созданные по умолчанию в классах BAS, DIR операции-функции присваивания имеют разные типы параметров. Операции-функции с приведенными прототипами несложно явно определить в наших классах. Но как сделать их виртуальными? Чтобы сделать функции базового и производного классов виртуальными, необходимо, чтобы у них были одинаковые имена и списки параметров!!! Решение в нашем примере может быть таким. В базовом классе BAS явно определяем виртуальную операцию-функцию (Р15_03.срр): virtual BAS & operator^ const BAS & b) { k=b.k; return *this; } Хотя эта функция не расширила (не изменила) возможностей присваивания, выполняемого по умолчанию, но без нее не обойтись — эта виртуальная функция позволит ввести виртуальную функцию в производном классе. В производном классе DIR вводим функцию с таким же именем и параметром того же типа: virtual DIR & operator^const BAS & d) { this -> BAS::operator=(d); const DIR & r = dynamicjcast <const DIR &>(d); z-r.z; return *this; } 29 - 2762
450 Глава 15 В теле функции явно вызывается функция присваивания BAS::operator=(), выполняющая присваивание базовой части производного объекта. Обратите внимание на ее аргумент d. Это константная ссылка на объект базового класса — параметр функции DIR::operator=(). Далее определена ссылка г на объект производного класса, в инициализации которой использовано "понижающее" приведение типов с помощью операции dynamicjcast. Напомним, что эта операция предназначена только для приведения указателей или ссылок. Операция автоматически проверяет допустимость приведения типов. В случае недопустимого преобразования генерируется исключение std::bad_cast, которое можно перехватывать и обрабатывать обычным образом. (Допустимо и применение операции static_cast, но в этом случае не будет проверки допустимости преобразования.) С помощью ссылки const DIR & г мы обращаемся к полю double z производного класса. Тем самым объект *this полностью определяется. Обратите внимание, что у виртуальных функций в нашем примере разные типы возвращаемых значений. Для иллюстрации правильности выполнения присваивания объекту производного класса значения с помощью указателей базового класса можно выполнить для модифицированных классов BAS и DIR уже приведенную выше программу (Р15_ОЗ.срр) с оператором *р2 = *р1;. 15.3. Деструкторы при наследовании Рассмотрим следующие классы с явно определенными деструкторами, которые "сообщают" о своем выполнении: class BAS {// Базовый класс public: ~BAS() {cout«"~BAS"«endl;} }; class DIR: public BAS { // Производный класс int * iArr; public: DIR(): iArrf new int [10]) { } ~DIR() {
Виртуальные функции и абстрактные классы 451 delete [ ] iArr; cout«"~DIR"«endl;} }; Особенность производного класса: его объекты включают динамически выделяемую память, адресуемую указателем int * iArr. Память в виде массива выделяется при выполнении конструктора. Деструктор явно освобождает память и выводит сообщение "-о/я”. В основной программе определим в динамический памяти объект производного класса DIR и указатель Ы базового класса, адресующий этот объект. Затем явно уничтожим объект с помощью оператора delete Ы; и создадим новый объект производного класса в автоматически распределяемой памяти. Текст функции main( ) (Р15_04.срр): int main() { BAS *Ь1 = new DIR; delete b 1; cout«"Newobject: "«end!; DIR obj; return 0; } Результаты выполнения программы: ~BAS New object: ~DIR ~BAS При явном уничтожении оператором delete объекта производного класса выполнен только деструктор базового класса, а деструктор производного класса не вызывался! Тем самым в памяти остался "беспризорный" массив из 10 элементов типа int. Это полностью соответствует рассмотренным ранее правилам выбора невиртуального метода с помощью указателя на базовый класс. Выбор определяет статический тип указателя. При завершении программы объект производного класса DIR obj уничтожен "естественным" образом - последовательно вызваны два деструктора, первый из которых удаляет динамический массив, включенный в объект. 29*
452 Глава 15 Этот пример показывает, что если производный класс является классом ресурсоемких объектов, то такое поведение деструкторов может привести к появлению неосвобожденных ресурсов уже удаленных объектов. В нашем примере деструкторы определены явно, что позволило показать действие правил, которые мы рассматриваем. Однако достаточно часто деструкторы классов определяются неявно, и при этом показанная на приведенном примере опасность некорректного удаления объекта производного класса при наследовании не видна. Поэтому в каждом классе, который может служить базовым, рекомендуется явно определять деструктор как виртуальную функцию и при необходимости переопределять его в производном классе. Для нашего примера деструктор базового класса можно сделать таким: virtual ~BAS() {cout«’'~BAS"«encll;} После этого изменения деструктор производного класса автоматически становится виртуальным и результат выполнения в той же самой главной программе (Р15_04_1.срр) оператора delete Ы; станет таким: -DIR -BAS New Object: ~DIR ~BAS В данном случае указатель базового класса BAS *Ь1 обеспечил доступ к деструктору производного класса, а тот по правилам уничтожения объектов производного класса, в свою очередь, вызвал деструктор базового класса. Опасность появления неудаленных ресурсов при такой схеме устранена. 15.4. Реализация виртуальных функций При построении компиляторов наиболее распространенный подход к "внутренней" реализации классов с виртуальными функциями предусматривает использование таблиц виртуальных функций. Обычно таблица виртуальных функций — это массив
Виртуальные функции и абстрактные классы 453 или связный список с указателями на виртуальные функции. Такой список или массив компилятор автоматически включает в реализацию каждого класса, в котором определены или унаследованы виртуальные функции. Тем самым каждый класс (реализация каждого класса) имеет собственную таблицу виртуальных функций; Элементы такой таблицы адресуют коды виртуальных функций именно этого класса. Рассмотрим схему определения класса, справа от которого поместим схематичное изображение его таблицы виртуальных функций (массива указателей на виртуальные функции). class В {... public: virtual ~В(){...} virtual void f1(){...} virtual intf2(){...} virtual char f3(){...} double f4(){...} }; код B::~B код B::f1 код B::f2 код B::f3 Каждый элемент массива - это указатель (virtual table pointer - vptr) на реализацию (код) виртуальной функции. Отметим, что коды обычных (невиртуальных) методов не адресуются указателями таблицы виртуальных функций (в нашей схеме — это конструктор В::В() и метод В::Щ)). Пусть на базе класса В определен следующий производный класс: class D: public В {... public: D(){...} virtual ~D(){...} virtual void f1(){...} irtual void f5(){...} virtual char f3(){...} double f6(){...} }; код D;:~D -► код D::f1 ^ код B::f2 код D::f3 код D::f5 Обратите внимание, что в схеме таблицы виртуальных функций для производного класса присутствуют указатели на
функ454 Глава 15 ции, которые унаследованы от базового класса. Среди функций производного класса: f2() — унаследована; f1(), f3() — переопределены; f5() - новая функция. Функция /6(), как невиртуальная, в таблице не отражена. В классах показаны деструкторы. При реализации каждого класса с виртуальными функциями для него создается только одна таблица виртуальных функций. Каждый объект такого класса включает (рис. 15.1) дополнительный член — указатель (pointer) на таблицу виртуальных функций его класса. Объект В Объект D Объект В Таблица виртуальных функций В + В * В * в * в :~В() :П() :f2() :«() Объект D Данные Данные ... pointer » nninter а Таблица виртуальных функций D D::~D() 7j ► D"f20 * D:: f3() > D\\ f5() Рис. 15.4. Связи объектов с таблицами виртуальных функций Чтобы пояснить внутреннее (не видимое программисту) использование таблиц виртуальных функций, Мейерс ([7], с. 128) рассматривает правила, которыми пользуется компилятор для формирования кода такой функции: void такеСаЩ В * ptr) {ptr-> f3(); } Статический тип параметра ptr это В* - указатель на объект базового класса. При обращении к функции такеСаЩ) аргументом может быть либо адрес объекта класса 6, либо адрес объекта производного класса D. До обращения к функции makeCall() непонятно, какую из функций B::f3() или D::f3() необходимо вызвать в теле функции. Поэтому в коде функции makeCall() реализуется, например, такой алгоритм:
Виртуальные функции и абстрактные классы 455 • по заданному (динамическому) типу аргумента определить адрес таблицы виртуальных функций соответствующего класса (базового или производного); • в таблице найти указатель виртуальной функции B::f3() для класса В или D::f3() для класса D; • вызвать функцию по найденному указателю. Как мы говорили, каждый объект класса с виртуальными функциями включает указатель pointer на таблицу виртуальных функций своего класса. Пусть таблица реализована, например, как массив с именем vptr. Если компилятор поместил адрес функции /3() в элемент vptr[i], то обращение ptr-> f3(); будет обрабатываться как следующее выражение: (*(ptr-> vptr[i]))(this) Механизм виртуальных функций позволяет реализовать динамическое, или позднее, связывание. Наиболее яркий пример — информационные конструкции (например, структуры или массивы), элементы которых имеют тип указателя на базовый класс. Динамическими значениями этих элементов могут быть адреса объектов разных производных классов. Перебирая элементы такого массива, можно вызывать методы разных производных классов. Термины «динамическое», или «позднее связывание» отражают тот факт, что на этапе компиляции неизвестно, методы каких классов будут вызываться при исполнении программы. 15.5. Абстрактные классы Если по смыслу решаемой задачи все виртуальные функции базового класса планируется переопределять в производных классах, то такой базовый класс можно определить как абстрактный. Для абстрактного класса при реализации не создаются таблицы виртуальных функций и для некоторых методов не определяются коды. Тем самым сокращается общий объем кода программы. Абстрактным называется класс, в котором есть хотя бы одна чистая виртуальная функция, которая вводится с помощью такого определения: virtual тип имя (спецификация_параметров) = 0;
456 Глава 15 Конструкция = 0 называется чистым спецификатором. По его присутствию функция распознается как чистая виртуальная. Пример определения чистой виртуальной функции: virtual void fpure () - 0; Назначение чистой виртуальной функции — служить основой для заменяющих ее функций в производных классах. Перечислим основные свойства абстрактных классов, а затем приведем пример их использования. Не существует объектов абстрактных классов (их нельзя создать), абстрактный класс может использоваться только в качестве базового. В абстрактные классы могут входить поля данных, а также методы, отличные от чисто виртуальных. Как всякий класс, абстрактный класс может иметь конструктор. (Этот конструктор не может создавать полноправных объектов, а используется при наследовании. С его помощью инициализируются поля данных абстрактного базового класса, входящие в объект производного класса.) Из конструктора абстрактного класса можно вызывать методы абстрактного класса, но никаких обращений к чистым виртуальным функциям быть не должно. Абстрактные классы не могут определять тип возвращаемого функцией значения. Они не могут использоваться для приведения типов. Однако указатель на абстрактный класс можно использовать в качестве формального параметра. Соответствующий аргумент должен иметь тип указателя на объекты производного (уже не абстрактного) класса. Абстрактный класс может быть производным классом, как от абстрактного, так и от обычного классов. В качестве примера абстрактного класса рассмотрим класс "фигура на плоскости". В таком классе можно определить в качестве полей данных габаритные размеры фигуры (размеры вдоль координатных осей). В качестве чисто виртуальных функций введем два метода. Первый из них будет вычислять площадь фигуры, второй — возвращать название конкретной фигуры. Оба метода совершенно бесполезны до определения на базе класса "фигура" конкретного производного класса. Сделаем абстрактный класс "фигура на плоскости" производным от не абстрактного класса "точка на плоскости".
Виртуальные функции и абстрактные классы 457 Определение базового класса "точка на плоскости": //point.h - Базовый класс "точка на плоскости" class point { protected: // Защищенные поля данных - double х, у; // Координаты точки, public: // Методы класса: point(double xi=0.0, double yi-O.O) :x(xi), y(yi) { } // Переместить точку в новое место: void move(double xn=0.0, double yn=0.0) { x = xn; у = yn; } }; В классе два защищенных поля данных (double х, у - координаты точки на плоскости), конструктор (одновременно общего вида, умолчания и приведения типов — можно назвать его универсальным) и метод move() для изменения полей данных, т. е. для изменения положения точки. В производном классе поля класса точка будет определять центр фигуры. (Объект класса point можно было бы включать в класс "фигура", по-другому определив отношение между классами, т. е. использовать не наследование, а включение классов.) Разместив текст класса point в файле с названием point.h, так определим абстрактный класс, производный от класса "точка": //figure.h - абстрактный класс "фигура на плоскости" #include "point.h".. //- Включаем базовый класс #include <string> class figure : public point { protected: // Защищенные поля данных - double dx, dy; // Tэбариты" фигуры (размеры вдоль осей) public: // методы: figuref double xi=0.0, double yi=0.0, double dxi=0.0, double dyi=0.0) : point (xi, yi), dx(dxi), dy(dyi) { } // Изменить на заданную величину габариты: void grow(double d) { dx += d; dy += d; } //Вычислить площадь (еще неизвестной!) фигуры:
458 Глава 15 virtual double area () = 0; // Получить название фигуры: virtual string className()=0; friend ostream & operator «(ostream & ou, figure &); }; Абстрактный класс figure наследует поля данных и методы базового класса point. В классе figure два новых поля данных double dx, dy, они определяют размеры фигуры вдоль координатных осей. Наиболее важен для наших целей набор методов класса. Конструктор (универсальный) вначале явно обращается к конструктору базового класса, затем инициализирует поля данных dx, dy. Класс наследует метод point::move(). Явно определен новый метод grow(), позволяющий изменять габариты фигуры на заданную величину. Для простоты размеры по каждой из осей изменяются одинаково. В классе две чисто виртуальные функции. Функция агеа() после ее реализации в производном классе должна вычислять площадь конкретной фигуры, центр и размеры которой определяют поля данных базовых классов point и figure. Метод className() должен возвращать в виде строки название конкретной фигуры, создаваемой производным классом. В классе определена дружественная функция для перегрузки операции вывода: ostream & operator «(ostream & ou, figure & fig) { ou « fig.classNamef)« "->\tcenter: x=” << fig.x « ". \ty="«fig-y; ou « ";\n\tdx=" « fig.dx « ",\tdy="« fig.dy « ";\tarea="« fig.areaf); return ou; } Параметр дружественной функции — ссылка на тот самый абстрактный класс figure, для которого функция является дружественной. В теле функции operator <<() использованы обращения к чисто виртуальным функциям fig.className() и fig.area. Кроме того, с помощью ссылки fig в теле функции выполняются обращения к полям данных класса figure. Так как класс figure абстрактный, то не существует объектов этого класса и невозможно обратиться к функции operator «() до определения
объВиртуальные функции и абстрактные классы 459 ектов производного класса. Текст класса figure разместим в файле figure, h. На базе абстрактного класса figure можно построить разные классы, определяющие конкретные фигуры на плоскости. Разумно создавать классы фигур, для которых имеет смысл указывать центральную точку и площадь которых можно вычислять по значениям габаритных размеров. Определим два таких класса - "эллипс" и "прямоугольник". Пусть оси эллипса и стороны прямоугольника будут параллельны координатным осям. Тогда поля данных double dx, dy класса figure (габаритные размеры) позволят вычислить площадь каждой из названных фигур. Центры этих фигур определят поля данных double х,у, унаследованные каждым из соответствующих классов от (не прямого) базового класса point. Определение класса "эллипс": #include "figure.h"// - Включаем абстрактный класс //Класс, производный от абстрактного figure: struct ellipse : public figure { // Эллипс ellipse (double xi=0.0, double yi-O.0, double dxi-O.O, double dyi=0.0) : figurefxi, yi, dxi, dyi) { } virtual double area () { return (dx/2) *(dy/2) *3.14159; } string classNamef) { return stringf "ellipse"); > }; Определение класса "прямоугольник": It include "figure, h" // - Включаем абстрактный класс //Класс, производный от абстрактного figure struct square : public figure { // Прямоугольник square (double xi=0.0, double yi=0.0, double dxi=0.0, double dyi=0.0) : figurefxi, yi, dxi, dyi) { } virtual double area () {return dx * dy;} string classNamef) { return stringf "square"); ) };
460 Глава 15 Классы определены практически одинаково. В каждом из них универсальный конструктор выполняет обращение к конструктору базового класса. Метод агеа(), реализующий виртуальную функцию базового абстрактного класса, явно определен как виртуальный. В каждом из двух классов он по-своему вычисляет площадь соответствующей фигуры. Метод className() в каждом из классов виртуален по умолчанию и возвращает названия конкретных фигур. Поместим тексты определений классов ellipse и square в файл realFigures.h. Продемонстрируем возможности созданных классов и применение их методов на следующей программе. //15_05.срр - Объекты классов, производных от figure #include <iostream> using namespace std; #include "realFigures.h"// Включаем определения классов intmainf) { ellipse A( 10.0, 8.0, 20.0, 20.0), B; cout« "Object A:\n"« A « endl; A.move(5.0, 5.0); cout« "A.move(5.0, 5.0):\n" « A « endl; A.grow(-18.0); B=A; cout« "Object B:\n" « В « endl; square C( 1.0, 3.0, 5.0, 6.0), D; cout« "Object C:\n"« C « endl; C. grow(-5.0); cout« "C.grow(-5.0):\n" <<C« endl; D. movef-10,50); cout« "D.movef-10,50):\n"; operator«(cout,D)« endl; D.growf 10.0); cout« "D.growf 10.0):\n"; cout« "D.classNamef): "« D.classNamef) « ",\t" « "D.area()="« D.areaf); return 0; } В программе определены два объекта Л, В класса ellipse и два объекта D, Е класса square. Продемонстрировано применение к
Виртуальные функции и абстрактные классы 461 этим объектам методов базовых и производных классов. Обратите внимание, что обращение к дружественной для абстрактного базового класса figure операции-функции operator«() выполнено по-разному: для объектов А, В, С она вызывается неявно, а для объекта D к ней выполнено непосредственное обращение. Как при явном, так и при неявном обращениях к операции- функции operator«(), вместо параметра figure & fig, имеющего тип ссылки на абстрактный базовый класс, в качестве аргументов используются объекты производных классов. Операция- функция выполняется именно для этих объектов. Следует отметить, что методы, реализующие в конкретных классах виртуальные функции агеа() и className(), применяются дважды. Во- первых, обращение к ним выполнено из дружественной для абстрактного базового класса figure функции operator«(). Во- вторых, для объекта D конкретного класса square эти методы вызываются непосредственно в основной программе. Следующие результаты выполнения программы иллюстрируют изложенные ранее особенности и возможности наследования абстрактных классов и виртуальных функций: Object А: ellipse -> center: x=10, У=8; dx=20, dy=20; area=314.159 A.move(5.0, 5.0): ellipse -> center: x=5, У=5; dx=20, dy-20; area-314.159 Object B: ellipse -> center: x=5, y=5; dx-2, dy=2; area-3.14159 Object C: square -> center: x=1, У=3; dx=5, dy-6; area-30 C.grow(-5.0): square -> center: x=1, y=3; dx-0, dy=1; area-0 D.movef-10,50): square -> center: x--10, У=50; dx=0, dy-O; area-0 D.growf 10.0): D.className(): square, D.area()= 100
462 Глава 15 15.6. Массивы и списки указателей на абстрактные классы Используя указатели на абстрактные классы и присваивая этим указателям значения адресов объектов производных классов, можно (как мы уже показывали) выполнять динамическое (позднее) связывание вызова (виртуальной) функции с ее конкретной реализацией в производном классе. Обратим внимание, что элементам одного и того же массива указателей базового класса можно присваивать значения адресов объектов разных классов. Это допустимо в том случае, если эти разные классы являются производными от одного базового. Имея такой массив и перебирая его элементы, можно из одного и того же вызова функции обращаться к методам разных классов. Выбор метода (и класса) будет зависеть от динамического типа указателя. Воспользуемся рассмотренным примером с абстрактным классом figure для иллюстрации этой возможности. Напишем функцию для вывода сведений об объектах классов, производных от абстрактного класса figure. Параметром функции будет массив указателей на объекты абстрактного класса, а в качестве аргументов будут использоваться массивы адресов объектов конкретных классов, производных от figure. В теле функции выполним перебор элементов массива-параметра и обращение с их помощью к дружественной для абстрактного класса операции-функции operator«(). Определение функции может быть таким: //Функция печати значений объектов производных классов: void display! figure * fig[ ], intk) { forf int i=0; i<k; i++) cout« *fig[i] « endl; } Первый параметр функции display() — массив указателей на объекты класса figure. Второй параметр задает количество начальных элементов массива, которые нужно обработать. Выражение cout « *fig[i] служит неявным обращением к операции- функции operator«(). Как мы видели ранее, в теле этой операции-функции помещены обращения к чисто виртуальным функциям className() и агеа(). В соответствии с принципом
динаВиртуальные функции и абстрактные классы 463 мического связывания реальные обращения будут выполняться к тем реализациям этих функций, которые находятся в производных классах. При каждом обращении реальный производный класс определяется динамическим типом элемента массива-аргумента. Для иллюстрации применения функции display() напишем основную программу, где определим несколько объектов производных классов ellipse и square, затем присвоим их адреса элементам массива указателей на абстрактный базовый класс figure. Текст программы (Р15_06.срр) может быть таким (опущено определение функции): int main() { ellipse А( 10.0, 8.0, 20.0, 20.0),В(6.0); square С( 1.2, 3.3, 5.0, 6.0), 0(12,80), E; E = C; figure * ar[ ]={&A, & В, & C, &D, &E); cout« "Array of Objects:" « endl; display(ar,5); return 0; } Обратите внимание, что размер массива указателей на объекты базового (абстрактного) класса аг[ ] и значения его элементов определяются инициализацией. Результаты выполнения программы (их не требуется пояснять для тех, кто познакомился с предыдущим примером, где использовались те же классы): Array of Objects: ellipse -> center: x=10, У=8; dx-20, dy=20; area- 314.159 ellipse -> center: x=6, y=o; dx=0, dy=0; area-0 square -> center: x=1.2, y=3.3; dx=5, dy=6; area=30 square -> center: x=12, y=80; dx=0, dy=0; area=0 square -> center: x-1.2, y=3.3; dx-5, dy=6; area=30
464 Глава 15 Теперь построим односвязный список объектов разных классов. Повторим условие построения классов в предыдущих примерах программ. На основе базового класса "точка" определен абстрактный класс "фигура". В абстрактном классе "фигура" введены компоненты, определяющие размеры фигуры вдоль координатных осей, а базовый класс "точка" задает размещение фигуры на плоскости. Чисто виртуальная функция введена для вычисления площади фигуры. Вторая чисто виртуальная функция возвращает название производного класса. На основе абстрактного класса определим несколько классов, описывающих конкретные фигуры на плоскости. Для объектов этих классов базовая точка определяет центр фигуры, а размеры вдоль осей позволяют вычислять площади. (Например, прямоугольник, стороны которого параллельны координатным осям, эллипс с соответствующим направлением осей и т.п.) Определим класс, объединяющий свои объекты в односвязный список. В качестве поля данных в элементах списка используем указатель на объекты абстрактного класса "фигура". Введем статический метод для вычисления суммарной площади всех конкретных фигур, включенных в список. В основной программе определим несколько объектов классов, производных от класса «фигура». Для них вычислим площади. Включив их в связный список, подсчитаем общую площадь фигур в списке. Так как в стандартной библиотеке (ей будут посвящены следующие главы) имеются средства для программирования связных списков и других динамических конструкций (стеков, деревьев), то используем очень простое решение. Определим связный список таким образом, что он будет всегда в программе единственным. В классе будет открытое поле — указатель на начало списка, и закрытое поле — указатель на последний элемент в списке. Конструктор, получив объект класса, производного от класса "фигура", создает элемент списка и, используя указатель на последний элемент, включает объект в список. Текст программы: //Р15_07.срр - Односвязный список объектов разных классов #include <iostream> using namespace std; #include "realFigures.h"//Конкретные классы, производные от figure
Виртуальные функции и абстрактные классы 465 //Класс объектов-элементов в односвязном списке class chain { static chain * last; chain * next; figure * pfig; public: static chain * begin; chain( figure * p); // Указатель на последний элемент в списке // Указатель в объекте на следующий элемент // Указатель на фигуру в элементе списка // Указатель на начало списка //Конструктор static double areaAllf); // Площадь всех фигур списка };//Конец спецификации класса // Инициализация статических компонентов класса: chain * chain::begin = 0; // Начало списка chain * chain::last = 0; // Последний элемент в списке double chain::areaAII() { //Площадь всех фигур списка double ss=0.0; chain * uk=begin; //Настройка на начало списка while (uk!=NULL) { // Цикл до конца списка ss += uk- >pfig->area(); // Площадь конкретной фигуры uk=uk- >next; // Настройка на следующий элемент } return ss; } // Конструктор создает и включает в список объект, связав его с //конкретной фигурой из класса, производного от абстрактного chain:: chain( figure * р) { //р-адрес включаемой фигуры if(begin==NULL) begin=this; // Определили начало списка else last->next=this; // Связь с предыдущим элементом pfig=p; // Запоминаем адрес включаемой фигуры next=NULL; // Пометим окончание списка last=this; // Запоминаем адрес последнего элемента списка } intmainf) { ellipse А( 10.0, 8.0, 30.0, 20.0); cout« "Area of " « A.className() <<"="<< A.areaf) « endl; square Q1.2, 3.3, 5.0, 6.0); chain ca(&C); // Включить в список прямоугольник chain се(&А); // Включить в список эллипс cout« 'Area of"« C.className() <<”="<< C.areaf) « endl; 30- 2762
466 Глава 15 chain с2(&С); //Вторично включить в список прямоугольник cout «"аНАгеа = "<< chain::areaAII()«endl; } Многочисленные комментарии в тексте программы позволят читателю понять технические подробности ее построения. Отметим, что в функции main() созданы два объекта (А, С) классов ellipse и square. Адреса этих объектов использованы в качестве аргументов конструктора класса chain. Объект С дважды включен в список (созданы объекты са и с2). Результаты выполнения программы: Area of ellipse = 471.238 Area of square = 30 allArea = 531.238 В общую площадь всех объектов списка площадь прямоугольника входит два раза — объект С дважды включен в список.
ШАБЛОНЫ ФУНКЦИЙ И КЛАССОВ 16.1. Шаблоны функций Шаблон — синтаксическая конструкция языка, позволяющая создавать классы и функции с отложенным определением одного или нескольких типов, использованных в тексте определения класса или функции. Рассмотрим вначале шаблоны функций. Цель введения шаблонов функций — автоматизация создания одноименных функций. В отличие от механизма перегрузки, когда для каждой сигнатуры заранее определяется своя функция, шаблон семейства функций определяется один, но в нем определение функции параметризуется. Параметризовать в шаблоне функций можно тип возвращаемого функцией значения, типы локальных объектов тела функции и типы любых параметров. Формат шаблона (семейства) функций: template <список_параметров_шаблона> определение_шаблонной_функции Определение шаблона семейства функций начинается со служебного слова template. Для параметризации используется список параметров шаблона, который заключается в обязательные угловые скобки < >. Отметим, что среди параметров шаблона могут быть типизирующие, не типизирующие и параметры-шаблоны. Пока будем считать, что все параметры шаблона являются типизирующими. Каждый типизирующий параметр шаблона обозначается служебным словом class либо typename, за которым следует имя параметра (идентификатор). Пример определения шаблона функций, вычисляющих абсолютные значения числовых величин разных типов: 30*
468 Глава 16 template <class type> type absftype x) { returnx>0?x: -x; } Шаблон семейства функций состоит из двух частей — заголовка шаблона template <список_параметров_шаблона> и параметризованного определения функции, в котором тип возвращаемого значения, типы любых локальных объектов тела функции и типы любых параметров обозначаются именами типизирующих параметров шаблона, введенных в его заголовке. Параметризованное определение функции, входящее в шаблон, будем называть шаблонной функцией. В качестве еще одного примера рассмотрим шаблон семейства функций для обмена значений двух передаваемых им аргументов: template < typename Т> void swap (Т* х, Т* у) { Tz = *х; *х = У; *у = z; ) Здесь параметр Т шаблона используется не только в заголовке для спецификации параметров функции, но и в теле функции, где он задает тип вспомогательной переменной z. Обратите внимание, что в заголовке параметр Т определяет тип параметров- указателей, а в теле шаблонной функции задает тип переменной. Шаблон семейства функций служит для автоматического формирования конкретных определений функций, называемых специализациями шаблонной функции. Специализации создаются на основе шаблона по тем вызовам, которые транслятор обнаруживает в тексте программы. Например, если программист употребляет обращение abs(-10.3), то на основе приведенного выше шаблона компилятор сформирует такое определение (такую специализацию функции): double absfdouble х) { return х > 0 ? х: -х; )
Шаблоны функций и классов 469 Далее компилятор организует выполнение именно этой функции и в точку вызова в качестве результата вернется числовое значение 10.3. Формирование на основе шаблона функций и имеющегося вызова функции конкретного кода функции (создание специализации) называют инстанцированием шаблона. Если в программе присутствует приведенный выше шаблон семейства функций swapQ и появится последовательность операторов long к = 4, d = 8; swap (&к, &d); то компилятор сформирует определение функции (специализацию) void swap(long* х, long* у) { long z = *x; *x = *y; *y = z; } Затем будет выполнено обращение именно к этой функции и значения переменных к, d поменяются местами. Если в той же программе присутствуют операторы double a = 2.44,b = 66.3; swap (&a, &b); то сформируется и выполнится функция void swap (double* x, double* у) { double z = *x; *x = *y; *y = z; } Для иллюстрации сказанного о шаблонах с типизирующими параметрами рассмотрим следующую программу, в которой используем некоторые возможности функций, возвращающих значение типа "ссылка". Тип ссылки будет определяться параметром шаблона: //Р16_01.срр - Шаблон функций для поиска в массиве #include <iostream> using namespace std; // Шаблонная функция определяет ссылку на элемент
470 Глава 16 //с максимальным значением: template <typename type> type& rmaxfint n, type d[ ]) { int im = 0; for (int i = 1; i < n; i++) im = d[im] > d[i] ? im : i; return d[im]; } int main() { int n = 4; intx[]={ 10, 20, 30, 14}; //Аргумент - целочисленный массив: cout« ”rmax(n,x) = " << rmax(n,x) « endl; rmax(n,x) = 0; // Обращение с целочисленным массивом for (int i = 0; i < n; i++) cout« ”\tx["« i« "] = ”« x[i]; double arx[] = { 10.3, 20.4, 10.5 }; //Аргумент - массив double: cout« endl« "rmax(3,arx)="« rmax(3,arx) « endl; rmax(3,arx) = 0; // Обращение с массивом типа double for (int i = 0; i < 3; i++) cout« "\tarx[”« i« "/="<< arx[i]; return 0; } Результат выполнения программы rmax(n,x) = 30 x[0] =10 x[1] = 20 x[2] = 0 x[3] = 14 rmax(3,arx)=20.4 arx[0] = 10.3 arx[1] = 0 arx[2] = 10.5 В программе используются два разных обращения к функции rmax(). В одном случае параметр — целочисленный массив, и возвращаемое значение — ссылка на int. Во втором случае аргумент - имя массива типа double[ ], и возвращаемое значение имеет тип ссылки на double. По существу, механизм шаблонов функций позволяет автоматизировать подготовку определений перегруженных функций. При использовании шаблонов уже нет необходимости готовить
Шаблоны функций и классов 471 заранее все варианты перегруженных функций. Компилятор автоматически, анализируя вызовы шаблонных функций в тексте программы, формирует необходимые определения именно для таких типов аргументов, которые использованы в обращениях (рис. 16.1). Процесс логического (автоматического) определения типов аргументов шаблона функций по типам аргументов в конкретном обращении к шаблонной функции называют выведением (deduction) аргументов шаблона функций. Рис. 16.1. Схема применения шаблона функций Перечислим основные свойства параметров шаблона. 1. Список параметров шаблона функций не может быть пустым, так как при этом теряется возможность параметризации и шаблон становится обычным определением конкретной функции. 2. Кроме типизирующих параметров, у шаблона могут быть параметры нетипизирующие. (Примеров шаблонов с нетипизирующими параметрами мы пока не приводили.) 3. Нетипизирующие параметры явно специфицируются в заголовке шаблона (как обычные параметры функций) и могут иметь тип как базовый, так и производный. Однако на типы нетипизирующих параметров наложены ограничения. Они не могут быть вещественными, не могут быть классами, не могут иметь тип viod. Они могут быть:
472 Глава 16 • целочисленными; • указателями на объект или функцию; • ссылками на объект или функцию; • указателями на поля данных и методы классов. 4. Аргумент, соответствующий нетипизирующему параметру шаблона, должен быть выражением соответствующего параметру типа. 5. В списке параметров шаблона функций может быть несколько типизирующих параметров. Каждый из них должен начинаться со служебного слова class или typename. Например, допустим такой заголовок шаблона: template < typename typel, typename type2> Соответственно неверен заголовок template < typename t1, t2, t3> 6. Аргумент, соответствующий типизирующему параметру шаблона, может быть именем любого типа, как базового, так и производного. Единственное ограничение - для этого типа текст шаблонной функции должен иметь смысл. 7. Недопустимо использовать в заголовке шаблона параметры с одинаковыми именами. Имена параметров должны быть уникальными во всем определении шаблона. 8. Имя типизирующего параметра шаблона (в наших примерах type 1, type2 и т.д.) имеет в шаблонной функции все права имени типа, т.е. с его помощью могут специфицироваться параметры, определяться тип возвращаемого функцией значения и типы любых объектов, локализованных в теле функции. Имя параметра шаблона видно во всем определении и скрывает другие использования того же идентификатора в области, глобальной по отношению к шаблону. Если внутри тела шаблонной функции необходим доступ к внешним объектам с тем же именем, нужно применять операцию указания области видимости. Следующая программа иллюстрирует указанную особенность имени параметра шаблона функций: //Р16_02.срр - Одноименные параметр шаблона и переменная It include <iostream> using namespace std; #include "cyrToDos.h”
Шаблоны функций и классов 473 int N; // Инициализирована по умолчанию нулевым значением // функция определяет максимальное из двух значений параметров template <typename N> N myMax(N x, N у) { N a-x; cout« cyrToDosf "Счетчик обращений N = ") « ++::N « endl; if (a < y) a = y; return a; } intmainf) { int a= 12, b = 42; cout« "myMax(a,b)="« myMax(a,b) « endl; double z = 66.3, f = 222.4; cout« "myMax(z,f)="« myMax(z,f) « endl; return 0; } Результат выполнения программы: Счетчик обращений N = 1 max(a,b)=42 Счетчик обращений N = 2 max(z, f)=222.4 Итак, одно имя нельзя использовать для обозначения нескольких параметров одного шаблона, но в разных шаблонах могут быть одинаковые имена у параметров. Ситуация здесь такая же, как и у параметров обычных функций, и на ней можно не останавливаться подробнее. Действительно, раз действие параметра шаблона заканчивается в конце его определения, то соответствующий идентификатор свободен для последующего использования, в том числе и в качестве имени параметра другого шаблона. До сих пор мы применяли схему обращения к шаблонным функциям, при которой типы аргументов шаблона логически выводятся по типам аргументов в обращении к функции. Для этой схемы все параметры шаблона должны быть обязательно использованы в спецификациях параметров шаблонной функции. Этим требованиям не соответствует, например, такой шаблон:
474 Глава 16 template <typename A, typename B, typename C> В func(A n, Cm) { ...B vatu;... } В данном примере остался не использованным в спецификации параметров шаблонной функции параметр шаблона с именем В. Его применений в качестве типа возвращаемого функцией значения и для определения объекта valu в теле функции недостаточно, чтобы компилятор мог по обращению к функции узнать тип аргумента, соответствующего параметру В. В связи с возможностью подобных затруднений Стандарт определяет общий формат обращения к шаблонным функциям: имя_шаблонной_функции <список_аргументов_шаблона> (список_аргументов_шаблонной_функции) Используя приведенные выше программы, приведем фрагменты с примерами разных обращений к шаблонным функциям: intx[ ] = { 10, 20, 30, 14 };//-определен массив //Аргумент шаблона задан явно: cout« "rmax(n,x) = " « rmax<int>(n,x) « endl; Если типы аргументов шаблона могут быть установлены из списка аргументов шаблонной функции, то можно использовать пустой список аргументов шаблона. Пример: double агх[] = { 10.3, 20.4, 10.5 }; //- Определен массив В следующем обращении пустой список аргументов. Аргумент double выводится из вызова функции: cout« endl« ”rmax(3,arx)=" « rmax<>(3,arx) « endl; Наряду с приведенным обращением допустимо и такое: rmax<double>(3, агх) = О; Здесь тот же аргумент — тип double, задан явно: Примеры обращений к другой шаблонной функции: int а = 12, b = 42;
Шаблоны функций и классов 475 max<int>(a,b); //Аргумент шаблона задан явно: double z = 66.3, f = 222.4; // Пустой список аргументов: max< >(z,f); Как и при работе с обычными функциями, для шаблонов функций существуют определения и описания. В качестве описания шаблона функций используется прототип шаблона: template <список_параметров_шаблона> Прототип_шаблонной_функции Прототип шаблонной функции — это ее заголовок (возможно без имен параметров), вслед за которым помещен разделитель "точка с запятой". В списке параметров прототипа шаблона имена параметров не обязаны совпадать с именами тех же параметров в определении шаблона. Шаблонная функция может иметь любое количество непара- метризованных параметров. Может быть непараметризовано и возвращаемое функцией значение. Например, в следующей программе шаблон определяет семейство функций, каждая из которых вычисляет количество нулевых элементов одномерного массива параметризованного типа: //Р16_03.срр - Прототип шаблона функций #include <iostream> using namespace std; template <typename D> // Прототип шаблона long countOfint, D *); int main() { intA[ ] = {0, 1,0, 0, 6, 0, 4, 10 }; int n = sizeof(A)/sizeofA[0]; cout« "countO(n,A) = " << countO(n,A) « endl; doubleX[] = { 10.0, 0.0, 3.3, 0.0, 2.1 }; n = sizeoff X)/sizeof X[0]; cout« "countO(n,X) = " << countO(n,X) « endl; return 0; }
476 Глава 16 // Шаблон функций для определения количества нулевых //элементов в массиве template < type name Т> long countOfint size, T* array) { long k = 0; forfint i = 0; i < size; i++) if (array[i] == T(0)) k++; return k; > Результат выполнения программы: countO(n,A) = 4 countO(n,X) = 2 В шаблоне функций countO() типизирующий параметр Т (в прототипе D) используется только в спецификации одного параметра array. Параметр size и возвращаемое функцией значение имеют явно заданные ^параметризованные типы. При вызове шаблонной функции типы аргументов, соответствующие одинаково параметризованным параметрам, должны быть одинаковыми. Для шаблона функций с прототипом template <class Е> void round(E,E); недопустимо использовать такое обращение: int п = 4; double d = 4.3; round(n,d); // Ошибка в типах параметров Для правильного обращения к такой функции требуется явное приведение типа одного из параметров. Например, допустим вызов round(double(n),d); // Одинаковые типы аргументов Специализация в данном примере будет иметь параметры типа double. Чтобы продемонстрировать применение нетипизирующих параметров шаблона и нетипизированных параметров шаблонной функции, а также показать разные обращения к шаблонным функциям, напишем программу с шаблоном функций для вычисления суммы значений элементов "сегмента" массива.
Шаблоны функций и классов 477 Параметры шаблона: Т — тип элементов массива и тип возвращаемого шаблонной функцией значения; N - индекс начала сегмента. Параметры шаблонной функции: 71 ] аг — указатель на массив, из которого выбираются суммируемые элементы; int len - длина сегмента (количество суммируемых элементов). По умолчанию 1. Для простоты не будем включать в программу никаких проверок (например, не будем проверять диапазоны изменения индекса). Это позволит больше внимание уделить вопросам, относящимся собственно к шаблонам функций. В основной программе определим два массива с параметрами типов int и double и обработаем массивы с помощью шаблонной функции. //Р16_04.срр - Шаблон функций с параметром #include <iostream> using namespace std; template < int N, typename T > T sum( T ar[ ], int len= 1) { T ss=T(0); for(inti=N; i<N+len; i++) ss += ar[i]; return ss; } int main() { int A[] = { 11, 1, 0, 0, 6, 0, 4, 10}; intn = sizeof(A)/sizeof A[0]; cout« "sum<3, int>(A, n-3) = " << sum<3, int>(A, n-3) « endl; cout« "sum<3>(A, n-3) = ” << sum<3>(A, n-3) « endl; doubleX[] = { 10.0, 0.0, 3.3, 0.0, 2.1 }; cout« "sum<0>(X) = " << sum<0>(X) « endl; cout« "sum<2, double>(X) = " << sum<2, double>(X) « endl; return 0; }
478 Глава 16 Результат выполнения программы: sum<3, int>(A, п-3) =20 sum<3>(A, п-3) = 20 surn<0>(X) = 10 sum<2, double>(X) =3.3 16.2. Явная специализация шаблонной функции При использовании шаблонов функций возможна перегрузка. Могут быть шаблоны с одинаковыми именами шаблонных функций, но разными спецификациями параметров. Наряду с этим с помощью шаблона может создаваться функция с таким же именем, что и явно определенная функция. В обоих случаях "распознавание" конкретного вызова выполняется по сигнатуре, т.е. по типам, порядку и количеству аргументов и по тому пространству имен, к которому относится имя функции. В некоторых случаях для конкретного варианта типов аргументов может потребоваться особое поведение шаблонной функции. Особое в данном случае означает отличное от того поведения, которое предусмотрено алгоритмом тела шаблонной функции. При этом программист может написать отдельное определение шаблона функции, которое называют явной специализацией шаблонной функции. Покажем на примере, как это сделать. Напомним назначение параметров определенного ранее шаблона функций, вычисляющих сумму элементов сегмента массива. Прототип шаблона: template < int N, typename T > T sum( Т ar[ ], int len= 1); Здесь N - начало сегмента; Т - тип элементов массива. Параметры шаблонной функции: аг[ ] - массив, сегмент которого обрабатывается; 1еп - длина сегмента. Предположим, что для массивов с элементами типа char нужно особым образом обрабатывать сегменты, индекс начала которых равен нулю. Пусть, например, нужно возвращать символ, код которого наиболее близок (снизу) к среднему значению кодов символов сегмента. Тогда обращению sum<0,char>( массив, длина) должно
соответствоШаблоны функций и классов 479 вать особое определение шаблонной функции. Это особое определение, т. е. явную специализацию шаблона нужно разместить в том же пространстве имен, что и общее определение шаблона. В нашем примере поместим явную специализацию в том же файле, где находиться общая специализация. При этом общий шаблон должен быть определен или описан прототипом до явной специализации. Пример программы с общим шаблоном и специализацией: //Р16_05.срр - Явная специализация шаблонной функции #include <iostream> using namespace std; template < int N, typename T > T sum(Tar[ ], int len= 1) { T ss=T(0); forfint i=N; i<N+len; i++) ss += ar[i]; return ss; > template < >//Явная специализация шаблона char sum <0, char> (charar[ ], intlen) { int sr = 0; for(inti=0; i<len; i++) sr += ar[i]; return (char)(sr/len); } int main() { char hh[] = {'Г,'2\ ’3\ *4\’5’, ’6’, V, 37; cout« "sum<0,char>(hh,8)="« sum<0,char>(hh,Q); return 0; } Результат выполнения программы: sum<0, char>(hh, 8)-4 В программе явная специализация шаблона функций введена заголовком: template < > char sum <0, char> (char аг[ ], intlen)
480 Глава 16 Такому определению шаблонной функции соответствует приведенное в функции main() обращение: sum<0,char>(hh,8), где hh - имя массива с элементами типа char. Каждый из элементов представляет символ цифры от Т до '8'. Усреднив их коды, функция возвращает символ Ч\ Префикс template < > не содержит параметров — это означает, что в этой конкретной специализации параметры шаблона отсутствуют. Однако в общем шаблоне может быть несколько параметров, и те или иные специализации могут содержать их разные сочетания. Специализацию называют более специализированной по сравнению с другой, если каждый список аргументов, соответствующих первой специализации, также соответствует и второй специализации, но не наоборот. Мы не будем приводить примеры нескольких специализаций одного шаблона, а рекомендуем заинтересованному читателю обратиться к соответствующим монографиям (Б. Страуструп [1], А. Александреску [18]). 16.3. Шаблоны классов Шаблоны классов, которые иногда называют родовыми или параметризованными типами, позволяют создавать (конструировать) семейства "родственных" классов. Определение шаблона семейства классов имеет такой формат: template <список_параметров_шаблона> спецификация_шаблонного_класса Здесь угловые скобки являются неотъемлемым элементом определения. Список параметров шаблона должен быть заключен именно в угловые скобки. Как и параметры шаблона функций, параметры шаблона классов и соответствующие им аргументы могут быть трех видов: 1) типизирующие (задающие тип в спецификации шаблонного класса); 2) нетипизирующие; 3) параметры-шаблоны. Каждый типизирующий параметр шаблона вводится служебным словом class или typename. Шаблон семейства классов вводит "архитектуру" конкретных классов.
Шаблоны функций и классов 481 Шаблон семейства классов служит для автоматического формирования определений конкретных классов — специализаций шаблона (классов). Специализации создаются исходя из тех обращений к шаблонному классу, которые транслятор встречает в тексте программы. Как и в случае шаблона функций, инстанцирование шаблона — это генерация текста класса из определения шаблона на основе его использования с конкретными аргументами. Рассмотрим пример шаблона классов, в котором все методы определены в спецификации шаблонного класса: template <typename А> class ourPair { А х, у; public: ourPair(A хп=А(0), А уп=А(1)): х(хп), у(уп) { } A getDiv(void) { return А(х/у);} ourPair <A> opera tor+( о и rPair <A> & p) { return ourPair <A> (x+p.x, y+p.y);} void displayf) { cout <<"x="<<x<<", y="«y«endl;} }; В определении шаблона классов особую роль играет имя (в нашем примере ourPair), использованное в спецификации шаблонного класса. Оно является не именем отдельного класса, а параметризуемым при последующих использованиях именем семейства классов. В шаблонный класс входят: два закрытых поля данных х ,у (их тип определяет параметр шаблона), конструктор и три метода, иллюстрирующие разные применения параметра шаблона классов и имени шаблонного класса. Метод getDivf) возвращает частное от деления значений полей данных объекта класса. Тип возвращаемого этой функцией значения определяется параметром шаблона. Метод operator^) выполняет перегрузку операции сложения. Тип параметра и возвращаемого значения — параметризованное имя шаблонного класса ourPair<A>. Метод displayf) не имеет параметров и ничего не возвращает, параметр шаблона в нем не используется. 3 j -2762
482 Глава 16 Как и в случае обычных (не шаблонных) классов компилятор автоматически включит в класс ourPair<A> деструктор, конструктор копирования и операцию-функцию присваивания с такими прототипами: -ourPairf); our Pair (const ourPair <4> &); ourPair <A> & operator={const ourPair <A> &); В данном примере все методы определены в спецификации класса. В этом случае они обычно реализуются компилятором как встроенные. Их синтаксис мало отличается от синтаксиса определений методов обычного класса. Исключение — параметризация имени шаблонного класса при обозначении типа. Остановимся на отличии имени шаблонного класса (в примере ourPair) от обозначения типа. Классу, вводимому приведенным шаблоном, соответствует тип ourPair<A>, где А - параметр шаблона, a ourPair - имя шаблонного класса. Таким образом, если нам нужно указать тип, определяемый шаблонным классом, то необходимо использовать конструкцию: имя_шаблонного_класса<список_имен_параметров>. Например, в заголовке операции-функции для перегрузки операции суммирования + используется конструкция ourPair<A>. Имя шаблонного класса без списка имен параметров входит в определения конструкторов и деструктора. Параметр шаблона (в примере А) в спецификации шаблонного класса используется как имя типа. Когда шаблон классов введен, у программиста появляется возможность определять конкретные объекты конкретных классов, каждый из которых параметрически порожден из шаблона. Формат определения объекта одного из классов, порождаемых шаблоном классов: имя_класса<аргументы_шаблона> имя_объекта {аргументы_конструктора); Для обозначения конкретного класса используется тип шаблонного класса, в котором вместо параметров указаны аргументы.
Шаблоны функций и классов 483 Пример применения шаблона ourPair <А> (Р16_06.срр): intmainf) { ourPair <int> one, two( 12,5); //Два объекта одного класса cout«"one:\t"; one.displayf); // one. ourPair <int>::display(); cout< < "two. getDiv()="< < two. getDivf) < <endl; one = two; // one. ourPair <int>::operator=(two); cout«"one:\t"; one.display(); one=one+two; // one = one. ourPair <int>::operator+(two); cout«"one:\t"; one.displayf); ourPair <double> next( 10.0,3.0); // Объект другого класса cout< < "next. getDiv()= "< <next. getDiv()« end!; return 0; } Результаты выполнения: one: x-0, y= 1 two.getDivf )=2 one: x=12,y=5 one: x=24,y=10 next.getDiv()= 3.33333 В приведенной программе на основе шаблона классов создано два определения конкретных классов с элементами типов Int и double соответственно. Конструкторы именно этих классов создают объекты one, two, next, и для них вызываются методы getDivf), display()} operator+( ). При каждом из этих вызовов конкретная специализация каждой из функций соответствует тому типу, который задан для соответствующего объекта. Этот тип можно указать явно, квалифицируя каждую функцию типом конкретного класса, что несколько загромождает записи. Использованные в программе обращения к методам можно заменить такими конструкциями: one. ourPair <int>::display() эквивалент one. displayf) two.ourPair <int>::getDiv() эквивалент two.getDivf) next.ourPair <double>::getDiv() эквивалент next.getDivf) one.ourPair <int>::operator+(two) эквивалент one.operator+ftwo) 3f
484 Глава 16 При выполнении присваивания one = two; неявно вызывается созданный конструктором метод с прототипом: ourPair <int> & operator^const ourPair <lnt> &); Ее вызов можно сделать явным, заменив оператор one = two; таким обращением, указанным в комментарии. 16.4. Внешнее определение методов и дружественные функции шаблонных классов Отметим, что спецификация шаблонного класса (как и обычного класса) может не включать полных определений его методов. В этом случае необходимо их внешнее определение. При этом синтаксис определения этих функций в спецификации шаблонного класса и вне ее различен. Особым образом необходимо определять и дружественные функции шаблонных классов. Рассмотрим это на примере. Шаблоны классов часто используются для создания классов- контейнеров, "хранящих" элементы, тип которых заранее неизвестен и определяется при каждом инстанцировании шаблона. Введем шаблон классов "последовательность значений", тип которых определяет параметр шаблона. В шаблонном классе введем как поля данных: • реальную длину последовательности (int size)', • массив фиксированных размеров (достаточно большой) для хранения значений последовательности data[MAXSIZEJ; В качестве методов определим: • конструктор, позволяющий формировать объект (последовательность) по массиву-параметру; • конструктор умолчания, формирующий объект — пустую последовательность; • операцию-функцию конкатенации последовательностей (перегрузка операции +=). Кроме того, определим для шаблонного класса как дружественные: операцию-функцию вывода (перегрузка операции <<) и функцию, возвращающую ссылку на первый элемент последовательности.
Шаблоны функций и классов 485 Шаблон классов «последовательность значений» (Р16_07.срр и файл lineT.h): const int MAXSIZE=255; template <typename T> class lineT { Tdata[MAXSIZE]; int size; public: lineT(Tarray[], int к); // Конструктор общего вида НпеТ(): size(O) { } //Конструктор умолчания // Конкатенация последовательностей: HneT<T> & operator+=(lineT<T>); // Шаблон дружественной операции-функции для вывода: template <typename М> friend ostream & operator«(ostream &, HneT<M> &); //Первый элемент последовательности: template <typename F> friend F& getFirst(lineT<F>); }; В шаблонном классе с именем НпеТ поля данных собственные (закрытые): int size - длина реальной последовательности; Т data[MAXSIZE] - массив для значений членов последовательности. Элементы массива имеют тип, заданный параметром шаблона Т. До текста шаблона определена глобальная константа MAXSIZE, задающая предельный размер массива для элементов (членов) последовательности. Реальное количество использованных элементов массива определяет поле данных size. В шаблонном классе два конструктора. Конструктор умолчания формирует пустую последовательность. Конструктор общего вида получает в качестве параметров указатель на массив объектов, тип которых определяет параметр шаблона, и целое значение — размер массива-параметра. Операция-функция operator+=( ) в качестве параметра получает объект шаблонного класса и возвращает ссылку на объект того же типа.
486 Глава 16 Еще раз остановимся на отличии имени шаблонного класса от обозначения типа, вводимого шаблоном. Конструкция НпеТ<Т>, т.е. «параметризованное» имя шаблонного класса, — это название типа. Обратите внимание, что параметризацию не нужно использовать в имени конструктора, в имени деструктора и в обозначении класса в самом начале после ключа класса (т. е. служебного слова class или struct). Во всех остальных случаях после имени класса необходимо указать имена его параметров, т.е. использовать параметризованное имя типа, вводимого шаблоном. Отметим также, что описание дружественной функции в спецификации класса является прототипом шаблона функций. Имена параметров этого шаблона (typename М, typename F) параметризуют заголовок шаблонной функции. Второй параметр дружественной функции operator« () - ссылка на объект шаблонного класса НпеТ<М> &. Первый параметр и возвращаемое значение не зависят от параметров шаблона. Параметр дружественной функции friend F& getFirstf lineT<F>); - объект шаблонного класса, возвращаемое значение - ссылка на первый элемент последовательности, представленной объектом- параметром. Методы шаблонного класса при внешнем (по отношению к спецификации класса) определении вводятся как шаблоны функций, "построенные " по такой схеме: template <список_параметров_шаблона_классов> тип_возвращаемого_значения имя_класса < список_имен_параметров_шаблона >:: имя_шаблонной_функции (спецификация_параметров_функции) тело_шаблонной_функции В заголовке шаблона функций используются параметры шаблона того класса, которому принадлежит метод (функция). Тип возвращаемого значения (для конструкторов он отсутствует) может зависеть от параметров шаблона классов. Конструкция имя_класса <список_именпараметров_шаблона>\\ определяет принадлежность метода именно той специализации класса, для объектов которой будет использован метод.
Шаблоны функций и классов 487 Приведем внешние определения методов шаблонного класса для нашего примера. Определение шаблона метода конкатенации последовательностей: template <typename Т> НпеТ<Т> &// Тип возвращаемого значения НпеТ<Т>:: //Пространство имен operator+=(lineT<T> аг) { iffsize+ar.size >= MAXSIZE) { cerr«"Size error!"; ехЩ 1); } for (inti = 0; i < ar.size; i++) data[i+size] = ar.data[ij; size+=ar.size; return *this; } Чтобы подчеркнуть соответствие структуры шаблона метода приведенному формату, мы разместили на отдельных строках тип возвращаемого значения (НпеТ<Т> &) и конструкцию НпеТ<Т>::, входящую в полное определение имени шаблонной функции. Алгоритм шаблонной функции, мы надеемся, пояснений не требует. Внешнее определение конструктора шаблонного класса: template <typename Т> НпеТ<Т>::ИпеТ(Таггау[], intk) { Щк < 0 11 к >= MAXSIZE) { cerr«"Size error!"; ехЩ 1); } size=k; for(int i=0; i < k; i++) data[i]=array[i]; } Еще раз обратите внимание, что имя конструктора совпадает с именем шаблонного класса, но не параметризируется. Кроме того, у конструктора как обычно нет возвращаемого значения.
488 Глава 16 Как мы показали на примере спецификации шаблонного класса НпеТ, дружественные функции вводятся в спецификацию класса как прототипы шаблонов функций. Единственное отличие — применение служебного слова friend. Имена параметров прототипа шаблона дружественных функций не должны совпадать с именами параметров самого шаблона классов. Еще раз покажем эти прототипы: template <typename М> friend ostream & operator«(ostream &, НпеТ<М> &); template <typename F> friend F & getFirstf lineT <F>); Вне шаблона классов дружественные для него функции определяются как обычные шаблоны функций: template <списокпараметров_шаблона_классов> шаблонная_функция Особенность такой шаблонной функции — применение параметризованного обозначения шаблонного класса во всех позициях, где нужен тип класса, вводимого шаблоном. В нашем примере определение шаблона дружественных функций вывода может быть таким (отметьте независимость имен параметров в прототипе и в шаблоне): template <typename Т> ostream & operator«(ostream & ou, HneT<T> & ar) { ou« "size="«ar.size« "\nLine elements: "«endl; forfint i=0; i < ar.size; i++) ou«ar.data[i]«((i+1)%11?”; ou«endl; return ou; } Отметим, что при выводе запланировано ограниченное количество значений последовательности, размещаемых на одной строке (не более 11). Шаблон дружественных функций для обращения к первому элементу последовательности:
Шаблоны функций и классов 489 template <typename S> S & getFirstflineT <S> f) {return f.data[0];} Проиллюстрируем использование шаблона классов «последовательность элементов». Для этого поместим определение шаблонного класса и внешние определения методов и дружественных функций в заголовочном файле lineT.h. и используем включение этого файла в следующей программе: //Р16_07.срр - Применение шаблона классов #include <iostream> using namespace std; #include lineT.h"//Шаблон классов int main() { intar[ ]={ 1,2,3,4,5,6,7,8,9,01; int ien=sizeof(ar)/sizeof(ar[0]); HneT <int> lar(ar,len); cout«lar; HneT <int> lor(ar,len); lar+=lor; cout«lar; double dom[]={2.2,4.4,6.6,8.8}; lineT<double> HneD(dom,s\zeof(dom)/sizeof(dom[0])); cout«lineD; cout< < "getFirst(lineD)="< <getFirst(lineD)« endl; lineT<char> str("asdfgh",6); cout«str; lineT<char> row( "0123456789", 10); str+=row; cout«str; return 0; } Результаты выполнения программы: size=10 Line elements: 1; 2; 3; 4; 5; 6; 7; 8; 9; 0; size=20 Line elements:
490 Глава 16 1; 2; 3; 4; 5; 6; 7; 8; 9; 0; 1; 2; 3; 4; 5; 6; 7; 8; 9; 0; size=4 Line elements: 2.2; 4.4; 6.6; 8.8; getFirstf HneD)=2.2 size=6 Line elements: a; s; d; f; g; h; size=16 Line elements: a; s; d; f; g; h; 0; 1; 2; 3; 4; 5; 6; 7; 8; 9; В программе шаблон семейства классов НпеТ<Т> создает три класса с последовательностями значений типов Int, double и char. В соответствии с требованием синтаксиса имя класса используется в нашей функции main() только с аргументами, заключенными в угловые скобки. Использовать имя шаблонного класса НпеТ без указания аргумента шаблона нельзя — никакое умалчиваемое значение параметра в данном шаблоне классов не предусматривается. Однако в отличие от шаблонов функций для параметров шаблона классов можно указывать умалчиваемые значения параметров. В качестве примера приведем шаблон классов для представления массивов фиксированного размера, определяемого нетипизирующим параметром шаблона. Другой (типизирующий) параметр шаблона будет задавать тип элементов массива. Для параметров шаблона зададим умалчиваемые значения. В шаблонном классе определим конструктор умолчания и операцию-функцию индексирования, в которой предусмотрим проверку выхода индекса за границы массива. //Р16_08.срр - Шаблон классов с нетипизирующим параметром. // Умалчиваемые значения параметров, ttinclude <iostream> using namespace std; template <typename T=char, int size = 64> class row { T data[size];
Шаблоны функций и классов 491 int length; public: row(): lengthf size) { } T& operator [] (int i) { if (i<0 11 />= size) { cerr«"lndex error!"; exit(1); } return data[i]; ) }; // Конец шаблона классов ttdefine LENGTH 5 intmainf) { row <double,LENGTH> rf; row <int> ri; // Умалчиваемое значение для size row о гс; // Умолчание для обоих параметров for (inti = 0; i < LENGTH; i++) { rf[i] = i; ri[i] = / * i; rc[i] = 'A'+i;f for (int i = 0; i < LENGTH; i++) cout« rf[i] «"« ri[i]«' '«rc[i]« "\t"; return 0; ) Результат выполнения программы: 00A 11В 24C 39D 4 16E В качестве аргумента, заменяющего при первом обращении к шаблону параметр size, взята константа, определенная препро- цессорным идентификатором LENGTH. В общем случае может быть использовано константное выражение, однако выражения, содержащие переменные, использовать в качестве аргументов шаблона нельзя. Параметры шаблона в нашем примере имеют умалчиваемые значения. Для параметра Т это имя базового типа char. Параметр int size имеет умалчиваемое значение, заданное константой 64. Именно это значение использовано в нашем примере при определении объектов row<int> ri и row<> гс. Для последнего из них и первый (типизирующий) параметр шаблона выбран по умолчанию. Обратите внимание, что использовать в этом случае только имя шаблонного класса row без скобок О нельзя.
492 Глава 16 16.5. Специализации шаблонов классов Шаблоны по существу можно считать одной из разновидностей макросов. Они обладают их достоинствами и недостатками. С помощью шаблонов программист определяет метасредства языка и описывает действия, которые должен выполнить компилятор после стадии препроцессорной обработки текста программы. Изобразим схематично этот процесс для приведенной программы с шаблоном классов row< >. (рис. 16.2). Исходный текст Шаблон классов class row {...} Функция main () Объект row <double,8>^ Объект row <int> Объект row < > Обработка объектов Текст для компиляции Специализация class row <double,8> Специализация class row <int,64> Специализация class row <c/iar,64> Функция main () Объект row <double,8> Объект row <int,64> Объект row <char,64> Обработка объектов Рис. 16.2. Конкретизация шаблона классов В исходном тексте программы на рис. 16.2 определен один шаблон классов (параметризованный класс), а количество определений классов, т.е. количество сгенерированных специализаций, равно числу «настроек» шаблона на разные типы данных. Таким образом, объем формируемого компилятором кода зависит от количества специализаций шаблона классов. Это незаметно для несложных классов, подобных row< >, но для более громоздких классов может существенно увеличивать размеры исполнимого модуля. Зная определение шаблона классов и набор аргументов, использованных при обращении к шаблону, компилятор
формируШаблоны функций и классов 493 ет спецификацию конкретного класса и создает определения его методов. Сформированные классы и методы называют сгенерированными специализациями. Термин "сгенерированные" подчеркивает "автоматизированный" характер получения конкретного определения класса. При такой автоматизированной генерации каждый вариант класса получает одинаковую по набору полей данных спецификацию и одинаковые алгоритмы методов. В ряде случаев это неудобно, так как для конкретного набора аргументов шаблона могут существовать более эффективные алгоритмы методов и/или более подходящие наборы полей данных. В таких ситуациях автор шаблона классов может предусмотреть пользовательскую специализацию шаблона класса. (Иногда ее называют просто специализацией шаблона, а о сгенерированных специализациях вообще не упоминают.) Пользовательская специализация шаблона классов может быть явной (полной) либо частичной. Явная (полная) специализация шаблона классов представляет собой конкретную реализацию класса для фиксированной комбинации аргументов шаблона. Явная специализация размещается после определения шаблона классов и вводится конструкцией template < > спецификацияпараметризованного_класса Обратите внимание на пустой список параметров шаблона в скобках < >. В спецификации параметризованного класса после служебного слова class указывается имя шаблонного класса с аргументами в угловых скобках. Таким образом, заголовок параметризованного класса имеет вид: class имя_шаблонного_класса <список_аргументов_шаблона> В теле класса специализации каждое появление параметра шаблона должно заменяться конкретным аргументом шаблона, указанным в заголовке. Имена конструкторов и деструктора не параметризуются. В качестве примера введем пользовательскую специализацию рассмотренного ранее шаблона классов ourPair <А>. Выполним эту специализацию для данных типа char (Р16_09.срр):
494 Глава 16 //Пользовательская специализация шаблона классов ourPair <А>: template <> class ourPair <char> { char x, у; public: ourPair(charxn=A', charyn='Z'): x(xn), y(yn) { } char getDivfvoid) { return char(x > у ? y+x/y: x+y/x); } ourPair <char> operator*!ourPair <char> & p) {return ourPair <char> (x+p.x, y+p.y);} void display() {cout «"x="«x«" y="«y«endl;} }; В специализации по сравнению с шаблонным классом изменены конструктор и метод getDiv(). В конструкторе параметры имеют в качестве умалчиваемых значений символьные константы. В функции getDiv() изменен алгоритм. Функция теперь возвращает символ, код которого вычисляется по кодам полей данных х, у. К меньшему из кодов данных прибавляется частное от деления максимального кода на минимальный. Следующая программа (Р16_09.срр) иллюстрирует применение шаблона классов ourPair <А> при наличии пользовательской специализации. (Предполагается, что определение шаблона классов и пользовательской специализации включены в текст программы.) intmainf) { ourPair <int> one, two( 12,5); cout«"one:\t"; one.display!); cout«"two.getDiv( )="«two.getDiv( )< <endl; ourPair <char> ch1, ch2('В', Y), ch3( <', T); cout<<"ch 1:\t"; ch 1.display!); cout«”ch2:\t"; ch2.display!); cout«"ch3:\t”; ch3.display!); cout< < ”ch2. getDiv()=,,«ch2. getDiv()«endl; ch3=ch1+ch3; cout<< "ch 1 +ch3:\ t"; ch3. display!); return 0; }
Шаблоны функций и классов 495 Для определения объектов one, two( 12,5) используется автоматически формируемая (генерируемая) специализация. Объекты ch1, ch2('B','y'), ch3('<','!') создаются с использованием пользовательской специализации ourPair<char> того же шаблона классов. Аргументы конструктора подобраны так, чтобы результаты выполнения getDivf) и суммирования объектов были кодами отображаемых символов. Результаты выполнения: one: х-0, у= 1 two. getDivf )-2 ch1: х=А, y=Z ch2: х=В, у=у ch3: х=<, у=/ ch2.getDivf )=С ch1+ch3: х=},у={ В качестве второго примера введем специализацию рассмотренного ранее шаблонного класса НпеТ < > для случая, когда представляемая объектом класса НпеТ < > последовательность должна содержать строку в стиле Си, т.е. включает терминальный символ '\0’ в конце последовательности элементов типа char. Здесь придется вводить специализации методов при их внешнем определении и определять дружественные функции для явной пользовательской специализации шаблона классов. Предположим, что исходный шаблон классов уже определен и присутствует в тексте (он размещен в файле lineT.h). Тогда может быть введена следующая явная пользовательская специализация шаблона классов НпеТ: template о class НпеТ <char> { char data[MAXSIZE]; int size; public: lineTfchar * str=0) { //Конструктор size=(int)strlen(str); iff size >=MAXSIZE) { cerr«"Size error!”; exitf 1); }
496 Глава 16 if(str != 0) strcpy(data,str); } //Конкатенация строк (прототип): lineT<char> & operator+=(lineT<char>); // Вывод строки (дружественная функция): friend ostream & operator«(ostream & ou, lineT<char> & lin); }; Напомним, что MAXSIZE — это константа. При использовании строки (в стиле Си) в качестве аргумента нет необходимости передавать функции ее длину Поэтому для класса lineT<char> в специализации введен дополнительный конструктор, которого нет в общем шаблоне классов НпеТ< >. Его параметр — строка в стиле Си. В теле конструктора с помощью библиотечной функции strlen() вычисляется длина строки, и это значение присваивается полю данных size. Поле (массив) data получает значение при выполнении библиотечной функции strcpy(). Эта функция выполняет копирование строки в стиле Си, адресуемой вторым параметром, в массив, адресуемый первым параметром. Внешнее определение дружественной функции вывода (это не шаблон!): ostream & operator«(ostream & ou, lineT<char> & ar) { ou« "My string: size= "<<ar.size«endl; ou«ar.data«endl; return ou; } В специализации lineT<char> поле данных data является строкой в стиле Си. Поэтому в теле операции-функции operator«() отсутствует оператор цикла, который был необходим для вывода последовательностей общего вида. Оператор ou«ar.data\ обеспечивает вывод всех символов строки (массива) data до терминального кода '\0 Внешнее определение операции-функции конкатенации: HneT<char> & lineT<char>::operator+=(lineT<char> ar) { if(size+ar.size >= MAXSIZE) { cerr< < "Size error!
Шаблоны функций и классов 497 exit( 1); } strcat(data,ar.data); size+=ar.size; return *this; } В операции-функции operator+= () выполняется конкатенация двух строк в стиле Си с помощью библиотечной функции strcat(). Применение специализации lineT<char> иллюстрирует следующая программа (Р16_10.срр). intmainf) { lineT<char> str( ”Figures: "); // Специализация для char cout«str; lineT<char> row( "0123456789"); // Специализация для char str+=row; cout«str; int ar[ ]={ 1,2,3,4,5,6,7,8,9,0}; int len=sizeof(ar)/sizeof(ar[0]); lineT <int> lar(ar,len); // Общий шаблон cout«lar; return 0; } Результаты выполнения программы: My string: size=9 Figures: My string: size=19 Figures: 0123456789 size=10 Line elements: 1; 2; 3; 4; 5; 6; 7; 8; 9; 0; Напомним, что текст общего определения шаблона классов должен быть размещен до его специализации. В основной программе определены два объекта с именами (str и chain) класса, вводимого пользовательской специализацией. В ней особым образом определена операция +=, она выполняет конкатенацию строк. Объект lar создается классом, полученным на основе 32"2762
498 Глава 16 сгенерированной специализации шаблона, т.е. имеет тип lineT <int>. Для него вывод выполняет дружественная функция общего шаблона классов operator«(). Она введена, как мы уже рассматривали, шаблоном функций и в данном случае «настраивается» аргументом int. 16.6. Частичная пользовательская специализация Итак, мы уже знаем, что в шаблоне классов текст определения шаблонного класса параметризирован, т.е. для его использования в качестве конкретного типа, шаблонный класс должен быть «настроен» с помощью аргументов шаблона. Результат указанной настройки, т.е. текст определения класса, называют специализацией шаблонного класса. Специализация шаблонного класса может быть сгенерированной (создается компилятором на основе контекста) и пользовательской. Сгенерированная специализация не видна пользователю, ее текст формируется автоматически и непосредственно компилируется. Пользовательская специализация представляет собой текст, непосредственно подготовленный программистом и включенный в исходный текст программы. Пользовательская специализация может быть явной (полной) и частичной. Явная специализация представляет собой полностью конкретизированное с помощью аргументов шаблона определение шаблонного класса. Примеры явной специализации мы уже приводили. Напомним, что при специализации шаблона классов необходимо специализировать все его методы и не забыть о дружественных функциях. Теперь коротко рассмотрим частичную специализацию. Как мы уже знаем, явная специализация шаблонного класса вводится конструкцией template О, за которой после имени класса помещаются аргументы, для которых выполняется специализация. Примеры явной специализации, которые мы уже приводили: template о class ourPair <char> template <> class lineT <char>
Шаблоны функций и классов 499 После каждой из этих конструкций размещается тело класса, в котором каждый параметр шаблона заменен специализированным типом. Следовательно, текст шаблонного класса в этом случае превращается в результат подстановки вместо параметров шаблона фактических значений (аргументов), указанных после имени класса. Таким образом, при явной специализации список параметров шаблона пуст, а после имени класса указаны все аргументы, для которых это определение шаблонного класса должно быть использовано. Явная (иначе полная) специализация входит в механизм шаблонов с начала его появления. Частичная специализация появилась гораздо позже. Шаблоны частичной специализации создаются при частичной специализации на основе исходного (первичного) шаблона классов. В каждом из них после имени класса указаны в угловых скобках некоторые аргументы шаблона, а имена тех параметров первичного шаблона, для которых аргументы не заданы, непосредственно входят в определение шаблонного класса, параметризуя его. Так как применение частичной специализации в практике программирования используется далеко не всегда, то мы приведем здесь только основные схемы, а за подробностями отошлем читателя к монографии Д.Вандервуда, Н.Джосаттиса [14 с. 51 — 52, 225 - 227]. Пусть определен такой шаблон семейства классов (первичный или основной шаблон): template <typename Т1, typename Т2> class myClass { ; Специализация, в которой оба параметра имеют один тип: template <typename Т> class myClass <Т,Т> { ; Специализация, в которой второй параметр шаблонного класса нетипизирующий: 32*
500 Глава 16 template <typename T> class myClass <T,int> { } Специализация, в которой оба параметра указатели (возможно, разных типов): template <typename Т1, typename Т2> class myClass <Т1*,Т2*> { } Определения объектов с помощью приведенных шаблонных классов: myClass <int, double> mif; //myClass <T1, T2> myClass <double, double> mff; //myClass <T, T> myClass <double, int> mfi; //myClass <T, int> myClass <int *, double *> mp; //myClass <T1 *, T2*> Объект mif определен с помощью первичного шаблона классов. Для определения объектов mff, mfi, mp используются частичные специализации. Ошибочные применения (неоднозначность указания шаблона): myClass <lnt, lnt> m; - претенденты myClass <T, T> и myClass <Т, int>. myClass <lnt *, int *> m; - претенденты: myClass <T1 *, T2 *> и myClass <T, T>. Формат частичной специализации: template <список_параметров_шаблона> class <список_аргументов_шаблона> { частичнопараметризованное_тело_шаблонного_класса }; Список параметров_шаблона называют списком параметров частичной специализации (в случае частичной специализации он не может быть пустым). В списке параметров частичной специализации могут присутствовать не все параметры первичного шаблона. Параметры частичной специализации не могут иметь умалчиваемых значений. Список_аргументов_шаблона называют списком аргументов частичной специализации. Он не может быть пустым. Аргумент
Шаблоны функций и классов 501 частичной специализации должен соответствовать виду параметра (т.е. задавать тип, значение или шаблон). Аргумент частичной специализации, не задающий тип, должен быть либо независимым значением, либо простым параметром, не являющимся типом. Список аргументов шаблона частичной специализации не должен быть идентичен списку параметров первичного шаблона. 16.7. Объекты и массивы объектов шаблонных классов Объект шаблонных классов. При определении и использовании объектов шаблонных классов и массивов с элементами-объектами шаблонных классов имеются некоторые синтаксические особенности. Вначале рассмотрим разные способы определений отдельных объектов, точнее, приведем форматы таких определений. В автоматически распределяемой памяти для создания объектов могут применяться перечисляемые ниже конструкции имя_класса <аргументы_шаблона> имя_объекта\ При таком определении компилятор, учитывая аргументы шаблона, формирует специализацию шаблонного класса, и для создания объекта вызывается конструктор умолчания специализации. Для определения значений полей данных объекта могут использоваться умалчиваемые значения параметров этого конструктора; имя_класса <аргументы_шаблона> имя_объекта (аргументы_конструктора); Компилятор, учитывая аргументы шаблона, формирует специализацию шаблонного класса, и для создания объекта вызывается ее конструктор. В этом случае значения полей данных объекта задает конструктор, используя явно заданные аргументы. имя_класса <аргументы_шаблона> имя_объекта = существующий_объект_шаблонного_класса;
502 Глава 16 Компилятор, учитывая аргументы шаблона, формирует специализацию шаблонного класса, и для создания объекта вызывается ее конструктор копирования. Для определения значения полей данных объекта используются значения полей данных существующего (указанного справа от знака присваивания) объекта той же специализации шаблонного класса. имя_класса <аргументы_шаблона> имя_объекта = имя класса <аргументы_шаблона> (аргументы_конструктора); Компилятор, учитывая аргументы шаблона, формирует специализацию шаблонного класса, и справа от знака присваивания ее конструктор создает безымянный объект этой специализации. Далее значения полей данных этого объекта конструктор копирования присваивает полям данных объекта, поименованного слева от знака присваивания. В динамически распределяемой памяти также существует несколько возможностей создания объектов тех классов, которые определены шаблоном. Для последующего доступа к создаваемому объекту обычно сначала определяется указатель, тип которого задан конкретизацией типа шаблона: имя_класса <аргументы_шаблона> * point', Компилятор, учитывая аргументы шаблона, формирует специализацию шаблонного класса, и эта специализация определяет тип создаваемого указателя (point). Этому указателю можно присвоить адрес объекта, размещаемого при создании в динамической памяти. Используются приводимые ниже схемы. point = new имя_класса <аргументы_шаблона>; Компилятор, учитывая аргументы шаблона, формирует специализацию шаблонного класса. Операция new размещает в динамической памяти безымянный объект типа этой специализации, инициализированный ее конструктором умолчания point = new имя_класса <аргументы_шаблона> (аргументы_конструктора);
Шаблоны функций и классов 503 Компилятор, учитывая аргументы шаблона, формирует специализацию шаблонного класса. Операция fieiv размещает в динамической памяти безымянный объект типа этой специализации, инициализированный ее конструктором с учетом конкретных аргументов конструктора point = newимя_класса <аргуметы_шаблона> - существующий_объект; Компилятор, учитывая аргументы шаблона, формирует специализацию шаблонного класса. Операция new размещает в динамической памяти безымянный объект типа этой специализации, для инициализации этого объекта используется объект, указанный справа от знака присваивания. Применяя динамическое распределение памяти для объектов шаблонных классов, не забывайте об освобождении памяти (delete point). Чтобы проиллюстрировать некоторые особенности определения объектов шаблонных классов, введем шаблон классов «точечная масса» в трехмерном пространстве: template <typename Tt typename D> class massPoint { T x, y, z; //Координаты точки D mass; //Масса точки public: //Конструктор общего вида и умолчания: massPoint(Txn=T(0)t Tyn=T(0), Tzn=T(0), D mn=D(0)) : х(хп), у(уп), z(zn), massfmn) {} //Прототип шаблона функций для перегрузки вывода: template <typename X, typename Z> friend ostream & operator« (ostream & ou, massPoint<X, Z> & point); // Сравнение точечных масс по массам: bool operator<(massPoint<T, D> & point) { return (mass<point. mass?true:false); ) }; У шаблона классов два типизирующих параметра, один задает тип полей данных для координат точки (typename 7), второй
504 Глава 16 — тип поля данных для массы точки (typename D). Обратите внимание на спецификацию параметров конструктора. В ней для задания умалчиваемых значений аргументов использованы нулевые значения, приведенные к тому типу, который будет определен соответствующим аргументом шаблона классов. В операции- функции для сравнения объектов по массам точек operator<() для нас уже нет ничего нового. В спецификации шаблонного класса размещен прототип шаблона дружественных функций. У этого шаблона два параметра, имена которых (X, Z) отличны от имен Т, D параметров шаблона классов. Шаблон функций вывода, дружественных для шаблона классов, должен быть определен вне шаблонного класса, например, следующим образом: template <typename Т, typename D> ostream & operator«(ostream & ou, massPoint<T, D> & point) { ou< <" x= ”< <point.x; ou< < "\ ty= "< <point. y; ou« "\ tz= "< <point. z; ou« "\ tmass= ”< <point. mass; return ou; } Поместим спецификацию шаблона классов и шаблон дружественных функций в заголовочный файл massPoint.h. Используем этот файл в следующей программе, где определены разными способами объекты классов, каждый из которых является специализацией введенного шаблона классов: intmainf) { // Объекты в автоматической памяти: massPoint<int, double> obj1; massPoint<double, double> obj2(1.1f,2.2f,3.3f,4.4); massPoint<double, double> obj3=obj2; massPoint<double, double> obj4 =massPoint<double, double>(4.4,3.3,2.2,1.1); PRINT(obj1); PRINT(obj2); PRINT(obj3); PRINT(obj4);
Шаблоны функций и классов 505 //Объекты в динамической памяти: massPoint<double, int> * point; // Указатель point=new massPoint<double, int>;// Конструктор умолчания PRINT( *point); delete point; //Используем конструктор общего вида: point=new massPoint<double, int!>(5.5, 5.5, 5.5, 5); PRINT('point); delete point; return 0; } Результаты выполнения: obj1: x=0 y=0 z-0 mass=0 obj2: x=1.1 у-2.2 z-3.3 mass-4.4 obj3: x=1.1 у=2.2 z=3.3 mass=4.4 obj4: x-4.4 y-3.3 z-2.2 mass=1.1 *point: x-0 y-0 z-0 mass=0 *point: x=5.5 y=5.5 z=5.5 mass=5 Массивы объектов шаблонных классов. Если читатель понимает синтаксис определения объектов шаблонных классов (их специализаций), то определение массивов таких объектов его не затруднит. Как и обычные массивы, массивы объектов шаблонных классов можно создавать в автоматически и динамически распределяемой памяти. В автоматически распределяемой памяти возможны приводимые ниже форматы определений имя_класса <аргументы_шаблона> имя_массива [размер_массива]\ При таком определении компилятор, учитывая аргументы шаблона, формирует специализацию шаблонного класса, и для создания каждого элемента массива вызывается конструктор умолчания специализации. имя_класса <аргументы_шаблона> имя_массива [ ]= {список_инициализаторов}; Размер массива определяется количеством элементов списка инициализаторов. Компилятор, учитывая аргументы шаблона, формирует специализацию шаблонного класса, и при создании
506 Глава 16 каждого элемента массива вызывается конструктор копирования специализации. Для определения значений полей данных каждого объекта (элемента массива) используются значения полей данных соответствующего объекта из списка инициализаторов. Необходимо соответствие типов элементов списка инициализации и конкретной специализации шаблона класса. имя_класса <аргументы_шаблона> имя_массива [размер_массива]= {<список_инициализаторов}; Размер массива задан явно. Компилятор, учитывая аргументы шаблона, формирует специализацию шаблонного класса, и при создании каждого элемента массива вызывается конструктор копирования специализации. Для определения значений полей данных каждого объекта (элемента массива) используются значения полей данных соответствующего объекта из списка инициализаторов. Если список инициализаторов короче размера массива - остальные ("правые") элементы массива определяются конструктором умолчания специализации шаблона классов. Необходимо соответствие типов элементов списка инициализации и конкретной специализации шаблона класса. Элемент списка инициализаторов может иметь две формы: имя_класса <аргументы_шаблона> //конструктор умолчания или имя_класса <аргументы_шаблона> (аргументы_конструктора) Пример массива объектов «точечная масса»: massPoint <int, double> [] = {massPoiint <int, double> (1,2,3,4.0), massPoiint < int, double> (8,7,6,5.5), massPoiint <int, double> };//число элементов определено списком инициализации Для последующего доступа к создаваемому массиву в динамически распределяемой памяти обычно сначала определяется указатель, тип которого задан конкретизацией типа шаблона: имя_кпасса <аргументы_шаблона> * point, Компилятор, учитывая аргументы шаблона, формирует специализацию шаблонного класса, и эта специализация определяет тип создаваемого указателя (в нашем случае point). Этому
укаШаблоны функций и классов 507 зателю можно присвоить адрес массива объектов, размещаемого при его создании в динамической памяти. Используется следующая схема: point = new имя_класса <аргументы_шаблона > [размер_массива] Компилятор, учитывая аргументы шаблона, формирует специализацию шаблонного класса, и эта специализация определяет тип элементов массива. Массив создается как безымянный. Каждый элемент массива инициализируется конструктором умолчания созданной специализации шаблона классов. Пример формирования в динамической памяти массива объектов «точечная масса» с вещественными данными: massPoint <double, double> * pif; pif = new massPoint <double, double> [24]; Уничтожение массива и освобождение памяти выполняется как обычно выражением с операцией delete [ ]: delete [ ] point; Для нашего примера с массивом объектов «точечная масса»: delete [ ] pif; Некоторые особенности обработки массивов из объектов шаблонных классов рассмотрим на примере задачи сортировки. В следующей программе определим массив объектов шаблонного класса massPointO и выполним их упорядочение по убыванию масс точек. //Р16_12.срр - Массивы с объектами специализации шаблонного класса #include <iostream> using namespace std; #include "massPointh” // Определение шаблонного класса int main(){//Массив в автоматической памяти: massPoint<int, int> piAr[]={massPoint<int, int>( 1,2,3,4), massPoint<int, int>( 1,2,3,2),massPoint<int, int>(1,2,3,7), massPoint<int, int>( 1,2,3,5),massPoint<int, int>( 1,2,3,9)}; int len = sizeof(piAr)/sizeof(piAr[0]); //Длина массива //Явная сортировка по убыванию масс:
508 Глава 16 massPoint<int, int> work; // Вспомогательный объект forint j=0;j<len- 1;j++) forfint k=j+ 1;k<len;k++) if(piAr[j] < piAr[k]) { work=piAr[j]; piAr[j]=piAr[k]; piAr[k]=work; } cout«"Sorting array<int, int>:"«endl; forfint i=0;i<len;i++) cout< <piAr[i]< < endl; return 0; } Комментарии в тексте программы делают не обязательными подробные пояснения. Определен массив объектов типа massPoint<int, int>. Оба аргумента шаблона — обозначения базового класса int. Размеры массива и значения его элементов заданы списком инициализации. Для сортировки введен вспомогательный объект work той же специализации шаблонного класса. Сортировка выполнена примитивным алгоритмом. Отметим, что в классе massPointO перегружена операция вывода и поэтому для печати сведений об отдельном элементе массива достаточно оператора cout«piAr[i]. Результаты выполнения: Sorting array<int, int>: х-1 у=2 z=3 mass=9 х=1 у=2 z-3 mass=7 х=1 у=2 z-3 mass=5 Х=1 у=2 z-3 mass=4 Х=1 у=2 z-3 mass=2 Другие содержательные пр ментами которых являются объекты шаблонных классов (их специализаций), будут рассмотрены в следующей главе.
509 Глава 17 МЕХАНИЗМЫ, ИСПОЛЬЗОВАННЫЕ ПРИ ПОСТРОЕНИИ STL 17.1. Краткие сведения об STL Стандарт языка Си++ включает обширную библиотеку функций и классов, создающих эффективную среду для разработки программ на языке Си++. Библиотека функций в основном унаследована из языка Си, а библиотека классов разработана специально для языка Си++ в процессе его развития и стандартизации. Особое место в стандартной библиотеке Си++ занимает ее подмножество — Standard Template Library — стандартная библиотека шаблонов (STL). Основное назначение STL — обеспечить программиста типовыми структурами данных и наиболее эффективными алгоритмами, "настроенными" на обработку информации, представленной этими структурами данных. В отличие от традиционных библиотек функций и классов в STL типовые структуры данных и алгоритмы для их обработки представлены в виде шаблонов. Тем самым у программиста-поль- зователя имеется возможность настраивать структуры данных STL и ее алгоритмы на обработку самых разных типов данных. Указанная возможность "настройки" существенно расширяет универсальность и гибкость STL по сравнению с библиотеками функций и классов. Платой за названные преимущества STL является сложность ее применения. От программиста-пользователя требуется общая высокая квалификация и хорошее понимание принципов построения STL. Особенно требования к квалификации возрастают при необходимости расширения STL. А возможности такого расширения существуют. Программист может написать новый алгоритм, пригодный для работы с имеющимися в STL структурами данных, или добавить свою структуру данных, для обработки которой можно будет применять имеющиеся в STL алгоритмы.
510 Глава 17 Стандартная библиотека шаблонов состоит из двух основных частей - множества контейнеров (пригодных для включения элементов разных типов) и набора обобщенных алгоритмов, позволяющих выполнять типовые операции над контейнерами (над их элементами). Алгоритмы STL называют обобщенными потому, что они не зависят ни от вида контейнера, в котором находятся элементы, ни от типа находящихся в нем элементов. Независимость алгоритма от вида контейнера достигается за счет применения в STL еще одного механизма — итератора. Итератор представляет собой звено связи алгоритма с контейнером. Назначение итератора — обеспечить алгоритму универсальный доступ к элементам контейнера, не зависящий от его вида. Концепция итераторов более трудна для понимания по сравнению с обобщенными алгоритмами и контейнерами STL. Поэтому рассмотрим сначала обобщенные алгоритмы. 17.2. Шаблоны функций и обобщенные алгоритмы Под обобщенным алгоритмом в STL понимается алгоритм, пригодный для обработки множества контейнеров. Например, поиск элемента с заданными свойствами можно выполнить в массиве, в списке или в векторе, используя один и тот же алгоритм, но в каждом случае параметризуя (настраивая) его на конкретное представление последовательности. Обобщенный алгоритм независим как от вида контейнера, так и от типа хранимых в нем элементов. Указанная независимость обеспечивается следующими средствами: 1) алгоритм никогда не обращается к элементам контейнера непосредственно — он оперирует только с итераторами, которые ’’знают", к какому контейнеру они относятся и какой тип имеют объекты — элементы контейнера; 2) алгоритм реализован в виде шаблона; 3) алгоритмы ориентированы на работу с интервалами последовательностей, сохраняемых в контейнерах. Границы интервалов задаются с помощью итераторов. Независимость обобщенных алгоритмов от типов элементов контейнеров достигается за счет того, что каждый обобщенный
Механизмы, использованные при построении STL 511 алгоритм STL строится в виде шаблона функций. Обобщенные алгоритмы применимы не только к контейнерам библиотеки, но и к обычным массивам. Воспользовавшись этим обстоятельством и не рассматривая пока контейнеры, покажем, как создавать обобщенные алгоритмы, и продемонстрируем их возможности при работе с массивами (хотя обобщенные алгоритмы можно использовать и для обработки других последовательностей, например списков). При построении обобщенных алгоритмов авторами STL принято решение передавать в функцию адрес (beg) первого обрабатываемого элемента последовательности и адрес (end) элемента, дойдя до которого нужно окончить обработку. Это показано на рис. 17.1. Обрабатываемые элементы beg | | end Рис. 17.1. Пределы перебора последовательности элементов В теле обобщенного алгоритма цикл обработки нужно выполнять, последовательно увеличивая значение указателя beg до тех пор, пока не выполнится условие (beg == end). Указанной схеме соответствует такой шаблон функций для печати элементов последовательности (например, одномерного массива или списка) в несколько колонок: // Обобщенный алгоритм печати элементов последовательности: template <int к, typename Е> void iterPrint(E beg, E end) { int i=0; whilefbeg != end) { cout«"["«i++<< "]="< < *beg< <((/+1 )%k?"\t": "\n"); ++beg; ) cout«endl; )
512 Глава 17 Первый параметр к шаблона нетипизирующий, он позволяет указать количество столбцов, в которых размещаются выводимые элементы последовательности. Второй параметр Е задает тип элементов последовательности и тип адресов начала beg и окончания end поиска. Поместим определение шаблона в заголовочном файле "iterPrint.h”. В следующей программе (Р17_01.срр) функция (специализация) нашего шаблона печатает в трй столбца элементы целочисленного массива. Массив формируется динамически. Элементам присваиваются псевдослучайные значения в диапазоне от 0 до (хМах-1). Количество элементов в массиве и их предельное значение (хМах) вводит пользователь. #include "iterPrint.h"//Шаблон функций вывода последовательности #include <cstdlib> //Для библиотечной функции rand() intmainf) { intxMax, * array, n; cout«"Enter array size: cin » n; cout«"Enter max array element: cin »xMax; array = newint[n]; for(inti=0; i<n; i++) array[i] = rand()%xMax; iterPrint<3>( array, array+n); return 0; } Массив формируется в динамической памяти и имеет элементы типа int. Элементам с помощью библиотечной функции randQ присвоены случайные значения. При обращении к шаблонной функции нетипизирующий параметр int к заменен аргументом — константой 3. Значение аргумента, соответствующего второму — типизирующему параметру, определено типом аргументов в вызове iterPrint<3>(array, array+n) шаблонной функции. Они оба имеют тип int *. Первый адресует начало массива, второй - участок памяти, непосредственно расположенный за последним элементом массива. Так как никаких операций с данным участком памяти в функции не выполняется, то этот выход за пределы массива совершенно безопасен.
Механизмы, использованные при построении STL 513 Результаты выполнения программы: Enter array size: 8<ENTER> Enter max array element: 10<ENTER> [0]-2 [ 1]=9 [2]=2 [3]=9 [4]=8 [5]=5 [6]-1 [7]=1 В качестве второго примера обобщенного алгоритма рассмотрим следующий шаблон функций для поиска в последовательности (например, в массиве или списке) элемента с заданным значением. В шаблоне введем два параметра. Первый Т — для указания типа элементов и искомого в последовательности значения. Второй It — для задания типа "адресов" начала и окончания поиска. //Шаблон функции для поиска заданного значения: template <typename Т, typename lt> Т* findValueflt beg, It end, T value) { whilef beg != end) { if (* beg == value) return beg; ++beg; } return 0; ) У шаблонной функции findValue() три параметра, и тип каждого из них определен соответствующим параметром шаблона. Функция получает разыскиваемое в последовательности значение (параметр value), а также адреса начала beg и окончания end поиска; возвращает "адрес" найденного в последовательности (ближайшего к началу поиска) элемента, значение которого совпадает со значением параметра value. Если такой элемент отсутствует — возвращается нулевой адрес. Особенность алгоритма — тип искомого значения value должен совпадать с типом разыменования "адреса", т. е. с типом выражения * beg. Таким образом, параметры шаблона взаимозависимы и соответствующим образом должны быть выбраны аргументы. Поместим определение шаблона в заголовочном файле ,ffindValue.h,\ Теперь проиллюстрируем применение наших двух шаблонов (обобщенных алгоритмов) в следующей программе: 33 - 2762
514 Глава 17 //P17JD2.CPP - Обобщенные алгоритмы. #include <iostream> using namespace std; #include "iterPrint, h" // Шаблон функций для вывода #include "findValue.h" // Шаблон функции для поиска значения: #include <cstdlib> //Для библиотечной функции rand() intmainf) { double хМах, * array; int n; cout<< "Enter array size: cin » n; cout«"Enter max array element: cin » xMax;; array = new double[n]; for(inti=0; i<n; i++) array[i] = rand()%int(xMax); iterPrint<3>( array, array+n); double xVai-13, * result; result = findValuef array, array+n, xVai); cout«endl; if (result == 0) { cout«xVal«" is absent in array!"«endl; return 0; } cout<<"Index of ] "< <xVal< <"[ is "< <result-array«endl; return 0; } В программе создан динамический массив, адресуемый указателем double * array. Количество (переменная п) элементов массива и максимальное значение хМах каждого из них вводит пользователь. Элементы массива получают случайные значения. Обращение к специализации iterPrint<3>(array, array+n) шаблонной функции выводит массив в виде таблицы из трех столбцов. Оператор result = findValuef array, array+n, xVai); позволяет выполнить поиск в массиве фиксированного значения double xVal-13.
Механизмы, использованные при построении STL 515 Результаты выполнения программы: Enter array size: 9<ENTER> Enter max array element: 18<ENTER> [0]=10 [1]=13 [2]=10 [3]-5 [41=8 [5]=1 [6]=7 [7]=3 [8J= 13 Index of ]13[ is 1 Теперь обратим внимание на параметры наших обобщенных алгоритмов iterPrintf), findValuef), которые в заголовках шаблонов введены так: typename Е и typename It. Эти параметры шаблонов определяют типы параметров шаблонных функций, задающих пределы перебора элементов последовательностей. Для каждой из рассмотренных двух шаблонных функций эти параметры играют роль указателей. К ним в теле функции применяются: операция разыменования *, операция инкремента ++ и операция сравнения на неравенство !=. Запомните это — нам эти сведения понадобятся при изучении итераторов. Еще обратим внимание, что в STL рекомендуется использовать префиксные операторы перемещения итераторов (Джосаттис [14], с. 259). Дело в том, что при префиксной форме не нужно сохранять для последующего возврата старое значение операнда. 17.3. Контейнеры и итераторы Контейнер — представление коллекции (динамической структуры данных), в которую можно помещать объекты и из которой их можно получать обратно. В языке Си++ контейнер можно представить классом, объекты которого приспособлены для включения других объектов. В то же время тривиальным контейнером служит обыкновенный массив, встроенный как в язык Си++, так и в большинство других языков программирования. Назначение итератора - предоставить единый метод последовательного доступа (перебора) к элементам контейнера, не зависящий (метод) от вида контейнера и типа элементов в нем. Итератор может «пробегать» как все элементы контейнера, так и некоторое их подмножество. 33'
516 Глава 17 В результате использования итераторов алгоритм может «не замечать», с каким контейнером он работает. Тем самым появляется возможность унификации алгоритмов. Итератор обеспечивает: • единый механизм перебора элементов контейнера, не зависящий ни от его вида, ни от его реализации, ни от типа элементов; • множественный доступ к контейнеру, позволяющий работать с контейнером одновременно нескольким объектам-клиентам (при этом каждый из клиентов будет пользоваться своим итератором). При последовательном переборе элементов контейнера итератор не обязательно перемещается только по смежным (соседним) элементам. Алгоритм последовательного перебора для каждого вида итераторов и для различных контейнеров может быть определен по-своему. Но для алгоритма-клиента, который пользуется итератором, обращаясь к элементам контейнера, совокупность перебираемых элементов представляется в виде последовательности. Последовательность предполагает упорядоченность элементов. У последовательности есть начало - элемент, адресуемый итератором при первом обращении, и конец - не обрабатываемый алгоритмом элемент, при достижении которого необходимо прекратить обработку (перебор). Иллюстративную схему последовательности (см. рис. 17.1) мы привели, рассматривая шаблоны функций печати элементов последовательности и поиска в последовательности элемента с заданным значением. Названные шаблоны функций мы применили для обработки массивов с элементами встроенных типов. Предельные значения последовательностей (в наших примерах — массивов) в обращениях к шаблонным функциям были заданы значениями указателей (адресов начала и конца перебора). Итератор представляет собой абстракцию, обобщающую понятие указателя. Подобно тому, как значением указателя является адрес элемента массива, так значением итератора служит позиция элемента в контейнере. Итератор — это взаимосвязанный с контейнером объект, который обеспечивает обход элементов последовательности, представленной контейнером, и доступ к конкретному элементу, адресованному в данный момент итератором.
Механизмы, использованные при построении STL 517 Для итератора должны быть определены по крайней мере такие операции: * получение значения элемента, на который в данный момент «смотрит» итератор. Если элементом контейнера служит объект, включающий несколько компонентов (полей данных и методов), то для итератора определяют операцию ->, позволяющую обращаться к этим компонентам через итератор; ++ «переход» итератора к следующему элементу последовательности. Эта операция часто имеет две формы — постфиксную и префиксную; == сравнение итераторов «на равенство» позволяет сравнивать позиции (не значения элементов), представляемые двумя итераторами; != сравнение итераторов «на неравенство»: сравниваются позиции, а не размещенные в этих позициях элементы; = присваивание: итератору присваивается некоторое значение, означающее позицию в контейнере. Приведенный список операций не является обязательным. Например, операция инкремента ++ может быть заменена либо дополнена операцией декремента --. Список операций над итераторами совпадает с набором операций, определенных для указателей в языках Си и Си++, когда они адресуют элементы массивов. Различие состоит в том, что итераторы обеспечивают последовательный перебор элементов не только в массивах, но и в более сложных структурах данных, представляемых контейнерами. Второе отличие (которого может и не быть) состоит в "интеллектуальности" итераторов. Например, итератор может выполнять проверку допустимости обращения к конкретному элементу последовательности. Для массива это может быть проверкой граничных значений индекса. Внешне одинаковые результаты, получаемые при операциях с итераторами разных контейнеров, могут быть по-разному получены внутри контейнеров. Например, перемещение итератора к следующему элементу контейнера (операция ++) по-разному реализуется в векторе и в списке. Следовательно, для каждого класса контейнеров должен быть определен свой класс (или несколько классов) итераторов. Другими словами, каждый контейнер
518 Глава 17 должен предоставлять итератор для обхода всего множества или подмножества его элементов и средства, необходимые для создания итераторов. Договорившись о том, что каждый контейнер определяет итератор, необходимый для доступа к его элементам, перейдем к другим свойствам контейнеров. Мы уже знаем, что контейнер содержит элементы последовательности, обеспечивает их взаимосвязь, а также имеет средства для представления начала и конца последовательности. Начало и конец последовательности представляются значениями итераторов. Эти значения доступны с помощью методов контейнерного класса: begin() - возвращает значение итератора (позицию в контейнере), установленного на начало последовательности элементом контейнера; end() - возвращает значение итератора (позицию в контейнере), установленного за последним элементом последовательности. Значения, возвращаемые функциями begin() и end()} определяют как бы полуоткрытый интервал [begin(), end())t нижняя граница которого — первый элемент последовательности, а верхняя позиция размещена непосредственно за последним элементом. Достоинства полуоткрытого интервала: • легко определяется пустой интервал. В этом случае значения, возвращаемые методами begin() и end(), совпадают; • удобно задавать условие окончания цикла перебора элементов последовательности: очередная позиция совпадает со значением end(). После приведенных общих сведений о контейнерах, алгоритмах и итераторах можно было бы приступить к описанию особенностей их реализации в STL и на примерах продемонстрировать их возможности. Но хотелось бы перед этим развеять пелену таинственности, которая может скрыть от читателя внутренние механизмы STL, если он будет оперировать со средствами этой библиотеки, используя каждое из них только как черный ящик. Поэтому начнем с того, что самостоятельно построим контейнер с итератором, чтобы показать отсутствие волшебства в замечательной библиотеке STL и убедить читателя, что он может самостоятельно разработать что-нибудь подобное (вероятно, не
Механизмы, использованные при построении STL 519 такое большое). Например (при необходимости) легко создать контейнер с итератором, пригодный для обработки обобщенными алгоритмами STL, или разработать собственный алгоритм, настроенный на работу с контейнерами STL. Простейшие примеры обобщенных алгоритмов мы уже рассмотрели. Это шаблон функций для вывода значений элементов последовательностей (помещен в файл iterPrint.h) и шаблон функций для поиска среди значений элементов последовательности заданного значения (файл findValue.h). Напишем собственный контейнер с итератором. Итератор в виде локального класса. Сначала не будем использовать механизм шаблонов. Для хранения элементов последовательности в контейнере используем массив фиксированных размеров. Для представления (хранения) элементов последовательности используем только элементы массива с четными значениями индексов. (Элементы массива с нечетными значениями индекса вообще не будем использовать!) Для простоты не будем вводить проверок выхода индекса за пределы допустимого диапазона. Контейнер и его итератор разработаем так, чтобы можно было использовать обобщенный алгоритм iterPrint<>() из подразд. 17.2. Напомним, что iterPrint<>() - шаблон функций для вывода значений элементов последовательности в виде таблицы с заданным числом столбцов. Спецификация класса (контейнера с итератором): #define MAXSIZE 25 class evenArray { int data[MAXSIZE]; //Для элементов последовательности intsize;//Количество элементов(включая неиспользуемые) public: evenArray(): size(0) { } //Конструктор умолчания void pushfint п) { // Поместить значение в контейнер data[size]=n; size+=2; } class evenlter { //Локальный класс итераторов int * cur; //Адрес элемента в контейнере public:
520 Глава 17 evenlterfint * init=0): curfinit) { } int operator *() { return *cur; } evenlter & operator++() { cur+=2; return *this; } bool operator!=(evenlter who) { return who.cur!=cur; } bool operator==(evenlter who) { return who.cur--cur; } }; // End of class evenlter evenlter begin() { return evenlter(&data[0]); } evenlter end() { return evenlter(&data[size]); } ); // End of class even Array //Конструктор умолчания // Вернуть значение элемента // Префиксный инкремент // Сравнение итераторов // Сравнение итераторов //Начало последовательности // Конец последовательности Комментарии в тексте определения класса поясняют назначения каждого метода и поля данных. В классе-контейнере evenArray для хранения элементов последовательности используется массив data[ ]. Реальное количество элементов последовательности определяет поле данных size (точнее, выражение size/2, но мы не планируем его использование). Оба компонента: и массив, и целая величина size - собственные (закрытые). В классе-контейнере только один конструктор (умолчания). Он присваивает нулевое значение полю size, т. е. создает пустой контейнер. Метод push() помещает новый элемент в конец последовательности, уже размещенной в контейнере. Для этого используются (в соответствии с нашей договоренностью) только элементы массива data[ ] с четными индексами. Далее самое интересное — определен локальный (внутренний) класс evenlter, объекты которого соответствуют требованиям, предъявляемым к итератору обобщенными алгоритмами.
Механизмы, использованные при построении STL 521 Каждый объект этого класса содержит поле данных сиг - указатель на тот элемент последовательности, который в текущий момент адресует итератор. Конструктор итераторов позволяет настраивать итератор на конкретный элемент последовательности, представленной объектом класса evenArray. Для объектов класса итераторов определены те операции, о которых мы уже упоминали. Операция * разыменования возвращает значение того элемента последовательности, на который в текущий момент "смотрит " итератор. Операция ++ префиксный инкремент "переводит" итератор на следующий элемент последовательности. В нашем примере указатель после выполнения этой операции будет адресовать в массиве следующий элемент с четным значением индекса. Операции == и != сравнивают значения двух итераторов. В нашем примере сравниваются адреса двух элементов массива data[ ], с помощью которого в контейнере представлена последовательность. К конструктору внутреннего класса evenlter выполняются обращения из двух методов begin( ) и end( ) класса-контейнера evenArray. Названные методы в соответствии с принципами построения библиотеки STL должны входить во все контейнеры, приспособленные для перебора элементов с помощью итераторов, т. е. пригодные для обработки с помощью обобщенных алгоритмов. Метод begin( ) возвращает итератор, адресующий начало последовательности, а метод end( ) возвращает итератор, установленный в конец последовательности (за ее последним элементом). Соответствующие объекты-итераторы создает в их телах конструктор класса eventlter. В первом случае его аргументом служит адрес &data[0] первого элемента массива, во втором - адрес & data [size]. Рассмотрим еще раз класс итераторов evenlter. Его собственный (закрытый) компонент mf *ci/r используется для представления адреса объекта в контейнере. При создании объекта класса evenlter ему передается адрес некоторого элемента массива &data[init], который становится «текущим» для этого итератора. Операция-функция operator*( ) возвращает значение того элемента из контейнера, на который «указывает» в итераторе указатель сиг. Операция-функция operator++(int) переводит итератор на следующий элемент последовательности, сохраненной в контейнере. (Обратите внимание, что операции ++ над
итерато522 Глава 17 ром соответствует смещение на два элемента в массиве data[ ].) Операция-функция operatorl=() сравнивает текущее значение адреса итератора со значением адреса из параметра-итератора. Рассмотрим использование контейнера evenArray с функцией iterPrint(), формируемой шаблоном - обобщенным алгоритмом. Создадим в программе объект-контейнер row, поместим в него несколько элементов - значений типа int, и выведем их значения с помощью функции, полученной как специализация обобщенного алгоритма iterPrint(). При обращении к обобщенному алгоритму начало и конец обработки зададим с помощью методов begin() и end()t примененных к объекту-контейнеру: iterPrint<6>(row.beginf), row.end()); Используем тот же контейнер для иллюстрации приемов непосредственной работы с итераторами. Создадим объект-итератор, адресующий начало, последовательности. Для определения объекта итератора будет применена конструкция: имя_класса-контейнера::имя_кпасса-итератора и мя_объекта-итератора (аргументы_конструктора_итератора); Создав итератор, применим к нему операцию инкремента и выведем адресуемое после этого итератором значение. Комментарии в программе и результаты ее выполнения поясняют сказанное. Текст программы: //Р17 03. срр - Контейнер evenArray с объектами типа int, //Итератор evenlter как внутренний класс контейнера. #include <iostream> using namespace std; It include "evenArray. h" It include "iterPrint.h" intmainf) { // Создание, заполнение и печать контейнера: evenArray row; int d; cout«"Enter integers (0 is the end): "«endl; while( cin »d, d!=0) row.push(d); iterPrint<6>(row. beginf), row. end());
Механизмы, использованные при построении STL 523 // Определяем итератор, настроенный на начало последовательности: even Array r.evenlter etr(row.begin()); ++efr; // - значением итератора станет &d[2] cout« *etr « end!; //Выводится значение d[2] etr-row.begin(); // Значением итератора станет &d[0] cout« *etr«endl; // Выводится значение d[0] return 0; } Результаты выполнения программы: Enter integers (0 is the end): 2 4 68 0<ENTER> [0]=2 [ 1]=4 [2]=6 [3]=8 4 2 Результаты, полученные при выполнении программы Р17_03.срр, т. е. заполнение элементов массива data[ ] из объекта row, иллюстрирует рис. 17.2. Индекс: 0 1 3 3 4 5 6 7 Значение: 2 4 6 8 Рис. 17.2. Схема размещения конкретных данных в объекте класса evenArray: -!—обозначение “мусора” Итератор в виде дружественного класса. Мы на примере рассмотрели особенности построения итератора в виде локализованного в контейнере класса. Для представления элементов последовательности в этом контейнере использовался массив фиксированных размеров. Теперь разработаем контейнер, в котором элементы последовательности будут объединены в связный список. Итератор для этого контейнера определим в виде дружественного класса. Пусть элементами последовательности, сохраняемой в контейнере, будут (для конкретности) строки в стиле Си++. Каждый элемент (узел) связного списка представим объектом следующего класса:
524 Глава 17 // Узел (элемент) связного списка для контейнера строк: class node { node * next; // На следующий узел списка string str; // Строка, сохраняемая в узле friend class listStr; // Односвязный список friend class listlter; // Класс итераторов }; В классе node как дружественные введены (еще не определенные нами) класс-контейнер listStr и класс итераторов listlter. Чтобы в классе listStr можно было использовать итераторы, первым нужно определить (или по крайней мере описать) класс listlter. Естественно, перед ним размещается класс node. В том же файле, где определен node, поместим текст определения класса итераторов: //Класс итераторов для списка с узлами типа node: class listlter { node * current; // Указатель на элемент списка public: listlterf node * newNode=0): currentfnewNode) { } string & operator^) {// Разыменование итератора return current -> str; } listlter & operator ++() { // Инкремент current = current -> next; return * this; } bool operator!=(listlter other) {// Сравнение итераторов return other.current != current; } bool operator==(listlter other) {// Сравнение итераторов return other.current == current; } }; В классе итераторов один закрытый компонент - указатель current на объект класса node, представляющий элемент (узел) связного списка. В конструкторе указателю current присваивается адрес узла списка либо 0 по умолчанию.
Механизмы, использованные при построении STL 525 Базовые операции для итераторов нашего класса введены как операции-функции: operator * () operator ++ () operator != () operator == () возвращает ссылку на строку из узла списка (на значение элемента последовательности, сохраняемой в контейнере); префиксный инкремент — перемещает итератор к следующему элементу последовательности; возвращает ссылку на итератор, для которого внутренний указатель node * current адресует следующий узел списка; сравнивает два итератора; возвращает значение true, если они не равны, т.е. указывают на разные узлы списка; сравнивает два итератора; возвращает значение true, если они равны. Используя классы node и listlter, контейнер в виде связного списка можно определить таким образом: //Класс контейнеров "односвязный список class listStr { node * begNode; //Первый элемент node * endNode; // Последний элемент public: listStr (string & st); // Конструктор void append (string & st); //Добавить элемент в конец списка -listStrf); //Деструктор listlter begin() {// Начало списка return listlterf begNode); } listlter end() {//Конец списка // Вернуть ссылку на пустой элемент!!! return listlterf endNode->next); } }; В классе listlter два собственных (private) указателя типа node *, адресующих начало списка (begNode) и последний элемент (узел) списка (endNode). Обратите внимание, что последний узел принадлежит списку, в отличие от значения,
возвраща526 Глава 17 емого методом end(). Ранее, вводя понятие последовательного контейнера, мы отмечали, что end() возвращает позицию за последним элементом контейнера. В классе listStr мы (для простоты) определили только один конструктор, формирующий контейнер из одного узла, включающего строку, заданную аргументом типа string. (Это нарушение правил построения классов ресурсоемких объектов, где должны быть определены конструктор копирования, операция присваивания и деструктор ~listStr(), который должен, последовательно пройдя по списку, освободить динамическую память, выделенную для всех узлов.) Метод append() добавляет в конец списка новый узел, данными которого служит строка, определяемая аргументом. Методы begin() и end() совместно с классом итераторов превращают «обыкновенный» связный список в контейнер, пригодный для использования с обобщенными алгоритмами стандартной библиотеки. Они возвращают значения итераторов (объектов класса listlter), соответствующие началу и концу последовательности, представляемой контейнером. Определим методы класса контейнера. //Конструктор (внешнее определение): listStr::listStr (string & st) { node * temp = new node; temp -> next = 0; temp -> str = st; begNode = temp; endNode = temp; } //Добавить элемент в конец списка: void listStr:’.append (string & newStr) { node * temp = new node; temp -> next = 0; temp -> str = newStr; endNode -> next = temp; endNode = temp; } Разместим спецификации классов node, listlter и listStr, a также определения методов класса listStr в заголовочном файле
Механизмы, использованные при построении STL 527 listStr.h. Для простейших экспериментов с нашим контейнером создадим объект класса listStr, затем добавим в этот контейнер несколько элементов и применим к контейнеру уже известный нам обобщенный алгоритм iterPrint<>{). Текст программы: // Р17_04.срр - Итератор как дружественный класс. #include <iostream> using namespace std; #include "listStr.h" #include "iterPrint.h" intmainf) { string num = "12345"; // Создание строки listStr spisok(num); // Создание списка string abc("abcde"); spisok. append(abc); //Дополнение списка abc = "word"; spisok.append(abc); abc="hew word"; spisok. append(abc); iterPrint<2>( spisok. beginf), spisok. end( )); return 0; } Результаты выполнения программы: [0]= 12345 [1]=abcde [2]=word [3]=new word В программе определяется строка в стиле Си++ (string num), создается объект-контейнер (spisok) из одного элемента, затем в него добавляются еще три элемента с помощью метода append(). При обращении к функции iterPrint<2>() аргументами служат значения, возвращаемые методами begin() и end(). Результаты выполнения программы, по-видимому, в комментариях не нуждаются. Явных обращений к итератору контейнера listStr в этой программе нет. Чтобы пользоваться итератором, нужно определить его как объект, а затем "настраивать" его на разные элементы последовательности. Для этого у нас есть метод begin() и операции ++,*. Например, продолжая нашу программу, введем
528 Глава 17 (Р17_04_1.срр) в функцию main() после заполнения контейнера spisok такую последовательность операторов: listlter it = spisok.beginf); cout« *it«endl; ++it; cout« *it«endl; Результат выполнения программы дополнится такими строками: 12345 abode 17.4. Взаимодействие средств STL с контейнерами и алгоритмами пользователя Библиотека STL, как мы уже говорили, достаточно сложна, и поэтому было бы легкомысленно рассчитывать, что наши контейнеры (классы evenArray и listlter) и обобщенные алгоритмы (шаблоны функций iterPrintO и findValueO) будут пригодны для работы со многими компонентами STL. Однако для наших целей сейчас будет достаточно продемонстрировать принципиальную возможность применения некоторых алгоритмов STL к нашим контейнерам и наших обобщенных алгоритмов к некоторым контейнерам STL. Сначала не будем ничего изменять в уже созданных нами упомянутых программных средствах. Покажем, как применить к нашему контейнеру listStr (с элементами последовательности в виде строк) алгоритмы STL для поиска минимального и максимального значений. Приведем прототипы шаблонов этих функции STL (см. Стандарт Си++, с. 568): template<class Forwardlterator> Forwardlterator min_element( Forwardlterator first, Forward Iterator last); Результат, возвращаемый этой функцией, — первый (самый левый) итератор / в диапазоне [first, last), для которого при сравнении с любым итератором j из диапазона [first, last) выполняется условие (*/ < =*/).
Механизмы, использованные при построении STL 529 template<class Forwardlterator> Forward Iterator max_element( Forward Iterator first, Forwardlterator last); Результат, возвращаемый этой функцией, - первый (самый левый) итератор / в диапазоне [first, last), для которого при сравнении с любым итератором j из диапазона [first, last) выполняется условие (* j < =*/). Обратите внимание, что в этих шаблонных функциях используются прямые итераторы, т. е. итераторы, которые можно перемещать в прямом направлении с помощью операции ++. Параметр first определяет начало просмотра последовательности в контейнере, параметр last — окончание просмотра. Этим требованиям соответствуют итераторы (объекты класса listlter) нашего контейнера listStr. Применим специализации этих шаблонных функций к конкретному уже сформированному объекту spisok класса listStr. Чтобы использовать алгоритмы STL, в программу необходимо включить заголовок <algorithm>. Приведем основные фрагменты соответствующей программы (Р17_05.срр): #include <algorithm> //Для алгоритмов STL ft include listStr. h" // Контейнер с итератором intmainf) { /* ...создание и заполнение контейнера spisok... */ listlter it; // Прямой итератор контейнера listStr it = max_element(spisok.begin(),spisok.end()); cout« "Max element:"« *it«endl; it = min_element(spisok.begin(),spisok.end()); cout« "Min element: " << *it« endl; Результаты выполнения этого фрагмента программы: Max element: word Min element: 12345 Как вы помните контейнер spisok (объект класса listStr) заполнен последовательностью строк: "12345", "abode”, "word”, "new word". Строки сравниваются лексикографически, поэтому минимальной является строка ,,12345,,| а максимальной — "worcf \ Это определило результаты выполнения программы. 34- 2762
530 Глава 17 Применим те же алгоритмы STL к контейнеру класса evenArray, в котором итератор определен внутренним классом evenlter. Текст программы: //Р17_06.срр - контейнер evenArray с объектами типа int, // применение алгоритмов STL. #include <iostream> using namespace std; #include "evenArray.h" #include "iterPrint.h" ttinclude <cstdlib> //Для библиотечной функции rand() #include <algorithm> //Для алгоритмов STL int main() { // Создание, заполнение и печать контейнера: evenArray series; int n=MAXSIZE/2, xMax=100; for (int i=0; i<n; i++) series. push( rand() %xMax); iterPrint<6>( series. beginf),series. end()); evenArray:.evenlter iter;//Прямой итератор контейнера iter = max_element( series.beginf ),series.end()); cout« "Max element: "« *iter«endi; iter = min_element( series.beginf),series.end()); cout« "Min element:"« *iter « endl; return 0; } Результаты выполнения программы: [0]=72 [1]=9 [2]=62 [3]=49 [4]=28 [5]=55 [6]=91 [7]=1 [8]=17 [9]=10 [ 10J-76 [ 11]=47 Max element: 91 Min element: 1 Читатель не должен обольщаться и считать, что так же легко можно применить к нашим контейнерам и другие алгоритмы STL. Разные алгоритмы STL предъявляют к контейнерам и их итераторам весьма различные требования, которым могут не соответствовать наши контейнеры. Поэтому к проектированию собственных контейнеров, расширяющих STL, нужно присту-
Механизмы, использованные при построении STL 531 пать, только глубоко изучив возможности и правила построения алгоритмов, итераторов и контейнеров, уже входящих в STL [23]. Теперь рассмотрим, какие возможности уже есть у наших обобщенных алгоритмов. Создадим контейнеры на основе шаблонов, входящих в STL, и применим к ним шаблонные функции для вывода iterPrintO и поиска findValueO. Наиболее подходящими для этих целей контейнерами будут так называемые последовательные контейнеры. К ним относятся шаблон классов vectorO. Он позволяет сохранять элементы в динамическом массиве. Для использования этого контейнера в программу нужно включить заголовок <vector>. Формат определения объекта специализации шаблона классов "вектор": vector <аргумент_шаблона> имя_объекта; Для добавления элемента в конец последовательности, сохраняемой в векторе, можно использовать метод этого класса: void push_back(const T& x); Здесь T — параметр шаблона. Для определения итератора используется конструкция: vector <int> :: iterator имя_итератора; В следующей программе наши шаблоны функций применены для обработки вектора с целочисленными элементами. //Р17_07.срр - Обобщенные алгоритмы и контейнер. #include <iostream> ttinclude <vector> using namespace std; #include "iterPrint.h" #include "findValue.h" const int DIMENSION=25; int main() { vector <int> variety; // Определение вектора intxMax=100; // Предельное значение элементов for (int i=0; i < DIMENSION; i++) variety.push_back(rand()%xMax); //Добавление элемента iterPrint< 7>( variety. begin(), variety. end()); 34*
532 Глава 17 vector <int> :: iterator iT; // Определение итератора /Т = findValue<>( variety, beginf ),variety.end(), 10); cout«uSearch element: iT-variety.beginf) «-]= "<<*iT«endl; return 0; } Обратим внимание, что размер вектора variety определен константой DIMENSION. Значения элементов, сохраняемых в векторе, целочисленные и выбираются случайно из диапазона О - 99. После заполнения вектора функция iterPrint<6>() выводит значения его элементов. Далее определен итератор /Т, и ему присваивается результат выполнения шаблонной функции findValue<>(). При выводе результата поиска индекс найденного в векторе элемента определяется как разность итератора и начала последовательности в векторе iT-variety.begin(). Значение найденного элемента получено как разыменование итератора */Т. Результаты выполнения программы: [0]=72 [1]=9 [2]=62 [3]=49 [4]=28 [5]=55 [6]=91 [71=1 [8]=17 [9]=10 [10]=76 [11]=47 [ 12]=63 [13]=32 [14]=75 [15]=38 [16]=75 [17]=79 [18]=59 [19]=72 [20]=82 [21]=40 [22]=10 [23]=62 [24]=96 Search element: [9] = 10
533 Глава 18 ОСНОВНЫЕ СРЕДСТВА БИБЛИОТЕКИ STL 18.1. О концепции построения STL Стандартная библиотека шаблонов (Standard Template Library, STL) вначале была создана как отдельная библиотека. Ее разработали сотрудники Hewllet-Packard А.А.Степанов и МЛи совместно с Д.Р.Муссером. Сегодня после стандартизации STL - часть стандартной библиотеки C++, предназначенная для представления коллекций данных и работы с ними при помощи наиболее эффективных алгоритмов. Стандартная библиотека шаблонов построена на трех базовых и двух вспомогательных понятиях. К базовым понятиям относятся: контейнеры - содержат коллекции (наборы) объектов (элементов контейнеров); алгоритмы - предназначены для обработки элементов коллекций, представленных контейнерами; итераторы - обеспечивают универсальный доступ к элементам контейнеров и перебор этих элементов. Вспомогательными понятиями служат адаптеры и функторы, к объяснению которых нужно приступать после изучения базовых понятий STL. В основе архитектурных решений STL лежат следующие особенности. Первая особенность состоит в том, что принципы построения STL противоположны принципам объектно-ориентированного программирования. В ООП объект объединяет данные (состояние объекта) с операциями (поведение объекта, т. е. относящиеся к нему методы). Концепция STL основана на отделении данных от операций. Контейнеры представляют данные,
алгорит534 Глава 18 мы — операции. За счет этого отделения достигнуты гибкость и компактность библиотеки STL. В ней разные алгоритмы по мере необходимости применяются к разным контейнерам. Это позволило не тиражировать алгоритмы, применимые к разным контейнерам, что имело бы место при традиционном объектном подходе. Вторая фундаментальная особенность SLT- параметризация и контейнеров, и алгоритмов. Все компоненты STL реализованы в виде шаблонов, которые могут работать с любыми типами, для которых допустимы операции, используемые компонентом. Третья особенность - применение итераторов для взаимодействия контейнеров с алгоритмами. 18.2. Контейнеры STL Анализируя особенности проектных решений, положенных в основу STL, Б. Страуструп отмечает существование двух возможных подходов к проектированию библиотеки классов: • использование общего базового класса для всех классов, представляющих контейнеры; • применение не связанных между собой контейнеров. В STL использован второй подход, и у контейнеров нет общего базового класса. Вместо этого каждый контейнер реализует все стандартные контейнерные интерфейсы в виде набора операций. В STL контейнер — это параметризованный класс, представляемый шаблоном классов. Объект такого параметризованного класса пригоден для включения других объектов. Для представления различных коллекций (структур данных) в STL определены контейнеры двух видов: последовательные и ассоциативные. Последовательные контейнеры представляют собой упорядоченные коллекции, в которых каждый элемент занимает конкретную позицию, не зависящую от значения элемента. Позиция может зависеть от того, какой была коллекция в момент помещения в нее элемента и/или от явного указания места для размещаемого элемента.
Основные средства библиотеки STL 535 Последовательные контейнеры: • deque - дек. Термин происходит от сокращения фразы double-ended queue - двусторонняя очередь. Дек создается как динамический массив, в который можно включать элементы и из которого можно брать элементы с обоих концов. Вставка в произвольную позицию дека — трудоемкая по числу операций и тем самым по времени исполнения процедура. Для применения дека в программу необходимо включить заголовок <deque>. • list - двусвязный список. Каждый элемент списка содержит некоторое значение (данные) и две ссылки (два указателя) — на предыдущий и на последующий элементы списка. Списки не допускают произвольного обращения (доступа) к элементам. Достоинство списка — быстрое выполнение вставки и удаления элемента, размещенного в любой позиции. Требуется только изменение ссылок. Для применения списка в программу необходимо включить заголовок <list>. • vector - вектор — «безопасный», автоматически расширяющийся массив, обеспечивающий произвольный доступ к находящимся в нем элементам. Доступ осуществляется как с помощью индексирования, так и с помощью специального метода at(). Для применения вектора в программу необходимо включить заголовок <vector>. Ассоциативные контейнеры автоматически упорядочивают включаемые в них элементы по некоторому критерию. Критерий реализуется в виде функции, которая сравнивает либо значения элементов, либо специальные ключи, заданные для этих значений (для элементов). Если программист не определил функцию сравнения, то по умолчанию ключи или значения сравниваются с помощью операции "меньше" (<). Ассоциативные контейнеры реализуются в виде бинарных деревьев, где у каждого узла-элемента есть два узла-потомка (правый и левый). Все производные узлы слева имеют меньшие значения, а все производные справа - большие. Ассоциативные контейнеры различаются по виду элементов и по допустимости дубликатов. По виду элементов различают множества и отображения. • set — множество — коллекция, в которой элементы уникальны и упорядочены по значениям элементов. Для применения множества в программу необходимо включить заголовок <set>.
536 Глава 18 • multiset - мультимножество — множество, в котором допустимы элементы с одинаковыми значениями. Элементы в мультимножестве упорядочены по значениям. • тар - отображение — коллекция, элементами которой являются пары «ключ—значение». Элементы (пары) упорядочены по значениям ключей. Все ключи элементов уникальны (дубликаты не допускаются). Отображение может использоваться как ассоциативный массив, т.е. для обращения к элементу можно в качестве индекса использовать ключ. В отличие от обычного массива, где индекс всегда целочисленный, ключ ассоциативного массива может иметь любой тип. Для применения отображения в программу необходимо включить заголовок <тар>. • multimap — мультиотображение — это отображение, в котором допустимы элементы (пары) с одинаковыми ключами (заголовок <тар>). Мультиотображение может использоваться в качестве словаря, в котором один термин (ключ) может иметь несколько переводных эквивалентов. Множество можно считать частным случаем отображения, в котором ключ и значение совпадают. Кроме последовательных и ассоциативных контейнеров стандартная библиотека C++ включает специализированные контейнеры (называемые также контейнерными адаптерами). Они отнесены Стандартом к последовательным контейнерам. • queue - очередь, реализуется как динамический массив, элементы в который можно включать только в конец, а извлекать только из начала (классический буфер данных). Для применения в программе очереди необходимо включить заголовок <queue>. • stack - стек, реализуется как динамический массив, включение и извлечение элементов которого можно осуществлять только с одного конца. Для применения стека в программу необходимо включить заголовок <stack>. • priority_queue - очередь с приоритетами, очередь, в которой элементы упорядочены и доступны в соответствии с приоритетами, приписанными всем элементам (заголовок <queue>). При выборке из очереди выбирается элемент с максимальным приоритетом. Если несколько элементов имеют одинаковый приоритет, то порядок этих элементов не определен.
Основные средства библиотеки STL 537 Специализированные контейнеры реализуются на базе основных контейнеров STL. Кроме названных контейнеров, введен контейнер для хранения битовых последовательностей: • bitset - битовое поле, в отличие от других контейнеров, где для представления каждого логического значения используется 1 байт, в этом контейнере каждое значение представлено одним битом. Такой контейнер получает при создании фиксированный размер, который не изменяется при обработке. Размер битового поля определяется при создании объекта-контейнера нетипизирующим параметром шаблона. Шаблон классов-контейнеров bitset определен в заголовке <bitset>. При создании библиотеки STL ее авторы приняли решение хранить в контейнерах копии тех элементов, которые заносятся в контейнер. При модификации элемента контейнера никак не изменяется тот внешний объект, который определил значение этого элемента контейнера. Таким образом, необходимо, чтобы в классе сохраняемых в контейнере объектов был определен открытый конструктор копирования. 18.3. Основные методы контейнеров Возможности разных контейнеров STL существенно различны, но для всех контейнеров имеются общие методы и операции. Н. Джосьютис [5] перечислил (см. также [4] и [23]) эти общие методы и операции, сгруппировав их следующим образом. Создание и уничтожение контейнера (ContType — условное обозначение типа контейнера): ContType С - создает пустой контейнер С, не содержащий элементов. ContType C(int N) - создает С, содержащий N элементов. ContType С1(С2) - создает копию С1 контейнера С2, типы С1 и С2 одинаковы. ContType C(beg, end) - создает контейнер типа ContType и инициализирует его копиями всех элементов из интервала [beg, end), где beg, end - итераторы некоторого уже существующего контейнера (не обязательно того же типа, что и С). С. ~ContType() - деструктор, удаляет все элементы контейнера С и освобождает память.
538 Глава 18 Оценка числа элементов и размеров контейнера: C.size() - возвращает фактическое количество элементов, находящихся в контейнере С. C.emptyQ - возвращает значение true, если контейнер С пуст. C.max_size() — возвращает максимальное количество элементов, которые можно поместить в контейнер С. Проиллюстрируем применение разных конструкторов на следующем примере: //P18J01 .срр - Последовательные контейнеры. Конструкторы. #include <iostream> #include <vector> #include <list> using namespace std; intmainf) { intira[ ]= {10, 20, 30, 40, 50, 60, 70, 80, 90}; vector <double> seriesfira, ira+sizeof(ira)/sizeof(ira[0])); list <long double> myListf series, beginf), series.end()-4); list <long double> oneListf series,beginf),series.begin()+5); list <long double> twoList(myList); cout«"The twoList. size() = "< < two List. size()« endl; cout«"The twoList.max_size() = "«twoList.max_size()«endl; cout«"The sizeof twoList = "«sizeof twoList«endl; return 0; } Результаты выполнения программы: The twoList.size() = 5 The twoList. max_size() = 4294967295 The sizeof twoList = 4 В программе определен массив intira[], и указатели на его элементы играют роль итераторов. Этот массив использован для построения и инициализации контейнера series с помощью конструктора ContType C(beg, end). Типы элементов контейнера отличаются от типов элементов массива, и выполняется автоматическое приведение типов. intira[ ] = {10, 20, 30, 40, 50, 60, 70, 80, 90}; vector <double> seriesfira, ira+sizeoffira)/sizeof(ira[0]));
Основные средства библиотеки STL 539 Здесь /га - целочисленный массив, размер и значения элементов которого определены инициализацией. Для инициализации контейнера-вектора series с элементами типа double используются все элементы массива /га, т. е. в контейнере ser/es девять элементов. Приведение типов выполнено автоматически. Значения элементов контейнера одного вида (так же как элементы массива) можно использовать для инициализации элементов контейнера другого вида. Однако в этом случае нужно применять не адреса (указатели), а итераторы: list <long double> туList(series.beginf), series. end() -4); Здесь создан контейнер-список myList с элементами типа long double. Для инициализации используются первые пять элементов вектора series. Таким образом, в списке пять элементов со значениями: [0]=10 [ 1 ]=20 [2]=30 [3]=40 [4]=50 Тот же результат можно получить, используя такое обращение к конструктору: list <long double> oneListfseries, beginf),series.beginf)+5); Конструктор копирования позволяет создавать объект-контейнер того же вида и с тем же типом параметров, что и имеющийся контейнер. Создадим еще один список так: list <long double> twoListf myList); Список twoList полностью идентичен списку myList. Как ожидалось, число элементов в контейнере равно 5, а предельное количество элементов достаточно велико. Применение операции sizeof к контейнеру позволяет обратить внимание, на тот факт, что контейнер является динамическим объектом, и результат операции sizeof - не размер контейнера, а размер указателя на выделенную для контейнера память. Сравнение контейнеров. Для однотипных контейнеров сравнение может быть выполнено с помощью стандартных операций отношений ==, !=, <, <=, >, >=. Два однотипных контейнера равны (операции ==, !=), если значения их элементов, размещенных на одинаковых местах, совпадают. Отношения <, <=, >, >= для контейнеров проверяются по лексикографическому критерию.
540 Глава 18 В этом случае два контейнера сравниваются с учетом интервалов размещения элементов [beg1, end1) и [beg2, end2). Элементы из интервалов сравниваются попарно до тех пор, пока не будет выполнено одно из следующих условий: • если очередные два элемента не равны, то результат этих элементов сравнения определяет результат сравнения контейнеров; • если интервалы не равны, то при попарном сравнении элементов может быть достигнут конец меньшего интервала, а истинность проверяемого условия еще не установлена. В этом случае контейнер с меньшим количеством элементов считается меньшим. В следующем иллюстративном примере создадим два дека с разным числом элементов. Значение меньшего из них присвоим третьему деку. Выведем содержимое полученного дека с помощью написанной ранее шаблонной функции iterPrint(). //Р18_02.срр - Последовательные контейнеры. Сравнение. #include <iostream> #include <deque> using namespace std; #include "iterPrint.h"// Шаблон функций для вывода последовательности intmainf) { intira[ ]= {10, 20, 30, 40, 50, 60, 70, 80, 90}; deque <double> deq1(ira, ira+sizeof( ira)/sizeof( ira[0])); cout«"The deque deq1: "«endl; iterPrint<5>(deq1 .begin(), deq1.end()); deque <doubte> deq2(ira, ira+7); deque <double> deq3; iffdeql < deq2) deq3 = deq1; else deq3 = deq2; cout«"The result deque: "«endl; iterPrint<5>(deq3. beginf), deq3. end()); return 0; } Результаты выполнения программы: The deque deq1: [0]=10 [ 1 ]=20 [2]=30 [3]=40 [4]=50 [5]=60 [6]=70 [7]=80 [8]=90
Основные средства библиотеки STL 541 The result deque: [0]=10 [ 1 ]=20 [2]=30 [3]=40 [4]=50 [5]=60 [6]=70 Присваивание контейнеров и обмен содержимыми. При выполнении операции присваивания все элементы из контейнера-источника копируются в контейнер-приемник. Перед копированием (мы это уже знаем, познакомившись с классами ресурсоемких объектов) все элементы контейнера-приемника должны быть удалены, а память, занимаемая им, освобождена и вновь выделена для новых элементов. Таким образом, присваивание является дорогостоящей операцией. Для контейнеров одного типа присваивание может быть заменено применением метода или внешней функции swap(). С1 .swap(C2) - метод меняет местами элементы контейнеров С1 и С2. swap(C1, С2) - внешняя функция, которая меняет местами элементы контейнеров С1 и С2. В следующей программе определим и инициализируем массив строк, на его основе создадим два списка и поменяем их содержание (элементы): //Р18_03.срр - Последовательные контейнеры. Обмены значений. #include <iostream> #include <list> #include <string> using namespace std; #include ,,iterPrint.h,,// Шаблон функций для вывода последовательности intmainf) { string strArray[] = Гопе", "two", "three", "four", "five", "six"}; int count = sizeoff str Array)/sizeof( str Array[0]); list <string> fig 1(strArray, strArray+count); cout«"The list fig 1: ”«end!; iterPrint<4 >( fig 1. beg inf), fig 1. endf)); list <string> fig2(strArray, strArray+2); swapffigl, fig2); cout«"The list fig 1 after swapf): "«endl; iterPrint<4>(fig 1.beginf), fig 1. endf)); return 0; }
542 Глава 18 Результаты выполнения программы: The list fig 1: [О]=опе [ 1]=two [2]=three [3]=four [4]=five [5]=six The list fig 1 after swap(): [O]=one [1]=two Перечислим методы получения итераторов: C.begin() — возвращает итератор для первого элемента; C.end() - возвращает итератор для позиции за последним элементом; C.rbegin() - возвращает обратный итератор для первого справа элемента; C.rendf) - возвращает обратный итератор для позиции за последним элементом при переборе в обратном направлении (т. е. для позиции, которая предшествует первому элементу поел едо вате л ь н ости). С методами begin() и end() для получения начального и конечного итераторов последовательности мы уже хорошо знакомы. Ничего таинственного нет и в методах rbegin(), rend(), позволяющих получить обратные итераторы. Первый из них возвращает итератор для позиции последнего элемента последовательности. Разыменование полученного методом rbeginf) итератора позволяет получить доступ к последнему элементу. Приращение этого обратного итератора (операция ++) переводит его на позицию предпоследнего элемента и т. д. В следующей программе последовательность в виде списка выводится одним обобщенным алгоритмом дважды — с применением прямых и обратных итераторов: //Р18_04.срр - Последовательные контейнеры. Обратные итераторы. ttinclude <iostream> ft include <list> ttinclude <string> using namespace std; # include "iterPrint.h" // Шаблон функций для вывода последовательности intmainf) { string strArray[] = {"one", "two", "three", "four", "five", "six"}; list <string> fig(strArray, strArray+5);
Основные средства библиотеки STL 543 cout«"The list: "«end!; iterPrint<5>( fig. begin(), fig. end()); cout«"The reverse list:"«end!; iterPrint<5>(fig.rbegin(), fig.rendO); return 0; } Результаты выполнения программы: The list: [0]=one [1]=two [2]=three [3]=four [4]=five The reverse list: [0]=five [1]=four [2]=three [3]=two [4]=one Для вставки и удаления элементов применяются перечисленные ниже методы. C.insertfpos, elem) - вставляет копию элемента elem перед позицией, определяемой первым параметром-итератором. Возвращает позицию (в виде значения итератора) нового (вставленного) элемента; C.insertfpos, elem) - выполняет вставку одного элемента — копии elem перед позицией pos. Возвращает позицию (в виде значения итератора) нового (вставленного) элемента; C.insertfpos, п, elem) - выполняет вставку нескольких (п) одинаковых элементов — копий elem перед позицией pos. Ничего не возвращает; C.insertfpos, beg, end) - выполняет вставку перед позицией pos копий элементов из диапазона [beg, end), определенного итераторами beg, end. Ничего не возвращает; C.erasefpos) — удаляет элемент из заданного (параметром-итератором) места контейнера. Возвращает позицию (итератор) элемента, следующего за удаленным; C.erasefbeg, end) - удаляет из контейнера элементы в диапазоне (beg, end) и перераспределяет память. Возвращает позицию (итератор) элемента, следующего за удаленными; С.с1еаг() - удаляет из контейнера все элементы, но не уничтожает сам контейнер. Метод insertfpos, elem) позволяет удобно заполнять контейнер элементами с нужными значениями. Это заполнение можно выполнять как с начала, так и с конца контейнера.
544 Глава 18 Например, в следующей программе определяется пустой вектор myVec - контейнер с целочисленными элементами, затем добавляется в этот контейнер элемент со значением 44. Вставка выполняется перед концом последовательности (хотя вектор пуст). //Р18_05.срр - Вставка в последовательный контейнер. #include <iostream> #include <vector> using namespace std; intmainf) { vector <int> myVec; myVec. insertf myVec. end(), 44); cout «"* myVec.beginf)="« *myVec. beginf) « endl; return 0; } Результат выполнения программы: * myVec. beginf )-44 В следующей программе вводимые из входного потока числовые значения становятся элементами дека - двусторонней очереди. Вставка выполняется перед первым элементом последовательности, уже помещенной в контейнер. //Р18_06.срр - Вставки в последовательные контейнеры. #include <iostream> #include <deque> using namespace std; #include "iterPrint. h" // Шаблон функций для печати intmainf) { deque <int> myDeque; int z; cout«"Enter integers(0 is the end): ”«endl; while ( cin » z, z != 0) myDeque. insertf myDeque. beginf ),z); cout«"The myDeque: "«endl; iterPrint< 10>(myDeque.beginf), myDeque.endf)); return 0; }
Основные средства библиотеки STL 545 Результаты выполнения программы: Enter integers (0 is the end): 3 69 2 1 8 0<ENTER> The myDeque: [0]=8 [1]=1 [2]=2 [3]=9 [4]=6 [5]=3 Методы частного применения. Для некоторых видов последовательных контейнеров определены и другие методы, применение которых имеет для разных контейнеров разную эффективность. Пусть С - объект-контейнер. Перечислим эти методы с указанием их применимости: C[ind] — индексирование применимо к вектору и деку. Не обеспечивает проверки принадлежности индекса допустимому интервалу; C.at(ind) - индексирование применимо к вектору и деку. При нарушении принадлежности индекса допустимому интервалу формируется исключение out_of_range; C.push_back(elem) - вставка элемента elem в конец контейнера. Применим к деку, вектору и списку. Значений не возвращает; C.push_front(elem) - вставка элемента elem в начало контейнера, Применим к деку и списку Значений не возвращает; С.рор_Ьаск() — удаление последнего элемента контейнера. Ничего не возвращает. Применим к деку, вектору и списку. Значений не возвращает; C.pop_front() - удаляет элемент из начала контейнера. Применим к деку и списку. Значений не возвращает. Рис. 18.1 иллюстрирует правила действия перечисленных методов. push_back() push_front() pop_back() pop_front() 35-2762 Рис. 18.1. Действия методов вставки и удаления
546 Глава 18 Для наглядного представления возможностей и эффективности методов (операций) последовательных контейнеров удобно использовать табл. 18.1 (см. [21, 22]), составленную на основе сведений из монографии Б. Страуструпа ([1], с. 520 - 523). Таблица 18.1 Методы (операции) последовательных контейнеров Название операции Метод vector deque list Добавление в начало push_front() — 0(1) 0(1) Удаление из начала pop_front() - 0(1) 0(1) Добавление в конец push_back() 0(1)+ 0(1) 0(1) Удаление из конца pop_back() 0(1)+ 0(1) 0(1) Вставка в произвольное место insert() о (n)+ О(Л) 0(1) Удаление из произвольного места erase() 0 (n)+ О(л) 0(1) Обращение по индексу с проверat() 0(1) 0(1) - кой допустимости Обращение по индексу без про[] 0(1) 0(1) — верки допустимости Доступ к первому элементу front() 0(1) 0(1) 0(1) Доступ к последнему элементу back() 0(1) 0(1) 0(1) В таблице приняты следующие обозначения: 0(1) - длительность операции не зависит от числа элементов в контейнере; О(п) —длительность операции пропорциональна числу элементов в контейнере; + - суффикс, означающий, что длительность исполнения операции может возрастать; - - неприменимость операции к контейнеру. Исходя из приведенной таблицы можно выбирать тот или иной вид последовательного контейнера, наиболее подходящий для вашей задачи. В качестве иллюстрации возможностей последовательных контейнеров приведем программу, в которой создается пустой дек (двусторонняя очередь) для элементов типа char; с клавиатуры вводятся символы (символ ‘! * — конец ввода) и помещаются в начало дека. Изменяя индекс от 0 до размера дека, выводим значения элементов. Затем удаляем два последних и один первый
Основные средства библиотеки STL 547 элемент и выводим все элементы дека в порядке, обратном заполнению. //Р18_07.срр - Дополнение, удаление, индексирование #include <iostream> #include <deque> using namespace std; intmain() { deque <char> deck; char ch; cout < < ”Enter symbols (\'!\' is the end): while ( cin » ch, ch != '!') deck.pushjron t( ch ); forfint i=0; i /= deck.sizef); i++) cout« deck[i] « "\t"; cout« endl; deck.pop_back(); // Удаляется последний элемент deck.popjrontf); // Удаляется первый элемент deck.pop_back(); // Удаляется последний элемент forfint i=0; i != deck.sizef); i++) cout« deck[deck.size() - i] « "\Г; return 0; } Результаты выполнения программы: Enter symbols (T is the end): 1234 5 67 !<ENTER> 7 6 5 4 3 2 1 2 3 4 5 18.4. Итераторы в STL Каждый контейнер включает тип с названием iterator. В шаблоне контейнера с параметром Т этому типу соответствуют Т* или const Т*, т. е. итератор ведет себя как указатель на элемент, помещенный в контейнер. Как мы уже знаем, итераторы "привязаны" к контейнерам, и для разных видов контейнеров используются свои итераторы. Существует три разновидности итераторов, обеспечивающих и запись, и чтение элементов контейнера: 35*
548 Глава 18 (For) - однонаправленные (прямые и обратные, иначе — реверсивные); (ВГ) — двунаправленные; (Ran) - обеспечивающие произвольное перемещение (произвольный доступ). Кроме того, существуют два специализированных вида итераторов: входной итератор (/л-итератор чтения потока) и выходной итератор (Out-итератор записи). Входной итератор обеспечивает только запись данных в выходной поток (в файл или устройство вывода). Классы итераторов определены в отдельном заголовке <iterator>, однако при использовании в программе того или иного заголовка, вводящего библиотечные контейнеры, в программу автоматически включается и заголовок <iterator>. Поэтому отдельно от контейнеров классы итераторов включаются в программу только при необходимости. Не все виды итераторов поддерживают один и тот же набор операций. Б.Страуструп ([1], с. 615) приводит таблицу операций 18.2, которые должны соответствовать каждому из перечисленных видов итераторов. Таблица 18.2 Операции с итераторами Вид итератора Операции чтение доступ запись итерация сравнение Out - выходной нет нет *Р= ++ нет In - входной =*Р -> нет ++ == != For - однонаправ=*Р -> *Р= ++ == != ленный В/ - двунаправлен=*Р -> *Р= ++ -- == != ный Ran - произволь=*Р ->П *Р= ++ -- == != ного доступа + м V V += -= V V II В таблице: р - итератор; "нет" - недоступность операции для итератора; * - операция разыменования. Чтение и запись производятся с помощью выражений:
Основные средства библиотеки STL 549 *р = х; — Запись значения х в позицию, определенную итератором р; ^ х = *р; - Чтение в х значения из позиции, определенной итератором р. Для создания итераторов необходимо знать, какие итераторы включены в каждый из контейнеров. В контейнеры vector и deque входят итераторы, обеспечивающие произвольный доступ (Ran). В контейнеры list, тар (multimap), set (multiset) входят двунаправленные итераторы (Bi). Продемонстрируем применение итераторов последовательного контейнера. Последовательные контейнеры мы уже определять умеем. Для каждого из них можно следующим образом определить соответствующий итератор. Прямой итератор для контейнера с элементами типа type: contType <type>::iterator имя итератора; Обратный (реверсивный) итератор для контейнера с элементами типа type: contType <type>::reverseJterator имя_итератора; Следующая программа иллюстрирует некоторые особенности применения контейнеров и их итераторов. В ней необходимо: определить и инициализировать объект-вектор с элементами типа int\ определить два итератора (прямой и обратный) для контейнера этого вида; используя итераторы, вывести значения элементов контейнера в прямом и обратном порядке. //Р18_08.срр - Контейнеры и их итераторы #include <iostream> #include <vector> using namespace std; intmainf) { vector <int> :: iterator is; vector <int> :: reverse Jterator ri; intari[] = {1,3,5,7,11,13,17,19}; vector <int> rowfari, ari+sizeof(ari)/sizeof(ari[0])); is = row.beginf); for (int i-O; i < sizeof(ari)/sizeof(ari[0]); i++) cout«*(is + i)«"\t”;
550 Глава 18 cout«endl; for(ri = row.rbegin(); ri != row.rendf); ri++) cout«*ri«"\t"; return 0«endl; } Результаты выполнения программы: 1 3 5 7 11 13 17 19 19 17 13 11 7 5 3 1 18.5. Функциональные объекты (функторы) Прежде чем изучать особенности алгоритмов STL, введем понятия, которые будут нужны для работы с некоторыми из них, точнее, познакомимся с функциональными объектами. В русскоязычной литературе их еще называют объектами-функциями и функторами. Рассмотрим пример использования функциональных объектов. Функции bsearchf) и qsort() из стандартной библиотеки языка Си требуют, чтобы им в качестве аргументов были переданы указатели на функции сравнения. Эти вспомогательные функции готовит программист-пользователь. Они служат для задания критериев сортировки или поиска. О концепции построения библиотечных функций, вызывающих вспомогательные функции, размещенные в коде пользователя, говорят, используя термин "обратный вызов" (callback). Достаточно часто в качестве функции, использованной в обратном вызове, выступает предикат. В зависимости от количества параметров (аргументов) выделяют унарные и бинарные предикаты. Унарный предикат проверяет значение одного параметра, бинарные предикаты обычно выполняют сравнение значений своих аргументов. В обоих случаях результат — логическое значение. Применение для обратного вызова вспомогательных функций-предикатов существенно повышает гибкость библиотечных функций и служит прообразом обобщенного программирования. Б. Страуструп ([I], с. 377) определяет обобщенное
программироОсновные средства библиотеки STL 551 вание как "программирование с использованием типов в качестве параметров". В примере с предикатами библиотечные функции параметризуются функциями обратного вызова. Следующим шагом приближения к обобщенному программированию служит применение функциональных объектов (функторов, объектов- функций). Функциональным объектом в самом общем виде называют любой объект, к которому можно обратиться, используя синтаксис вызова функции. Так как этому определению соответствуют обращения к макросам и собственно функциям, а они не являются объектами, то уточним определение и будем считать функциональными объекты тех классов, в которых определена операция "круглые скобки". Применение этой операции неотличимо от обращения к функции, имеющей имя объекта, и выглядит так: имя_объекта{список_аргументов) Заметим, что эта конструкция неотличима от обращения к некоторой функции с помощью адресующего ее указателя (эта возможность унаследована из языка Си): имя_указателя(список_аргументов) Основное отличие функционального объекта от указателя на функцию — возможность задавать для функционального объекта некоторое состояние. Для этого в классе функциональных объектов могут присутствовать поля данных. Тем самым один класс функциональных объектов позволяет создавать объекты- функции с разными состояниями. При обращении к функциональным объектам с помощью операции "круглые скобки" возвращаемое значение (результат) зависит не только от значений аргументов, но и от внутреннего состояния функционального объекта. В качестве примера определим класс функциональных объектов, позволяющих формировать значения членов обобщенной последовательности Фибоначчи. Операция-функция operator() будет при каждом обращении возвращать значение очередного члена. В классе определим два поля данных, в которых будут сохраняться два последних члена последовательности. Таким образом, после каждого обращения эти поля должны изменяться, т. е. функциональный объект будет менять свое состояние. При
соз552 Глава 18 дании объекта конструктору будем передавать значения двух первых членов последовательности, они определят начальное состояние функционального объекта. Определение класса функциональных объектов (в файле functorFib.h): //functorFib.h - функтор (класс функциональных объектов), class functorFib { intone, two; public: functorFib(intx=1, inty-1): one(x), two(y){ } int operator ()() { int z; z=one+two; one-two; two-z; return z; } }; В классе functorFib поля данных int one, two при создании каждого объекта инициализируются значениями параметров конструктора. При каждом обращении к методу operator ()() вычисляется очередной член ряда, а два предыдущих сохраняются как значения полей данных. Обратите внимание на две пары круглых скобок в заголовке метода. Чтобы подчеркнуть отсутствие параметра, заголовок можно записать в таком виде: int operator () (void) Используем объекты класса functorFib для задания значений элементов векторов с целочисленными элементами. Для этого можно применить новый для нас алгоритм [23], определяемый шаблоном функций с заголовком template <tyрепа me Fwdlt, typename Gen> void generatef Fwdlt first, Fwdlt last, Gen g); Здесь Fwdlt - прямой итератор некоторого контейнера; Gen - класс функциональных объектов. Эта шаблонная функция присваивает значение д() каждому из элементов контейнера, адресуемых итераторами со значениями в диапазоне от [first, last).
ОбраОсновные средства библиотеки STL 553 тим внимание, что функция д() вызывается заново для каждого из элементов. В следующей программе определим два целочисленных вектора с фиксированным числом элементов. Для их создания используем конструктор с одним целочисленным параметром: ContType C(intN) - формирует объект С, содержащий N элементов. Затем, используя алгоритм generatef) и в качестве аргументов функциональные объекты класса functorFib, присвоим элементам векторов значения членов последовательностей Фибоначчи с разными начальными членами. Для вывода значений элементов последовательных контейнеров с помощью препро- цессорных средств определим макрос PRINT_ELEM. Параметры макроса — имя объекта-контейнера CONT и имя итератора IT. Текст программы: //Р18_09.срр - Функторы и последовательные контейнеры. #include <iostream> #include <vector> using namespace std; #include <algorithm> #include "functorFib.h" #define PR IN TEL EM( CON TJT) \ for(IT=CONT.begin(); IT!= CONT.end(); ++/7Д cout«*IT«"\t"; \ cout«endl; #define NUM 7 int main() { functorFib fib(2,3); vector <int> seriesl(NUM); vector <int> series2(NUM); vector <int> :: iterator is; generatef series 1. begin(), series 1. end(), functorFib( -1, -2)); generate(series2. begin( ),series2. end(), fib); PRINT_ELEM( series 1, is); PRINT_ELEM(series2, is); return 0; } Количество элементов в контейнерах-векторах определено препроцессорной константой NUM.
554 Глава 18 При обращении к алгоритму generatef) для вектора series 1 в качестве аргумента формируется безымянный объект класса functorFib. Аргументы его конструктора (-1,-2) определяют первые члены последовательности, образующей значения элементов вектора seriesl. Отдельно создан функциональный объект fib, он определяет последовательность с первыми элементами 2 и 3. Этот объект используется в качестве аргумента при присваивании значений элементам вектора series2. Результаты выполнения программы: -3 -5 -8 -13 -21 -34 -55 5 8 13 21 34 55 89 18.6. Алгоритмы STL Для выполнения операций над контейнерами и другими последовательностями в программе на языке Си++ можно использовать обобщенные алгоритмы STL и функции стандартной библиотеки (см. Приложение 7). Стандартные алгоритмы обработки данных контейнеров и последовательностей вводятся заголовками: <algorithm> - основные алгоритмы STL; <numeric> - алгоритмы для выполнения простых арифметических операций над элементами последовательностей; <cstdlib> - алгоритмы из стандартной библиотеки, унаследованной из языка Си. К ним относятся знаменитые функции bsearch() и qsort(). Сейчас основное внимание мы уделим алгоритмам, специфицированным в заголовке <algorithm>.Стандарт группирует эти алгоритмы STL следующим образом: • не модифицирующие последовательности; • модифицирующие последовательности; • сортировки и относящиеся к сортировке. С некоторыми обобщенными алгоритмами мы уже познакомились. В приводимых ранее программах использовались max_element(), min_element(), swap(), generatef). Однако это совсем малая часть из более чем шестидесяти стандартизованных шаблонных функций.
Основные средства библиотеки STL 555 Авторы библиотеки STL [23] объединяют алгоритмы (шаблонные функции, вводимые заголовком <algorithm>) в более мелкие группы. Кратко охарактеризуем особенности алгоритмов каждой из этих групп и приведем примеры. В некоторых примерах используем функциональные объекты (функторы). Мы не можем подробно останавливаться на всех алгоритмах и их применениях ко всем видам контейнеров. Наша задача — изложить общие принципы работы со средствами STL. Обратим внимание, что для применения стандартных алгоритмов к тому или иному контейнеру необходимо хорошо знать интерфейс контейнера. Указанный интерфейс формируется методами контейнера и теми итераторами, которые определены для конкретного контейнера. В приводимых примерах будем использовать последовательные контейнеры и соответствующие им итераторы. Не все алгоритмы STL манипулируют последовательностями, некоторые шаблонные функции обрабатывают пары элементов. К ним относятся шаблонные функции min(), тах(), swap(), iter_swap(). Функции min(), тах() реализованы в двух вариантах каждая. Заголовки шаблонов для одной из них выглядят так: template <typename Т> const TVS maxfconst Т& х, const Т& у) template <typename Т> const Т& max(const TVS x, const TVS y, Predpr); В первом случае функция возвращает значение у, если х<у. Во втором случае для использования функции необходимо определить предикат и использовать его имя в качестве третьего аргумента. Предикат определяет условие, которому должен соответствовать параметр, значение которого будет возвращено. В качестве примера приведем программу, в которой нужно выбрать целое число, количество единиц в котором меньше, чем у другого целого. Для сравнения целочисленных переменных включим в программу заголовок <algorithm> и используем обобщенный алгоритм min(). Для задания правила сравнения определим функцию-предикат. Текст программы: //Р18_10.срр - Стандартные алгоритмы. #include <iostream>
556 Глава 18 using namespace std; #include <algorithm> ttdefine PRiNT(c) cout« #c << " = " << c << endl; bool minRfint x, int y) { return (x%10 < y% 10); } intmainf) { intb = 13, d-41; PRINT(min(b, d, minR)); swap(b,d); PRINTfb); PRlNT(d); return 0; > Для удобного вывода результатов в программе определен макрос. Так как для предиката minR() не требуется задавать состояния, то он оформлен не как функциональный объект, а как обычная функция. Обобщенный алгоритм min(bfd,minR) невидимо для нас обращается к предикату, передавая ему значения своих первых двух аргументов. Остальное очевидно из результатов выполнения программы: min(b,d,minR) = 41 b = 41 d= 13 В программе для иллюстрации кроме вызова функции min(b,d,minR) выполнено обращение к алгоритму swap(b,d). Он поменял местами значения переменных intb=13, d =41. Сканирование последовательностей без их изменения. Такое сканирование выполняют несколько алгоритмов. Среди них уже использованные нами max_element(), min_element(). Добавим к уже известному, что для этих шаблонных функций можно задавать с помощью функционального объекта правило сравнения элементов. Алгоритмы этой группы позволяют разыскивать в последовательностях элементы с конкретными значениями, выполняют поэлементное сравнение элементов, позволяют подсчитывать количество элементов с требуемыми свойствами.
Основные средства библиотеки STL 557 Сканирование последовательностей с возможным изменением значений их элементов. К алгоритмам этой группы относится уже использованный алгоритм generatef), а также еще более интересная шаблонная функция for_each(): template <typename I nit, typename Fun> Fun for_each(lnlt first, I nit last, Fun f); Функция для каждого элемента, адресуемого итераторами из диапазона [first, last), выполняет функцию ft передавая в нее разыменованное значение соответствующего итератора. Тем самым возможно как получение значений элементов, так и их модификация. В следующей программе определены две вспомогательные функции, предназначенные для применения в алгоритме for_each(). Первая из них printOnef) выводит значение своего параметра и помещает вслед за ним пробел. Вторая div10() - уменьшает в 10 раз значения параметра-ссылки. //Р18_11.срр - Применение алгоритма forjeach к массиву и вектору ft include <iostream> ft include <vector> #include <algorithm> using namespace std; // функция для вывода элемента последовательности void printOnef const int & c) { std::cout« c «' } // Функция для изменения элемента последовательности void div10(int & e) { e /- 10; } intmainf) { int ira[ ]= {10, 20, 30, 40, 50, 60, 70, 80, 90}; for_each(ira, ira+size off ira)/size off ira[0]), printOne); cout« endl; vector <int> seriesfira, ira+sizeof(ira)/sizeof(ira[0])); for_each(series.begin(), series.end(), printOne); cout« endl; for_each(series, beginf), series.end(), div10);
558 Глава 18 for_each( series.beginf), series.end()f printOne); cout«endl; return 0; } В программе определен целочисленный массив int ira[ 7, затем на его основе создан вектор vector <int> series. Алгоритм for_each( ) применен трижды для вывода значений элементов последовательностей. Третий аргумент — имя функции printOne. В первом случае выведены значения элементов массива, а затем -элементы вектора. Потом алгоритм for_each( ) применен для изменения значений элементов вектора series. В этом случае третий аргумент алгоритма — имя функции div10. Результаты выполнения программы: 10 20 30 40 50 60 70 80 90 10 20 30 40 50 60 70 80 90 1 2 3 4 5 6 7 8 9 Для экспериментов с другими алгоритмами введем функциональный объект генерации псевдослучайных чисел из диапазона [miR, maR). Границы диапазона заданы полями данных объекта. Определение функционального объекта в файле functorRand.lv. //functorRand.h - Функтор (класс функциональных объектов). #ifndef _functorRand_ ttdefine _functorRand_ # include <cstdlib> class functorRand { int miR, maR; public: functorRandf int x=0, int y= 100): miR(x), maR(y){ } int operator ()() { return rand()%(maR-miR) + miR; } }; В программе Pl8_09.cpp мы уже использовали для присваивания элементам контейнера алгоритм generatef). Для его применения требуется, чтобы последовательность была не пустой, т. е. все элементы, которым присваиваются значения, были уже включены в контейнер. Для дополнения контейнеров новыми
Основные средства библиотеки STL 559 элементами с одновременным присваиванием этим новым элементам нужных значений можно воспользоваться алгоритмом generate_п(). template <typename Outlt, typename Pred, typename Gen> void generate_n(Outlt first, Dist n, Gen g); Здесь Outlt - итератор вывода некоторого контейнера; Dist п - количество добавляемых элементов; Gen - класс функциональных объектов. Эта шаблонная функция присваивает значение д() каждому из п элементов контейнера, начиная с элемента, адресованного итератором Outlt first. Обратим внимание, что функция д() вызывается заново для каждого из элементов. В следующей программе создадим дек для элементов типа int и, используя в алгоритме generate_n() функциональный объект класса functorRand, заполним дек элементами с псевдослучайными значениями. Так как значения элементов случайны, то удобно продемонстрировать на этой последовательности применение других алгоритмов, например сортировки. Один из них sort() мы уже использовали. Применим к контейнеру (к деку) алгоритм частичной сортировки: template <typename Ranlt > void partial_sort( Ran It first, Ranlt middle, Ranlt last); Алгоритм обрабатывает элементы из интервала [first, last) таким образом, чтобы элементы в интервале [first, middle) оказались упорядоченными по возрастанию их значений. Точнее, все наименьшие элементы последовательности разместились слева от middle, а справа элементы не подвергались упорядочиванию. Текст программы: //Р18_12.срр - Применение алгоритмов forjeach и generate_n #include <iostream> ft include <deque> #include <algorithm> ft include "functorRand.h" using namespace std; // функция для вывода элемента последовательности void printOnef const int & c) { stdr.cout« с «' 7 ;
560 Глава 18 intmainf) { deque <int> drill; functorRand fra(0,10); generate_n(back_inserter(drill), 12, fra); for_each(drill.beginf), drill. end(), printOne); cout« endl; partial_sort(drill. begin(), // Начало интервала drill, beg inf) +6, // Конец сортировки drill.endf)); //Конец интервала forjeachfdrill.beginf), drill.endf), printOne); cout« endl; reversefdrill, beginf), // Начало интервала drill.endf)); //Конец интервала forjeachf drill, beginf), drill.endf), printOne); cout«endl; return 0; } Результаты выполнения программы: 292985 1 1 7067 О 1 1225998767 767899522 1 1 О В программе создан вначале пустой контейнер deque <int> drill и функциональный объект functorRand frafO,10). Алгоритм generate_n() вставляет в конец последовательности 12 элементов, присваивая им псевдослучайные значения с использованием объекта fra. Аргументы конструктора этого объекта задают пределы [0,10) для формируемых значений. Первая строка результатов, выведенная с применением уже известного нам алгоритма foreachf), показывает первоначальное заполнение контейнера. Применение к этому контейнеру алгоритма упорядочило первые шесть элементов. (Вторая строка результатов.) С помощью алгоритма reversef) элементы переставлены в обратном порядке. (Третья строка результатов.) Запись существующей последовательности или некоторых значений в другую последовательность. К алгоритмам этой группы относятся алгоритмы копирования, перестановки значений двух последовательностей и присваивания некоторого значения всем элементам диапазона.
Основные средства библиотеки STL 561 Более многочисленная группа алгоритмов осуществляет замену элементов последовательностей с заданными значениями другими элементами. Простейший из этих алгоритмов replace разыскивает в последовательности элементы с конкретным значением и заменяет это значение другим. Искомое и заменяющее значения задаются аргументами шаблонной функции. Более интересные возможности у алгоритма template <typename Fwdlt, typename Pred, typename T> void replace_if(Fwdlt first, Fwdlt last, Pred pr, const T& value); Для каждого элемента в диапазоне [first, last) вызывается предикат рг и при его истинности этому элементу присваивается значение параметра value. Например, можно заменить нулями все элементы с четными значениями. Несколько алгоритмов этой группы позволяют удалить из последовательности элементы с особыми свойствами, например: Fwdlt remove_if(Fwdlt first, Fwdlt last, Pred pr); Алгоритм удаляет в диапазоне [first, last) все элементы, для которых предикат рг равен true. Например, из контейнера можно удалить все отрицательные элементы. Алгоритмы, изменяющие порядок элементов последовательности. К ним отнесены алгоритмы реверсирования элементов контейнера и особый алгоритм перетасовки элементов (размещение их в случайном порядке): void randomstufflef Randlt first, Randlt last); void random_stuffle( Randlt first, Randlt last, Fun & f); В первом случае перемешивание элементов в диапазоне [first, last) выполняется с использованием равномерного закона случайного распределения, а во втором — пользователь, определяя функцию f(), выбирает закон распределения. Алгоритмы, формирующие новый порядок размещения элементов последовательности. К ним отнесены методы различной сортировки. Отметим, что для этих алгоритмов необходимы итераторы произвольного доступа. Алгоритмы sort() и partial_sort() мы уже использовали в примерах программ. Еще один интересный алгоритм этой группы: Зб-2762
562 Глава 18 void nth_element(Randlt first, Randltnth, Randltlast); void nth_element( Randlt first, Randltnth, Randlt last Pred pr); В диапазоне [first, last) выбирается итератор nth, адресующий некоторый элемент последовательности. Элементы контейнера перемещаются таким образом, что все элементы слева меньше *nth, а справа - больше *nth. Предикат рг позволяет определить правило сравнения элементов. Алгоритм удобно применять, если из последовательности нужно выбрать заданное количество наименьших или наибольших элементов, не выполняя полную сортировку. Алгоритмы объединения двух упорядоченных последовательностей. Эти алгоритмы представлены двумя шаблонными функциями: тегде() и inplace_тегде(). Назначение алгоритма тегде() - создание на основе двух последовательностей одной, число элементов которой равно общей сумме элементов. Алгоритм inplace_тегде() выполняет слияние смежных последовательностей, т. е. конец первой является началом второй. Алгоритмы сканирования упорядоченной последовательности для поиска в ней элементов с особыми свойствами. Найти первый (или последний) элемент, не меньший заданного значения lower_bound(), upper_bound(), или найти верхнюю (нижнюю) грань относительно определенного значения equal_гапде(). Шаблонная функция: bool binare_search(Fwdlt first, Fwdltlast, const T& val); bool binare_search(Fwdlt first, Fwdltlast, const T& val, Pred pr); позволяет проверить наличие в последовательности элемента, эквивалентного заданному значению. Под эквивалентностью в частном случае понимается равенство значений. Алгоритмы сканирования двух упорядоченных последовательностей для сравнения или объединения их элементов в одной последовательности. Шаблонная функция includes проверяет, есть ли в одной последовательности все элементы другой. Четыре алгоритма, названия которых включают префикс sef_, формируют новую последовательность, занося в нее те элементы первой последовательности, которые по каким-то показателям похожи или отличны от элементов второй последовательности. Например, шаблонная функция
Основные средства библиотеки STL 563 Outlt set_difference(Intltl firstl, Intltl lastl, lntlt2 first2, Intlt2last2, Outltx); Outlt set_difference(Intlt 1 firstl, Intltl lastl, Intlt2 first2, Intlt2last2, Outltx, Pred pr); включает в последовательность, представленную итератором Outlt х, те элементы первой последовательности, которых нет во второй. Следующая программа демонстрирует принципы использования этого алгоритма. С помощью set_difference() сравниваются два строковых массива (strArray[ ], line). Элементы массивов (строки) упорядочены по алфавиту. В результирующий массив строк string result[9] переносятся только те строки (элементы), которых нет во втором массиве. //Р18_13.срр - Алгоритм setjdifference. #include <iostream> #include <string> #include <algorithm> using namespace std; intmainf) { string strArray[] - {"five", "four", "one", "six", "two", "three"}; intlen = sizeoffstrArray)/sizeof(strArray[0]); string line[] = {"four", "two"}; intlenLine = sizeoff line)/sizeof( line[0]); string result[9]; cout«"The begin figures: "«end!; for(intj=0; j < len; j++) cout« strArray0] << ""; setjdifferencefstrArray, strArray+len, line, line+lenLine, result); cout« endI« "The result figures: "« endl; for(intj=0; resuit[j]-length()!=0; j++) cout« result [j] « ""; cout« endl; return 0; } Результаты выполнения программы: The begin figures: five four one six two three The result figures: five one six three 36*
Глава 19 СТАНДАРТНАЯ БИБЛИОТЕКА и ввод-вывод 19.1. Обзор стандартной библиотеки Си++ Как пишет Б. Страуструп ([2], с. 199) библиотеки в самом общем смысле можно разделить на две группы: базовые и специализированные. В свою очередь базовые библиотеки делятся на"горизонтальные' и "вертикальные \ Горизонтальная библиотека содержит набор средств для разработки программ на разных платформах и для разных предметных областей. Обычно в такую библиотеку помещают основные структуры данных (списки, деревья и т.д.) а также достаточно универсальные или часто используемые алгоритмы (сортировка, поиск, ввод-вывод, дата и время и т.д.). "Цель вертикальной библиотеки — предоставление полного набора сервисов для конкретного окружения, например X Windows System, MS Windows, MacApp и т.д." Специализированные библиотеки можно разделить на библиотеки поддержки собственно программирования и библиотеки, ориентированные на предметные области. К первой группе относятся, например, библиотеки для численных расчетов и библиотеки для работы с базами данных. Ко второй — библиотеки для электротехнических расчетов и библиотеки для распознавания образов. Описывая библиотеки языка Си++, Б. Страуструп начинает со следующего сравнения ([2], с. 189): "Библиотека FORTRAN'a - это набор подпрограмм, библиотека Си - набор функций и ассоциированных с ними структур данных, библиотека Smalltalk — иерархия классов с корнем в определенном месте стандартной иерархии Smalltalk". Язык Си++ допускает различные подходы к построению библиотек, и в настоящее время их создано и
проСтандартная библиотека и ввод-вывод 565 мышленно используется очень много. В стандарт языка Си++ включены только базовые горизонтальные библиотеки, причем это не только библиотеки классов. На ранней стадии развития языка Си++, когда стандарт еще не начал разрабатываться, в компиляторы включали стандартную библиотеку языка Си и библиотеку классов потокового ввода-вывода, призванную заменить библиотечные функции ввода- вывода языка Си. Впоследствии эти средства были стандартизованы и существенно дополнены. В настоящее время стандартная библиотека языка Си++ включает 10 относительно небольших библиотек (не смешивайте библиотеки со стандартными заголовками): • language support - поддержка языка. Включает средства, непосредственно используемые компилятором. Это предельные значения числовых значений, средства динамического распределения памяти, идентификация типов, механизмы обработки исключений, поддержки библиотеки языка Си, завершения программ, системное время; • diagnostic - диагностика — стандартные исключения, диагностика кодов, формирование сообщений об ошибках; • general utilities - основные утилиты — набор средств общего пользования, которые трудно отнести к другим более специализированным библиотекам; • strings - строки — средства для обработки строк, элементы которых могут представлять символы в разных кодировках, функции для обработки строк в стиле Си; • locales - средства локализации — позволяют отображать национальные и культурные различия, например правила записи дат, символы для обозначения валют; • containers - контейнеры — это объекты, содержащие другие объекты. В стандартной библиотеке контейнеры представляют коллекции элементов, тип которых параметризирован. Говорят также, что контейнеры управляют коллекциями элементов (Н. Джосьютис [5], с. 88). Примеры контейнеров мы уже приводили; • iterators - итераторы. Итератором называется объект, предназначенный для последовательного перебора элементов контейнера. Говорят, что итератор — это средство навигации по коллекции, представленной контейнером. Итераторы
обеспечи566 Глава 19 вают "настройку" стандартных алгоритмов библиотеки на работу с конкретными контейнерами; • algorithms - алгоритмы — обобщенные алгоритмы обработки данных, представляемых контейнерами. Кроме того, к алгоритмам отнесены функции bsearchf), qsort() стандартной библиотеки языка Си, которые применимы к встроенным массивам языка; • numerics - представления чисел и вычисления — комплексные числа, математические функции, генераторы псевдослучайных чисел в стиле Си; • input/output - ввод-вывод — представление стандартных потоков ввода-вывода и операции с этими потоками, буферизация потоков, манипуляторы, потоковый обмен со строками, потоки для работы с файлами, обмен в стиле Си. Средства стандартной библиотеки определены в пространстве имен std. Для использования тех или иных возможностей стандартной библиотеки в программу нужно включить соответствующий заголовок. Самостоятельное описание библиотечных средств без применения заголовочного файла не соответствует стандарту. Пользователям или авторам библиотеки не разрешается добавлять или убирать описания из заголовочных файлов. Стандарт определил 32 заголовка для библиотечных средств языка Си++: <algorithm> <bitset> <complex> <deque> <exception> <fstream> <functional> <iomanip> <ios> <iosfwd> <iostream> <istream> <iterator> <limits> <list> <locale> <map> <memory> <new> <numeric> <ostream> <queue> <set> <sstream> <stack> <stdexcept> <streambuf> <string> <typeinfo> <utility> <valarray> <vector> Кроме того, определены 18 заголовков для средств языка Си, также входящих в стандартную библиотеку Си++: <cassert> <cctype> <cerrno> <cfloat> <climits> <clocale> <cmath> <csetjmp> <csignal> <cstdarg> <cstddef> <cstdio> <cstdlib> <cstring> <ctime> <cwchar> <cwctype>
Стандартная библиотека и ввод-вывод 567 В Приложении 6 приведены сведения о библиотечных функциях языка Си, описанных в этих заголовках. Как уже говорилось, из "старомодных" имен заголовков удалено расширение “.h”. Кроме того, заголовок средств языка Си имеет вид <сИМЯ>, где ИМЯ определено стандартом языка Си (ISO/IEC 9890:1990) как одно из имен, входящих в имена заголовочных файлов <ИМЯ./7> стандартной библиотеки. Полный список заголовков с описанием их назначения приведен на с. 487—490 монографии Б. Страуструпа [1] и, конечно, в Стандарте языка [4] на с. 319 и далее. В Приложении 9 приведены сведения о средствах для работы с комплексными числами, которые доступны в программе при включении заголовка <complex>. Перечисляя принципы, положенные в основу проекта стандартной библиотеки, Б. Страуструп отмечает, что основной упор делался на следующие три роли: • переносимость создаваемых программ; • возможность расширения за счет добавления своих типов (например, ввод-вывод в стиле ввода-вывода стандартных типов); • фундамент для других библиотек. «Стандартную библиотеку нельзя назвать простой и понятной. Чтобы работать с ее компонентами и пользоваться ее преимуществами, недостаточно простого перечисления классов и их функций — требуется хорошее объяснение основных концепций и важных подробностей» ([5], с. 19). К объяснению и демонстрации механизмов стандартной библиотеки мы обращались на протяжении книги уже несколько раз. Во-первых, целая глава была посвящена строкам в стиле Си++. Во-вторых, в главах 17 и 18 мы с разных сторон рассматривали принципиальные особенности и возможности механизмов библиотеки STL. И наконец, нельзя забывать, что невозможно было бы привести никакие работающие примеры программ, не введя хотя бы некоторые средства библиотеки ввода-вывода. Однако вопросы обмена данными между программой и внешними устройствами настолько важны, что библиотеку ввода-вывода нужно изучить гораздо подробнее.
568 Глава 19 19.2. Ввод-вывод в языке Си++ Система ввода-вывода в языке Си++ построена на основе потоков, с которыми программа при исполнении обменивается данными. Поток - логическое понятие, которое можно воспринимать как последовательность байтов (символов). Система ввода-вывода позволяет связывать потоки с различными физическими устройствами. После установления такой связи программист может работать с потоком, не задумываясь о том, к какому внешнему устройству поток подключен. Однако нужно иметь хотя бы некоторое представление о внутренних механизмах обменов, выполняемых между исполняемой программой и внешним миром (внешними устройствами). Итак, поток — последовательность байтов, не зависящая от тех конкретных устройств, с которыми ведется обмен данными. Буфер потока — вспомогательный участок основной памяти, используемый для промежуточного хранения информации при обменах между программой и внешним устройством, представленным потоком. В буфер потока (рис. 19.1) помещаются выводимые программой данные перед тем, как они будут переданы к внешнему устройству При вводе данных (рис. 19.2) они вначале помещаются в буфер и только затем передаются в область памяти выполняемой программы. Использование буфера как промежуточной ступени при обменах с внешними устройствами повышает скорость передачи данных, так как реальные пересылки осуществляются только тогда, когда буфер уже заполнен (при выводе) или пуст (при вводе). Работу, связанную с заполнением и очисткой буферов ввода- вывода, операционная система очень часто берет на себя и выполняет без явного участия программиста. Поэтому поток в прикладной программе обычно можно рассматривать как последовательность байтов. При этом очень важно, что никакой связи значений этих байтов с кодами какого-либо алфавита не предусматривается. Задача программиста при вводе-выводе с помощью потоков — установить соответствие между участвующими в обмене типизированными объектами и последовательностью байтов потока, в которой отсутствуют всякие сведения о типах представляемой (передаваемой) информации.
Стандартная библиотека и ввод-вывод 569 Передача при заполнении буфера или по специальной команде “пересылка буфера" 1 Внешний носитель информации Рис. 19.1. Схема буферизации при выводе данных из программы Внешний источник информации Передача при пустом буфере ввода Буфер ввода Пересылки (извлечения) по командам прикладной программы Принимающие объекты Прикладная программа Основная память Рис. 19.2. Схема буферизации при вводе данных в программу В современной версии языка Си++ каждый поток представлен объектом некоторого класса. Потоки делятся на три группы:
570 Глава 19 • входные, из которых читается информация; • выходные, в которые выводятся данные; • двунаправленные, допускающие как чтение, так и запись. В каждый момент времени для двунаправленного потока определены две позиции — позиция записи и позиция чтения. Для каждого из однонаправленных потоков определена либо позиция записи, либо позиция чтения. При операции обмена позиция перемещается на длину прочитанного или записанного в поток фрагмента данных. Достоинства механизма потоков: • потоки обеспечивают буферизацию при обменах с внешним устройством; • создают независимость от конкретных ЭВМ и операционной системы; • выполняют контроль типов передаваемых данных; • делают возможным удобный обмен для типов, вводимых пользователем. В соответствии с теми устройствами, на которые могут быть настроены потоки, различают: • стандартные (основные) потоки; • строковые потоки; • файловые потоки. Стандартные потоки по умолчанию соответствуют передаче данных от клавиатуры к исполняемой программе и от исполняемой программы к консоли (к дисплею). Если обмены (ввод-вывод) выполняются со строкой, то говорят о строковом потоке. Если данные потока размещены на внешнем носителе информации, то для обменов применяются файловые потоки. Иерархия шаблонов классов стандартной библиотеки ввода-вывода. Библиотека потоковых классов первоначально была построена на базе двух классов: ios и streambuf. Класс streambuf обеспечивал буферизацию данных. Класс ios содержал компоненты, которые являются общими как для ввода, так и для вывода. В процессе развития библиотеки ввода-вывода на базе класса ios (модифицированного и переименованного в ios_base) была построена иерархия шаблонов классов, схема которой приведена на рис. 19.3. Вместо класса streambuf теперь используется шаблон классов basicjstreambufO, на основе которого созданы производные шаблоны классов для поддержки
Стандартная библиотека и ввод-вывод 571 файловых и строковых потоков. Переход от библиотеки классов к библиотеке шаблонов позволил выполнять настройку средств ввода-вывода на разные представления и разные трактовки данных. В стандартную библиотеку введены специализации шаблонных классов для типов данных char и wchar_t. Именно одной из этих специализаций (для данных типа char) мы пользовались во всех программах книги. ios_base j i basic_ i о (Л Л V basic istreamo virtual basic_ostream<> тг z— |/lasicjstringstreamo basicJostreamo ~ t ' ' ' basic_stringstreamo basic_ostringstream<> basic_ifstream<> basic_fstream<> basic_ofstream<> Рис. 19.3. Иерархия шаблонов потоковых классов Остановимся на назначении шаблонов классов стандартной библиотеки ввода-вывода и их специализаций. Начнем с иерархии шаблонов потоковых буферов (рис. 19.4). Их основное назначение — обеспечить абстрагирование от внешнего представления данных (например, в строках или в файлах). Для работы со стандартными (основными) потоками используется шаблон классов basic_streambuf<> и его специализации streambuf - для данных типов char и wstreambuf — для данных типа wcharjt. Для работы с файловыми потоками предназначен шаблон basic_filebuf<> и его специализации filebuf и wfilebuf (созданные, соответственно, для данных типа char и wcharjt). Для работы со строковыми потоками предназначен шаблон basicjstringbufO и его специализации stringbuf и wstringbuf (созданные соответственно для данных типов char и wcharjt). В прикладных программах обычно не требуется обращаться к методам и данным классов потоковых буферов. Возможностями этих
572 Глава 19 Рис. 19.4. Иерархия шаблонов потоковых буферных классов шаблонов классов и их специализаций пользуются при настройке программ на новые внешние устройства. Обратимся к иерархии, представленной на рис. 19.3: ios_base — базовый потоковый класс; basic_ios<> - шаблон базовых потоковых классов (см. Приложение 8) определяет общие свойства потоковых классов, зависящие от типа символов. Его специализации ios и wios созданы соответственно для данных типов char и wchar_t. В число свойств шаблонного класса входит объект шаблона классов basicjstreambufO, представляющий буфер обмена для каждого класса потоков, построенного на базе специализации шаблонного класса basic_ios<>; basicJstream О - шаблон классов входных потоков. Его специализации istream и wistream определяют объекты, которые представляют в программе входные потоки; basic_ostream О - шаблон классов выходных потоков. Его специализации ostream и wostream определяют объекты, которые представляют в программе выходные потоки; basicJostream О - шаблон классов двунаправленных потоков. Его специализации iostream и wiostream определяют объекты, которые представляют в программе потоки для чтения и записи; basicjstingstream О - шаблон классов входных строковых потоков. Его специализации istringstream и wistringstream определяют объекты, которые представляют в программе входные строковые потоки; basic_ostingstream О — шаблон классов выходных строковых потоков. Его специализации ostringstream и wostringstream определяют объекты, которые представляют в программе выходные строковые потоки; basic_stingstream О - шаблон классов двунаправленных строковых потоков (для чтения и записи). Его специализации
Стандартная библиотека и ввод-вывод 573 stringstream и wstringstream определяют объекты, которые представляют в программе строковые потоки для чтения и записи; basic Jfstream О - шаблон классов входных файловых потоков. Его специализации ifstream и wifstream определяют объекты, которые представляют в программе входные файловые потоки; basic_ofstream О - шаблон классов выходных файловых потоков. Его специализации ofstream и wofstream определяют объекты, которые представляют в программе выходные файловые потоки; basic_fstream О - шаблон классов двунаправленных файловых потоков (для чтения и записи). Его специализации fstream и wfstream определяют объекты, которые представляют в программе файловые потоки для чтения и записи. Все шаблонные классы библиотеки ввода-вывода имеют два типизирующих параметра. Так как полное описание библиотеки слишком велико для нашей книги, то приведем заголовок только шаблона базовых классов: namespace std { template <typename charT, typename traits = char_traits<chart> > class basicjos; Обратите внимание, что шаблон классов принадлежит пространству имен std (пространству имен стандартной библиотеки). Первый параметр шаблона charT определяет тип символов, для которых определен поток. Второй параметр шаблона traits определяет класс трактовок символов. По умолчанию этот параметр заменяется аргументом char_traits<chart>. Здесь char_traits< > - шаблон класса стандартной библиотеки, который содержит информацию о разных аспектах представления символов и операции, необходимые для реализации строк и потоков данных с этими символами. Обратите внимание, что в умалчиваемом значении аргумента шаблон специализирован первым параметром шаблона. Таким образом, подставляя первый аргумент и не указывая второго, мы настраиваем специализацию шаблона на трактовку символов именно того типа, который определен первым аргументом.
574 Глава 19 Для двух самых распространенных типов символов char и wchar_t определены специализации шаблона классов basic _ios<>: namespace std { typedef basic_ios<char> ios; typedef basic Jos < wchar_t> wios; } Подобным же образом специализируются для двух важнейших типов символов все шаблоны потоковых классов иерархий, изображенных на рис. 19.3 и 19.4. Основные потоки ввода-вывода. Стандартные (основные) потоки ввода-вывода представлены классами: istream, ostream, iostream - это специализации для данных типа char, соответственно шаблонов basicjostream О, basicjstream О, basicjostream О. Таким образом, классы istream, ostream, iostream "настроены" на представление потоков, с которыми выполняются посимвольные обмены, и тип символов определен как char, istream - класс входных потоков (для чтения данных программой); ostream — класс выходных потоков (для записи программой данных); iostream - класс двунаправленных потоков и ввода и вывода; Названные потоки становятся доступными, когда в программу помещен заголовок <iostream>. В этом же заголовке определены глобальные потоковые объекты: с/п - объект класса istream; cout - объект класса ostream; сегг - объект класса ostream; clog - объект класса ostream. Эти объекты по умолчанию настроены на потоки, представляющие стандартные системные устройства: клавиатуру (поток istream), экран дисплея (поток ostream). При исполнении программы можно назначить ввод или вывод на другие устройства. Вместе с классами потоков и глобальными потоковыми объектами в библиотеке определены операция чтения (ввода или извлечения из потока)» и операция вывода (записи или вставки в поток) <<.
Стандартная библиотека и ввод-вывод 575 Зная механизм перегрузки (расширения действия) операций и познакомившись с процедурным полиморфизмом, мы уже понимаем, как одна и та же операция (например, вывода) применима к операндам разных типов. Существуют варианты операций, которые «настроены» на конкретные типы данных. Например, для ввода определены три операции: для вещественных чисел, для целых чисел и для строк. Какой именно вариант операции применяется — зависит от типа ее правого операнда. Распознавание типа этого операнда выполняется автоматически и не требует вмешательства программиста. Для типов, вводимых программистом, ввод-вывод в стандартные потоки обеспечивается, как мы уже знаем, операциями- функциями соответствующих классов: ссылка_на_поток operator «( ссылка_на_поток, тип_объекта имя) ссылка_на_поток operator »(ссылка_на_поток, ссылка_на_объект имя) Возвращаемым значением должна быть ссылка на поток, с которым мы работаем: при вводе istream <£; при выводе ostream &. 19.3. Форматирование данных при обменах с потоками Флаги форматирования. При вводе-выводе с помощью операций чтения из потока » или записи в поток « используются умалчиваемые форматы внешнего представления пересылаемых значений. Таким образом, информация вводится или выводится в том виде, который заранее определен. Но это не всегда удобно. Для влияния на форму вводимой или выводимой информации у программиста есть возможность использовать ниже перечисленные поля данных базового класса ios_base, называемые флагами форматирования. Им в стандарте поставлен в соответствие тип fmtflags, отнесенный к типам битовых масок (bitmask types). Список флагов форматирования: boolalpha - представлять логические значения не в числовом а в символьном виде, т.е. в виде true и false',
576 Глава 19 dec - десятичная система счисления (используется по умолчанию); fixed - для вещественных чисел использовать представление в формате с фиксированной точкой; hex - шестнадцатеричная система счисления; internal - символ заполнения пустых позиций помещается между числовым значением и знаком числа; left - выводимое значение выравнивается по левому краю; oct - восьмеричная система счисления; right - выводимое значение выравнивается по правому краю; scientific - для вещественных чисел указать мантиссу и порядок; showbase - показать основание системы счисления (Ох - шестнадцатеричное основание, 0 - восьмеричное основание); showpoint - при выводе вещественных чисел печатать десятичную точку и следующие за ней нули; showpos - печатать знак положительного числа; skipws - при чтении данных из входного потока игнорировать начальные обобщенные пробельные символы; unitbuf - очищать все потоки (выгружать содержимое буфера) после каждого ввода или вывода; uppercase - при выводе шестнадцатеричных чисел использовать буквы верхнего регистра. Любой из флагов форматирования доступен с помощью переменной вида ЮБ::имя_флага. Например: ios::dec. Флаги анализируются при обменах и влияют на представление информации. Существуют несколько методов класса ios, которые позволяют проверить значения любых флагов и установить или сбросить их: fmtflags setf(0narn) - устанавливаются флаги аргумента (объединенные операцией поразрядной дизъюнкции); fmtflags setf(0narn, маска) — устанавливаются флаги первого аргумента (объединенные операцией поразрядной дизъюнкции), второй аргумент позволяет отметить флаги, которые сбрасываются перед установкой флагов, указанных первым аргументом; void unsetf(0narn) - сбрасываются флаги, указанные аргументом;
Стандартная библиотека и ввод-вывод 577 fmtflags flags() - возвращает как значение std::ios::fmtflags весь набор форматных флагов; fmtflags flags (флаги) - устанавливает флаги в соответствии со значением аргумента. Возвращает предыдущее значение флагов. Примеры: // В выходном потоке установить флаги left и fixed: std::cout.setf(std::ios::left\std::ios::fixed); 11 Сохранить текущие значения всех флагов: using namespace std; ios::fmtflags flagsMem = cout.flagsf ); Наряду с перечисленными флагами форматирования в классе ios_base определены родственные им константы, для которых значения фиксированы: adjustfield - значения: left, right, internal; basefield - значения: dec, oct,hex; floatfield - значения: scientific, fixed. Каждая из этих констант имеет тип fmtflags и объединяет несколько установленных флагов форматирования. Все они статические, следовательно, к ним надо обращаться с помощью конструкции юе::имя_константы. Эти константы обычно используют как аргументы функции setf( ). Например, их удобно использовать в качестве второго аргумента, чтобы сбросить именно те флаги, которые нельзя сохранять установленными при заданном сочетании флагов первого аргумента. Методы форматирования данных. Кроме флагов форматирования, для управления внешним представлением данных используются следующие методы класса ios_base: streamsize width( streamsize prec) - задает минимальную ширину поля вывода; streamsize widthf) const - возвращает ширину поля вывода; streamsize precisionf streamsize wide) - задает точность представления вещественных чисел (количество цифр после точки); streamsize precisionf) const - возвращает точность представления вещественных чисел. В приведенных прототипах методов streamsize - это синоним одного из знаковых базовых обобщенных целых типов. Этот 37-2762
578 Глава 19 тип используется в большинстве случаев, когда должен использоваться тип sizej, введенный стандартом ISO языка Си. На базе класса ios_base в иерархии классов ввода-вывода определен шаблон классов basicjos О. Некоторые из его методов чрезвычайно полезны для форматирования данных при обменах с потоками. Рассмотрим только один из этих методов: charjype fill ( ) const - функция возвращает текущий символ, размещаемый в незанятых позициях поля вывода (символ заполнения пустых полей); charjtype fill (charjtype fillch) - позволяет установить новый символ fillch заполнения пустых полей. В следующей программе демонстрируются основные приемы форматирования данных с помощью перечисленных средств классов ios_base и basicjos О. //Р19_01.срр - Форматирование при выводе #include <iostream> using namespace std; void arPrintfint len, double ar[ ], int wid) { for(inti=0; i<len; i++) { cout.width(wid); // Минимальная ширина поля! cout« ar[i]; } cout« end!; } intmainf) { double dar[] = {-12e-2, +34, -5678, 91.23}; intarLen = sizeof(dar)/sizeof(dar[0]); arPrintfarLen, dar, 1); cout.setf(ios::scientific); arPrintfarLen, dar, 1); cout fill( ’J); // Символ для заполнения пустых позиций coutsetf(ios::left, ios::floatfield \ ios::adjustfield); arPrintfarLen, dar, 12); coutsetffios:-.internal, ios::floatfield \ ios::adjustfield); arPrintfarLen, dar, 12); cout.precision(2); cout.setf( ios:: scientific | ios:: right); arPrintfarLen, dar, 12); return 0; )
Стандартная библиотека и ввод-вывод 579 В программе определена функция arPrint(intlen, double ar[], int wid) для вывода значений элементов вещественного массива. Первый параметр — количество выводимых элементов, второй — указатель на начало выводимой последовательности, третий параметр — ширина (в символах) поля для размещения значения одного элемента. Обратите внимание, что оператор cout.width(wid); выполняется в цикле. Метод width() действует только один раз — задает минимальную ширину поля одного выводимого значения. Поэтому к нему приходится обращаться перед каждым выводом. В основной функции программы определен и инициализирован массив с элементами типа double. Значения его элементов выводятся с помощью обращений к функции arPrintf). Результаты выполнения программы: -0.1234-567891.23 -1.200000е-013.400000е+01 -5.678000е+039.123000е+01 -0.12 34 -5678 91.23 - 0.12 34- 5678 91.23 - 1.20е-01 3.40е+01 -5.68е+03 9.12е+01 При первом и втором обращениях ширина поля установлена слишком малой, и выводимые значения размещены подряд. При первом обращении используются умалчиваемые значения флагов. Затем вызов метода cout.setffios::scientific); изменил форматирование — вещественные числа представлены в научной нотации. В следующих обращениях к функции arPrintf) ширина поля с помощью третьего аргумента установлена равной 12 позициям. Вызов метода cout.fill('_'); изменил символ заполнения пустых позиций. По умолчанию этим символом был пробел, теперь его заменили подчеркиванием. Обращение к методу cout.setf() включает два аргумента. Второй отменяет установку флагов размещения в поле выводимого значения (adjust- field) и форму нотации (floatfield). Первый аргумент устанавливает флаг смещения в поле вывода значения влево (left). Перед следующим обращением к arPrintf) устанавливаются флаги scientific и iright. Обращение к методу precisionf2) задает количество цифр после точки в каждом представлении вещественного числа. 37*
580 Глава 19 Манипуляторы потоков. Несмотря на гибкость и большие возможности управления форматами с помощью методов классов ios_base и basicjos О, их применение достаточно громоздко. Более простой способ изменения параметров и флагов форматирования обеспечивают манипуляторы, к возможностям которых мы перейдем. Манипуляторами называют специальные функции, позволяющие программисту изменять состояния и флаги потока. Особенность манипуляторов и их отличие от обычных функций состоит в том, что их имена (без параметров) и вызовы (с параметрами) можно использовать в качестве правого операнда для операции обмена << или >>. В качестве левого операнда в этом выражении, как обычно, используется поток (объект, представляющий поток), и именно на этот поток оказывает влияние манипулятор. Прежде чем переходить к перечислению манипуляторов и их свойств, напомним, что мы уже многократно пользовались одним из них. Манипулятор endl при использовании в выражении cout« endl помещает в выходной поток символ перехода на новую строку (в той кодировке, которая используется в потоке) и принудительно пересылает содержимое буфера потока тому устройству, на которое настроен поток. В качестве параметра каждый манипулятор автоматически (без явного участия программиста) получает ссылку на тот поток, с которым он используется в выражении. После выполнения манипулятора он возвращает ссылку на тот же поток. Поэтому манипуляторы можно использовать в цепочке включений в поток или извлечений из потока. Манипуляторы определены в шаблонах классов basicjos О, basicJstream О, basic_ostream О. В basicjos О определены манипуляторы следующих групп: • форматных флагов (fmtflags); • размещения (выравнивания) в поле (adjustfield); • основания счисления (basefield)', • представления вещественного числа (floatfield). В basic Jstream О определен шаблон функций ws(), играющих роль манипуляторов, назначение которых — извлечь из входного потока пробельные символы. В basicjostream О определены манипуляторы endl, ends, flush.
Стандартная библиотека и ввод-вывод 581 Кроме того, в заголовке <iomanip> определен специальный тип smanip и несколько связанных с ним функций, отнесенных к стандартным манипуляторам. К сожалению, дальнейшее изучение подробностей потребовало бы существенного увеличения объема книги. Поэтому отсылаем читателя к [ I ] и [4] и остановимся на практических вопросах применения наиболее значимых манипуляторов. Манипуляторы потоков ввода-вывода для программиста- пользователя удобно разделить на две группы: манипуляторы с параметрами и манипуляторы без параметров. Манипуляторы без параметров: dec - при вводе и выводе устанавливает флаг десятичной системы счисления; hex - при вводе и выводе устанавливает флаг шестнадцатеричной системы счисления; oct - при вводе и выводе устанавливает флаг восьмеричной системы счисления; ws - действует только при вводе и предусматривает извлечение из входного потока (и игнорирование) пробельных символов: пробела, знаков табуляции '\Г и '\v', символа перевода строки '\л’, символа возврата каретки V'. символа перевода страницы VV endl — действует только при выводе, обеспечивает включение в выходной поток символа новой строки м сбрасывает буфер (выгружает содержимое) этого потока; ends - действует только при выводе и обеспечивает включение в поток нулевого признака конца строки (терминальный символ '\0'); flush - действует только при выводе и очищает выходной поток, т.е. сбрасывает его буфер (выгружает содержимое буфера). Обратите внимание, что не все перечисленные манипуляторы действуют как на входные, так и на выходные потоки: ws действует только при вводе; endl, ends, flush - только при выводе. Манипуляторы dec, hex, oct, задающие основание системы счисления, изменяют состояние потока, и это изменение остается в силе до следующего явного изменения. Манипулятор endl рекомендуется использовать при каждом выводе, который должен быть незамедлительно воспринят
поль582 Глава 19 зователем. Например, его использование просто необходимо в таком операторе: cout« "Ждите! Идет сбор статистики."« endl; При отсутствии endl здесь нельзя гарантировать, что сообщение пользователю не останется в буфере потока cout до окончания набора статистики. Рекомендуется с помощью манипулятора flush сбрасывать буфер входного потока при выводе на экран подсказки до последующего ввода информации: cout« "Введите название файла:"« flush; cin » fileName; // Здесь fileName - символьный массив Манипуляторы с параметрами определены в заголовке <iomanip>: setbasefint п) устанавливает основание (п) системы счисления. Значениями параметра п могут быть: 0, 8, 10 или 16. При использовании параметра 0 основание счисления при выводе выбирается десятичным. При вводе параметр 0 означает, что целые десятичные цифры из входного потока должны обрабатываться по правилам стандарта ANSI языка Си; resetiosflagsfмаска) сбрасывает отдельные форматные флаги потоков ввода и вывода на основе значения параметра; setiosflagsfмаска) устанавливает форматные флаги потоков ввода-вывода на основе значения параметра; setfill(char_type п) значение параметра п в дальнейшем используется в качестве кода символа-заполнителя, который помещается в незанятых позициях поля при выводе значения; setprecisionfint п) определяет с помощью значения параметра п точность представления вещественных чисел, т.е. максимальное количество цифр дробной части числа при вводе и выводе; setwfint п) значение параметра п задает минимальную ширину поля вывода. С помощью манипуляторов можно управлять представлением информации в выходном потоке. Например, манипулятор setwfint п) позволит выделить для числового значения поле фиксированной ширины, что удобно при печати таблиц.
Стандартная библиотека и ввод-вывод 583 19.4. Функции для обмена с потоками Функции вывода. Кроме операции включения (записи) в поток << и извлечения (чтения) из потока >>, в классах библиотеки ввода-вывода есть функции, обеспечивающие программиста альтернативными средствами для обмена с потоками. При выводе используют две функции для двоичного вывода данных, шаблоны которых входят в шаблон классов basic_ostream О. Пусть ostream - специализация шаблона basic_ostream О. Тогда прототипы названных функций примут следующий вид: ostream& ostream::риЦchar ch); ostream& ostream::write(const char *array, streamsize n); Функция put() помещает в тот выходной поток, для которого она вызвана, символ, использованный в качестве аргумента. Таким образом, эквивалентны операторы: cout « 'Z'; и cout.put('Z'); Функция write() имеет два параметра — указатель array на участок памяти, из которого выполняется вывод, и целое значение п, определяющее количество выводимых из этого участка символов (байт). В отличие от операции « включения в поток функции put() и write() не обеспечивают форматирования выводимых данных. Например, если при выводе одного символа с помощью операции << можно, используя метод width() или манипулятор setw(), разместить символ в поле из нужного количества позиций, то функция put() всегда разместит символ в единственной позиции выходного потока. Флаги форматирования также не применимы к функциям put() и write(). Так как функции put() и write() возвращают ссылки на объект того класса, для которого они выполняются, то можно организовать цепочку вызовов: char ss[ ] = "Merci"; cout.put('\n').write(ss,sizeof(ss)-1).put('!')«endl; На экране (в потоке cout) появится: Merci!
584 Глава 19 Функции позиционирования: posejype tellp() определяет текущую позицию записи в поток; ostream& seekp(pose_type pos, seek_dir dir); выполняет перемещение позиции записи вдоль потока в направлении, определенном параметром dir, принимающим значения из перечисления епит seekjdir { beg, cur, end}. Относительная величина перемещения (в байтах) определяется значением параметра posjype pos. Если направление определено как beg, то смещение от начала потока; сиг — от текущей позиции; end - от конца потока; ostream& seekp(posejype pos); устанавливает абсолютную позицию записи в поток. Функции ввода (чтения из потока). Если необходимо прочитать из входного потока строку символов, содержащую пробелы, то с помощью операции извлечения >> это делать неудобно - каждое чтение строки выполняется до пробела, а ведущие (левые) пробельные символы игнорируются. Если мы хотим, набрав на клавиатуре строку "Qui vivra verra — будущее покажет (лат.)", ввести ее в символьный массив, то с помощью операции извлечения >> это сделать несколько хлопотно: все слова будут читаться отдельно (до пробела). Гораздо удобнее воспользоваться функциями бесформатного (двоичного) чтения. Функции двоичного (бесформатного) чтения данных принадлежат шаблону классов basicJstream О. Прежде чем перечислить их, отметим основное свойство двоичного чтения данных. Данные читаются без преобразования их из двоичного представления в текстовое. Например, если во входном потоке размещено представление вещественного числа 1.3£-3, то оно будет воспринято как последовательность из шести байт, и читать эту последовательность с помощью функций двоичного ввода можно только в символьный массив или в строку в стиле Си++. Для дальнейшего примем, что istream - специализация шаблона basic Jstream О. Итак, функции чтения. Во-первых, это перегруженные функции get(). Первая из них имеют следующий прототип: istream& getfchar *array, streamsize max Jen, chardelim =’\n'); Функция выполняет извлечение (чтение) последовательности байтов из стандартного входного потока и перенос их в
симСтандартная библиотека и ввод-вывод 585 вольный массив, задаваемый первым параметром. Второй параметр определяет максимально допустимое количество прочитанных байтов. Третий параметр определяет ограничивающий символ (код), при появлении которого во входном потоке следует завершить чтение. По умолчанию третий параметр имеет значение '\п' — переход на следующую строку, однако при обращении к функции его можно задавать и по-другому. Значение этого третьего параметра из входного потока не удаляется, он в формируемую строку (символьный массив) не переносится, а вместо него автоматически добавляется "концевой" символ строки '\0\ Если из входного потока извлечены ровно max Jen - 1 символов, однако ограничивающий символ (например, по умолчанию Чл1) не встретился, то концевой символ помещается после введенных символов. Массив, в который выполняется чтение, должен иметь длину не менее max Jen символов. Если из входного потока не извлечено ни одного символа, то устанавливается код ошибки. Если до появления ограничивающего символа и до извлечения maxjen - 1 символов встретился конец файла EOF, то чтение прекращается, как при появлении ограничивающего символа. Функция с прототипом istreamA get(streambuf & buf, chardelim = '\n'); извлекает из входного потока символы и помещает их в буфер, определенный первым параметром. Чтение продолжается до появления ограничивающего символа, которым по умолчанию является '\п\ но он может быть установлен явно любым образом. Следующие варианты функции get() позволяют прочесть из входного потока один символ. Функция istream& get(char& ch); присваивает извлеченный символ аргументу и возвращает ссылку на поток, из которого выполнено чтение. Функция intjype get(); получает код извлеченного из потока символа в качестве возвращаемого значения. Если поток пуст, то возвращается код конца файла EOF. Функция "чтения строки" istreamA getlinefchar * array, streamsize ten, char='\n'); подобна функции get() с теми же параметрами, но переносит из входного потока и символ-ограничитель.
586 Глава 19 Функция int реек(); позволяет "взглянуть" на очередной символ входного потока. Точнее, она возвращает как результат код следующего символа потока (или EOF, если поток пуст), но оставляет этот символ во входном потоке. При необходимости этот символ можно в дальнейшем извлечь из потока с помощью других средств библиотеки. В следующей программе показано, как применять описанные функции. В программе цикл ввода выполняется до конца строки (до сигнала от клавиши Enter). //Р19_02.срр - Функции для обменов с потоками #include <iostream> using namespace std; ttdefine PRlNT(c) cout <<#c<<"="<<c<< endl; intmainf) { char ss[ ] = ”Merer; cout.putf '\t').write( ss,sizeof( ss)-1 ).put( T)«endl; cout« "Enter symbols: "« endl; char cim; while(cin.peek() != ’\n') { cin.getfcim); cout.putfcim); } return 0; } В программе две смысловые части. В первой части определена в виде символьного массива строка в стиле Си. Затем выполняется оператор с обращениями к функциям вывода: cout.put(,\t,).write(ss,sizeof(ss)-1).put(,!,)«endl; Обращение cout.putf '\t') выводит в выходной поток код табуляции и возвращает ссылку ostreamA. Далее "по цепочке" вызываются функция write() и еще раз функция put('!'). Цикл с заголовком while(cin.peek() != '\п') выполняется до появлении во входном потоке кода конца строки — до сигнала от клавиши Enter. Оператор cin.getfcim); прочитывает из входного потока один символ, оператор cout.put(cim); выводит этот символ в выходной поток.
Стандартная библиотека и ввод-вывод 587 Результаты выполнения программы: МегсН Enter syimbols: wee gg<ENTER> we egg Функция istreamA putbackfchar ch); не извлекает ничего из потока, а помещает в него символ ch, который становится текущим и будет следующим извлекаемым из потока символом. Аналогичным образом функция int gcountf); подсчитывает количество символов, которые были извлечены из входного потока при последнем обращении к нему. Функция istreamA ignore (int п = 1, intjype delim = EOF); позволяет извлечь из потока и "опустить" то количество символов п, которое определяется первым параметром. Второй параметр определяет символ-ограничитель, при появлении которого выполнение функции нужно прекратить, даже если из потока еще не извлечены все п символов. Функция istreamA readfehar *array, streamsize numb); выполняет чтение заданного количества numb символов в массив array. Полезны следующие функции того же класса istream: istreamA seekg(posjype pos); устанавливает позицию чтения из потока в положение, определяемое значением параметра. istreamA seekg(posjype pos, seekjdir dir); выполняет перемещение позиции чтения вдоль потока в направлении, определенном параметром dir, принимающим значения из перечисления епит seekjdir { beg, cur, end }. Относительная величина перемещения (в байтах) определяется значением параметра pos_type pos. Если направление определено как beg, то смещение от начала потока; сиг - от текущей позиции; end - от конца потока; posjype tellg() определяет текущую позицию чтения из потока. 19.5. Работа с файлами Потоки для работы с файлами в умалчиваемых режимах. Если включить в программу заголовок <fstream>, то доступными будут классы, обеспечивающие работу с файлами:
588 Глава 19 ifstream - класс входных файловых потоков; ofstream - класс выходных файловых потоков; fsream — класс двунаправленных файловых потоков. Для использования файлов нужно иметь следующий набор процедур: • для создания файла; • для определения потока, который мы хотим связать с файлом; • для открытия файла; • для "присоединения" потока к файлу; • для обменов с файлом с помощью потока; • для отсоединения потока от файла; • для закрытия файла; • для уничтожения файла. Создание файлового потока связывает имя потока с буфером и инициализирует переменные потока. Перед выполнением обмена с файловыми потоками нужно открыть файл и связать его с конкретным потоком. К радости программиста, не все из перечисленных действий нужно явно программировать при работе со стандартной библиотекой ввода-вывода. Файл автоматически создается (при необходимости) и открывается при определении соответствующего объекта потокового класса, и файл закрывается при уничтожении объекта. Эти действия обеспечены соответствующими конструкторами файловых потоковых классов, если им в аргументах сообщить имена нужных файлов. При создании файлового потокового объекта соответствующему конструктору передается в виде строки в стиле Си (тип char *) имя файла. (К сожалению, в классах файловых потоков нет конструктора, который принимал бы имя файла в виде строки в стиле Си++.) Конструктор автоматически пытается открыть соответствующий файл для чтения или записи. Результат этой попытки можно проверить по состоянию объекта файлового потокового класса. Так как некоторый опыт работы с файлами у нас уже есть, то, отложив изучение подробностей, приведем программу, в которой выполним копирование текстового файла в другой файл.
Стандартная библиотека и ввод-вывод 589 //Р19_03.срр - Переписать файл #include <string> #include <fstream> using namespace std; intmainf) { char next; string source; // Имя исходного файла cout« Input file name:"; cin » source; ifstream inFilef source. c_str()); // Входной файловый поток if(!inFile) { cerr« "\nError file: "«source; exit( 1); } string result; // Имя создаваемого файла cout« "Output file name: "; cin » result; of stream outFile( result. c_str()); //Выходной файловый поток if(ioutFile) { cerr « "\nError file:" << result; exit( 1); } whiief in File, get(next)) outFile.putfnext); cout« endl; return 0; Имена исходного и создаваемого файлов вводятся из стандартного входного потока как значения строковых объектов source и result. Метод c_str() используется в аргументах конструкторов для получения этих имен в виде строк в стиле Си. Конструкторы создают объекты inFile и outFile потоковых классов ifstream и ofstream. Проверка состояний этих объектов позволяет защититься от ошибок при открытии файлов. Если ошибок не возникло, то выполняется цикл while, в заголовке которого — обращение к методу get() входного потока. В теле цикла единственный оператор — обращение к методу put() выходного потока. Метод get() прочитывает в переменную next один
сим590 Глава 19 вол входного потока, затем метод put() записывает значение этой переменной в выходной поток. Цикл завершается, как только будет достигнут конец файла во входном потоке. Задание режимов работы с файлом. В приведенной программе потоки, настроены на файлы, для которых режимы работы выбираются по умолчанию. В классе basejos имеется группа флагов типа openmode для управления режимами файлов. В нее входят: арр - запись данных только в конец файла; ate - после открытия файла выполняется позиционирование в конец файла; binary - обмен ведется в двоичном режиме (специальные символы не заменяются кодами); in - открыть для чтения (по умолчанию для потоков ifstream)\ out - открыть для записи (по умолчанию для потоков ofstream); trunc- удалить предыдущее содержимое файла. Как и для флагов форматирования, для обращения к перечисленным флагам режимов открытия файлов нужно обращаться с помощью полного имени: std::ios::ИMЯ, где ИМЯ - одно из имен перечисленных флагов. Если для открытия файла необходимо использовать несколько флагов, то их объединяют с помощью операции поразрядной дизъюнкции |. Полученное выражение используется в качестве второго аргумента конструктора при создании файлового потока. Например, при создании файла для записи и пополнения потребуется такое обращение к конструктору файлового потока: std::ofstream имя_потока( "имя_файла ", std::ios::out \ std::ios::app); Для определения потока, с помощью которого нужно данные и читать из файла, и записывать в него, можно так обратиться к конструктору: stdr.ifstream имя_потока("имя_файла", std::ios::out | std::ios::in); Соответствие между традиционными обозначениями режимов файлов в языке Си и сочетаниями флагов можно представить следующим образом. (В кавычках - обозначения режимов из языка Си.) "w" std: :ios::out\std::ios::trunc - файл создается для записи. Если файл уже существовал, то предыдущее содержимое стирается.
Стандартная библиотека и ввод-вывод 591 "г" std::ios::in - существующий файл открывается для чтения. ”а” std::ios::out | std::ios::app - файл открывается для записи в конец. Если файл не существовал — файл создается. "w+ " std::ios::out\ std::ios::in | std::ios::trunc - файл создается для записи и чтения в любой позиции, т. е. файл может '’расти". Если файл существовал — предыдущее содержимое стирается. ”r+” std::ios::out\std::ios::in - существующий файл открывается для чтения и записи, но без изменений размеров файла. "а+" std::ios::in\std::ios::out\std::ios::app - файл создается для записи и чтения в любой позиции, т. е. файл может "расти". Если файл существовал — предыдущее содержимое сохраняется. Здесь приведены режимы для текстовых обменов с файлами. В текстовом режиме прочитанная из потока комбинация символов CR (десятичное значение кода 13) и LF (десятичное значение кода 10), т. е. управляющие коды "возврат каретки" (эскейп-пос- ледовательность V) и "перевод строки" (эскейп-последователь- ность \п’) преобразуются в один символ новой строки '\п'. При записи в поток в текстовом режиме выполняется обратное преобразование — код символа новой строки '\п' заменяется последовательностью из двух кодов CR и LF. Указанные замены выполняются в операционной системе Windows. В системе UNIX этого нет. Если в потоке передается не текстовая, а произвольная двоичная информация, то указанные преобразования не нужны. Для преобразования без описанных замен необходимо выбирать двоичный (бинарный) режим. Для этого устанавливается флаг binary. В языке Си для этого используются обозначения "wb", "rb", "ab", "w+b", "r+b", "a+b". ВязыкеСи++ при создании файлового потока к последовательности флагов добавляется stdr.ios:-.binary. В приведенной выше программе для копирования одного файла в другой (Р19_03.срр) каждый из файлов работает в режиме последовательного доступа: один открыт для чтения, другой — для записи. Для первого монотонно меняется позиция чтения, для второго - перемещается позиция записи. Если файл уже прочитан и необходимо прочитать его еще раз, то следует выполнить переустановку позиции чтения. Для этого можно закрыть файл,
592 Глава 19 связанный с потоком, и вновь открыть его. При этом сам поток (объект класса ifstream) может остаться тем же самым. Для открытия и закрытия связанных с потоками файлов применяются следующие методы классов файловых потоков: void closef); - закрывает файл, связанный с тем потоком, для которого метод вызван. Метод очищает буфер потока, отсоединяет поток от файла и закрывает файл. Метод closef) необходимо явно вызывать при изменении режимов работы с файловым потоком. Автоматически эта функция вызывается только при завершении программы. void openfchar *array, iosjbaser.openmode:: mode); - открывает файл с именем array и связывает его с тем потоком, для которого метод вызван. Параметр mode позволяет установить режимы работы с файлом. Если режим работы с файлом должен устанавливаться по умолчанию в соответствии с типом потока, то следует применять метод. void open (char *array); bool is_open(); - позволяет проверить открыт ли файл для того потока, для которого метод вызван. Прежде чем привести пример применения перечисленных методов, обратим внимание на два метода, наследуемые потоковыми классами из bas_ios: bool eoff) - позволяет проверить достижение конца файла. void clearfiostate state) - сбрасывает флаги состояния, установленные, например, при достижении конца файла. Здесь iostate - один из типов битовых масок. В классе ios_base определены следующие константы этого типа: badbit, eofbit, failbit, goodbit. Возможно применение этого метода без аргументов, т. е. с прототипом void clearf);. Предположим, что в программе Р19_03.срр после копирования исходного файла необходимо прочитать его еще один раз и вывести его текст на экран дисплея. Напомним, что имя файла введено как значение строкового объекта source, а имя потокового объекта, связанного с этим файлом, — inFile. Эти действия обеспечит следующая последовательность операторов (Р19_03_1.срр): inFile. clearf); // Сброс флагов потока inFile. closef); // Закрытие файла
Стандартная библиотека и ввод-вывод 593 inFile. ореп(source. c_str()); // Повторное открытие файла для чтения while( in File. ge t( next)) cout« next; Произвольный доступ к файлу. Если файловый поток предусматривает и чтение из него, и запись в него, то переход между этими двумя режимами обработки требует по крайней мере установки соответствующей позиции чтения или записи в нужное положение. Существуют и особенности обращений к файлам в произвольном порядке, т. е. чтение и запись не подряд, а с предварительным позиционированием в соответствии с целями решаемой задачи. Продемонстрируем основные принципы изменения режимов работы с файлами и доступа к его данным в произвольном порядке на следующей задаче. Переписать строки текстового файла в новый файл, упорядочив их по возрастанию длин. Пустые строки удалить. Исходный файл не изменять. Имя исходного файла ввести в командной строке. Программное решение задачи: //Р19_04.срр - Сортировка строк файла #include <iostream> #include <string> ttinclude <fstream> #include <vector> #include <algorithm> using namespace std; struct comp { // Позиции и длины строк исходного файла long beg; // Позиция начала строки size_t len; //Длина строки } ruct; // Конкретная структура // Предикат - сравнение длин строк при сортировке структур: bool opfcomp a, comp b) { return a. len < b.len; } int mainfint k, char * args []) { if(k < 2) { cerr « "The main() arguments Error!”; exit( 1); 38-2762
594 Глава 19 } if stream inFile(args[1]); //Входной файловый поток if(HnFile) { cerr « "Error file: "<<args[ 1 ]; exit( 1); } string line; // of stream outFilef "result, txt"); // Выходной файловый поток if(loutFile) { cerr « "Error file result"; exit( 1); } vector <comp> begins; //Позиции и длины строк файла while(!inFile.eof()) { ruct.beg = inFile. tellgf); // Позиция начала очередной строки getlinefinFileJine); // Чтение строки из файла ruct.len = line. size();//Длина строки begins.push_back(ruct); // Запись структуры в вектор } //Сортировка элементов вектора по значениям длин строк: sort(begins.begin()f begins.endf), op); inFile. clear();// сброс флагов потока inFile.close();//закрытие файла inFile. open(args[1],ios::in); // повторное открытие файла для чтения long pos; //для позиции начала строки в файле char next; //для чтения очередного символа из файла //Цикл переписывания строк файлов: for( unsigned int i=0; i<begins.size(); i++) { pos=begins[i]. beg; // Установка позиции чтения: inFile. seekgfpos, iosr.beg); line = //Цикл чтения символов и формирование выходной строки: fort unsigned int j=0; j<begins[i].len; j++) { inFile.get( next); iff next != '\n' && next != '\r')line+=next; else ]--; } // Запись строки в файл результатов: if(line.sizef) != 0)
Стандартная библиотека и ввод-вывод 595 outFile«line« '\п } cout« endl; return 0; } В программе сделано предположение, что количество строк в файле меньше предельного размера контейнера vectorO. Элемент этого контейнера - объект вспомогательного класса struct comp { long beg; // Позиция начала строки sizej len; //Длина строки } ruct; В классе comp два поля: long beg - позиция начала строки исходного файла, поле size_t len - длина этой строки. Конкретный объект этого класса ruct определен как глобальный, хотя его можно было бы ввести и как локальный. Схема выполнения программы: • прочитать построчно исходный файл, занося в вектор vector <comp> begins элементы, сохраняющие сведения о размещении и длинах строк; • выполнить сортировку элементов вектора по длинам строк; • открыть исходный файл в режиме чтения в произвольном порядке; • перебирая элементы вектора, "доставать" из них значения позиций начал строк, используя эти позиции выполнять чтение данных из файла и переносить эти данные построчно в выходной файл (в файл результатов). Для сортировки контейнера vector <comp> begins по значениям полей size_t len используем обобщенный алгоритм void sortf Ran It first, Ranlt last, Pred pr); Для обращения к этому алгоритму необходимо задать начало и конец сортируемой последовательности — диапазон [first, last). Кроме того, нужно ввести предикат, определяющий отношение между сравниваемыми элементами. В качестве предиката в программе использована функция 38*
596 Глава 19 bool ор(сотр a, comp b) { return a.len < b.len; } Обращение к алгоритму сортировки вектора begins по длинам строк в файле: sort(begins.begin(), begins.end(), op); Исходный файл представлен в программе файловым потоком inFile. Имя файла вводит пользователь в командной строке при запуске программы на выполнение. В цикле с заголовком while(!inFile.eof()) построчно читается содержимое файла. Окончание чтения — достижение конца файла. Перед чтением очередной строки оператор ruct.beg = inFile. tellg(); сохраняет в поле объекта ruct значение позиции ее начала в файле. Оператор getline(inFile,line); прочитывает в объект string line строку из файла и переводит позицию чтения к началу следующей строки в файле. Оператор ruct.len = line.size() заносит в поле объекта ruct значение длины прочитанной строки. После этого оператор begins. push_back(ruct); добавляет в контейнер begins значение объекта ruct. После выполнения сортировки контейнера begins выполняется подготовка потока к чтению того же файла в режиме произвольного доступа: inFile. clearf); // Сброс флагов потока inFile.close();//Закрытие файла inFi!e.open(args[1],ios::in); // Повторное открытие файла для чтения Далее выполняется цикл последовательного перебора элементов контейнера begins. На каждой итерации из элемента контейнера выбирается значение позиции начала строки в файле (pos=begins[i].beg), и файл настраивается на чтение данных из этой позиции (inFile.seekg(pos,ios::beg);). Чтение выполняется посимвольно, и значащие символы помещаются во вспомогательный объект string tinе. Незначащими считаются символы ’\л’ и У'. Запись значения объекта line в результирующий файл, представленный потоком outFile, выполняется в том случае, когда длина строки line не нулевая. Для файла результатов выбрано фиксированное имя "result.txt".
Приложения Приложение 1 Разработка консольных приложений в среде Microsoft Visual Studio.NET 2005 Технологии программирования и инструментальные средства разработки развиваются так же стремительно, как и технические средства компьютерных систем. Однако изучение языков программирования, их синтаксиса и базовых механизмов остается необходимым компонентом подготовки программистов. В этом процессе обучения не следует пропускать «ступеньку» создания консольных приложений. Понимая это, крупнейшие фирмы, создающие инструментальные средства для автоматизации разработки программных продуктов, в обязательном порядке предусматривают в этих средствах возможности для создания программ, выполняемых в консольном режиме. Рассмотрим один из возможных вариантов разработки консольных приложений на языке Си++ с помощью интегрированной среды разработки (ИСР) Microsoft Visual Studio.NET 2005. В интегрированной среде есть и пошаговая отладка программ, и мощная справочная система, и еще многое, упрощающее разработку, особенно Windows-приложений. Средств автоматизации разработки консольных приложений в ИСР гораздо меньше, но для изучения языка нужны именно они. Приступая к работе с Microsoft Visual Studio.NET 2005, нужно, во-первых, хорошо понимать, что такое проект (project). «Проект - это набор взаимосвязанных исходных файлов, компиляция и компоновка которых позволяет создать исполнимую программу» [27]. Как создаются файлы проекта и какие роли они играют, будем объяснять постепенно. Главное - нужно знать, что часть файлов ИСР создает автоматически, и только некоторые из них программист должен написать самостоятельно. Исходные файлы одного проекта рекомендуется размещать в одном каталоге и называть этот каталог выбранным вами именем проекта. Взаимозависимости между отдельными частями проекта
опи598 Приложения сываются в текстовом файле проекта, имеющем расширение VCPROJ. Этот файл создается средой автоматически, и его нельзя удалять и редактировать вручную. Во-вторых, фундаментальное понятие Visual C4-4-.NET — это "решение" (solution). Решение объединяет несколько проектов, отнесенных к одной разработке. Для описания решения Microsoft Visual Studio.NET 2005 вводит специальный текстовый файл с расширением SLN. Этот файл создается автоматически, и его нельзя удалять и редактировать вручную. Чтобы выполнить свою программу, вначале создайте каталог, в котором вы будете работать. Для примера создадим каталог на диске С: и назовем его своей фамилией, т. е. для Иванова полное имя каталога будет "С:\Иванов". Теперь, подготовившись, войдите в ИСР Microsoft Visual Studio.NET 2005. Для этого существует несколько возможностей. Например, можно дважды щелкнуть мышкой на имени файла с расширением ".срр" или, выделив имя этого файла, нажать клавишу ENTER. Но наиболее прямой путь, не требующий наличия файлов с расширениями ".срр", — последовательный выбор пунктов: Пуск | Программы | Microsoft Visual Studio.NET 2005 | Microsoft Visual Studio.NET 2005 Эта последовательность выполнения пунктов приводит к запуску Microsoft Visual Studio.NET 2005 и выводу на экран (рис. П1.1) начальной страницы (Start Page). Чтобы создать новый проект и включить его в решение (Solution), нужно выполнить следующие шаги. Вначале нужно открыть окно нового проекта. Сделать это можно одним из трех способов. • На начальной странице ИСР на панели Recent Projects выберите (щелкните мышкой) пункт Create: Project. • В основном меню ИСР выполните: File | New | Project. • Нажмите сочетание клавиш Ctrl4-Shift4-N. Любое из этих действий приводит к появлению окна нового проекта (рис. П1.2) с заголовком New Project и двумя панелями Project Types (типы проектов) и Templates (шаблоны). Открыв окно нового проекта, надо задать его свойства. Нам нужен проект консольного приложения на языке Си4-4-. На левой панели (Project Types) выделите (щелкните левой кнопкой мыши)
Приложение 1 599 Рис. П1.1. Начальная {стартовая) страница интегрированной среды разработки Microsoft Visual Studio.NET 2005 тип проекта Visual C++ | Win32, на панели шаблонов выделите иконку Win32 Console Application. В текстовое поле Name (Имя) введите выбранное вами имя проекта. На рис. П1.2 выбрано имя myProject (обратите внимание на отсутствие расширения в имени). В текстовое поле Location (Местоположение) введите полное имя каталога (<имя_диска:\путь\каталог), в котором вы собираетесь разместить создаваемый проект (и решение в целом). Предварительно каталог, как мы уже договорились, создан стандартными средствами операционной системы. На рис. П1.2 предлагается поместить проект в каталоге с:\Иванов. Одновременно с набором имени проекта (myProject) то же самое имя будет автоматически занесено и в поле Solution Name. Тем самым ИСР предлагает для нового проекта создать новое решение с тем же именем, что и имя проекта. Если фла-
600 Приложения жок с названием "Create directory for solution" установлен, то для решения будет создан каталог именно с этим названием. Нажатие кнопки ОК приводит к открытию нового окна (рис. П1.3) с заголовком “Win32 Application Wizard - myProject” (Мастер приложений Win32). Рис. П1.3. Окно мастера (Wizard) приложений при его открытии Рис. П1.2. Окно нового проекта
Приложение 1 601 Выбрав в окне мастера приложений (панель слева на рис. Ш.З) вкладку "Application Settings" (Установленные параметры приложения), изменяем вид этого окна (рис. П1.4). Рис. П1.4. Установлены параметры пустого консольного приложения Для задания свойств создаваемого проекта установите, как показано на рис. ГГ 1.4, кнопку Console application (Консольное приложение) и флажок Empty project (Пустой проект). Нажатие кнопки Finish приводит к созданию решения и в нем — проекта консольного приложения. Используя средства операционной системы, можно убедиться, что в каталоге С:\Иванов создан подкаталог myProject. В нем размещены файлы myProject. neb, myProject.sin, myProject. suo и подкаталог проекта myProject. Разбирать назначение этих файлов нет необходимости — в интегрированной среде сведения о созданном проекте представлены по-другому. Итак, по нажатию кнопки Finish окно мастера приложений будет закрыто. В основном окне справа откроется (рис. П 1.5) новое окно с заголовком “Solution Explorer - myProject”. Если это окно (окно инспектора решений) не будет открыто (причины не важны), его можно открыть, введя в основном меню последовательность View | Solution Explorer (эквивалент - сочетание клавиш: СМ+ A/f+L). В этом окне отобразятся строка"Solution 'myProject' (1
proj602 Приложения ect)" и в виде дерева с заголовком myProject имена (папок): Header Files, Resource Files, Source Files. Рис. П1.5. Окно инспектора решений для проекта myProject Поскольку мы создавали пустой проект, то в нем отсутствуют прототипы (заглушки) файлов с текстами программы на языке C++. Предположим, что мы создаем программу с иллюстрированным классом комплексных чисел myComplex и в проект необходимо включить три текстовых файла: два заголовочных (oneComplex.h, twoComplex.h) и файл с основной функцией main(). Рис. П1.6. Выбор категории, вида и имени включаемого в проект файла Для того чтобы поместить один из файлов (пока еще пустой) программы в проект, нужно открыть диалоговое окно с заголовком
Приложение 1 603 "Add New Item" и указать свойства этого файла. Для открытия диалогового окна нужна следующая последовательность Project \ Add New Item (или сочетание клавиш Ctrl+Shift+A). В открывшемся окне (рис. П1.6) с заголовком "Add New Item" нужно выбрать категорию (Categories) и шаблон (Templates) подключаемого к нашему проекту элемента (Item). Выберем категорию code и шаблон Header File (.h) (заголовочный файл). В поле Name введем имя oneComplex.h, которое мы хотим присвоить добавляемому заголовочному файлу. Кнопка Add закрывает окно и открывает окно текстового редактора ИСР, в котором можно подготовить текст нашего заголовочного файла. Одновременно изменяется "состав" нашего решения (рис П1.7) — в него включается заголовочный файл oneComplex.h. Рис. П1.7. Окно инспектора решений после включения заголовочного файла Если окно инспектора решений закрыто - откройте его последовательностью View | Solution Explorer (эквивалент - сочетание клавиш: Cfr/+ A/f+L). Если по каким-то причинам окно редактора не открылось — откройте его, например, через окно инспектора решений (см. рис. П1.7). Для этого щелкните два раза по имени заголовочного файла oneComplex.h. Можно использовать и последовательность Widow | oneComplex.h. На рис. П1.8 изображено открытое окно редактора, в которое уже введен текст заголовочного файла oneComplex.h с определением класса. Этот текст можно набирать "с чистого листа", а можно скопировать в окно редактора уже подготовленный где-то в другом
604 Приложения файле текст на языке Си++. В обоих случаях перед следующими шагами работы с проектом текст нужно сохранить. (Последовательность File | Save oneComplex.h или сочетание клавиш Ctrl + S.) Рис. П1.8. Окно редактора с текстом заголовочного файла Аналогичным образом подключаем к проекту второй заголовочный файл. Project | Add New Item (или сочетание клавиш Cfr/+S/i/ft+A), затем Categories (Code), Templates (Header File (.h)), Name (twoComplex.h). В открывшееся окно редактора ИСР вводим текст файла twoComplex.h и сохраняем его (последовательность File | Save oneComplex.h или сочетание клавиш Ctrl + S). Кроме заголовочного файла в программе должен быть основной текст. Для его добавления вновь открываем диалоговое окно с заголовком "Add New Item", где выберем категорию code и шаблон C++File.cpp (рис. П1.9). В поле Name введем имя test.cpp, которое мы хотим присвоить добавляемому файлу с текстом программы.
Приложение 1 605 Кнопка Add закрывает окно и открывает окно текстового редактора ИСР, в котором можно подготовить текст нашего файла test.cpp. Рис. П1.9. Категория, вид (шаблон) и имя подключаемого файла Находясь в редакторе, поместим в окно с заголовком test.cpp текст основной программы (рис. ШЛО). Затем сохраним ее текст (File | Save test.cpp или сочетание клавиш Ctrl + S). Подключение файлов изменит окно инспектора решений (рис. П1.11). Теперь можно выполнять компиляцию и отладку программы. Простейший вариант для запуска цепочки действий "компиляция, редактирование связей, построение решения, исполнение без отладки' - применение последовательности Debug | Start Without Debugging или сочетания клавиш Ctrl + F5. Результат цепочки действий, приводящих к построению решения, отображается в специальном окне (рис. П1.12). Если в программе не выявлено ошибок, то при таком режиме исполнения автоматически открывается консольное окно и в него выводятся результаты выполнения программы и вводимых пользователем данных. На рис. П1.13 приведены результаты для тех данных, которые вводил пользователь при выполнении программы.
606 Приложения Рис. П1.10. Окно редактора с текстом основной функции программы Рис. П1.11. Окно инспектора решений после подключения файлов twoComplex.h и test.cpp
Приложение 1 607 Рис. П1.12. Результат построения решения без ошибок Рис. П1.13. Консольное окно с результатами выполнения программы Если при компиляции выявлены ошибки или результаты выполнения программы не соответствуют ожиданиям автора (допущены семантические, т. е. алгоритмические, ошибки), то в исходный текст программы нужно вносить изменения. Если ошибки выявлены на этапе компиляции, то двойной щелчок по сообщению об ошибке в выходном окне (с заголовком Output) откроет окно редактора, а курсор будет установлен на строке, соответствующей сообщению об ошибке. Для проверки логики и для исправления алгоритмических ошибок в ИСР используют отладочный режим. В него можно войти, выполнив последовательность Debug | Step Over или нажав клавишу F10. В этом режиме программа выполняется "по шагам". Каждый шаг — исполнение операторов очередной строки текста программы в
608 Приложения окне редактора. Переход к следующему шагу - по нажатию клавиши F10 (или Debug | Step Over). Находясь в этом режиме, можно посмотреть значение каждого объекта (переменной). Для этого имеются две возможности. Во-первых, если подвести курсор мыши к имени объекта в тексте программы (в окне редактора), то в специальном всплывающем окошке появится значение полей данных этого объекта (значение переменной). Вторая возможность - применение специального окна (рис. П1.14), которое открывается последовательностью Debug | Windows | Watch | Watch/. Всего можно открыть до четырех окон (т. е. i может принимать значения от 1 до 4). Рис. П1.14. Окно со значениями объектов на очередном шаге программы В столбец "Name" (см. рис. П1.14) необходимо занести имя объекта. После активации строки (щелчок клавишей мыши в поле "Value" или "Туре") в столбце "Value" появится значение объекта, а в поле "Туре" - имя его типа (класса). После каждого шага выполнения программы, меняющего значения объектов, эти изменения будут отображаться в таблице. При этом строки с последними изменениями выделяются цветом. Завершает режим отладки (без окончания выполнения программы) последовательность Debug | Stop Debugging (или сочетание клавиш Shift+F5). Второй режим отладки (Debug | Step Into или клавиша F11) предусматривает пошаговое прохождение не только того текста программы, который находится в окне редактора, но и автоматический переход к текстам вызываемых методов (функций). Например, в нашей программе при каждом создании объекта класса myComplex будет выполняться переход к тексту конструктора в файле
Приложение 1 609 oneComplex.fi. Возможности наблюдения за значениями объектов в этом режиме (F11) те же, что и в режиме "без захода в тексты вызываемых методов" (F10). В режимах отладки (F10, F11) имеется возможность устанавливать контрольные точки и проходить по тексту программы с остановками только в этих точках. Эти средства (и многие другие) здесь не затронуты. Мы рассмотрели только малую часть возможностей ИСР Microsoft Visual Studio.NET 2005. Этих сведений достаточно, чтобы создавать консольные приложения на языке Си++ и изучать возможности языка. Однако это только начало знакомства с Microsoft Visual Studio.NET 2005. Даже в редакторе ИСР есть очень много средств ускорения разработки программ. Назовем (но оставим читателю для самостоятельного изучения) следующие механизмы редактора: • сворачивание текста (скрытие кода) и последующее разворачивание; • автоматизация набора слов; • контекстная подсказка по полям и методам объекта; • автоматическое форматирование текста программы; • цветовая индикация слов и символов языка; • выделение синтаксически ошибочных конструкций до этапа компиляции.
Приложение 2 Константы предельных значений Предельные значения вводятся каждой реализацией для данных целочисленных типов и арифметических значений, представляемых в форме с плавающей точкой. Предельные значения определяются набором констант, названия (имена) которых стандартизированы и не зависят от реализаций. Таблица П2.1 Предельные значения для целочисленных типов — заголовок <climits> Имя константы Назначение CHAR_BIT Число битов в байте SCHARJMIN Минимальное значение для signed char SCHAR_MAX Максимальное значение signed char UCHAR_MAX Максимальное значение unsigned char CHAR_МIN Минимальное значение для char CHAR_MAX Максимальное значение для char MB__LEN_MAX Минимальное число байт в многобайтовом символе SHRT_MIN Минимальное значение для short SHRT_MAX Максимальное значение для short USHRTJMAX Максимальное значение unsigned short INT_MIN Минимальное значение для int INT_MAX Максимальное значение для int UINT_MAX Максимальное значения unsigned int LONGJMIN Минимальное значение для long LONG_MAX Максимальное значение для long ULONG_MAX Максимальное значение unsigned long В табл. П2.2 префикс FLT_ соответствует типу float; для типа double используется префикс DBL_, для типа long double - префикс LDBL_.
Приложение 2 611 Таблица П2.2 Константы для вещественных типов — заголовок <cfloat> Имя константы Назначение FLTJRADIX Основание экспоненциального представления, например 2, 16 FLTJDIG FLT_EPSILON FLT_MANT_DIG Количество верных десятичных цифр Минимальное х, такое, что 1.0 + х* 1.0 Количество цифр по основанию FLTJRADIX в мантиссе FLT_MAX FLT_MAX_EXP Максимальное число с плавающей точкой Максимальное л, такое, что FLTJRADIXP - I представимо в виде числа типа float FLT_MAX_ 10JEXP Максимальное целое л, такое, что Ю" представимо как float FLT_MIN Минимальное нормализованное число с плавающей точкой типа float FLT_MIN_EXP Минимальное л, такое, что Ю" представимо в виде FLT_MIN_ 10JEXP нормализованного числа Минимальное отрицательное целое л, такое, что Ю" — в области определения чисел типа float DBLJDIG Количество верных десятичных цифр для типа cfoi/- Ые DBLEPSILON Минимальное х, такое, что 1.0 + х ф 1.0, где х принадлежит типу double DBLMANTDIG Количество цифр по основанию FLT_RADIX в мантиссе для чисел типа double DBLJMAX Максимальное число с плавающей точкой типа double DBL_MAX_EXP Максимальное л, такое, что FLTJRADIX" — 1 представимо в виде числа типа double DBL_MAX_ 10_EXP Максимальное целое л, такое, что 10" представимо как double DBLMIN Минимальное нормализованное число с плавающей точкой типа double DBL_M!N_EXP Минимальное л, такое, что 10" представимо в виде DBL_MIN_ 10_EXP нормализованного числа типа double Минимальное отрицательное целое л, такое, что 10" — в области определения чисел типа double 39*
Приложение 3 Таблицы кодов КОДЫ ASCII Таблица П3.1 Коды управляющих символов (0 + 31) Символ Код Ю Код 08 Код 16 Клавиши Значение nul 0 0 00 Л@ Нуль soh l l 01 ДА Начало заголовка stx 2 2 02 ЛВ Начало текста etx 3 3 03 ЛС Конец текста eot 4 4 04 AD Конец передачи enq 5 5 05 ЛЕ Запрос ack 6 6 06 AF Подтверждение bel 7 7 07 AG Сигнал (звонок) bs 8 Ю 08 AH Забой (шаг назад) ht 9 ll 09 AI Горизонтальная табуляция If Ю 12 0A AJ Перевод строки vt ll 13 0B ЛК Вертикальная табуляция ff 12 14 OC AL Новая страница cr 13 15 0D AM Возврат каретки so 14 16 0E AN Выключить сдвиг si 15 17 OF AO Включить сдвиг die 16 20 10 ЛР Ключ связи данных del 17 21 11 AQ Управление устройством 1 dc2 18 22 12 AR Управление устройством 2 dc3 19 23 13 AS Управление устройством 3 dc4 20 24 14 AT Управление устройством 4 nak 21 25 15 ли Отрицательное подтверждение syn 22 26 16 AV Синхронизация etb 23 27 17 AW Конец передаваемого блока can 24 30 18 AX Отказ em 25 31 19 AY Конец среды sub 26 32 1A AZ Замена esc 27 33 IB A[ Ключ fs 28 34 1C A\ Разделитель файлов gs 29 35 ID A] Разделитель группы rs 30 36 IE Л/ Разделитель записей US 31 37 IF A_ Разделитель модулей
Приложение 3 613 В графе "клавиши" обозначение Л соответствует нажатию клавиши Ctrl, вместе с которой нажимается соответствующая "буквенная" клавиша, формируя код символа. Таблица П3.2 Символы с кодами 32+127 Символ Код 10 Код 08 Код 16 @ 64 100 40 А 65 101 41 В 66 102 42 С 67 103 43 D 68 104 44 Е 69 105 45 F 70 106 46 G 71 107 47 Н 72 ПО 48 1 73 111 49 J 74 112 4А К 75 113 4В L 76 114 4С М 77 115 4D N 78 116 4Е О 79 117 4F Р 80 120 50 О 81 121 51 R 82 122 52 S 83 123 53 т 84 124 54 и 85 125 55 V 86 126 56 W 87 127 57 X 88 130 58 Y 89 131 59 Z 90 132 5А [ 91 133 5В \ 92 134 5С ] 93 135 5D А 94 136 5Е 95 137 5F * 96 140 60 Символ Код 10 Код 08 Код 16 пробел 32 40 20 / 33 41 21 м 34 42 22 # 35 43 23 $ 36 44 24 % 37 45 25 & 38 46 26 ■ 39 47 27 ( 40 50 28 ) 41 51 29 * 42 52 2А + 43 53 2В 9 44 54 2С - 45 55 2D , 46 56 2Е / 47 57 2F 0 48 60 30 1 49 61 31 2 50 62 32 3 51 63 33 4 52 64 34 5 53 65 35 6 54 66 36 7 55 67 37 8 56 70 38 9 57 71 39 i 58 72 ЗА 9 59 73 зв < 60 74 зс = 61 75 3D > 62 76 ЗЕ ? 63 77 3F
614 Приложения Продолжение Символ Код 10 Код 08 Код 16 Символ Код 10 Код 08 Код 16 а 97 141 61 Q 113 161 71 Ь 98 142 62 г 114 162 72 с 99 143 63 S 115 163 73 d 100 144 64 t 116 164 74 е 101 145 65 и 117 165 75 f 102 146 66 V 118 166 76 9 103 147 67 W 119 167 77 h 104 150 68 X 120 170 78 i 105 151 69 У 121 171 79 j 106 152 6А Z 122 172 7А к 107 153 6В { 123 173 7В 1 108 154 6С 1 124 174 7С m 109 155 6D ; 125 175 7D n ПО 156 6Е ~ 126 176 7Е о 111 157 6F del 127 177 7F P 112 160 70 Таблица ПЗ.З Символы с кодами 128н- 255 (Кодовая таблица 866 - MS-DOS) Символ Код 10 Код 08 Код 16 Символ Код 10 Код 08 Код 16 А 128 200 80 П 143 217 8F Б 129 201 81 Р 144 220 90 В 130 202 82 С 145 221 91 Г 131 203 83 т 146 222 92 д 132 204 84 У 147 223 93 Е 133 205 85 ф 148 224 94 Ж 134 206 86 X 149 225 95 3 135 207 87 ц 150 226 96 и 136 210 88 ч 151 227 97 й 137 211 89 ш 152 230 98 к 138 212 8А щ 153 231 99 л 139 213 8В ъ 154 232 9А м 140 214 8С ы 155 233 9В н 141 215 8D ь 156 234 9С О 142 216 8Е э 157 235 9D
Приложение 3 615 Продолжение Символ Код 10 Код 08 Код 16 Т 194 302 С2 I- 195 303 СЗ 196 304 С4 + 197 305 С5 \ 198 306 С6 199 307 С7 [ 200 310 С8 г 201 311 С9 Л 202 312 СА т 203 313 СВ } 204 314 СС 205 315 CD I 206 316 СЕ 1 207 317 CF JL 208 320 D0 Т 209 321 D1 т 210 322 D2 1 211 323 D3 L 212 324 D4 Р 213 325 D5 г 214 326 D6 i 215 327 D7 + 216 330 D8 j 217 331 D9 г 218 332 DA 1 219 333 DB 1 220 334 DC 1 221 335 DD \ 222 336 DE ■ 223 337 DF р 224 340 E0 с 225 341 El т 226 342 E2 у 227 343 E3 ф 228 344 E4 X 229 345 E5 Символ Код 10 Код 08 Код 16 Ю 158 236 9Е Я 159 237 9F а 160 240 АО б 161 241 А1 в 162 242 А2 г 163 243 АЗ А 164 244 А4 е 165 245 А5 ж 166 246 А6 3 167 247 А7 и 168 250 А8 й 169 251 А9 к 170 252 АА л 171 253 АВ м 172 254 АС н 173 255 AD о 174 256 АЕ п 175 257 AF - 176 260 ВО - 177 261 В1 - 178 262 В2 179 263 ВЗ -1 180 264 В4 - 181 265 В5 1 182 266 В6 и 183 267 В7 184 270 В8 185 271 В9 1 186 272 ВА 1 187 273 ВВ J 188 274 ВС J 189 275 BD J 190 276 BE 1 191 277 BF L 192 300 СО 1 193 301 С1
616 Приложения Символ Код 10 Код 08 Код 16 ц 230 346 Е6 ч 231 347 Е7 ш 232 350 Е8 Щ 233 351 Е9 ъ 234 352 ЕА ы 235 353 ЕВ ь 236 354 ЕС э 237 355 ED ю 238 356 ЕЕ я 239 357 EF ■ или Ё 240 360 F0 ± или ё 241 361 F1 £ 242 362 F2 Продолжение Символ Код 10 Код 08 Код 16 £ 243 363 F3 Г 244 364 F4 J 245 365 F5 + 246 366 F6 » 247 367 F7 ° 248 370 F8 • 249 371 F9 • 250 372 FA V 251 373 FB п 252 374 FC 2 253 375 FD ■ 254 376 FE 255 377 FF Таблица П3.4 Символы с кодами 128 + 255 (Кодовая таблица 1251 — MS Windows) Символ Код 10 Код 08 Код 16 Ъ 128 200 80 Г 129 201 81 1 130 202 82 г 131 203 83 и 132 204 84 ... 133 205 85 t 134 206 86 * 135 207 87 * 136 210 88 %о 137 211 89 /ь 138 212 8А < 139 213 8В УЪ 140 214 8С к 141 215 8D ъ 142 216 8Е м 143 217 8F Символ Код 10 Код 08 Код 16 ь 144 220 90 ( 145 221 91 9 146 222 92 и 147 223 93 99 148 224 94 • 149 225 95 - 150 226 96 — 151 227 97 □ 152 230 98 тм 153 231 99 Л> 154 232 9А > 155 233 9В кь 156 234 9С к 157 235 9D 11 158 236 9Е Y 159 237 9F
Приложение 3 617 Продолжение Символ Код Ю Код 08 Код 16 Ж 198 306 С6 3 199 307 С7 И 200 310 С8 Й 201 311 С9 К 202 312 СА Л 203 313 СВ М 204 314 СС Н 205 315 CD О 206 316 СЕ П 207 317 CF Р 208 320 D0 С 209 321 D1 т 210 322 D2 У 211 323 D3 ф 212 324 D4 X 213 325 D5 ц 214 326 D6 ч 215 327 D7 ш 216 330 D8 щ 217 331 D9 ъ 218 332 DA ы 219 333 DB ь 220 334 DC э 221 335 DD ю 222 336 DE я 223 337 DF а 224 340 E0 б 225 341 El в 226 342 E2 г 227 343 E3 А 228 344 E4 е 229 345 E5 ж 230 346 E6 3 231 347 E7 и 232 350 E8 й 233 351 E9 к 234 352 EA л 235 353 EB Символ Код 10 Код 08 Код 16 160 240 АО У 161 241 А1 У 162 242 А2 J 163 243 АЗ п 164 244 А4 t 165 245 А5 1 1 166 246 А6 § 167 247 А7 Ё 168 250 А8 © 169 251 А9 e 170 252 АА « 171 253 АВ 172 254 АС - 173 255 AD © 174 256 АЕ Y 175 257 AF ° 176 260 ВО ± 177 261 В1 1 178 262 В2 i 179 263 ВЗ 180 264 В4 M 181 265 В5 11 182 266 В6 ■ 183 267 В7 ё 184 270 В8 № 185 271 В9 e 186 272 ВА » 187 273 ВВ j 188 274 ВС S 189 275 BD s 190 276 BE Y 191 277 BF A 192 300 СО Б 193 301 С1 В 194 302 С2 Г 195 303 СЗ д 196 304 С4 E 197 305 С5
618 Приложения Символ Код 10 Код 08 Код 16 м 236 354 ЕС н 237 355 ED о 238 356 ЕЕ п 239 357 EF р 240 360 F0 с 241 361 F1 т 242 362 F2 У 243 363 F3 ф 244 364 F4 X 245 365 F5 Продолжение Символ Код 10 Код 08 Код 16 ц 246 366 F6 ч 247 367 F7 ш 248 370 F8 Щ 249 371 F9 ъ 250 372 FA ы 251 373 FB ь 252 374 FC э 253 375 FD ю 254 376 FE я 255 377 FF
Приложение 4 Вывод на консоль русского текста Используя в программах, выполняемых в консольном режиме, строковые и символьные константы с русскими буквами, программист сталкивается с проблемой различия кодировок. В консольном режиме применяется OEM-кодировка, в которой для представления символов со значениями кодов 128-255 используется кодовая таблица 866 - MS-DOS. В программах, исполняемых и создаваемых под управлением MS Windows, применяется для тех же символов кодовая таблица 1251 (ANSI-кодировка). Таким образом, текст с русскими буквами (а их коды находятся в диапазоне 128-255), подготовленный в каком-либо редакторе MS Windows, нельзя правильно отобразить в консольном окне - нужна перекодировка. Существуют программы перекодировки из MS Windows в MS-DOS и обратно, более того, некоторые текстовые процессоры обеспечивают выбор кодовой таблицы при сохранении текста в файле. Однако для достижения независимости программы от внешних инструментальных средств удобно выполнять перекодировку непосредственно в коде создаваемой программы. Делать это можно по-разному. Например, в [22] предложены макросредства, использующие одну из функций MS Windows. Это функция CharToOemf ) выполняет преобразование строки из ANSI в OEM. В программах этой книги используется другой подход - перекодировка из Windows в MS-DOS выполняется только в процессе вывода русского текста в стандартный выходной поток, представляемый объектом cout. Для этого используются следующие ниже функции. //cyrToDos.h - Перегруженные функции для перекодировки //Из кода MS DOS в код MS Windows и вывода на консоль русских букв. /* Способы обращения: cyrToDosf аргумент); cout«cyrToDos( аргумент); Аргумент - строковая константа, указатель на строку в стиле Си или ссылка на объект типа string (на строку в стиле Си++). Результат - выведенный в стандартный выходной поток (в консольное окно) после перекодирования текст из строки-аргумента. 7
620 Приложения #ifndef _cyrToDos_ define _cyrToDos_ #include <iostream> #include <string> using namespace std; // Вспомогательная функция перекодировки и вывода на консоль, inline char win_Dos(char * rus) { char * p = rus, res; do { res = *p; iff *p >= 'A'&& *p <= 'n') res = *p - 64; else if(*p >= 'p'&& *p <= 'я') res = *p - 16; else if Гр == ’£) res = char(240); else if(*p -- 'e') res = char(241); cout« res; } while (*p++ != ’\0’); return ’\07 } char cyrToDosf char * rus) { win_Dos(rus); return ’\07 ; char cyrToDosf const string & rus) { char * job = const_cast<char *> (rus.c_str()); win_Dos(job); return ’\0V ; ttendif В приведенном тексте определены три функции. Первая из них с прототипом inline char win_Dos(char * rus); выполняет при каждом обращении вывод в стандартный выходной поток (представленный объектом cout) символов строки в стиле Си, адресованной параметром функции. Вывод выполняется
посимПриложение 4 621 вольно, и для каждого символа русского алфавита изменяется кодировка из ANSI в OEM. Для символов, отличных от русских букв, указанная перекодировка не выполняется. Следующие две функции имеют одинаковое имя. Их прототипы: char cyrToDosfchar * rus); char cyrToDosf const string & rus); Параметр первой — указатель на строку в стиле Си, параметр второй — ссылка на строку в стиле Си++. Таким образом, имеется возможность выводить с перекодировкой, как строки в стиле Си, так и объекты класса string. Каждая из трех функций возвращает значение '\0' — терминальный символ строки в стиле Си. Тексты приведенных функций размещены в файле "cyrToDos.h" и могут быть включены в любую программу, в которой нужен вывод в консольное окно русского текста. Функция cyrToDos может использоваться двумя способами. Во- первых, обращение в виде отдельного оператора cyrToDos (аргумент); обеспечивает перекодировку в коды MS-DOS и вывод в консольное окно символов значения аргумента. При этом возвращаемое функцией значение игнорируется. Более наглядно использовать вызов этой же функции в выражении cout« cyrToDos (аргумент); В приведенном операторе, справа от обращения к функции cyrToDos ( ), можно размещать выражения, значения которых планируется ввести в тот же выходной поток. В программе Р02_10.срр используется, например, такой оператор: cout« cyrToDos ("Внешняя переменная k=")«::k«endl; Предупреждение: не следует в цепочку выражений, связанных знаками операций <<, помещать более одного обращения к функции cyrToDosf ) и размещать такое обращение в конце цепочки вывода. Связано это с тем, что действия в теле функции выполняются ранее вывода с помощью операции <<, что может изменить ту последовательность размещения выводимой информации, которую запланировал программист.
Приложение 5 Методы класса string Опущены распределители памяти, строковые итераторы и те методы класса string, которые предназначены для работы с итераторами. Тем самым не рассматриваются возможности строк как контейнеров STL (см. [5]). Обозначения в прототипах функций Каждый метод класса string применяется (как обычно) к некоторому объекту *this этого класса. Именно для этого объекта (над этой вызывающей строкой) выполняются действия метода. tar - целевая (target) позиция, начиная с которой выполняются обращения к вызывающей строке (к объекту действия). пТаг - количество символов (длина подстроки), выделяемых в вызывающей строке (в объекте действия). line - ссылка на строку-параметр (строка-источник — объект класса string). pos - позиция (индекс) начала подстроки в строке-параметре line. nPos — количество символов (длина подстроки), выбираемых из параметра (из строки-источника). str - указатель-параметр на строку в стиле Си (на с_строку). ch - отдельный символ - объект типа char, п — количество символов (длина подстроки или число повторений символа). Напомним, что суффикс const после списка параметров, указывает на невозможность с помощью действий функции изменить состояние (данные) того объекта, для которого эта функция вызывается. В классе string определены: std:’.string::size_tyре — тип целочисленных значений (индексов символов строк). Он определяется как беззнаковый целый. std:’.string::npos — максимальное беззнаковое значение типа string::size_type, определенное в классе как — 1. Если поиск безрезультатен, то поисковые функции возвращают значение stringr.npos. Поэтому результат поиска нужно присваивать
переПриложение 5 623 менной типа string::size_type и сравнивать полученное значение со stringr.npos. Прототипы методов в алфавитном порядке append — конкатенация строк (присоединение символов). string & appendfconst string & line, sizejtype pos=0, sizejtype nPos=npos); — присоединяет к вызывающей строке nPos символов, выбирая их из строки line, начиная с позиции pos. string & appendfconst string & line); - присоединяет к вызывающей строке все символы строки line. string & appendfconst char * str, sizejtype n); - присоединяет к вызывающей строке п символов, выбирая их из с_строки, адресованной указателем str. string & appendf const char * str); — присоединяет к вызывающей строке все символы с_строки, адресованной указателем str. string & appendf sizejtype n, char ch); - присоединяет к вызывающей строке п символов ch. assign — присваивание строк. string & assignfconst string & line, sizejtype pos=0, sizejtype nPos=npos); — присваивает вызывающей строке nPos символов, выбирая их из строки line, начиная с позиции pos. string & assignf const string & line); — присваивает вызывающей строке все символы строки line. string & assign (const char * str, sizejtype n); - присваивает вызывающей строке n символов, выбирая их из с__строки, адресованной указателем str. string & assign (const char * str,); — присваивает вызывающей строке все символы с_строки, адресованной указателем str. string & assign (sizejtype n, char ch); - присваивает вызывающей строке n символов ch. at — обращение к отдельному символу строки. char & atfsizejtype k); — обеспечивает доступ к символу вызывающей строки, индексированному значением параметра к. При выходе значения аргумента за пределы строки генерируется исключение outjofjrange. cjstr— константная с_строка, представляющая значение строки.
624 Приложения const char * c_strf) const; — возвращает значение указателя на неизменяемую последовательность символов, входящих в объект класса string. В конец этой последовательности метод cjstrf) помещает терминальный символ '\0\ Тем самым последовательность символов воспринимается как константная строка в стиле Си. capacity - емкость строки. sizejtype capacity() const; - возвращает количество символов, которые можно поместить в вызывающую строку без увеличения выделенной ей памяти. clear — очистка строки. void clearf) — удаляет все символы вызывающей строки. После выполнения метода для обработанной строки size()==0. compare — лексикографическое сравнение строк (последовательностей символов). Результат — нулевое значение, если последовательности совпадают, положительное — если подстрока-параметр лексикографически меньше подстроки из вызывающей строки, отрицательное — в противном случае. int comparefconst string & line) const; - вызывающая строка сравнивается со строкой-аргументом. int comparefsizejtype tar, sizejtype nTar, const string & line) const; — начиная с позиции tar, из вызывающей строки выбирается подстрока длиной пТаг символов и сравнивается со строкой- параметром line. int comparefsizejtype tar, size_type nTar, const string & line, sizejtype pos, sizejtype nPos) const; - начиная с позиции tar, из вызывающей строки выбирается подстрока длиной пТаг символов и сравнивается с подстрокой из nPos символов, выбранных из строки-параметра line, начиная с позиции pos. int comparefconst char * str) const; - сравнивает вызывающую строку с с_строкой, адресованной параметром-указателем. int comparefsizejtype tar, sizejype nTar, const char * str, size_type n=npos) const; - с последовательностью длиной n, адресованной указателем str, сравнивается подстрока вызывающей строки, выделенная значениями tar и пТаг. сору - копирование символов строки в символьный массив. sizejtype copyfchar * str, sizejtype nPos, sizejtype pos=0) const; — из вызывающей строки, начиная с позиции pos, выбираются nPos символов и помещаются в символьный массив,
адресоПриложение 5 625 ванный указателем str. Если значение параметра nPos превышает длину строки (количество символов в ней), то из строки-объекта выбираются все символы. Строка, для которой вызывается функция, не изменяется. Функция сору() не добавляет в массив терминальный символ ’\0\ data - обращение к константному символьному массиву, входящему в объект класса string. const char * data() const; - возвращает значение указателя на неизменяемую последовательность символов, входящих в вызывающую строку. В конец этой последовательности метод не помещает терминальный символ ’\0'. empty - проверка «пустоты» строки. bool empty() const; — возвращает значение true, если в вызывающей строке нет символов. erase — удаление символов из строки. string & erasef sizejtype tar=0, sizejtype nTar=npos); - начиная с позиции tar, из вызывающей строки удаляются пТаг символов. find — поиск с начала строки. sizejtype find(const string & line, sizejtype tar=0) const; - начиная с позиции tar, символы в вызывающей строке сравниваются с символами строки line. Метод возвращает номер самой левой позиции, где найдено совпадение последовательностей символов. При отсутствии совпадений (неудачный поиск) возвращается значение std: :string::npos. sizejtype find(const char * str, sizejtype tar, sizejtype n) const; - начиная с позиции tar символы в вызывающей строке сравниваются с п символами с_строки, адресованной указателем str. Метод возвращает номер самой левой позиции, где найдено совпадение последовательностей символов. При отсутствии совпадений (неудачный поиск) возвращается значение std::string::npos. sizejtype find(const char * str, sizejtype tar =0) const; - начиная с позиции tar, символы в вызывающей строке сравниваются с символами с_строки, адресованной указателем str. Метод возвращает номер самой левой позиции, где найдено совпадение последовательностей символов. При отсутствии совпадений (неудачный поиск) возвращается значение std::stringr.npos. 40 - 2762
626 Приложения sizejype find(const char ch, sizejype tar =0) const; - начиная с позиции far, символы в вызывающей строке сравниваются с символом ch. Метод возвращает номер самой левой позиции, где найдено совпадение символов. При отсутствии совпадений (неудачный поиск) возвращается значение std::string::npos. findjirstjof — поиск с начала строки одного (любого) из символов аргумента. sizejtype find_firstjof(const string & line, sizejtype tar=0) const; - начиная с позиции far, символы в вызывающей строке сравниваются с символами строки line. Метод возвращает номер самой левой позиции, где найдено совпадение любой пары символов. При отсутствии совпадений (неудачный поиск) возвращается значение std::string::npos. sizejype findjfirstjoffconst char * str, sizejype tar, sizejtype n) const; - начиная с позиции far, символы в вызывающей строке сравниваются с п символами строки, адресованной указателем str. Метод возвращает номер самой левой позиции, где найдено совпадение любой пары символов. При отсутствии совпадений (неудачный поиск) возвращается значение std::stringr.npos. sizejype findJirst_of(const char * str, sizejype tar =0) const; — начиная с позиции far, символы в вызывающей строке сравниваются с символами строки, адресованной указателем str. Метод возвращает номер самой левой позиции, где найдено совпадение любой пары символов. При отсутствии совпадений (неудачный поиск) возвращается значение std::string::npos. sizejype findJirstjoffconst char ch, sizejype tar =0) const; - начиная с позиции tar, символы в вызывающей строке сравниваются с символом ch. Метод возвращает номер самой левой позиции, где найдено совпадение символов. При отсутствии совпадений (неудачный поиск) возвращается значение std:: string: mpos. findJirstjnotjof- поиск с начала строки неизвестного (не заданного) символа. sizejype findJirst_notjof(const string & line, sizejype tar=0) const; - начиная с позиции far, символы в вызывающей строке сравниваются с символами строки line. Метод возвращает номер самой левой позиции, где найдено несовпадение последовательностей символов. При отсутствии несовпадений (неудачный поиск) возвращается значение std::string::npos.
Приложение 5 627 sizejtype find_first_not_of(const char * str, sizejtype tar, sizejtype n) const; — начиная с позиции tar, символы в вызывающей строке сравниваются с п символами с_строки, адресованной указателем str. Метод возвращает номер самой левой позиции, где найдено несовпадение последовательностей символов. При отсутствии несовпадений (неудачный поиск) возвращается значение std: -.string: :npos. sizejtype findJirst_not_of( const char * str, sizejtype tar =0) const; - начиная с позиции tar, символы в вызывающей строке сравниваются с символами с_строки, адресованной указателем str. Метод возвращает номер самой левой позиции, где найдено несовпадение последовательностей символов. При отсутствии несовпадений (неудачный поиск) возвращается значение std::string:: npos. sizejype findjfirst_not_of(const char ch, sizejype tar =0) const; - начиная с позиции tar, символы в вызывающей строке сравниваются с символом ch. Метод возвращает номер самой левой позиции, где найдено несовпадение последовательностей символов. При отсутствии несовпадений (неудачный поиск) возвращается значение std:: string:: npos. findjastjof— поиск с конца строки одного (любого) из символов аргумента. sizejtype findjastjof (const string & tine, sizejype tar=0) const; — начиная с позиции far, символы в вызывающей строке сравниваются с символами строки line. Метод возвращает номер самой правой позиции, где найдено совпадение любой пары символов. При отсутствии совпадений (неудачный поиск) возвращается значение std::string::npos. sizejype findJast_of(const char * str, sizejype tar, sizejype n) const; — начиная с позиции tar, символы в вызывающей строке сравниваются с п символами с_строки, адресованной указателем str. Метод возвращает номер самой правой позиции, где найдено совпадение любой пары символов. При отсутствии совпадений (неудачный поиск) возвращается значение std::string::npos. sizejype findJast_of(const char * str, sizejype tar =0) const; - начиная с позиции tar, символы в вызывающей строке сравниваются с символами с_строки, адресованной указателем str. Метод возвращает номер самой правой позиции, где найдено совпа40*
628 Приложения дение любой пары символов. При отсутствии совпадений (неудачный поиск) возвращается значение std::string::npos. sizejtype find_lastjof(const char ch, sizejtype tar=0) const; — начиная с позиции far, символы в вызывающей строке сравниваются с символом ch. Метод возвращает номер самой правой позиции, где найдено совпадение любой пары символов. При отсутствии совпадений (неудачный поиск) возвращается значение std:: string: :npos. findJastjiotjof — поиск последнего неизвестного (незаданного) символа. sizejtype findJast_not_of(const string & line, sizejtype tar=0) const; — начиная с позиции far, символы в вызывающей строке сравниваются с символами строки line. Метод возвращает номер самой правой позиции, где найдено несовпадение символов. При отсутствии несовпадений (неудачный поиск) возвращается значение std:: string ::npos. sizejtype findJast_not_of(const char * sfr, sizejtype tar, sizejtype n) const; - начиная с позиции far, символы в вызывающей строке сравниваются с п символами с_строки, адресованной указателем sfr. Метод возвращает номер самой правой позиции, где найдено несовпадение символов. При отсутствии несовпадений (неудачный поиск) возвращается значение std::string::npos. sizejtype findJast_not_of(const char * str, sizejtype tar =0) const; — начиная с позиции tar, символы в вызывающей строке сравниваются с символами с_строки, адресованной указателем sfr. Метод возвращает номер самой правой позиции, где найдено несовпадение символов. При отсутствии несовпадений (неудачный поиск) возвращается значение std::string::npos. sizejtype find_last_not__of(const char ch, sizejtype tar =0) const; — начиная с позиции far, символы в вызывающей строке сравниваются с символом ch. Метод возвращает номер самой правой позиции, где найдено несовпадение символов. При отсутствии несовпадений (неудачный поиск) возвращается значение std::string::npos. insert - вставка символов в строку. string & insertf sizejtype tar, const string & line, size_type pos, sizejype nPos); - начиная с позиции far, в вызывающую
Приложение 5 629 строку вставляются п символов, выбранных из строки line, начиная с позиции pos. string & insertf sizejtype tar, const string & line); - начиная c позиции tar, в вызывающую строку вставляются все символы строки line. string & insertf sizejtype tar, const char * str); - начиная с позиции tar, в вызывающую строку вставляются все символы из с_строки, адресованной указателем str. string & insertf sizejtype tar, const char * str, sizejtype n); - начиная с позиции tar, в вызывающую строку вставляются п симо- лов из с_строки, адресованной указателем str. string & insertf size _tyре tar, sizejtype n, charch); - начиная с позиции tar, в вызывающую строку вставляются п симолов ch. length - определение размера строки. size_type lengthf) const; - возвращает количество символов в вызывающей строке. maxjsize - определение предельной емкости строки. sizejtype max_size() const; - максимальное количество символов, которые можно поместить в вызывающую строку. replace - замена символов в строке. string & replacefsizejtype tar, sizejtype n, const string & line); - начиная с позиции tar, в вызывающей строке nTar символов заменяются символами из строки line, начиная с позиции pos. string & replacef sizejtype tar, sizejtype nTar, const string & line, sizejtype pos, sizejtype nPos); - начиная с позиции tar, nTar символов вызывающей строки заменяются nPos символами, выбранными из строки line, начиная с позиции pos. string & replacef sizejtype tar, sizejtype nTar, const char * str, sizejtype n); - начиная с позиции tar, nTar символов вызывающей строки заменяются п символами, выбранными из строки, адресованной указателем str. string & replacef sizeJype tar, sizejtype nTar, const char * str); - начиная с позиции tar, nTar символов вызывающей строки заменяются символами строки, адресованной указателем str. string & replacef size Jype tar, sizejtype nTar, sizejtype n, char ch); — начиная с позиции tar, nTar символов вызывающей строки заменяются п символами ch. reserve — резервирование памяти для строки.
630 Приложения void reservefsizejtype tar=0); - для вызывающей строки выделяется новый участок памяти не меньших размеров, чем число символов, находящихся в строке. Если tar больше, чем число символов в вызывающей строке, то емкость строки увеличивается до значения tar. При выполнении метода становятся недействительными все указатели, итераторы и ссылки, связанные с исходной вызывающей строкой. resize - изменение размеров строки. void resizefsizejtype п, char ch); - если п меньше размера строки, то в строке остаются п левых символов. В противном случае вызывающая строка увеличивается до размера п, и в ее конец дописываются символы ch. При некорректном значении п посылается исключение lengthjerror. void resize (sizeJtyре n,); — в качестве значения параметра char ch берется символ, выбранный авторами библиотеки. rfind - поиск с конца строки. sizejtype rfind(const string & line, sizejtype tar=npos) const; - начиная с позиции tar, символы в вызывающей строке сравниваются с символами строки line. Метод возвращает номер самой правой позиции, где найдено совпадение последовательностей символов. При отсутствии совпадений (неудачный поиск) возвращается значение std::string::npos. sizejtype rfind(const char * str, sizejtype tar, sizejtype n) const; — начиная с позиции tar, символы в вызывающей строке сравниваются с п символами с_строки, адресованной указателем str. Метод возвращает номер самой правой позиции, где найдено совпадение последовательностей символов. При отсутствии совпадений (неудачный поиск) возвращается значение std::string::npos. sizejtype rfind(const char * str, sizejtype tar = npos) const; - начиная с позиции tar, символы в вызывающей строке сравниваются с символами с_строкы, адресованной указателем str. Метод возвращает номер самой правой позиции, где найдено совпадение последовательностей символов. При отсутствии совпадений (неудачный поиск) возвращается значение std::string::npos. sizejtype rfindfconst char ch, size_type tar = npos) const; - начиная с позиции tar, символы в вызывающей строке сравниваются с символом ch. Метод возвращает номер самой правой позиции, где найдено совпадение символов. При отсутствии совпадений (неудачный поиск) возвращается значение std::string::npos. size — определение размера строки (число символов в строке).
Приложение 5 631 sizejtype size() const; - возвращает количество символов в вызывающей строке. string - конструктор. explicit stringf); — конструктор умолчания. Формирует пустую строку. Модификатор explicit требует, чтобы конструктор вызывался только явно. stringfconst string & line); — конструктор копирования. stringfconst char *str, sizejtype n); - конструктор общего вида. Помещает в формируемую строку п символов из с_строки, адресованной первым параметром. stringfconst string & line, sizejtype pos=0, sizejtype nPos=npos); — конструктор общего вида. Выбирает nPos символов из строки line, начиная с позиции pos, и помещает эти символы в создаваемую строку, npos — общее количество символов в строке line. stringfint п, char ch); — конструктор общего вида. В создаваемую строку п раз помещает символ ch. substr- выделение подстроки. string substrfsizejtype tar-О, sizejtype nTar-npos) const; - начиная с позиции far, выбирает nTar символов из вызывающей строки и формирует объект класса string. При s.size()<nTar формируется исключение outjofjrange. swap — обмен значениями. void swapf string & line); - обменивает значения (символы) вызывающей строки и строки, заданной параметром. Библиотечная функция для ввода строк getline — прочитать строку из входного потока. istream & getlinefistream & is, string & line, chardelim='\n’); — в строку line из входного потока is читаются символы до появления ограничителя, заданного параметром delim.
632 Приложение 6 Стандартные функции библиотеки Си Таблица П6.1 Математические функции — заголовок <cmath> Функция Прототип и краткое описание действий abs int absfint i); Возвращает абсолютное значение целого аргумента / acos double acosfdouble х); Функция арккосинуса. Значение аргумента должно находиться в диапазоне от — 1 до +1 asin double asin(double х); Функция арксинуса. Значение аргумента должно находиться в диапазоне от — 1 до +1 atari double at an (double х); Функция арктангенса atan2 double atan2(double у, double х); Функция арктангенса от значения у/х. (Учитываются знаки.) ceil double ceilfdouble х); Возвращает в форме double наименьшее целое, превышающее значение х cos double cosfdouble х); Функция косинуса. Угол (аргумент) задается в радианах cosh double cosh (double х); Возвращает значение гиперболического косинусах exp double exp(double х); Вычисляет значение е* (экспоненциальная функция) tabs double fabsfdouble х); Возвращает абсолютное значение вещественного аргумента X floor double floor(double х); Находит наибольшее целое, не превышающее значения х. Возвращает его в форме double
Приложение 6 633 Функция Прототип и краткое описание действий fmod double fmod(double х, double у); Возвращает остаток от деления нацело х на у frexp double frexpfdouble х, int *е); Возвращает нормализованную дробь (г) и степень (*е) числа 2, такие, что х равно г*2*с labs long labsflong х); Возвращает абсолютное значение целого аргумента long х Idexp double Idexpfdouble v, int e); Возвращает значение выражения v*2° log double logfdouble x); Возвращает значение натурального логарифма (In х) log 10 double Iog10(double x); Возвращает значение десятичного логарифма (/оу10 х). modf double modffdouble х, double * у); Возвращает как результат (г) дробную часть аргумента х и его целую часть (*у). Таким образом, г + *у равно х (с учетом знаков) pow double pow(double х, double у); Возвращает значение ху, т.е. х в степени у sin double sinfdouble х); Функция синуса. Угол (аргумент) задается в радианах sinh double sinh(double х); Возвращает значение гиперболического синуса для х sqrt double sqrt(double х); Возвращает неотрицательное значение квадратного корня X tan double tan (double х); Функция тангенса. Угол (аргумент) задается в радианах tanh double tanh(double х); Возвращает значение гиперболического тангенса для х Продолжение
634 Приложения Таблица П6.2 Функции проверки и преобразования символов — заголовок <cctype> Функция Прототип и краткое описание действий isalnum int isalnumfint с); Возвращает ненулевое значение, если с — код буквы или цифры (А т Z, а т z, 0 т 9), и нуль — в противном случае isalpha int isalpha(int с); Возвращает ненулевое значение, если с — код буквы (A ч- Z, а ч- zj, и нуль - в противном случае iscntrl int iscntrl(int с); Возвращает ненулевое значение, если с - управляющий символ с кодами 0x00 ч- 0x01 Гили 0x7F, и нуль — в противном случае isdigit int isdigit(int с); Возвращает ненулевое значение, если с — цифра (0 ч- 9) в коде ASCII, и нуль — в противном случае isgraph int isgraphfint с); Возвращает ненулевое значение, если с — видимый (изображаемый) символ с кодом (0x21 ч- 0х7Е), и нуль — в противном случае islower int islower(int с); Возвращает ненулевое значение, если с — код символа на нижнем регистре (а ч- z), и нуль — в противном случае isprint int isprintfint с); Возвращает ненулевое значение,если с — печатный символ с кодом (0x20 ч- 0х7Е), и нуль — в противном случае ispunct int ispunctfint с); Возвращает ненулевое значение, если с — символ-разделитель (соответствует iscntrl или /sspase), и нуль — в противном случае isspace int isspacefint с); Возвращает ненулевое значение, если с - обобщенный пробел: пробел, символ табуляции, символ новой стро-
Приложение 6 635 Функция Прототип и краткое описание действий isupper ки или новой страницы, символ возврата каретки (0x09 + OxOD, 0x20), и нуль — в противном случае int isupperfint с); Возвращает ненулевое значение, если с — код символа на верхнем регистре (А -н Z), и нуль — в противном случае isxdigit int isxdigitfint с); Возвращает ненулевое значение, если с — код шестнадцатеричной цифры (0 9, А + F, a -г- f), и нуль - в противном случае to! owe г int tolowerfint с); Преобразует код буквы с к нижнему регистру, остальные коды не изменяются toupper int toupper(int с); Преобразует код буквы с к верхнему регистру, остальные коды не изменяются Таблица П6.3 Функции преобразования с_строк — заголовок <cstdlib> Функция Прототип и краткое описание действий atof double atof (char *str); Преобразует с_строку в вещественное число типа double atoi int atoifchar *str); Преобразует с_строку в десятичное целое число atol long atoifchar *str); Преобразует с строку в длинное десятичное целое число Продолжение
636 Приложения Таблица П6.4 Функции для выделения и освобождения памяти —заголовок <cstdlib> Функция Прототип и краткое описание действий calloc void *calloc(unsigned п, unsigned т); Возвращает указатель на начало области динамически распределенной памяти для размещения п элементов по т байт каждый. При неудачном завершении возвращает значение NULL. free void free (void *b 1); Освобождает ранее выделенный блок динамически распределяемой памяти с адресом первого байта b 1 malloc void *malloc(unsigned s); Возвращает указатель на блок динамически распределяемой памяти длиной s байт. При неудачном завершении возвращает значение NULL realloc void *realloc(void *Ь1, unsigned ns); Изменяет размер ранее выделенной динамической памяти с адресом начала b 1 до размера ns байт.Если b 1 равен NULL, то функция выполняется как mallocQ Таблица П6.5 Специальные функции — заголовок <cstdlib> Функция Прототип и краткое описание действий abort void abort(void); Вызывает ненормальное завершение программы bsearch void * bsearchfconst void * key, const void * base, sizejt nmemb, size_t size, int (*compare)(const void *, const void *(); Выполняет двоичный поиск значения в упорядоченной последовательности, compare — указатель на функцию сравнения exit void exit (int status); Нормальное завершение программы
Приложение 6 637 Функция Прототип и краткое описание действий rand int rand(void); Возвращает псевдослучайное целое число из диапазона 0 + RAND_МАХ (не менее 32767), может использовать srand() qsort void qsortfvoid * base, sizejt nmemb, sizejt size, int (*compare)(const void \ const void*)) Выполняет упорядочение значений элементов последовательности. compare — указатель на функцию сравнения элементов srand void srand(unsigned seed); Функция инициализации генератора случайных чисел (rand); seed — любое беззнаковое целое число — ключ инициализации В таблицы данного приложения включены только те функции библиотеки языка Си, которые удобно применять в программах на языке Си++. Так, например, не рассматриваются функции для ввода-вывода в консольном режиме, функции для работы с файлами и другие средства, которые можно заменить средствами языка Си++ и его библиотеки. Продолжение
Приложение 7 Алгоритмы STL Таблица П7.1 Алгоритмы заголовка <algorithm> Алгоритм Манипуляция с парами элементов max const Г& max(const Г& х, const Г& у); const Г& max(const Г& х, const Г& у, Predpr); Для определения максимального из двух элементов, сравниваемых с помощью operator< или функционального объекта рг min const Г& min(const Г& х, const Г& yj; const Г& min( const Г& х, const Г& у, Pred рг); Для определения минимального из двух элементов, сравниваемых с помощью operator< или функционального объекта рг swap void swap(T& х, Т&у); Для перестановки двух значений, на которые ссылаются параметры iterjswap void iter_swap(Fwdltl x, Fwdlt2 y); Для перестановки двух значений, обозначенных (адресуемых) итераторами Алгоритм Сканирование последовательностей maxjelement Fwdlt max_element(Fwdlt first, Fwdlt last); Fwdlt max elementfFwdlt first, Fwdlt last, Pred pr); Для поиска наибольшего элемента в последовательности элементы сравниваются с помощью operator< или функционального объекта рг min_element Fwdlt minjeiement (Fwdlt first, Fwdlt last); Fwdlt minjeiement (Fwdlt, Pred pr); Для поиска наименьшего элемента в последовательности элементы сравниваются с помощью operator< или функционального объекта рг
Приложение 7 639 equl bool equl(tnlt1 first, Inltl last, Inlt2x); bool equl( Inltl first, Inltl last, Inlt2x, Predpr); Для поэлементного сравнения двух последовательностей на равенство с помощью operator= = или функционального объекта рг lexicographicaljcom- pare bool lexicographical__compare(Inltl firstl, Inltl lastl, Inlt2first2, Inlt2 Iast2); bool lexicographicaljcompare(Inltl firstl, Inltl lastl, Inlt2first2, Inlt2 Iast2, Predpr); Для сравнения двух последовательностей поэлементно с целью выяснения, находится ли одна последовательность ранее другой в лексикографическом порядке (по отношению к operator< или функциональному объекту рг) mismatch pair<lnlt1, 1пП2> mismatch/Inltl first, Inltl last, Inlt2 x); pair<lnlt1, Inlt2> mismatch/Inltl first, Inltl last, Inlt2x, Predpr); Для определения места первого несовпадения двух последовательностей, элементы которых сравниваются с использованием operator= = или функционального объекта рг find Inlt find/lnlt first, Inlt last, const Г& val); Для определения первого элемента в последовательности, который равен заданному значению val findjf Inlt findjf (Inlt first, Inlt last, Pred pr); Для определения первого элементах в последовательности, для которого при вызове рг(х) возвращается true, где рг — функциональный объект adjacentjind Fwdlt adjacent_find(Fwdlt first, Fwdlt last); Fwdlt adjacent Jind/Fwdlt first, Fwdlt last, Predpr); Для определения первой пары смежных элементов, которые при сравнении с использованием operator= = или функционального объекта рг дают true count countflnlt first, Inlt last, const Г& val); Для подсчета числа элементов в последовательности, которые равны значению val Продолжение
640 Приложения countjf countjf(Inlt first, Inltlast, Predpr) Для подсчёта числа элементов х в последовательности, для которых вызов рг(х) возвращает true, где рг — функциональный объект search Fwdltl search(Fwdlt1 firstl, Fwdltl lastl, Fwdlt2 first2, Fwdlt2 Iast2); Fwdltl searchf Fwdltl firstl, Fwdltl lastl, Fwdlt2 first2, Fwdlt2 Iast2, Pred pr); Для определения первого появления одной последовательности в другой, где равенство элементов определяется с помощью operator= = или функционального объекта рг search_n Fwdlt search_n(Fwdlt first, Fwdltlast, Distn, const T& val); Fwdlt search _n( Fwdlt first, Fwdlt last, Dist n, const T& val, Predpr); Для определения первого появления в последовательности определенного значения, повторенного п раз, где равенство элементов определяется с помощью operator= = или функционального объекта рг findjend Fwdltl find_end(Fwdltl firstl, Fwdltl lastl, Fwdlt2 first2, Fwdlt2 Iast2); Fwdltl find_end(Fwdltl firstl, Fwdltl lastl, Fwdlt2 first2, Fwdlt2 Iast2, Pred pr); Для определения самого позднего появления одной последовательности в другой, где равенство элементов определяется с помощью operator= = или функционального объекта рг find_first_of Fwdltl findJirstjoffFwdltl firstl, Fwdltl lastl, Fwdlt2 first2, Fwdlt2 Iast2); Fwdltl find_first_of(Fwdltl firstl, Fwdltl lastl, Fwdlt2 first2, Fwdlt2 Iast2, Pred pr); Для определения самого раннего появления в одной последовательности каких-либо членов другой последовательности, где равенство элементов определяется с помощью operator= = или функционального объекта рг Продолжение
Приложение 7 641 Алгоритм Обработка каждого элемента (с возможным изменением) forjeach Fun for_each(lnlt first, In It last, Fun f); Для каждого элементах последовательности вызывается функциональный объект Цх) generate void generatef Fwdlt first, Fwdlt last, Gen g); Для присвоения каждому элементу в последовательности результатов последовательных вызовов функционального объекта д() generate_n void generate_n(Outlt first, Dist n, Gen g); Для присвоения первым n элементам последовательности результатов последовательных вызовов функционального объекта д() transform Outlt transformflnlt first, Inlt last, Outlt res, Unop op); Присваивает каждому элементу, адресуемому итераторами из диапазона [res, res+( last-first)), значения функционального объекта ор(х), где х — элемент из последовательности, заданной итераторами first, last. Возвращает итератор, адресующий позицию за последним сформированным элементом. Outlt transform(lnlt first, Inlt last, Inltfirstl, Outlt res, Binary_op b_op); Последовательности [res, res+('ast-first)) присваиваются значения функционального объекта b_op(x,y) где х элемент из [first, last), у - элемент из [firstl ,first1+( last-first)); Алгоритм Запись существующей последовательности или повторяющегося значения в некоторую п осл е до вател ьность copy Outlt copyflnlt first, Inlt last, Outlt res); Для копирования одной последовательности в другую от начала до конца copy_backward Bidlt2 copy_backward( Bidltl first, Bid It 1 last, Bidlt2 res); Для копирования одной последовательности в другую в обратном порядке 4,-2762 Продолжение
642 Приложения fill void fill(Fwdlt first, Fwdit last, const T& x); Присваивает значение x каждому элементу последовательности fillji void fill_n(Outlt first, Size n, const T& x); Присваивает значение x первым n элементам п осл едо вател ьности swapjranges Fwdlt2 swap_ rangesf Fwdit 1 first, Fwdit 1 last, Fwdlt2x); Для перестановки значений, хранящихся в двух последовательностях Алгоритм Замена элементов последовательности replace void replacef Fwdit first, Fwdit last, const Г& vOld, const Г& vNew); Для замены каждого элемента со значением vOld значением vNew replacejf void replace_if(Fwdit first, Fwdit last, Pred pr, const Г& val); Для замены значением val значения каждого элемента х, для которого функциональный объект рг(х) принимает значение true replacejcopy Outlt replace_copy(lnlt first, Inltlast, Outltx, const Г& vOld, const Г& vNew); Для копирования последовательности с заменой каждого элемента, который имеет значение vOld, значением vNew replacejcopyjf Outlt replacejcopyJf(lnlt first, Inltlast, Outlt res, Pred pr, const Г& val); Для копирования последовательности с заменой значением val каждого элемента х, для которого функциональный объект рг(х) принимает значение true remove Fwdit removef Fwdit first, Fwdit last, const Г& val); Продолжение
Приложение 7 643 removejf Для удаления всех элементов, имеющих значение val Fwdtt remove JffFwdlt first, Fwdltlast, Predpr); Для удаления всех элементов х, для которых функциональный объект рг(х) принимает значение true removejcopy Outlt remove jcopy( Inlt first, Inlt last, Outlt res const Г& val); Копирует последовательность, удаляя все элементы, имеющие значение val removejcopyjf Outlt remove_сору_if( Inlt first, Inlt last, Outlt res, Predpr); Копирует последовательность, удаляя все элементы х, для которых функциональный объект рг(х) принимает значение true unique Fwdlt unique (Fwdlt first, Fwdlt last); Fwdlt unique (Fwdlt first, Fwdltlast, Predpr); Для удаления всех, кроме первого, элементов последовательности, которые при сравнении с применением operator== или функционального объекта дают true uniquejcopy Outlt unique _сору( Inlt first, Inlt last, Outlt res); Outlt unique _copy(lnlt first, Inlt last, Outlt res, Predpr); Для копирования в res последовательности с удалением всех элементов, кроме первого из этих элементов. Для сравнения operator== или функциональный объект Алгоритм Изменение порядка элементов в последовательности reverse void reversef Bidlt first, Bidlt last); Для реверсирования (перестановки в обратном порядке) элементов в последовательности reverse_copy Outlt reverse_сору(Bidlt first, Bidlt last, Outlt res); Для копирования последовательности с реверсированием ее элементов 41* Продолжение
644 Приложения rotate void rotate (Fwdlt first, Fwdlt middle, Fwdlt last); Для циклического сдвига последовательность на п позиций rotate_copy Outlt rotate_copy (Fwdlt first, Fwdlt middle, Fwdlt last, Outlt res); Для копирования последовательности с циклическим сдвигом её элементов на п позиций randomjshuffle void random_shuffle( Ranlt first, Rah It last); void random shufflefRanlt first, Ranlt last, Fun& f); Для перетасовки (т.е. переупорядочивания в случайном порядке) элементов последовательности Алгоритм Перестановки (сортировки) элементов последовательности partition Bid It partition(Bidlt first, Bid It last, Predpr); Перемещает в начало последовательности те элементы х, для которых функциональный объект рг(х) принимает значение true stable partition Bidlt stable_partition(Bidlt first, Bidlt last, Pred pr); Упорядочивает последовательность тем же способом, что и в предыдущем случае, но без перестановок каких-либо пар элементов внутри каждого подраздела sort void sort( Ranlt first, Ranlt last); void sort(Ranlt first, Ranlt last, Pred pr); Для размещения элементов последовательности в возрастающем порядке в соответствии со значениями operator< или функционального объекта рг stablejsort void stable__sort( Bidlt first, Bidlt last); void stable_sort(Bidlt first, Bidlt last, Predpr); Для сортировки последовательности так же, как и в предыдущем случае, но без перестановок каких-либо пар элементов, которые не могут быть упорядочены Продолжение
Приложение 7 645 partialjsort void partial_sort(Ranlt first, Rantt middle, Ran It last); void partial_sort( Ran It first, Ranlt middle, Ran It last, Predpr) Для сортировки только middle-first элементов из Rmna30Ha[first,last) в порядке, определяемом operator< или функциональным объектом рг, и перемещения их в начало последовательности partial_sort_copy Ranlt partial__sort_copy(lnlt first, Inltlast, Ranlt resjirst, Ranlt resjast); Ranltpartial_sort_copy(lnlt first, Inltlast, Ranlt resjirst, Ranlt resJast, Pred pr); Для копирования последовательности с сортировкой только наименьших (last_first) или (res_last — res first) элементов (как в предыдущем случае) nthjelement void nthjelement(Ranlt first, Ranlt nth, Ranlt last); void nth_element(Ranlt first, Ranlt nth, Ranlt last, Pred pr); Переупорядочивает последовательность таким образом, что для любых элементов х слева от nth и элементов у справа от nth x.operator(y) или рг(х,у) равен true Алгоритм Объединение двух упорядоченных последовательностей, формирование одной упорядоченной последовательности merge Outltmerge(Inlt firstl, Inltlastl, Inltfirst2, Inlt Iast2, Outlt res); OutltmergefInlt firstl, Inltlastl, Inltfirst2, Inlt Iast2, Outlt res, Predpr); Для слияния двух последовательностей, упорядоченных с помощью operator< или функционального объекта рг, и создания новой последовательности inplacejmerge void inplace _merge( Bid It first, Bidlt middle, Bidlt last); void inplace_merge( Bidlt first, Bidlt middle, Bidlt last, Pred pr); Для слияния двух смежных последовательностей, упорядоченных с помощью operator< или функционального объекта рг Продолжение
646 Приложения Алгоритм Сканирование последовательности, возрастающей в соответствии с operator< или функциональным объектом lower abound Fwdlt lower_bound(( Fwdlt first, Fwdltlast, const T& val); Fwdlt lower_bound((Fwdlt first, Fwdlt last, const T& val, Pred pr); Для определения первого элемента в упорядоченной последовательности, который не меньше значения val. Для сравнения используется operator< или функциональный объект рг upperbound Fwdlt upper _bound( Fwdlt first, Fwdlt last, const T& val); Fwdlt upper _bound( Fwdlt first, Fwdltlast, const T& val, Pred pr); Для определения последнего элемента в упорядоченной последовательности, который не меньше значения val. Для сравнения используется operator< или функциональный объект рг equal_ran ge pair< Fwdlt, Fwdlt> equal_range( Fwdlt first, Fwdlt last, const T& val); pair< Fwdlt, Fwdlt> equaljrangef Fwdlt first, Fwdltlast, const T& val, Pred pr); Для определения в упорядоченной последовательности нижней и верхней граней значения val binaryjsearch bool binary_search( Fwdlt first, Fwdltlast, const T& val); bool binary_search( Fwdlt first, Fwdltlast, const T& val, Pred pr); Проверяет, содержит ли упорядоченная последовательность член, эквивалентный значению val. Для сравнения используется operator< или функциональный объект рг Продолжение
Приложение 7 647 Алгоритм Обработка двух последовательностей с возрастающими значениями элементов. Для сравнения используется operator< или функциональный объектрг includes boolincludes(lntlt1 firstl, Intltl lastl, Intlt2 first2, Intlt2 Iast2); bool inciudesf Intltl firstl, Intltl lastl, Intlt2 first2, Intlt2 Iast2, Pred pr); Проверяет, включает ли одна упорядоченная последовательность элементы, эквивалентные каждому из элементов второй последовательности setjunion Outltset__union(Intltl firstl, Intltl lastl, Intlt2 first2, Intlt2 Iast2, Outlt res); Outltset_union(Intltl firstl, Intltl lastl, Intlt2 first2, Intlt2 Iast2, Outlt res, Pred pr); Создает новую последовательность, объединяя две упорядоченные. В создаваемую последовательность не включаются элементы второй последовательности, имеющие эквивалентные элементы в первой. (Операция похожа на логическое ИЛИ двух множеств.) setjntersection Outlt setJntersectionf Intltl firstl, Intltl lastl, Intlt2 first2, Intlt2 Iast2, Outlt res); Outlt setjntersection (Intltl firstl, Intltl lastl, Intlt2 first2, Intlt2 Iast2, Outlt res, Pred pr); Создает новую последовательность, объединяя две упорядоченные. В создаваемую последовательность включаются элементы первой последовательности, имеющие эквивалентные элементы во второй. (Операция похожа на логическое И двух множеств.) setjdifference Outltset_difference(Intltl firstl, Intltl lastl, Intlt2 first2, Intlt2 Iast2, Outlt res); Outltsetjdifference(Intltl firstl, Intltl lastl, Intlt2 first2, Intlt2last2, Outlt res, Pred pr); Создает новую последовательность, объединяя две упорядоченные. В создаваемую последовательность включаются элементы из первой последовательности, не имеющие эквивалентных элементов во второй. (Операция похожа на формирование разности двух множеств.) Продолжение
648 Приложения se tsym m e trie _di f- ference Outlt set_symmetricjdifference(lntlt1 firstl, Intlt 1 lastl, Intlt2 first2, Intlt2 Iast2, Outlt res); Outlt set_ symmetric_difference( Intlt 1 first 1, Intlt 1 lastl, Intlt2 first2, Intlt2 Iast2, Outlt res, Pred pr); Создает новую последовательность, объединяя две упорядоченные. В создаваемую последовательность включаются элементы из любой последовательности, не имеющие эквивалентных элементов в другой. (Операция похожа на формирование симметрической разности двух множеств.) Алгоритм Управление последовательностью как ’кучей”, значения которой упорядочены с помощью operator< или функционального объекта make_heap void make_heap(Ranlt first, Ranlt last); void make_heap(Ranlt first, Ranlt last, Pred pr); Создает из последовательности "кучу” (heap) push_heap void push_heap(Ranlt first, Ranlt last); void push_heap(Ranlt first, Ranlt last, Pred pr); Добавляет элемент в ’’кучу" с учётом упорядоченности popjheap void pop_heap(Ranlt first, Ranlt last); void pop_heap(Ranlt first, Ranlt last, Pred pr); Удаляет наибольший элемент из "кучи" sort_heap void sort_heap(Ranlt first, Ranlt last); void sort_heap(Ranlt first, Ranlt last, Pred pr); Сортирует элементы "кучи” Алгоритм Генерация перестановок последовательности, упорядоченной с помощью operator< или функционального объекта nextjpermutation bool next_permutation(Bidlt first, Bidltlast); bool next_permutation(Bidlt first, Bidltlast, Pred pr); Лексикографически упорядочивает элементы последовательности. Функция возвращает значение false, если все элементы удается разместить в возрастающем порядке Продолжение
Приложение 7 649 Продолжение prev_permutation bool prev_permutation(Bidlt first, Bidltlast); bool prev_permutation/Bidlt first, Bidlt last, Predpr); Лексикографически упорядочивает элементы последовательности. Функция возвращает значение false, если все элементы удается разместить в убывающем порядке Таблица П7.2 Алгоритмы заголовка <numerc> Алгоритм Суммирования и разности элементов п осл едо вате л ьносте й accumulate Т accumulate/ Inlt first, In It last, T val); T accumulate/ In It first, Inlt last, T val, Pred pr); Выполняет суммирование всех элементов последовательности. val — начальное значение для суммы. Правило суммирования может определять функциональный объект/?/* inner^product Tinner_product(lnlt1 firstl, Inltl lastl, Inlt2 first2, Inlt2 Iast2, T val); TinnerjyroductfInltl firstl, Inltl lastl, Inlt2 first2, Inlt2last2, T val, Predl pr1, Pred2pr2); Выполняет суммирование произведений соответствующих элементов двух последовательностей. val — начальное значение для суммы. Правила умножения и суммирования могут определять функциональные объекты prl и рг2 partial_sum Outlt partialjsum/Inlt first, Inlt last, Outlt result); Outlt partial_sum(Inlt first, Inlt last, Outlt result, Pred pr); Создает последовательность частичных сумм, к каждой из которых добавляется один элемент из исходной последовательности. Правило суммирования может определять функциональный объект рг adjacent jdifference Outlt adjacentjdifference/ Inlt first, Inlt last, Outlt result); Outlt adjacent jdifference (Inlt first, Inlt last, Outlt result, Pred pr); Создает последовательность разностей между смежными парами элементов последовательности. Правило вычитания может определять функциональный объект рг
650 Приложение 8 Средства ввода-вывода Си++ Константы типа iostate, задающие флаги состояния потока данных (класс iostate определен в iosjbase): goodbit- начальное состояние потока данных; eofbit - обнаружен признак конца файла; faiibit - ошибка операции ввода-вывода; badbit - неопределенное состояние потока (фатальная ошиб- Таблица П8.1 Методы класса basicjos Функция Прототип и краткое описание действий bad bool bad() const; При ошибке возвращает значение true (установлен флаг badbit) clear void clearf); Устанавливает состояние потока в нуль (сбрасывает все флаги). void clearfiostate state=goodbit); Сбрасывает все флаги, а затем устанавливает те из них, которые определены значением state copyfmt basicJos& copyfmtfconst basicJos& rhs); Устанавливает формат потока в соответствии с потоком, использованном в аргументе eof bool еоЦ) const; Возвращает true, если обнаружен признак конца файла (установлен флаг eofbit) fail bool fail() const; Возвращает true, при ошибке обмена с потоком (установлен флаг badbit или rdstate) fill charjtype fill() const; Возвращает текущее значение символа заполнения пустых позиций в поле вывода.
Приложение 8 651 Функция Прототип и краткое описание действий flags charjtype fiii(charjtype) const; Заменяет значением параметра символ заполнения пустых позиций в поле вывода; возвращает предыдущее значение символа заполнения fmtflags flagsf) const; Возвращает текущее значение флагов форматирования fmtflags flagsffmtflags state); Устанавливает флаги форматирования по значению параметра; возвращает ранее установленное значение флагов good bool good() const; Возвращает true, если не установлен ни один флаг состояния (ошибок нет, поток в нормальном состоянии) precision streamsize precisionf) const; Возвращает значение точности вывода вещественных чисел. streamsize precisionfstreamsize prec); Устанавливает точность вывода вещественных чисел по значению, параметра; возвращает ее предыдущее значение rdbuf basicjstreambuf< >* rdbuf0 const; Возвращает указатель на буфер, связанный с потоком rdstate iostate rdstatef) const; Возвращает текущее состояние потока (флаги состояния потока) setf fmtflags setfffmtflags fmtfl); Устанавливает флаги по значению параметра; возвращает предыдущие значения флагов fmtflags setfffmtflags fmtfl, fmtflags mask); Сбрасывает флаги в соответствии с mask, затем устанавливает флаги fmtfl & mask, возвращает предыдущие значения флагов setstate void setstatefiostate state); Устанавливает флаги в соответствии со значением state tie basic_ostream< >* tief) const; Возвращает указатель на выходной поток, связанный (синхронизированный по буерам) сданным потоком Продолжение
652 Приложения Продолжение Функция Прототип и краткое описание действий basic_ostream< > * tie(basic_ostream< >* tiestr); Связывает поток с потоком, на который указывает параметр; возвращает указатель на предыдущий связанный поток, если такой есть unself void unsetfffmtflags mask); Очищает флаги потока, отмеченные параметром width streamsize width() const; Возвращает текущее значение ширины (в символах) поля вывода streamsize width (streamsize wide); Устанавливает в соответствии со значением параметра ширину поля вывода; возвращает ее предыдущее значение Таблица П8.2 Методы класса istream (basic_istream<>) Функция Прототип и краткое описание действий gcount streamsize gcount() const; Возвращает число символов, извлеченных из потока при последнем неформатированном обращении get intjype get(); Читает из входного потока следующий символ или EOF. basic_istream< >& get(char_type& с); Присваивает параметру очередной символ входного потока, возвращает объект, по состоянию которого можно проверить успешность чтения basic_istream< >& get(char_type* s, streamsize n, charjype delim=’\n'); Извлекает из входного потока символы и помещает их в буфер, на начало которого указывает первый параметр. Передача символов завершается, если прочитано (л — 1) символов, или встретился сим вол-разделитель (третий параметр в функции), или достигнут конец файла EOF. Терминальный нуль-символ всегда поме-
Приложение 8 653 Функция Прототип и краткое описание действий getline щается в буфер, как конец принятой с_строки, разделитель (последний параметр) в буфер не переносится basic_istream< >& getline(charjype* s, stream- size n, charjype delim); Совпадает c get() с тремя параметрами, однако символ-разделитель считывается из потока, но не помещается в буфер ignore basicjstream< >& ignore (streamsize п= 1, int_type delim = traits::eof()); Пропускает до n символов входного потока; останавливается, если встретился разделитель (второй параметр), по умолчанию равный EOF peek intjtype реек(); Показывает следующий символ из входного потока putback basic Jstream< >& putback(char_type с); Помещает символ назад во входной поток read basic_istream< >& read(char_type* s, streamsize n); Извлекает из входного потока заданное вторым параметром число символов и помещает их в массив, на начало которого указывает первый параметр * seekg basic_istream< >& seekg(pos_type pos); Устанавливает указатель чтения входного потока на абсолютную позицию, заданную параметром pos. basic_istream< >& seekg(off_type& off, ios_base::seekdir dir); Перемещает указатель чтения входного потока на число символов, заданное первым параметром. Второй параметр задает точку отсчета (ios::beg — начало потока; ios::cur — текущая позиция потока; ios::end — конец потока) tellg posjype tellg(); Возвращает текущую позицию указателя чтения входного потока Продолжение
654 Приложения Таблица П8.3 Методы класса ostream (basic_ostream<>) Функция Прототип и краткое описание действий flush basic_ostream< > & flush(); Очищает буфер выходного потока (переносит данные в файл или канал вывода) put basic_ostream< > & put(char_type с); Помещает заданный параметром символ в выходной поток seekp basic_ostream<charT, traits>& seekp(pos_type); Устанавливает указатель записи выходного потока на абсолютную позицию, заданную параметром basic_ostream< > & seekpfoffjtype, iosjbase: :seekdir); Перемещает указатель текущей позиции выходного потока на число символов, заданное первым параметром. Второй параметр задает точку отсчета (iosr.beg - начало потока; ios::cur — текущая позиция потока; ios::end — конец потока) tellp posjtype tellpf); Возвращает текущую позицию указателя записи выходного потока write basic_ostream< >& writefconst charjtype* s, streamsize n); Помещает в выходной поток п символов из массива, на который указывает s. Нуль-символы включаются в число переносимых символов
Приложение 8 655 Таблица П8.4 Флаги класса ios_base, управляющие форматированием ввода-вывода Константа Название skipws Игнорировать начальные пробельные символы при вводе left "Прижимать” значение к левой стороне поля right "Прижимать" значение к правой стороне поля boolalpha Выводить логические значения в символьном виде (true, false) internal Поместить разделительные символы после знака или основания системы счисления ЮСС) dec Десятичная система счисления (ОСС = 10) oct Восьмеричная система счисления (ОСС = 8) hex Шестнадцатеричная система счисления (ОСС = 16) showbase Указывать ОСС при выводе showpoint Печатать десятичную точку и следующие за ней нули при выводе вещественных чисел uppercase Шестнадцатеричные цифры печатать на верхнем регистре Таблица П8.5 Константы класса ios_base для "очистки" флагов форматирования Константа "Сбрасываемые" флаги Действие basefield ios::hex, ios::oct, На основание системы iosr.dec счисления floatfield iosr.fixed, ios:.'scientific На представление вещественных чисел adjustifield ios::left, iosr.right, На выравнивание значений ios::internal в поле вывода
656 Приложения Таблица П8.6 Манипуляторы без параметров Манипулятор Краткое описание действий dec Устанавливает десятичное основание системы счисления hex Устанавливает шестнадцатеричное основание системы счисления oct Устанавливает восьмеричное основание системы счисления ws При вводе игнорирует во входном потоке обобщенные пробельные символы end1 При выводе помещает в поток символ новой строки и очищает буфер потока ends При выводе помещает в поток символ конца строки *\0* flush Очищает буфер выходного потока Таблица П8.7 Манипуляторы с параметрами (<iomanip>) Название Краткое описание действий setbase smanip setbasefint base); Устанавливает основание системы счисления (0 — при выводе — десятичное; при вводе — внутреннее представление вводимых цифр соответствует правилам ANSI для языка Си; 8 - восьмеричное; 10 — десятичное; 16 — шестнадцатеричное) resetiosflags smanip resetiosflags(ios_base::fmtflags mask); Очищает для потока форматные флаги, используя значение параметра setiosflags smanip set i os flags (ios_base:: fm tfla gs mask); Устанавливает для потока форматные флаги, используя значение параметра
Приложение 8 657 Название Краткое описание действий setfill smanip setfill(char_type с); Устанавливает символ-заполнитель setprecision smanip setprecision(int п); Устанавливает по значению параметра точность представления вещественных чисел setw smanip setwfint п); Устанавливает по значению параметра ширину поля ввода или вывода Таблица П8.8 Методы классов ifstream, ofstream, fstream Функция Прототип и краткое описание действий open void openfconst char* s, ios_base::openmode mode = ios_base::in); Открывает файл с буфером, на который указывает первый параметр. Второй параметр определяет режим использования файла, его значение по умолчанию: для входного потока ios_base::in, для выходного ios_base::out, для двунаправленного ios_base::in \ ios_base::out rdbuf basicjstreambuf<charT,traits>* rdbuf() const; Возвращает указатель на буфер, связанный с потоком 42-2762 Продолжение
658 Приложения Таблица П8.9 Режимы файла, устанавливаемые параметром mode в функции ореп() Обозначение Значение Краткое описание действия ios:: in 0x01 Открыть для чтения (режим по умолчанию устанавливается для потоков класса ifstream) ios:: out 0x02 Открыть для записи (режим по умолчанию устанавливается для потоков класса oifstream) ios:: ate 0x04 Открыть для записи в конец файла. Если файл не существует — создать его iosr.app 0x08 Открыть в режиме дополнения iosr.trunc 0x10 Открыть, уничтожив содержимое файла (устанавливается по умолчанию, если установлен режим out, либо один из режимов ate или арр) iosr.nocreate 0x20 Открыть существующий файл, если файла не существует — установить состояние ошибки ios:: noreplace 0x40 Создать и открыть не существующий файл. Если файл существует — установить состояние ошибки ios:: binary 0x80 Открыть для двоичного обмена
659 Приложение 9 Комплексные числа в Си++ В стандартную библиотеку входит шаблон классов для представления комплексных чисел: template <tyрепате Т> class complex; Параметр шаблона Т задает тип и действительной и мнимой частей комплексного числа. Шаблон классов complexO становится доступным в программе после подключения заголовка <complex>. Определены операции-функции, необходимые для работы с комплексными числами, а именно: • арифметические операции (+, * и т.д.); • операции сравнения (==, !=); • операции присваивания (=, += и т.д.); • потоковые операции записи << и чтения ». При выводе (<<) форма представления: {вещественная_часть, мнимаячасть) При чтении (») во входном потоке допустимы три формы: (вещественная_часть, мнимая часть); {вещественная часть) — мнимая часть равна 0; вещественная_часть — мнимая часть равна 0. Таблица П9.1 Функции для объектов шаблонного класса complexO Функция Прототип и краткое описание действий abs Т absfcomplex О z); Возвращает модуль комплексного числа z arg Т argfcomplexО z); Возвращает главное значение аргумента комплексного числаz (—л < arg z < л) conj Т conj (complexO z); Возвращает комплексносопряженное к комплексному числуZ 42*
660 Приложения Функция Прототип и краткое описание действий cos complexO cos (complexO z); Возвращает значение косинуса комплексного числа z cosh complex О cosh (complex< > z); Возвращает значение гиперболического косинуса комплексного числа z exp complexО expfcomplexO z); Возвращает значение функции е2 комплексного числа z imag Т imagfcomplexO z); Возвращает мнимую часть комплексного числа z log complex О logfcomplex О z); Возвращает значение натурального логарифма комплексного числа z Iog10 complexO log 10(complexO z); Возвращает значение десятичного логарифма комплексного числаz norm Т norm(complex< > z); Возвращает квадрат модуля комплексного числа z pow complexO powfcomplexO х, complexO у); Возвращает значение ху комплексных чисел х и у polar complex О polar(Т mag, Т angle); Возвращает комплексное число, имеющее модуль mag и значение аргумента angle real Т realfcomplex О z); Возвращает вещественную часть комплексного числа z sin complexO slnfcomplexO z); Возвращает значение синуса комплексного числа z sinh complexO sinhfcomplexO z); Возвращает значение гиперболического синуса комплексного числаz Продолжение
Приложение 9 661 Функция Прототип и краткое описание действий sqrt complexO sqrt(complex<> z); Возвращает одно из значений квадратного корня из комплексного числа z по формуле Г \ \ ( arg* • • arg*"\ Vz = zN cos—g—+ f-sin----- l 2 2 ) tan complexO tanfcomplexO z); Возвращает значение тангенса комплексного числа z tanh complexO tanhfcomplexO z); Возвращает значение гиперболического тангенса комплексного числа z Определены следующие специализаций шаблона классов complexO: template < > class complex<float>; template < > class complex<double>; template < > class complex<long double>; В этих специализациях определены методы real() и imag(), которые можно использовать наряду с функциями, приведенными в таблице. Продолжение
662 Приложение 10 Свободно распространяемый компилятор DJGPP В настоящее время на рынке программных продуктов компиляторы с языка Си++ представлены достаточно широко и разнообразно. Поэтому для изучения языка Си++ и его библиотеки можно использовать тот компилятор, ту операционную систему и тот тип компьютера, которые доступны. Однако не стоит забывать некоммерческие, свободно распространяемые 32-разрядные компиляторы, работающие на Intel-совместимых процессорах (начиная с 80386). Ориентация на свободно распространяемые некоммерческие компиляторы заслуживает некоторых пояснений. Система свободно распространяемых программных средств появилась и существует в противовес коммерческим программным продуктам, использование которых в благополучном обществе предусматривает покупку каждой копии и соблюдение лицензионных соглашений. Применение свободно распространяемых программных средств требует, чтобы при их распространении: • указывалось авторство программы; • не взималась плата за программу при ее распространении (могут оплачиваться затраты на тиражирование и транспортировку); • в передаваемую копию было включено лицензионное соглашение, действующее для конкретной программы; • автору программ не предъявлялись претензии при потерях или искажениях данных (информации) в процессе использования программ. Для учебных целей соблюдение перечисленных требований не создает никаких трудностей. Для программирования на языках Си и Си++ на компьютерах с 1теГовской архитектурой удобен свободно распространяемый 32-разрядный компилятор DJGPP (см. http://www.gnu.org/ software/gcc/). Его название представляет собой аббревиатуру, первые буквы которой DJ являются инициалами автора — инициатора проекта D.J. Delorie. (DJGPP - DJis GNU Programming Platform). Указанный программный продукт существует в двух
модификаПриложение 10 663 циях - для работы под управлением операционной системы Unix и для работы под управлением операционной системы MS Windows в сеансе MS-DOS. Из модулей DJGPP в первую очередь наиболее интересны и важны: • препроцессор; • компилятор языка Си (gcc); • компилятор языка C++ (gxx, gpp); • ассемблер; • редактор связей (компоновщик). Достоинством DJGPP по сравнению, например, с интегрированной средой разработки Microsoft Visual Studio.NET 2005 является простота и прозрачность подготовки исполняемых модулей для небольших программ. В нем нет таких понятий, как "решение" и "проект" (см. Приложение 1 и руководства [23, 27]). Пакет DJGPP при обработке программы на языке Си++ обычно выполняет: препроцессорную обработку, собственно компиляцию, ассемблирование и компоновку. Следует отметить, что DJGPP вначале разрабатывался для операционной системы Unix, поэтому синтаксис его команд очень похож на синтаксис команд, принятый в Unix [25]. Для работы с DJGPP необходимо знать принятые в нем соглашения об обозначениях файлов и не забывать о правильном использовании больших и малых букв (языки Си и Си++, а также команды системы Unix чувствительны к регистру). Список принятых в DJGPP расширений имен файлов: .С (строчная латинская буква) — текст на стандартном языке Си (требуется обработка препроцессором, потом компилятором); .h — заголовочный файл (текстовый); ./ - текстовый файл с программой на Си, уже обработанной препроцессором и требующей обработки компилятором (единица трансляции); .S - текстовый файл на языке ассемблера (направляется на ассемблирование, минуя при этом все предыдущие этапы); .S (прописная буква) - ассемблерный файл, требующий перед компиляцией обработки препроцессором (может включать директивы #include, #define); .сс, .схх, .срр, .C++, .C++ — программа (исходный текст) на Си++;
664 Приложения .// — текстовый файл с программой на Си++, уже обработанной препроцессором и требующей обработки компилятором (единица трансляции); .о — объектный файл, сформированный после компиляции и ассемблирования; .а — библиотечный файл (статическая объектная библиотека). Команды для работы с компилятором. Для обращения к компилятору Си++ альтернативно используют имена дхх и дрр. Как принято в системе Unix, в командах большую роль играют ключи (опции). Каждый ключ вводится символом (дефис). Основными из них для наших целей являются команды: -v - сообщить версию и дату создания компилятора или другого вызываемого компонента; --help - выдать справочную информацию о компоненте (обратите внимание на два дефиса); -Е - выполнить только препроцессорную обработку (не компилировать) — создать модуль с именем, имеющим расширение ./ (для Си-программ) или .// (для программ на Си++); -S - выполнить только компиляцию и не ассемблировать полученный модуль с именем, имеющим расширение .s; -с - выполнить компиляцию и ассемблирование (не выполнять компоновку); полученный объектный модуль должен иметь имя с расширением .о; -о имя_файла - поместить результат обработки в файл с заданным после ключа именем. Итак, чтобы получить текст с результатом препроцессорной обработки (единицу трансляции) программы на языке Си++, используется команда >дрр -Е -о файл.Н С++-файл.срр Основная команда, набираемая в командной строке для компиляции файла с текстом программы на языке Си++: >дрр имя_С++-файла.срр -о имя_исполнимого_файла.ехе Сокращенный вариант этой команды: >дрр имя_С++-файла.срр В этом сокращенном варианте создается исполнимый модуль транслируемой программы, всегда имеющий одно и то же имя: а.ехе.
Приложение 10 665 Если программа состоит из нескольких текстовых файлов, то каждый файл можно транслировать отдельно, а затем объединять объектные модули в исполнимый модуль с помощью такой последовательности команд (пример для трех файлов): >дрр -с С++-файл_1 .срр >дрр -с С++-файл_2.срр >дрр -с С++-файл_3.срр >дрр -о файл.ехе С++-файл_1.о С++-файл_2.о С++-файл_3.о. Инсталляция DJGPP. Дистрибутивы DJGPP размещены на многих FTP-серверах. В архивированном виде посетителю предлагается либо версия для Unix, либо версия для Windows XX, работающая как приложение в сеансе MS-DOS. Кратко рассмотрим процесс установки компилятора в системе MS Windows. После получения архивированного файла (или при наличии компакт-диска с копией такого файла) создайте каталог C:\DJGPP (или с другим размещением, например, D:\COMPILERS\DJGPP). Разархивируйте в этот каталог файлы DJGPP. Дальнейшие действия зависят от вашей операционной системы. В старых версиях Windows добавьте в autoexec.bat строку: SET DJGPP=C: \ DJGPP\DJGPP. ENV (или строку SET DJGPP=D:\COMPILERS\DJGPP\DJGPP. ENV, если выбрано размещение файлов компилятора из второго примера). В переменную пути (PATH) добавьте название каталога с исполнимыми модулями компилятора: C:\DJGPP\BIN (или D:\COMPILERS\DJGPP\BIN для второго примера). При работе в системе Windows ХР для установки переменных среды необходимо проделать следующую последовательность действий. «Пуск» -> «Настройка» -> «Панель управления» -> «Система» В появившемся окне «Свойства системы» гыбираем: «дополнительно» -> «переменные среды»
666 Приложения В окне «Переменные среды пользователя для имя пользователям определяем (нажав кнопку «Создать») переменную DJGPP со значением, например, C:\DJGPP\DJGPP.ENV. В окне «Системные переменные» изменяем (нажав кнопку «Изменить») значение переменной Path, добавляя в ее значение после точки с запятой, например, путь C:\djgpp\bin. Чтобы закончить установку переменных среды, нажмите кнопку «ОК». Теперь перезагрузите компьютер (для Windows ХР перезагрузка не нужна) и можете проверить правильность установки и версию компилятора командой: >gxx -v или >дрр -v В окне появится, например, такая информация: Reading specs from c:/djgpp/lib/gcc-lib/djgpp/2.953/specs gcc version 2.95.3 20010315/djgpp (release) Если это так, то можно приступать к обработке программ, используя команды приведенных выше форматов.
667 Библиографический список 1. Страуструп Б. Язык программирования C++. - 3-е изд./Б. Стра- уструп; пер. с англ. -СПб. — М.: Невский Диалект — БИНОМ, 1999.- 991 с. 2. Страуструп Б. Дизайн и эволюция языка C++/ Б. Страуструп; пер. с англ. - М.: ДМК Пресс, 2000. - 488 с. 3. Эллис М. Справочное руководство по языку программирования C++ с комментариями. Проект стандарта ANSI/ М. Эллис, Б. Страуструп; пер. с англ. — М.: Мир, 1992.- 445 с. 4. Programming Languages - C++. International Standard. ISO/IEC 14882. Second Edition, 2003-10-15: ISO/IEC/ANSI/ITI, 2003. - 758 p. 5. Джосьютис H. C++. Стандартная библиотека. Для профессиона- лов/Н. Джосьютис; пер. с англ. — СПб.: Питер, 2004. — 730 с. 6. Мейерс С. Эффективное использование C++: 50 рекомендаций по улучшению ваших программ и проектов/С. Мейерс; пер. с англ. - М.: ДМК, 2000. - 240 с. 7. Мейерс С. Наиболее эффективное использование C++: 35 новых рекомендаций по улучшению ваших программ и проектов /С. Мейерс; пер. с англ. - М.: ДМК Пресс, 2000. - 304 с. 8. Мейерс С. Эффективное использование C++: 55 верных способов улучшить структуру и код ваших программ /С. Мейерс; пер. с англ. - М.: ДМК Пресс, 2006. - 300 с. 9. Саттер Г. Решение сложных задач на C++: 87 головоломных примеров с решениями. Т.4 / Г. Саттер; пер. с англ. — М.: Издательский дом «Вильямс», 2002. - 400 с. (Серия C++ In-Depth). 10. Керниган Б. Практика программирования/ Керниган Б., Пайк Р.; пер. с англ. — СПб.: Невский Диалект, 2001. - 381 с. 11. Аммерааль Л. STL для программистов на Си++/Л. Аммерааль. - М.: ДМК, 1999.- 240 с. 12. Элджер Дж. C++: библиотека программиста / Дж. Элджер. — СПб.: ЗАО "Питер", 1999. - 320 с. 13. Лафоре Р. Объектно-ориентированное программирование в C++: Классика Computer Science. - 4-е изд./Р. Лафоре. - СПб.: Питер, 2003. - 928 с.
668 Библиографический список 14. ВандервудД. Шаблоны C++. Справочник разработчика/Д. Ван- дервуд, Н.М. Джосаттис; пер. с англ. — М.: Издательский дом «Вильямс», 2003. - 544 с. 15. Романов Е.Л. Практикум по программированию на языке C++: учеб, пособие /Е.Л. Романов. - СПб.: БХВ-Петербург; Новосибирск: Изд-во НГГУ, 2004. - 432 с. 16. Липпман С. Основы программирования на C++: Т. 1/С. Лип- пман; пер. с англ. — М.: Издательский дом «Вильямс», 2002. — 256 с. (Серия C++ In-Depth). 17. Кёниг Э. Эффективное программирование на C++: Практическое программирование на примерах. Т. 2/Э. Кёниг, Б. Му; пер. с англ. — М.: Издательский дом «Вильямс», 2002. — 384 с. (Серия C++ In-Depth). 18. Александреску А. Современное проектирование на C++: Обобщенное программирование и прикладные шаблоны проектирования. — Т. 3/А. Александреску; пер. с англ. — М.: Издательский дом «Вильямс», 2002. - 336 с. (Серия C++ In-Depth). 19. Бадд Т. Объектно-ориентированное программирование в действии/Т. Бадд; пер. с англ. - СПб.: Питер, 1997. — 464 с. 20. Буч Г. Объектно-ориентированное проектирование с примерами применения /Г. Буч; пер. с англ. — Киев: Диалектика; М.: АО ИВК, 1992.- 519 с. 21. Павловская Т.А. C/C++. Программирование на языке высокого уровня/Т. А. Павловская. — СПб.: Питер, 2006. — 464 с. 22. Давыдов В.Г. Технологии программирования C++ /В.Г. Давыдов. - СПб.: БХВ-Петербург, 2005. - 672 с. 23. STL - стандартная библиотека шаблонов С++/П. Плаугер, А. Степанов, М. Ли, Д. Массер; пер. с англ. — СПб.: БХВ-Петербург, 2004. - 656 с. 24. Шеферд Д. Программирование на Microsoft Visual C++.Net/ Д. Шеферд; пер. с англ. - М. : Издательско-торговый дом «Русская редакция», 2003. - 928 с. 25. Подбельский В.В. Программирование на языке Си: учеб, пособие / В. В. Подбельский — М.: Финансы и статистика, 2003. — 600 с. 26. Мейерс С. Эффективное использование STL: Библиотека программиста/С. Мейерс. - СПб.: Питер, 2002. — 224 с. 27. Рихтер Дж. Программирование на платформе Microsoft.NET Framework/Дж. Рихтер; пер. с англ. - М.: Издательско-торговый дом «Русская редакция», 2002. — 512 с.
Библиографический список 669 28. Керниган Б. Язык программирования Си/Б. Керниган, Д. Ритчи; пер. с англ. - М.: Финансы и статистика, 1992. - 272 с. 29. Эккель Б. — Философия C++. Введение в стандартный C++. — 2-е изд.; пер. с англ. — СПб.: Питер, 2004. — 572 с. 30. Саттер Г. Новые сложные задачи С++/Г. Саттер; пер. с англ. - М.: Издательский дом «Вильямс», 2005. - 272 с. 31. ХортонА. Visual С++2005: базовый курс/А. Хортон; пер. с англ. — М.: Диалектика, 2007. - 1152 с. Ресурсы Интернета* http://www.reserch.att.com/~bs/ http://www.boost.org * «Нет ничего более изменчивого, чем Wfeb, и более мучительного, чем давать ссылки на Wfeb в печатной книге: зачастую эти ссылки устаревают еще до того, как книга попадет в типографию». Г. Саттер [30].
670 Указатель символов , операция «запятая» (следования) 56 ; разделитель 64 ! операция логического отрицания 43 != операция сравнения на неравенство 51 # препроцессорная операция замещения параметра макроса, разделитель для препроцессорной директивы 255, 66 ## препроцессорная операция конкатенации лексем в макросе 255 % операция получения остатка целочисленного деления 49 %= операция получения остатка, совмещенная с присваиванием 53 & операция получения адреса 43 поразрядная операция И, 50 определение ссылки 66 && операция И (логическая) 51 &= поразрядная операция И, совмещенная с присваиванием 53 () операция «вызов функции» 57 описание функции, 60 преобразование типов 61 * операция разыменования, 43 операция умножения, 49 определение указателя 132 *= операция умножения, совмещенная с присваиванием 53 + операция сложения, 49 операция «унарный плюс» 43 ++ операция инкремента 43 += операция сложения, совмещенная с присваиванием 53 — операция вычитания, 49 операция «унарный минус» 43 — операция декремента 43 —= операция вычитания, совмещенная с присваиванием 53 —> операция доступа к компонентам объекта 55 —>*операция разыменования указателя на компонент класса 55 операция доступа к компоненту класса по имени объекта 55 .* операция разыменования указателя на компонент класса 55 ... многоточие 65 / операция деления 49 /* комментарий (начало) 26 */ комментарий (конец) 26 // комментарий (однострочный) 26 /= операция деления, совмещенная с присваиванием 53 : признак поля, 276 спецификатор метки 64 :: операция области видимости, 56
Указатель символов 671 операция пространства имен 56 описание указателя на компонент класса 308 < операция «меньше, чем» 51 << операция «сдвиг влево» 49 <<= операция «сдвиг влево», совмещенная с присваиванием 53 <= операция «меньше или равно» 51 = операция присваивания 53 = 0 «чистый спецификатор» 456 == операция сравнения на равенство 51 > операция «больше, чем» 51 >= операция «больше или равно» 51 » операция «сдвиг вправо» 49 »= операция «сдвиг вправо», совмещенная с присваиванием 53 ?: операция условная (тернарная операция)58 [] операция индексации, 57 описание массива 60 \ обратный слэш (литера) 39 А операция «симметрическая разность» (исключающее ИЛИ) 50 А= операция «симметрическая разность», совмещенная с присваиванием 53 _ подчеркивание (литера) 26 {} блок (составной оператор),61 определение класса, структуры, перечисления,62 список инициализаторов 63 | поразрядная операция ИЛИ 50 |= поразрядная операция ИЛИ, совмещенная с присваиванием 53 || логическая операция ИЛИ 51 ~ операция побитового отрицания 43 0 нуль 43 нулевой указатель 141 \0 нулевая литера - терминальный символ 41
672 Предметный указатель А Адрес 131 Адрес, см. Операция получения адреса Абстрактный класс 455, 462 Абстрактный тип 2, 282 Адаптер 533 Алгоритм обобщенный 510, 513, 533,566 - STL 554, 638 Аргумент - функции 181 - шаблона 472, 474, 501 Арифметические операции 41,48,59 Арифметический тип 114 Арифметическое выражение 114 Арифметическое преобразование, см. Операция преобразования Б Базовый класс 402 абстрактный 455 виртуальный, см. Виртуальный базовый класс непосредственный, см. Непосредственный базовый класс непрямой, см. Непрямой базовый класс прямой, см. Непосредственный базовый класс Базовый тип 137 Библиотека - STL 509 - ввода-вывода 568, 650 - стандартная 509, 565, 567 - функций языка Си 632 Бинарные операции 48 Битовое поле 275 безымянное 278 объединения 278 структуры 276 - - STL 537 Блок 61, 76, 81 - и составной оператор 61 - контроля за исключениями 384 Буфер потока 568, см. также Поток В Ввод, см. Операция ввода, см. также Поток ввода, Потоковый ввод-вывод Ввод-вывод в Си++ 568 Вектор 517, 531 Вещественная константа, см. Константа вещественная Видимость, см. Область видимости Виртуальная функция 443, 453 чистая 456 Виртуальный базовый класс 415 Включение классов 398 Вложение блоков - контролируемых блоков 393 Внешнее связывание, см. Связывание внешнее
Предметный указатель 673 Внешние (глобальные) переменные 307 Внутреннее связывание, см. Связывание внутреннее Возвращаемое функцией значение 179, 237 Восьмеричная константа, см. Константа восьмеричная Встраиваемая функция, см. Подставляемые функции Вывод, см. Операция вывода, см. также Поток вывода Вывод русских букв 619 Вызов — деструктора 299 — конструктора 298 — функции 15, 57, 181 Выражение 42, 101 — арифметическое 114 — леводопустимое, см. Леводопустимое выражение 52, 73 — первичное 102 — постфиксное 104 — праводопустимое 52 -префиксное 104 Выражение-условие 120, 122 Вычитание, см. Операция вычитания Г Генерация исключений 384 Глобальная переменная 47 Границы массива 490 Д Данные — класса, см. Компонентные данные (поля данных) класса — тип, см. Тип данных Декремент, см. Операция декремента — указателей 146 Деление, см. Операция деления Деструктор 200 — базового класса 452 — виртуальный 452 — при наследовании 428, 450 Десятичная константа, см. Константа десятичная Диагностика 565 Динамический тип указателя 443 Динамическое (позднее) связывание 455 Директива препроцессора 240 -# 241 - #define 240, 241, 249 - #elif 240, 246 - #else 240, 246 - #endif 240, 246 - #еггог 241, 356 - #if 240, 246 - #ifdef 240, 246 - #ifndef 240, 246 - #include 25, 183, 245 -#line 241, 255 - #pragma 241, 256 - #undef 240, 244 Доступ - к компонентам базового класса из производного класса 412 — к методам класса 285, 286 — к полям объекта 285, 286, 304 — к статическим компонентам класса 288, 305 Друзья класса 320 43-2762
674 Предметный указатель Дружественная функция 321, 323 Дружественный класс 325, 523 ЗЗаголовок переключателя, см. Оператор switch -функции 178 - цикла, см. Цикл Заголовки стандартной библиотеки 566 Заголовочный файл 370 Закрытие файла 588 Зарезервированное (ключевое) слово, см. Служебное слово Знаки операций 42, 59 Значение, возвращаемое функцией 179 - леводопустимое (/-значение) 52, 73 - параметра по умолчанию 178 И Идентификатор 27 - область действия, см. Область действия идентификатора - препроцессорный 241 Иерархия классов 412 - библиотеки ввода-вывода 571 - виртуальных 415 Именование - квалифицированное 102, 103 - неквалифицированное 102, 103 -операции-функции 103 - функции преобразования 103 - деструктора 103 - шаблона 103 Имя - деструктора 298 - квалифицированное 263, 285, 312 - класса 283 - конструктора 290 - массива 154, 156 - объединения 272 - структуры 263 - указателя 88 - уточненное 263, 266, 272, 285 - функции 178 Индекс массива 154 Индексация, см. также Операция [] Инициализатор 95, 287 - конструктора 291, 426 - поля данных 291 - цикла for 121 Инициализация 95 - константы 100 -массива 40, 152, 156 многомерного 163 - объекта 290 - переменной 95 - ссылки 223 на константу 227 - статических полей класса 287 - структуры 263 - указателя 132 на функцию 210 Инкапсуляция 293 Инкремент, см. Операция инкремента - указателя 146 Инстанцирование шаблона 469, 481
Предметный указатель 675 Интегрированная среда разработки 597 Интерфейс класса 369 Исключение 383 - в конструкторе 396 - обработка, см. Обработка исключений - обработчик, см. Обработчик исключений - порождение, см. Генерация исключений Итератор 515, 518, 533, 547, 565 - входной 548 - выходной 548 -двунаправленный 548, 549 - обратный 548 - произвольного доступа 548 - прямой 529 К Квалификатор 56 const 100 volatile 100 Класс 282, 284 - complex 659 -filebuf 571 - fstream 588 - ifstream 573, 588 - ios 570 - iostream 572, 574 - istream 572, 574 - ofstream 573, 588 - ostream 572, 577 - string 328 - абстрактный 455, 462 - базовый, см. Базовый класс - данные, см. Компонентные данные, поля данных — дружественный, см. Дружественный класс — локальный, см. Локальный класс — метод, см. Компонентная функция класса — наследник, см. Класс производный — определение, см. Определение класса — памяти 75 auto 75 register 75 static 76 extern 76 — порождающий, см. Базовый класс — порожденный, см. Производный класс — потомок, см. Производный класс — производный, см. Производный класс Ключ класса 283 Ключевое слово, см. Служебное слово Кодировка -ASCII 612 — OEM (Кодовая таблица MS- DOS) 614 — 1251 (Кодовая таблица MS Windows) 616 Команда препроцессора, см. Директива препроцессора Комментарий /* */ 26 — // 20, 26 Компонент класса, см. Методы и поля данных класса Компонентная функция (метод) класса 283 43*
676 Предметный указатель статическая 304 Компонентные данные (поля) класса 303 защищенные, см. также Модификатор protected нестатические 303 общедоступные, см. также Модификатор public открытые, см. общедоступные собственные, см. также Модификатор private статические 304 Компоновка, см. Связывание Конкатенация 334 Консольный ввод-вывод строк 350 Консольное приложение 597 Консольный поток, см. Поток консольный Константа 98 - булевская 34 - восьмеричная 29 - вещественная 32 - десятичная 29 - литерал 28 - многосимвольная (мультисим- вольная)35, 37 -перечислимая 31 - предельного значения 610 - с плавающей точкой, см. Константа вещественная - символьная 34 - строковая 38 на нескольких строках 39 - целая 29 - шестнадцатеричная 29 Конструктор класса 290, 297 абстрактного 456 при наследовании 424, 426 - копирования 295, 297 - общего вида 295, 297 - приведения типов 295, 365 - умолчания 295, 296 Контейнеры 515, 533, 565 - ассоциативные 535 - последовательные 531 - STL 510, 533, 537 Контролируемый блок 384 Копирование объектов глубокое 376 поразрядное (поверхностное) 376 Л Леводопустимое выражение, см. также /-значение 52, 73 Лексема 18, 26 Литерал, см. Константа литерал Ловушка исключений, см. Обработчик исключений Логическая операция, см. Операция логическое И (ИЛИ, НЕ) Локализация 565 Локальный класс 417, 519 М Макроимя предопределенное 257 -- DATE 257 -- FILE 257 --„LINE 257 -- STDC 257 TIME 257 Макроопределение 249, см. также Директива препроцессора #define - va_arg() 186
Предметный указатель 677 — va_end() 186 — va_start() 186 Макрос 249, см. также Макроопределение Манипулятор 580 — без параметров 581, 656 --dec 581 endl 581 ends 581 --Hush 581 hex 581 oct 581 ws 581 — с параметрами 582, 656 resetiosflagsO 582 setbase() 582 — - setfillO 582 setiosflags() 582 setprecision() 582 setw() 582 Массив 40 — динамический 157, 173, 207 — и указатель 150 — имя, см. Имя массива — индекс, см. Индекс массива — инициализация, см. Инициализация массива — многомерный 162, 203 — объединений 272, 275 — объектов классов 296 шаблонных классов 505 — описание, см. Описание массива — определение, см. Определение массива — параметр 199, 202 — символьный 150 — структур 262 — указателей 167, 204, 462 на строки 169 на функции 213 Метка 65 — case в переключателе 116 — default в переключателе 116 Метод класса 283 при наследовании 419 шаблонного 484 — контейнерного 518 — контейнера STL 537, 543, 546 — получения итератора 542 string 333, 622 Минус, см. Операция минус Многомерный массив, см. Массив многомерный Множественное наследование 413 Множество STL 535 Модификатор, см. также Служебное слово — const 100, 134 — friend 321 — private 293, 413, см. также Компонент класса закрытый — protected 293, 413, см. также Компонент класса защищенный — public 293, 413, см. также Компонент класса открытый -volatile 100 Мультимножество STL 536 Мультиотображение STL 536 Н Наследование 401, 433 — доступа к компоненту класса 405, 407 — закрытое 413 — защищенное 413
678 Предметный указатель - иерархия классов 413 - множественное 413 - открытое 413 Начальные значения параметров, см. Значение параметра по умолчанию Непосредственный базовый класс 412 Непрямой базовый класс 412 Нулевой указатель 141 О Область видимости 79 - действия 76 идентификатора 76 объекта 79, 81 Обобщенное программирование 550 Обобщенный алгоритм, см. Алгоритм обобщенный Обработка исключений 384 Обработчик исключений 385, 389 Обращение к функции, см. Вызов функции 181 - к шаблонной функции 474 - к элементу массива 154 Объединение 270, 272 Объединяющий тип 272, 281 Объект - класса 284 шаблонного 482, 501 - функциональный 551 Объектно-ориентированное программирование 7 Оператор, см. также Служебное слово -break 117, 126 - continue 130 -do 118, 120 -for 118, 121 - goto 121 -if 113 - return 126, 179 - switch 113, 115, 117 - throw 384, 389, 393, 395 -while 118, 119 - безусловного перехода, см. Оператор goto - возврата из функции, см. Оператор return - выбора, см. Метка case в переключателе 116 - переключатель, см. Оператор switch - передачи управления 124 - присваивания, см. Операция присваивания - пустой 64 - составной, см. Составной оператор - условный, см. Оператор if - цикла, см. также Цикл Операция -# 255 - ## 255 -() 57 -[] 57 - const_cast 42, 46 - defined 255 - delete 42 - dynamic_cast 42, 45 - new 48 и массив 157 - reinterpret__cast 42, 45 - sizeof 153
Предметный указатель 679 — static_cast 42, 45 — typeid 44 — аддитивная 49 — бинарная 48 — больше или равно, чем (>=) 51 — больше, чем (>) 51 — ввода (») 575 — выбора компонентов объекта (->,.) 55 — вывода («) 14, 575 — вычитания (-) 49 — декремента (—) 100 — деления (/) 49 — запятая (,) 56 — индексации, см. Операция [] — инкремента (++) 43, 100 — итератора 525 — логическое И (&&) 51 --ИЛИ (||) 51 НЕ (!) 43 — меньше или равно (<=) 51 — меньше, чем (<) 51 — минус унарный (-) 43 — мультипликативная 49 — над строками 330 — над указателями 142 — не равно (!=) 51 — отношения 51 — плюс унарный (+) 43 — получения адреса (&) 43, 132, 137 — получения остатка (%) 49 — поразрядная И 50 — - ИЛИ 50 исключающее ИЛИ 50 — - НЕ 50 — преобразования типов 104 const_cast 42, 106 dynamic_cast 42, 106 reinterpret_cast 42, 106 static_cast 42, 106 - препроцессорная 255 - - # 255 - - ## 255 defined 255 - префиксная 43 - приведения, см. Операция преобразования - приоритет, см. Приоритет операций - присваивания (=) 52 - разыменования (*) 43, 139 указателей на компоненты классов (.*, ->*) 55 - с итераторами 548 - сравнение на неравенство (!=) 51 - сравнения на равенство (==) 51 - сдвига влево («) 49 - сдвига вправо (») 49 - сложения (+) 49 - указания области видимости (::) 56 - умножения (*) 49 - унарная 43 - условная (?:) 58 - функция 357, 361 - явного (приведения) типов 45,431 Описание 94 - внешних массивов 151 объектов 81 ссыпок 229
680 Предметный указатель - и определение, см. Определение и описание - массива 151 - методов класса 311 - переменной 95 - ссылки 229 - функции (прототип) 177, 180 с переменным числом параметров 185 чисто виртуальной 456 - шаблона функций см. прототип шаблона функций Определение 94 - и описание 95 - класса 79 производного 406 - массива 151 - метода класса вне класса 311 - многомерного 162 - объединения 272 - объекта 284 - переменной 95 - перечисления 31, 67 - пространства имен 17, 19, 178 - структуры 261, 264 - ссылки 223 на функцию 230 - указателя 192 на объект 131 на функцию 210 - функции 178 виртуальной 456 компонентной (метода класса) 311 с переменным числом параметров 185 - шаблона 467 классов 480 функций 467 Отношения, см. Операция отношения Очередь STL 536 П Память, выделение автоматическое, см. Класс памяти auto (register) динамическое, см. Операция new явное, см. Операция new - локальная, см. Класс памяти auto - регистровая, см. Класс памяти register Параметр - функции 178 - шаблона 467, 471 типизирующий 467 Параметр-массив 198, 203 Параметр-ссылка 229 Параметр-указатель 229 Перегрузка 357 - конструкторов 295 - операции 356, 362 для ввода-вывода 359, 575 ++ 366 366 - - [] 364 - при наследовании 429 - функций 235 - шаблонных функций 478 Передача параметров (аргументов) - по значению 181 - по ссылке 229 Переключатель, см. Оператор switch Переменная 224
Предметный указатель 681 - автоматическая, см. также Класс памяти auto — глобальная, см. Глобальная переменная — индексированная, см. также Индексация Перечислимая константа, см. Константа перечисления Перечислимый тип 71 Подмена метода, см. Виртуальная функция Подставляемые функции 196, 311 Позднее связывание 455 Поле битовое, см. Битовое поле Поле (данных) класса 283, 303 статическое 287, 304 Полиморфизм 443 Поразрядные операции, см. Операция поразрядное (И, ИЛИ НЕ) Порождение исключений 384 Порядок вызова деструкторов 428 конструкторов 425 Постфиксная операция 43, 363 Последовательность (в контейнере STL) 511 Поток 568 — ввода, см. также Стандартный поток ввода - входной, см. Поток ввода — вывода 14, 352, см. также Стандартный поток вывода - выходной, см. Поток вывода — консольный 350 - стандартный, см. Стандартный поток - файловый 351, 588, 592 Потоковый ввод-вывод 568 Преобразование типов, см. Операция преобразования — ссылок 107 — указателей 107 Препроцессор 239 — команды, см. Директивы препроцессора Препроцессорная обработка 239 Префиксная операция 43, 363 Приведение типов, см. Операция преобразования необратимость ПО обратимость ПО операндов в арифметических выражениях 108 ссылок 107 стандартное, см. Операция преобразования типов указателей 107 Принцип подстановки 433 Приоритеты операций 59 Присваивание, см. Операция присваивания — контейнеров STL 541 Программирование — объектно-ориентированное 7 обобщенное 550 Продолжительность существования объектов 85 статическая 85, 86 локальная 85 динамическая 85, 87 Проект в интегрированной среде разработки 597 Производные типы 70 Производный класс 402 Пространство имен 17, 178
682 Предметный указатель - std 566 Прототип - метода класса 312 - функции 180 - шаблона классов 488 функций 475 Р Разделитель 60 {} 61 0 60 : 64 ; 64 ,63 Размер массива 153 Размер строки 334 Размещение в памяти - объединения 271, 274 - структуры 267 Разыменование указателей (обращение по указателю), см. Операция разыменования Ранги операций, см. Приоритет операций Расширение действия операции, см. Перегрузка операции Рекурсивная функция 192 Рекурсия 192 Ретрансляция исключений 393 Ресурсоёмкий объект 372, 377 С Связный список 317, 464 Связывание внешнее 51 - внутреннее 92 - отложенное, см. Динамическое связывание - позднее, см. Динамическое связывание Сдвиг вправо, см. Операция сдвига вправо — влево, см. Операция сдвига влево Сигнатура функции (метода) 178 Символ терминальный, '\0' 36, 328 '\n' 35, 36, 254 — подчеркивания, 27 Символьная константа 34 Слово зарезервированное, см. Служебное слово — ключевое, см. Служебное слово Сложение, см. Операция сложения Служебное (ключевое) слово 27, 68 asm 27 auto, см. Класс памяти auto 27 — - bool 27, 68 break, см. Оператор break 27 case, см. Метка case в переключателе 27 catch 384 char, см. Тип char 27, 68 class 260, 283 const, см. Модификатор const const_cast 42, 106 — continue, см. Оператор continue — default, см. Метка default в переключателе delete, см. Операция delete do, см. Оператор do double, см. Тип double 68 dynamic_cast 42, 106 else 113
Предметный указатель 683 enum, см. также Перечислимые константы 32, 71 explicit 328 export 27 extern, см. Класс памяти extern false 34 float, см. Тип float 27, 68 for, см. Оператор for friend, см. Модификатор friend goto, см. Оператор goto if, см. Оператор if inline, см. Спецификатор inline int, см. Тип int 27, 68 long, см. Тип long 27, 68 mutable 76 namespace 19 new, см. Операция new operator 322 private, см. Модификатор private protected, см. Модификатор protected public, см. Модификатор public register, см. Класс памяти register reinterpret_cast 42, 106 return, см. Оператор return short, см. Тип short 27, 68 signed 27, 69 sizeof, см. Операция sizeof static, см. Класс памяти static static_cast 42, 45, 106 struct 260, 283, см. также Структурный тип switch, см. Оператор switch template 467, см. также Шаблон this, см. Указатель this throw, см. Оператор throw true 34 - - try 384 typedef, см. Спецификатор typedef 27, 70 typeid, см. Операция typeid typename 467 union 260, см. также Объединяющий тип unsigned, см. Тип unsigned using 19 virtual 414, 444 void, см. Тип void 27, 68 volatile, см. Модификатор volatile wchar_t 27, 68 while, см. Оператор while Составной оператор 112 и блок 113 Специализация шаблонной функции 468 - шаблона классов 481, 493 Специальные методы класса 419 Спецификатор - inline 197 - typedef 98 - доступа 293 private 293 protected 293 public 293 -чистый 456 Спецификация - исключений 390, 393 - класса 282, 311, 313
684 Предметный указатель Список аргументов функции 181 Список инициализации, см. Инициализация - параметров шаблона 468 Сравнение, см. Операция сравнения на равенство - контейнеров STL 539 - строк 345 Ссылка 223 - на константу 225 - на объединение 273 - на структуру 266 - на функцию 230 - определение, см. Определение ссылки Стандартный поток 14 ввода cin 14, 351, 574 вывода cout 14, 351, 574 ошибок сегг 351, 574 Статические - методы класса 304 - поля класса 304 Статический тип указателя 443 Статус доступа 293, 305 Стек STL 536 Строка - в стиле Си 328 - в стиле Си++ 329, 565 - замещения 249 Строковая константа, см. Константа строковая 14 Структура 261, см. также Структурный тип Структурный тип 260, 263, 281 Сфера действия, см. Область действия Т Таблица виртуальных функций 453 Тело - функции 14, 179 - цикла 118 Тернарная (условная) операция 58 Тип 282, 284 - char 571, см. также Символьные константы - double 33, 68 - float 33, 68 - int 29, 68 - long 30, 68 - long double 33 - short 68 - signed 69 - size_type 329 - unsigned 30, 69 - void 68 - w_char 571 - базовый, см. Базовый тип - беззнаковый, см. Тип unsigned - возвращаемого значения 178 void, см. Тип void ссылка 232 - данных 33 абстрактный, см. Абстрактный тип - знаковый, см. Тип signed - массива 173 - компоновки 91 - объединяющий, см. Объединяющий тип - перечислимый, см. Перечислимый тип - преобразование, см. Преобразование типов
Предметный указатель 685 — производный, см. Производные типы — результата, см. Тип возвращаемого значения — структурный, см. Структурный тип — указателя 209 — функции 209 — чисел с плавающей точкой, см. типы float, double, long double Типизирующий параметр 467 У Указание области видимости, см. Операция указания области видимости Указатель 88 -this 315 — значение, см. Значение указателя — и массив, см. Массив и указатель — инициализация, см. Инициализация массива — константа 135 — константный, см. Указатель- константа — массив, см. Массив указателей — на компонент класса 107, 307 — на константу 135 — на массив 155 — на метод класса 307 — на объединение 272 — на объект класса 286, 310 — на поле класса 307 — на структуру 265 — на указатель 149 — на функцию 209 — нулевой, см. Нулевой указатель 132 - описание, см. Описание указателя 132 - определение, см. Определение указателя 132 - пустой, см. Нулевой указатель Умалчиваемое значение параметра 184 Умножение, см. Операция умнс жения Унарные операции 43 Управление памятью - в языке Си 87, 157 - в языке Си++ 157, 173, 269 Условная операция, см. Операция условная Условный оператор, см. Оператор if Утилиты 563 Уточнения области действия, см. Операция уточнения области действия Уточненное имя, см. Имя уточненное Ф Файл 588 - произвольный доступ 593 - режимы обменов 591 Файл заголовочный, см. Заголовочный файл Файловый поток, см. Поток файловый Фактические параметры, см. Аргументы Флаг 655 - ios::ate 590 - ios::boolalpha 575 - ios:app 590 - ios::binary 590
686 Предметный указатель - ios::dec 576 - ios::fixed 576 - ios::hex 576 - ios::in 590 - ios::internal 576 - ios::left 576 - ios::oct 576 - ios::out 590 - ios::right 576 - ios::scientific 576 - ios::showbase 576 - ios::showpoint 576 - ios::showpos 576 - ios::skipws 576 - ios::trunc 590 - ios::unitbuf 576 - ios::uppercase 576 - форматирования 575 Формальные параметры см. параметры Форматирование данных при вводе-выводе - флаг, см. Флаг форматирования Функциональный объект (функтор) 550 Функция main() 14 Функция библиотечная 632-637 - abs 468 -callocO 87 - close() 592 - exit() 195 - free() 87 -getline() 631 -mallocO 87, 157 - open() 592 - qsort() 218 - rand() 512 -sqrt() 196 -strcpy() 496 - strlen() 496 - виртуальная, см. Виртуальная функция - дружественная, см. Дружественная функция - и ссылки, см. Параметр-ссылка - имя, см. Имя функции - класса, см. Метод класса - обращение, см. Вызов функции - обмена с потоком get() 584 getline() 584 ignore() 587 put() 583 - - read() 587 seekg() 587 seekp() 584 write() 583 - операция, см. Операция-функция - описание, см. Описание функции, Прототип функции - определение, см. Определение функции - перегруженная, см. Перегрузка функций - подставляемая, см. inline- функция 196, 198 - прототип, см. Описание функции - рекурсивная, см. Рекурсивная функция - с переменным числом параметров 185
Предметный указатель 687 - самовызывающая, см. Рекурсивная функция - семейство, см. Шаблон семейства функций - сигнатура, см. Сигнатура функции - ссылка, см. Ссылка на функцию - указатель, см. Указатель на функцию ц Целая константа, см. Константа целая Целочисленный тип, см. Тип int (long, short, unsigned) Цепочка наследования 427 Цепочка подстановок 252 Цикл 118 - общего вида, см. Оператор for - параметрический, см. Оператор for 118 - с постусловием, см. Оператор do 118 - с предусловием, см. Оператор while 118 Ч Частичная специализация шаблонного класса 498 Числа комплексные в Си++ 659 Чисто виртуальная функция 455 Чистый спецификатор 456 Член класса 283 Ш Шаблон 467 - дружественной функции 486, 504 - классов 95, 481 - список параметров, см. Список параметров шаблона - функций 95, 467 определение, см. Определение шаблона функций Шаблонная функция 468, 475 Шестнадцатеричная константа, см. Константа шестнадцатеричная Э Элемент - массива 150 - структуры 261, 263 - объединения 272 Экземпляр класса, см. Объект класса Эскейп-последовательность 35, 36 Я Явная специализация шаблонной функции 478 шаблонного класса 499 Явное преобразование типа 431, см. Операция явного преобразования типов
Учебное издание Подбельский Вадим Валериевич СТАНДАРТНЫЙ СИ++ Заведующая редакцией Л. А. Табакова Ведущий редактор Л. Д. Григорьева Младший редактор Н. А. Федорова Художественный редактор Ю. И. Артюхов Технический редактор Т. С. Маринина Корректоры Т. М. Васильева, Г. В. Хлопцева Компьютерная верстка И. В. Зык Обложка художника Н. М. Биксентеева ИБ№ 5140 Подписано в печать 22.02.2008. Формат 60x88/16. Печать офсетная. Гарнитура «Таймс». Уел. п. л. 42,14. Уч.-изд. л. 38,0. Тираж 3000 экз. Заказ 2762. «С» 039 Издательство «Финансы и статистика» 101000, Москва, ул. Покровка, 7 Телефон (495) 625-35-02, факс (495) 625-09-57 E-mail: mail@fmstat.ru http://www.fmstat.ru ООО «Великолукская городская типография» 182100, Псковская область, г. Великие Луки, ул. Полиграфистов, 78/12 Тел./факс: (811-53) 3-62-95 E-mail: zakaz@veltip.ru