Текст
                    С+ +
язык
ПРОГРАММИРОВАНИЯ
"И.В.К.-СОФТ"
Москва 1991


НАУЧНО-ПРОИЗВОДСТВЕННОЕ ОБЪЕДИНЕНИЕ "И.В.К." предлагает по конкурентным ценам широкий диапазон I ВМ-совместимых персональных ЭВМ типа PC/XT, PC/AT, РС/АТ-386, PS/2, а также разнообразную периферию к ним, в том числе новейшие плоттеры и дигитайзеры производства компании Houston Instrument, предназначенные для специалистов, работающих с САПР. светокопировальные машины фирм Canon, Sharp, Sanyo. факсимильные аппараты (телефаксы). дискеты BASF 5.25м 2S/2D, 2S/HD. Все оборудование поставляется как за валюту, так и за советские рубли. Демонстрационные образцы Вы сможете увидеть на нашей постоянной экспозиции в павильоне "Профсоюзы" на ВДНХ. Наши специалисты подробно проконсультируют Вас по каждому из образцов, а также продемонстрируют их работоспособность. Получить оборудование Вы сможете на нашем складе, предварительно заключив с нами договор на взаимовыгодной основе. Наш адрес: 105023 Москва, Малая Семеновская, 5 (для писем) 107061 Москва, 2-ая Пугачевская, 10, корп. 1 ВДНХ, Павильон "Профсоюзы" - договорный отдел Телефоны: 311-52-08, 936-50-67, 135-57-41 (общие справки) 291-97-06 (договорный отдел) 181-43-39 (договорный отдел и демонстрационный зал ВДНХ) Факс: 203-93-55 Телекс: 411769 IVK SU i
НАУЧНО - ПРОИЗВОДСТВЕННАЯ ФИРМА "И.В.К. - СОФТ" предлагает СПРАВОЧНО-ДОКУМЕНТАЛЬНЫЕ КОМПЛЕКСЫ ПО MS - DOS 4.x OS/2 Turbo Professional 5.0 Foxbase + 2.10 FoxPro V 1.00 (1.01) MicroSoft C 5.0 (библиотеки функций) MicroSoft C 5.0 (оптимизирующий компилятор) MASM 5.0 DataBase ToolBox 4.0 DBASE IV R:Base System V Fastwire 11 LOTUS 1-2-3 LOTUS SYMPHONY V 2.0 MDBS III V 3.09 SPOT (программа преобразования сканированного текстового изображения в текстовые файлы (для настольных сканеров)) Microsoft WINDOWS 386 по использованию модема "SmartLink 1200S" BitCom - коммуникационному пакету программ для модема Xerox Ventura Publisher Turbo C+ + Turbo Debugger V 2.0 Turbo Assembler V 2.0 Turbo Profiler V 1.0
ПРОГРАММЫ Программы печати текста на принтерах: CITIZEN PANASONIC AMSTRAD DMP 4000 Epson FX-80, FX-100, FX-800, FX-1000,. EX-800, EX-1000 Canon A-55, Olivetti Hewlett Packard LaserJet STAR NX-15, NX-1500 ПРОГРАММЫ-СПРАВОЧНИКИ ПО: Turbo C 2.0 ZORTECH C+ + Turbo Assembler (справочное руководство) Turbo Prolog V 2.0 Periscope (многофункциональный отладчик) Clipper зит-87/Док(расш.) + Clipper Tools/Док книгам Стивенсона и Шилдта ''Техника программирования на Turbo С" "С для профессионалов" "Учебник по Turbo С" ПРОГРАММЫ Система "СЕРВИС" (оболочка операционной системы MS-DOS 3.30) Программа обучения программированию на языке Паскаль Программы печати текста на принтерах: CITIZEN PANASONIC AMSTRAD DMP 4000 Epson FX-80, FX-100, FX-800, FX-1000, EX-800, EX-1000 Canon A-55, Olivetti Hewlett Packard LaserJet STAR NX-15, NX-1500 iii
ПЕЧАТНАЯ ПРОДУКЦИЯ НПФ "И.В.К. - СОФТ" Вы можете приобрести: 1. Norton Commander версия 3.0 (для начинающего пользователя) 2. Краткий справочник по MS DOS 4.01 3. Руководство пользователя MS DOS 4.01 4. Операционная система MS DOS 4.01. Справочник программиста 5. Операционная система OS/2. Справочник программиста 6. Как работать с пакетом P-CAD 7. Работа с FoxPRO в интерактивном режиме 8. Процедуры и функции Turbo Pascal 9. Организация локальных сетей на базе персональных компьютеров 10. Язык Си для профессионалов 11. Справочное руководство по сети SFT/Advanced NetWare фирмы Novell V.2.15 Готовятся к изданию: 1. Учебники по программированию на Turbo С и Turbo Pascal 2. Краткий справочник программиста по операционной системе OS/2 3. Построение локальных NOVELL сетей 4. Справочник по локальным сетям 5. Усовершенствованная графика в Си 6. Язык С + + . Справочное руководство 7. С + + . Язык программирования 8. AutoCAD для начинающих 9. Работая с FoxPRO 10. Коммуникации и сети 11. Защита от вирусов 12. 486 микропроцессор 13. Пакет Norton Utilities 5.0 14. Серия книг по Norton Commander, Norton Utilities, Norton Guide и Т.Д. 15. Написание драйверов для персональных компьютеров 16. Совершенствование и ремонт персональных компьютеров 19. Краткие справочники по работе с Turbo С, FoxPRO, dBaselV, AutoCAD, Turbo Professional, Object Professional, C++. Наш» адрес: 105023 Москва, Малая Семеновская, 5 (для писем) 129090 Москва, ул. Щепкина, 22 iv
ПРЕДИСЛОВИЕ C++ - это язык программирования общего назначения, его предназначение - сделать работу серьезных программистов более приятным занятием. За исключением незначительных деталей, C++ - это надмножество языка программирование С. В дополнение к возможностям, предоставляемым С, C++ предоставляет гибкие и эффективные возможности определения новых типов. Программист может разделить прикладную программу на легко управляемые фрагменты, задавая новые типы, близкие по смыслу к понятиям прикладной программы. Такой способ разработки программ называется абстракцией данных. Объекты некоторых типов, определяемых пользователем, содержат информацию о типах. Подобные объекты можно удобно и надежно использовать в таких контекстах, где их тип нельзя определить во время компиляции. Программы, использующие объекты таких типов, часто называют объектно- ориеНтированными. При надлежащем использовании подобные методы дают более короткие и понятные программы, которые легче модифицировать. Ключевое понятие C++ - это класс. Класс - это тип, определяемый пользователем. Классы обеспечивают скрытие данных, гарантированную инициализацию данных, скрытую конверсию типов для типов, определяемых пользователем, динамическую типизацию, контроль памяти под управлением пользователя и механизмы перезагрузки операторов. Гораздо лучше, чем язык С, C++ обеспечивает возможности для контроля типов и для модульности. Кроме того, он содержит усовершенствования, непосредственно не связанные с типами, как-то: символьные константы, динамическая подстановка функций, аргументы функций по умолчанию, перезагружаемые имена функций, операторы управления свободной памятью и тип обращения по адресу. C++ сохраняет способность языка С эффективно работать с фундаментальными объектами аппаратуры (биты, байты, слова, адреса и т.д.). Это дает приятную легкость реализации типов, определяемых пользователем. C++ вместе со стандартными библиотеками C++ рассчитан на переносимость. Данная его реализация будет работать на большинстве вычислительных систем, поддерживающих язык С. Библиотеки языка С можно использовать из программы на C++, а большинство программных инструментов, поддерживающих программирование на языке С, можно применять и для С + +. Данная книга в первую очередь предназначена помочь серьезным программистам освоить этот язык и использовать его в нетривиальных проектах. Книга содержит полное описание C++, множество завершенных примеров и еще больше - фрагментов программ.
1 ОГЛАВЛЕНИЕ >ЕДИСЛОВИЕ ЧИТАТЕЛЮ Структура книги 5 Замечания по реализации 5 Упражнения 6 Замечания по проектированию 6 Исторические замечания 7 Эффективность и структура 9 Философские замечания 10 Размышления о программировании на С + + 11 Практические советы 12 Замечания к программирующим на языке С 13 Литература 14 ПАВА 1: ОБЗОР C+ + 16 1.1. Введение 16 1.2. Комментарии 19 1.3. Типы и декларации (объявления) 19 1.4. Выражения и операторы 21 1.5. Функции 26 1.6. Структура программы 27 1.7. Классы 29 1.8. Переопределение операторов 30 1.9. Обращения по адресу (ссылки) 31 1.10. Конструкторы 32 1.11. Векторы 33 1.12. Замена вызова функции 34 1.13. Производные классы 35 1.14. Еще об операторах 37 1.15. Дружественные функции 39 1.16. Родовые векторы 40 1.17. Полиморфные векторы 41 1.18. Виртуальные функции 42 ЛАВА 2: ДЕКЛАРАЦИИ И КОНСТАНТЫ 44 2.1. Декларации (объявления) 44 ■ 2.2. Имена 48 2.3. Типы 49 2.4. Константы 64 2.5. Экономия памяти 70 2.6. Упражнения 73 лава з: выражения и операторы 75 3.1. Настольный калькулятор 75 2
3.2. Синтаксис С + + 3.3. Семантика С + + 3.4. Комментарии и отступы 3.5. Упражнения 88 98 < 102 103 ГЛАВА 4: ФУНКЦИИ И ФАЙЛЫ 107 4.1. Введение 107 4.2. Линкование 108 4.3. Заголовочные файлы 110 4.4. Файлы как модули 118 4.5. Как создать библиотеку 119 4.6. Функции 120 4.7. Макросы ■ J.?';; : 133 4.8. Упражнения 135 ГЛАВА 5: КЛАССЫ 138 5.1. Введение и обзор 138 5.2. Классы и элементы 139 5.3. Интерфейсы и реализация 146 5.4. Дружественные функции и объединения П- 153 5.5. Конструкторы и деструкторы 161 5.6. Упражнения 171 ГЛАВА 6: ПЕРЕОПРЕДЕЛЕНИЕ ОПЕРАТОРОВ 173 6.1. Введение 173 6.2. Функции оператора 1 ; '' и • 174 6.3. Преобразование типов, определяемых пользователем •176 6.4. Константы 181 6.5. Большие объекты 181 6.6. Присваивание и инициализация 183 6.7. Индексация 185 6.8. Вызов функции 187 6.9. Строковый класс : 5 188 6.10. Дружественные функции и функцйийлёмен^Ы V 191 6.11. Предупреждение 193 6.12. Упражнения 193 ГЛАВА 7: ПРОИЗВОДНЫЕ КЛАССЫ 196 7.1. Введение 196 7.2. Производные классы 197 7.3. Альтернативные интерфейсы 7-7? 208 7.4. Добавление к классу 1 ;’.Г ' 217 7.5. Неоднородные списки 219 7.6. Полная программа L > ‘ L . 1 VV 219 7.7. Свободная память * М .. : . .А 227 7.8. Упражнения 229 -—
ГЛАВА 8: ПОТОКИ 231 8.1. Введение 231 8.2. Вывод 232 8.3. Файлы и потоки 239 8.4. Ввод . 242 8.5. Работа со строками 247 8.6. Буферизация 248 8.7. Эффективность 250 8.8. Упражнения ’ 250 СПРАВОЧНОЕ РУКОВОДСТВО 252 с.1. Введение 252 с.2. Лексические соглашения 252 с.З. Синтаксическая нотаЦия 255 с.4. Имена и типы 255 с.5. Объекты и величины lvalue 258 с.6. Преобразования 258 с.7. Выражения 260 с.8. Деклараций (объявления) ' 271 с.9. Операторы 298 с.10. Определения функций 302 с.11. Строки управления компилятором 304 с.12. Константные выражения v 306 с. 13. Соображения переносимости 307 с.14. Краткое изложение синтаксиса 307 с.15. Отличия от С 313
К ЧИТАТЕЛЮ Эта глава состоит из обзора книги, списка ..литературы, и некоторых второстепенных замечаний относительно С + + . Эти замечания касаются истории С + +., идей, повлиявших на его архитектуру, а также мыслей по поводу программирования на C+ +. Эта глава - не введение: замечания, содержащиеся в ней не обязательны для понимания следующих глав, некоторые же замечания предполагают знание С + +г ;; Структура книги ч Глава 1 - это краткий обзор основных характеристик С+ +, ее задача* дать читателю прочувствовать язык C++. Программисты на языке С могут очень быстро прочитать первую половину этой главы; в основном, она описывает общие для С и С + + характеристики. Во второй части главы описаны средства C++ по определению новых типов; нрвичку можно и отложить прочтение этой части до освоения глав 2, 3 и 4.( В главах 2, 3 и 4 описаны такие характеристики C+ +, ^оторые не связаны с определением новых типов: фундаментальные типы, выражения и управляющие структуры в программах на С + + . Другими словами, в этих главах описывается то подмножество C++, которое по сути является языком С. Информация в них гораздо подробнее, чем в главе 1, однако полные сведения можно найти только в справочнике. Однако, в этих главах содержатся примеры, мнения, рекомендации, предупреждения и упражнения, которым не нашлось места в справочнике. В главах 5, 6 и 7 описаны средства С+ + по определению новых типов, средства, которым нет аналогов в языке С. В главе 5 приводится основное понятие класса и показывается, как можно инициализировать объекты с типами, определяемыми пользователем, получать к ним доступ и, наконец, очищать их. В главе 6 разъясняется, как задавать унарные и бинарные операторы работы с типами, определяемыми пользователем; как задавать конверсии между типами, определяемыми пользователем, и как задавать способ обработки каждого случая создания, уничт<?жения и копирования значений типов, определяемых пользователем. В главе 7 описывается понятие производного класса, которое позволяет программисту строить сложные классы из более простых, обеспечивать альтернативные интерфейсы . с классом и эффективно и без противоречия с типами обрабатывать объекты в таких контекстах, где их тип неизвестен во время компиляции. В главе 8 описаны классы ostream и istream, обеспечивающие ввод и вывод в стандартной библиотеке. Эта глава преследует две цели; она описывает полезное средство и кроме того дает реальный пример использования языка С + +. Наконец, в книгу включен справочник по C++. Ссылки на части этой кнйги даются в форме $2.3.4 (Глава 2, раздел 3.4). Глава С означает справочник, например, $С.8.5.5. Замечания по реализации Во время написания книги все реализации C++ использовали 5
азличные версии однопроходного компилятора. Этот компилятор работает а многочисленных компьютерах с различной архитектурой, в том числе ,Т&Т ЗВ, DEC VAX, IBM 370 и Motorola 68000, поддерживающих какие-либо ерсии операционной системы UNIX. Фрагменты программ, включенные в Нигу, непосредственно перенесены из исходных файлов, скомпилированных а компьютерах: ЗВ20 с операционной системой UNIX System V, версия 2 15], VAX 11/750 с операционной системой UNIX 8-го выпуска [16] и CCI ower 6/32 с операционной системой BSD4.2 UNIX [17]. Язык, который писан в этой книге - это "чистый С + +", однако в использовавшемся омпиляторе реализованы некоторые "анахронизмы" (описанные в $с.15.3), оторые облегчают переход от языка С к С + + . Упражнения В конце каждой главы даются упражнения. В основном, упражнения тносятся к типу "напишите программу...". Следует всегда писать полный екст, который можно скомпилировать и пропускать по крайней мере на ескольких тестовых примерах. Упражнения сильно разнятся по сложности, оэтому они помечены в соответствии с оценкой их сложности. Шкала ценок - экспоненциальная, т.е. если упражнение сложности (ж1) займет у ас около пяти минут, то сложность (ж2) потребует часа, а сложность (ж3) южет занять целый день. Время написания и тестирования программы ависит скорее от опыта читателя, чем от сложности самого упражнения. На пражнение сложности (ж1) может уйти день, если для запуска программы итателю сначала надо ознакомиться с новой компьютерной системой. С ругой стороны, упражнение сложности (ж5) кто-то может сделать за час, ели у него есть под рукой необходимый набор программ. Для глав 2-4 качестве сборника упражнений можно взять любую книгу по рограммированию на языке С. Ахо и др. [1] описывают многие бщепринятые структуры данных и алгоритмы в терминах абстрактных типов энных. Таким образом, книгу Ахо и др. можно использовать как сборник пражнений по главам 5-7. Однако язык, используемый в книге Ахо и ,р., не использует функции с элементами и производные классы. Следовательно, более изящно определяемые пользователем типы можно ыразить в С + + . Замечания по проектированию Важным критерием разработки была простота; если возникала дилемма \ежду упрощением справочника и прочей документации или упрощением омпилятора, выбиралось всегда первое. Большое значение уделялось также овместимости с языком С; этот принцип препятствовал доводке синтаксиса зыка С. В C++ нет типов данных высокого уровня и нет примитивных пераций высокого уровня. Например, в нем нет типа "матрица" с •ператором инверсии и нет типа "строка" с оператором конкатенации. Если ользовдтелю нужен такой тип, его можно задать в самом 51зыке. Кстати, здание новых общеупотребительных и прикладных типов - наиболее фундаментальная программистская деятельность в С + +. Хорошо пределенный заданный пользователем тип отличается от встроенного типа олько тем, как он определен, а не тем, как он используется. При 6
разработке языка исключались такие возможности^ которые требуют дополнительных расходов времени выполнения или памяти даже без обращения к ним. Например, были отвергнуты идеи, для реализации которых пришлось бы в каждом объекте держать вспомогательную информацию; если пользователь объявляет структуру, состоящую из двух 16-битовых чисел, то эта структура будет помещаться в 32-битовый регистр. C++ спроектирован для использования в довольно традиционной среде компиляции и выполнения - среде программирования на языке С в системе UNIX. В C++ не включены такие средства, как обработка прерываний или параллельное программирование, требующие нетривиальной поддержки загрузчика и исполнительной системы. Соответственно, задачу на С + + можно очрнь легко согласовать с вычислительной системой. Тем не менее, имеются веские доводы использовать C++ в среде со значительно более мощной поддержкой. Можно без ущерба для языка и с пользой для дела применять такие средства, как динамическая загрузка, инкрементная компиляция и база данных по определениям типов. Такие средства C++, как типы и скрытие данных, предотвращающие случайное разрушение данных, основываются на компиляционном анализе программ. Они не обеспечивают секретности или защиты от предна¬ меренного нарушения правил. Тем не менее эти средства можно свободно использовать без дополнительных затрат на время прогона или памяти. Исторические замечания C+ +, очевидно, больше всего заимствовал из языка С [7]. Язык С сохраняется как подмножество так же, как и основное внимание языка С к средствам достаточно низкого уровня, позволяющего решать самые сложные задачи системного программирования. В свою очередь, язык С многое позаимствовал у своего предшественника - BCPL [9]; кстати, соглашение о комментариях // было (вновь) введено в С + + из BCPL. Если Вы знакомы с BCPL, то заметите, что в С + + по-прежнему нет блока VALOF. Вторым основным источником вдохновения был язык Simula67 [2,3]; из него заимствовано понятие класса (а также производного класса и виртуальных функций). Оператор языка Simula67 inspect специально не включен в С + + . Причина этого - стремление стимулировать модульность с помощью использования виртуальных функций. Такие возможности C++, как перезагружать операторы и обеспечивать свободное размещение деклараций в любом месте, где встречается соответствующий оператор, напоминают Algol68 [14]. Само название - C+ + - появилось весьма недавно {летом 1983 г.). Более ранние версии этого языка использовались с 1980 г. под обобщенным названием "Си с классами"[13]. Первоначальный импульс создания языка возник потому, что автор хотел написать некоторые эмуляции, управляемые событиями, для чего идеально подошел бы Simula67, если бы не соображения эффективности. Язык "Си с классами" применялся в крупных эмуляционных проектах, подвергших серьезной проверке возможности написания протрамм, работающих с (наиболее) минимальным временем и памятью. В "Си с классами" :не было перезагрузки операторов, ссылок, виртуальных функций и многих других деталей. Впервые за пределами исследовательской группы автора C+ + был применен в июле 1983 г.; однако, к тому времени были придуманы почти все возможности
современного языка С + + . Название C+ + придумал Rick Mascitti. Это название отражает эволюционный характер изменений языка С. " + +" - это оператор инкрементации в языке С. Более краткое название С + - это синтаксическая ошибка. Знатоки семантики С считают, что C++ хуже, чем + + С. Язык не называется D, потому что он - расширение С, и в нем не предприняты попытки решать проблемы путем исключения каких-либо средств. Еще одну интерпретацию названия C++ можно найти у Оруэлла [в]. Первоначальная цель создания C++ : избавить автора и его друзей от программирования на ассемблере, С или различных языках высокого уровня. Основная цель: помочь каждому отдельному программисту легче и приятнее создавать хорошие программы. C++ никогда не был спроектирован на бумаге: проектирование, документирование и реализация шли параллельно. Естественно, что однопроходный компилятор для C+ + написан на С + +. Не создавались ни "проект C++", ни "комитет по созданию С + +". • Прежде и теперь C++ развивается с целью решать проблемы, с которыми сталкиваются пользователи, посредством дискуссий автора с его друзьями и коллегами. В качестве базового языка для С + + был выбран язык С, поскольку он, во-первых, гибок, компактен и имеет относительно низкий уровень; во- вторых, подходит для программирования большинства системных задач; в- третьих, работает везде и на всем; и в-четвертых, согласуется со средой программирования UNIX. И в языке С есть свои трудные места, однако трудные места возникли бы и в языке, разработанном с нуля, а проблемы языка С нам известны, Очень важно и то, что благодаря С язык "Си с классами" стал мощным (разве что несколько неудобным) инструментом в течение нескольких месяцев с момента зарождения первых идей добавить Simula-подобные классы в язык С. По мере распространения С + + ,и роста значения его возможностей, дополняющих и расширяющих С, вновь и вновь поднимался вопрос, нужно ли сохранять совместимость с языком С. Ясно, что некоторых трудностей можно избежать, если отказаться от некоторого "наследства" языка С (см., например, Sethi [12]). Но этот путь не выбран потому, что, во-первых, написаны миллионы строк программ на языке С, которые могут воспользоваться преимуществами C++ при условии, что не требуется полностью перГеписывать программу с С на C+ +; во-вторых, существуют сотни тысяч строк библиотечных функций и вспомогательных программ, написанных на С, которые можно использовать из/в программах на C+ + при условии, что C++ полностью совместим с С по линкеру и очень близок к нему по синтаксису; в-третьих, существуют десятки тысяч программистов, знающих язык С, которым нужно только освоить новые возможности C++, а не . изучать заново основы языка; и в-четвертых, поскольку языки C++ и С будут в течение многих лет использоваться на одних и тех же вычислительных системах одними и теми же людьми, то чтобы уменьшить ошибки и путаницу, различия между двумя языками Должны быть либо очень большими, либо очень небольшими. Недавно было пересмотрело определение языка C++ с тем, чтобы гарантировать, что любой конструкт, правильный и в С, и в C+ +, имеет одно и то же значение в обоих языках. Сам язык С в последние годы эволюционировал, частично под 8
влиянием развития C++ (см. Rosier [11]). Предварительный проект стандарта ANSI на язык С[10] содержит синтаксис декларации функции, заимствованный из "Си с классами". Заимствование обогащает обе стороны; например, тип указателя void' был изобретен для С стандарта ANSI, а применен впервые в С + + . Когда стандарт ANSI будет несколько больше разработан, то наступит черед пересмотреть C++, чтобы устранить непринципиальные несовместимости. Например, будет модернизирован препроцессор ($с.11), и возможно, придется скорректировать правила выполнения арифметических действий с плавающей точкой. Этот процесс не будет "болезненным"; и С и С стандарта ANSI - почти подмнож-ества C+ + (см. $с.15). Эффективность и структура C++ был разработан на основе языка программирования С и с немногими исключениями сохраняет С как подмножество. Конструкция базового языка, С как подмножества С + +, задает весьма прямое соответствие между типами, операторами и выражениями языка, с одной стороны, и объектами, с которыми компьютер работает непосредственно: числами, символами и адресами, с другой стороны.* За исключением операторов свободной памяти new и delete отдельные операторы и выражения C++ при исполнении обычно не требуют скрытой поддержки или подпрограмм. В С + + применяются такие же последовательности вызова функций и возврата из них, что и в С. Если же даже этот относительно эффективный механизм оказывается непозволительно неэкономичным, функцию C++ можно заменить подстановкой вызова, имея тем самым преимущества нотационного удобства без потерь при исполнении. Одна из первоначальных целей создания языка С - заменить разработку программ на языке ассемблера для наиболее сложных задач системного программирования. При . разработке C++ уделялось особое внимание тому, чтобы не растерять преимущества, достигнутые в этой области. Различие между С и С + + в первую очередь заключается в степени внимания к типам и структурам. Язык С обладает значительной выразительной силой и нестрогостью. C++ имеет еще большую выразительную силу, но чтобы достичь повышения выразительности, программист должен уделять больше внимания типам объектов. Зная типы объектов компилятор сможет правильно обрабатывать выражения, тогда как в противном случае программисту пришлось бы расписывать операции с удручающими подробностями. Знания о типах объектов также позволяет компилятору обнаруживать ошибки, которые в противном случае не проявились бы до тестирования. Заметим, что само по себе применение системы типов для обеспечения проверки аргументов функций, для защиты данных от случайного разрушения, для создания новых типов и новых операторов и т.д. не увеличивает расходы на время или память при исполнении. Основное внимание к структурам при проектировании C+ + отражает рост размера программ со времени создания языка С. Силовым^ приемами можно заставить работать небольшую программу (менее 1000 строк), даже если она нарушает все правила хорошего стиля программирования. С программой большего размера это просто не получится. Если программа 9
длиной 10 000 строк плохо структурирована, то Вы обнаружите, что новые ошибки появляются в ней с той же скоростью, с какой устраняются старые. Конструкция С + + позволяет рационально структурировать и более крупные программы, так что программу в 25 000 строк сможет разработать и один человек. Существуют и более крупные программы, но те из них, которые действительно работают, на поверку обычно оказываются собранием нескольких почти независимых частей, каждая из которых намного меньше вышеупомянутого объема. Естественно, что трудность написания и ведения программы зависит от сложности .прикладной программы, а не только от количества строк в программе, так что не стоит слишком серьезно относиться к конкретным числам, иллюстрирующим высказанные идеи. Однако, не всякий фрагмент программы можно хорошо структурировать, сделать независимым от аппаратной части, легко читаемым и т.п. В C+ + имеются средства для непосредственной и эффективной работы с аппаратурой, при этом пренебрегая надежностью и легкостью понимания программы. Кроме того, в нем есть возможности скрыть подобные программные фрагменты в изящных и надежных интерфейсах. В этой книге особое внимание обращается на способы создания общеупотребительных программных механизмов, общеполезных типов и библиотек и т.п. Эти способы пригодятся разработчикам как небольших, так и Крупных программ. Более того, поскольку все нетривиальные программы состоят из' многих полунезависимых частей, способы написания таких частей пригодятся и системным программистам, и разработчикам прикладных программ. Кто-то может решить, что если в программе задать более подробную структуру типов, то это приведет к большему размеру исходного текста. С C+ + дело обстоит не так; программа на C++, в которой объявлены типы аргументов функций и используются классы и т.п., обычно получается немного короче, чем эквивалентная ей программа на С, не использующая эти средства. Философские замечания Язык программирования преследует две взаимосвязанных цели: он дает программисту средство определить действия, которые надлежит выполнить, и задает набор понятий, которым программист пользуется, обдумывая возможности выполнения. В идеале для первой цели требуется язык; ''близкий к машинному", с тем, чтобы все особенности компьютера обрабатывались простыми и эффективными способами, достаточно объяснимыми с точки зрения программиста. Именно это и было начальной целью создания языка С. Для второй цели в идеале требуется язык, "близкий к решаемой проблеме", с тем, чтобы конструкты решения можно было выразить точно и прямо. Именно это и было начальной целью средств, расширяющих С до С + + . Существует весьма тесная связь между языком, на котором мы мыслим/программируем, и проблемами и решениями, которые могут прийти нам на ум. Исходя из этого соображения следует считать по меньшей мере опасным ограничение возможностей языка с целью предотвратить программистские ошибки. Как и с естественными языками знание не менее Двух языков дает большие преимущества. Язык дает программисту набор концептуальных инструментов; если они не подходят для данной задачи, то 10
их просто игнорируют. Например, при серьезном ограничении понятия указателя программист просто вынужден применять векторную и целочисленную арифметику для реализации структур; указателей и т.п. Только языковые характеристики не могут гарантировать хорошую архитектуру и отсутствие ошибок. Система типов должна быть особенно полезна в нетривиальных задачах. Кстати, понятие класса уже доказало свою силу в качестве концептуального инструмента. Размышления о программировании на С + + В идеале разработчик решает задачу создания программы в три этапа: сначала ясно понять проблему, затем определить ключевые понятия, необходимые для решения, и наконец, перевести решение в программу. Однако, частности проблемы и концепты решения нередко становятся полностью понятны только в процессе выражения их в программе, а на этом этапе важен выбор языка программирования. В большинстве прикладных задач существуют понятия, которые трудно представить в программе в виде одного из фундаментальных понятий или в виде функции без связанных с ними статических данных. Если есть такое понятие, объявите класс, представляющий понятие в программе.. Класс -это тип; т.е. он определяет, каким образом ведут $ебя объекты этого типа: как они создаются, как с ними работают, как они уничтожаются. Также класс определяет, как представлены объекты, однако на ранних этапах разработки программы это не является (не должно быть) главной заботой. Ключ к написанию хорошей программы - разработка такого набора классов, чтобы каждый из них четко представлял ровно одно понятие. Это нередко означает, что программист должен сосредоточиться на таких вопросах: Как создаются объекты этого класса? Можно ли копировать и/или уничтожать объекты этого класса? Какие операции можно производить с объектами этого класса? Если на такие вопросы не найдено хороших ответов, то, во- первых, вероятно, само понятие недостаточно четкое, и, может быть, стоит еще немного подумать о самой проблеме и предлагаемом решении, а не хвататься за немедленное "кодирование" решения. Понятия, с которыми легче всего работать - это понятия традиционных математических формализмов: числа всех видов, множества, геометрические фигуры и т.п. Обязательно надо создать стандартные библиотеки классов, представляющих эти понятия, но не это главное сейчас, когда пишется эта книга. C++ еще молод, и его библиотеки не поспевают за его ростом. Понятие существует не в пустоте; всегда есть пучки взаимосвязанных понятий. Нередко бывает труднее организовать в программе отношения между классами, т.е. точно определить отношения между различными понятиями, включенными в решение, нежели сначала задать отдельные классы. Не хотелось бы, чтобы в результате получилась "каша", в которой каждый класс (понятие) зависит от всех остальных. Рассмотрим два класса, А и В: отношения типа "А вызывает функции из В", "А создает несколько В", "А включает В в качестве, элемента" редко вызывают серьезные проблемы, а отношений типа "А использует данные из В" обычно можно избежать (просто не использовать публичных элементов данных). Проблемы обычно начинаются с отношениями, которые на естественном языке выражаются как "А - это одно из В ...". 11
Одни из мощнейших интеллектуальных инструментов, позволяющих преодолеть сложность - это иерархическое упорядочивание; т.е. связанные понятия организуются в древовидную структуру, корнем которой является наиболее общее понятие. В C++ подобные структуры представлены производными классами. Нередко программу можно организовать в набор деревьев (лес?). Это значит, что программист определяет ряд базовых классов, где у каждого базового класса есть свой набор производных классов. Для определения набора операций с наиболее общей версией понятия (базового класса) часто можно использовать виртуальные функции ($7.2.8). При необходимости интерпретацию этих операций можно уточнить в отдельных частных случаях (для производных классов). Естественно, что у такой организации есть, свои пределы. В частности, иногда бывает лучше организовать набор понятий как ориентированный граф без циклов, в котором каждое понятие может прямо зависеть от более чем одного другого понятия; например, "А - это одно из В и’ одно из С и ...". Непосредственно такая организация в С + + не обеспечивается, но подобные отношения можно представить с некоторой потерей изящности и проделав определенную дополнительную работу ($7.2.5.). Иногда кажется, что даже ориентированного графа без циклов недостаточно для организации понятий в программе; представляется, что некоторые понятия проявляют присущую им взаимозависимость. Если множество взаимозависимых классов настолько мало, что его легко понять, то циклические зависимости не создадут трудностей. В C++ для представления взаимозависимых классов можно применять идею классов friend ($5.4.1). Если понятия программы Вы можете организовать только в общий граф (а не в дерево и не в ориентированный граф без циклов), и не можете установить взаимные зависимости, то скорее всего, Вашей беде не поможет ни один язык программирования. Если Вы не сможете выявить какие-нибудь легко формулируемые зависимости между основными понятиями, то программа, скорее всего, станет неуправляемой. Запомните, что большую часть программирования можно выполнить просто и четко, используя только примитивные типы, структуры данных, простые функции и несколько классов из стандартной библиотеки. Не стоит весь аппарат, связанный с определением новых типов, если только в этом нет реальной необходимости. Вопрос; "Как писать хорошие программы на С + + ?" очень похож на вопрос "Как писать хорошую английскую прозу?" На него есть ответы двух видов: "Знай, что хочешь сказать" и "Тренируйся. Имитируй хороший стиль". Оба совета одинаково хорошо подходят и к C++, и писательству, и их одинаково трудно придерживаться. Практические советы Вот "советы", которые можно держать в уме при изучении . С + +. По Miepe приобретения опьдта Вы можете развить их в нечто более соответствующее Вашим задачам и Вашему стилю программирования. Правила намеренно упрощены, поэтому в них нет подробностей. Не следуйте им буквально. Чтобы написать хорошую программу, нужно иметь Ум, вкус и терпение. С первого раза у Вас ничего не получится; старайтесь, пробуйте! 12
(1) Когда Вы программируёте, Вы создаете конкретное представлени( идей, связанных с Вашим решением некоторой задачи. Постарайтесь чтобы структура программы максимально прямо отражала эти идеи: (а) Если об "этом" можно мыслить как об отдельном понятии, пусть это будет классом. (б) Если об "этом" ’ можно* мыслить как об Отдельной сущности пусть это будет объектом некоторого класса. *' (в) Если у двух классов есть некая значительная общая часть, пусть она станет базовым классом. В Вашей программе многиё классь будут иметь нечто общее; создайте (почти) универсальный базовый класс - к его разработке отнеситесь'тщательнее всего При определений* Класса, не реализующего математическое понятие например, матрицу Или комплексное число или тип низкого уровня, например, связный Список: . . . . v (а) Не применяйте глобальных данных. ".'*** . (б) Не применяйте глобальных функций (бёз элементбв). (в) Не применяйте данных типа public. (г) Не . используйте дружественные функций, разве что для выполнения (а), (б) и (в). ' , (д) Не организуйте .непосредственный доступ элементам данных другого объекта. (е) Не размещайте "поле типа" в классе; используйте виртуальные] функций? (ж) Не применяйте функции с подстановкой вызова за исключением] случаев значительной оптимизации. Замечания к программирующим н0 языке С Чем лучше Вы знаете, язык С, тем ’ труднее Вам будет писать на С + + , избегая стиля программирования. на С и, соответственно^ не теряя некоторые потенциальные преимущества С++. Поэтому, пожалуйста, просмотрите раздел "Отличия от языка С" в справочнике ($с.15). Вот несколько указаний, в каких областях на С + + можно лучше работать, чем на языке С. Макросы (#define) практически необязательны в C+ +; для определения явных констант используйте const ($2.4.6) или erium ($2.4.7), а чтобы избежать потерь при вызове функций, применяйте inline ($1.12). Старайтесь описывать все функции и определять типы всех аргументов, практически нет причин этого не делать. Аналёгйчно, найдется мало доводов в пользу объявления локальной переменной без ее инициализации, поскольку объявление может, как и выражение, оказаться в любом месте - не объявляйте переменную до того, как она Вам понадобится. Не используйте mallocQ - оператор new ($3.2.6) лучше делает то же самое. Для многих объединений имя не требуется - попробуйте работать с анонимными объединениями ($2.5.2). ч Литература В тексте мало прямых ссылок, однако ниже дается список книг и статей, 13
упомянутых прямо или косвенно. A.V. Aho, J.E. Hopcroft, J.D. Ullman: Data Structures and Algorithms. Addison-Wesley, Reading, Massachusetts. 1983. Имеется русский перевод: Ахо, Хопкрофт, Ульман. Алгоритмы и структуры данных. М., Мир. O-J. Dahl, В. Myrhaug, and К. Nygaard: SIMULA Common Base Language. Norwegian Computing Center S-22, Oslo, Norway. 1970. O-J. Dahl and C.A.R. Hoare: Hierarchical Program Construction in "Structured Programming". Academic Press, New York. 1972. pp. 174-220. [4] A. Goldberg and D.Robson: SMALLTALK-80 The Language and Its Implementation. Addison Wesley, Reading, Massachusetts. 1983. ' [5] R.E. Griswold et al.: The Snobol4 Programming Language. Prentice-Hall, Englewood Cliffs, New Jersey. 1970.' [6] R\E. Griswold and M.T. Griswold: The ICON Programming Language. Prentice-Hall, Englewood Cliffs, New Jersey. 1983. [7] Brian W. Kernighan and Dennis M. Ritchie: The C Programming Language. Prentice-Hall, Englewood Cliffs, New Jersey, 1978. Имеется русский перевод: Б.У.Керниган, Д М.Ричи. Язык программирования Си. М., Мир [8] George Orwell: 1984. Seeker and Warburg, London. 1949. Имеется русский пёревод: Дж. Оруэлл. 1984. Джордж Оруэлл. Проза отчаяния и надежды. Л.: Лениздат, 1990, с. 3-248. [9] Martin Richards and Colin Whitby-Strevens: BCPL - The Language and Its Compiler. Cambridge University Press. 1980. [10] L. Rosier (Chairman, ANSI X3J11 Language Subcommittee): Preliminary Draft Proposed Standard - The C Language. X3 Secretariat: Computer and Business Equipment Manufacturers Association, 311 First Street, NW, Suite 500, ^Washington, DC 20001, USA. [11] L. Rosier: The Evolution of C - Past and Future. AT&T Bell Laboratories Technical Journal. Vol.63 No.8 Part 2. October 1984. pp. 1685-1700. [12] Ravi Sethi: Uniform Syntax for Type Expressions and Declarations. Software Practice & Experience, Vol. 11 (1981), pp. 623-628. [13] Bjarne Stroustrup: Adding Classes to C: An Exercise in Language Evolution. Software Practice &' Experience, 13 (1983), pp. 139-161. [14] P.M. Woodward and S.G. Bond’: Algol 68-R Users Guide. Her Majesty's Stationery Office, London. 1974. [15] ’ UNIX System V Release 2.0. User Reference Manual. AT&T Bell Laboratories, Murray Hill, New Jersey. December 1983. 14
[16] UNIX Time-Sharing System: Programmers Manual. Research Version. Eighth Edition. AT&T Bell Laboratories, Murray Hill, New Jersey. February 1985. [17] UNIX Programmer s Manual. 4.2 Berkeley Software Distribution University of California, Berkeley, California. March 1984.
’’И.В.К.” 105023 Москва, Мал. Семеновская, д. 5 Тел.: 936-50-67, 311-52-08 Факс: 203-93-55 ГЛАВА 1 ОБЗОР С++ Данная глава содержит краткий обзор основных характеристик языка программирования С + + . Сначала демонстрируется программа на С ++, и показывается, как ее скомпилировать й запустить и как эта программа может выполнять выврд и считывать ввод. После введения примерно треть главы посвящена более традиционным характеристикам C++: фундаментальным типам, декларациям, выражениям, операторам, функциям и структуре программы. В остальной части главы рассматриваются средства C++ по определению новых типов, скрытию данных, операторов, определяемых пользователем, и иерархий типов, определяемых пользователем. 1.1 ВВЕДЕНИЕ Этот обзор будет своеобразной экскурсией по ряду программ и фрагментов программ на C+ +..В конце экскурсии у Вас появится общее представление о возможностях C++- и достаточно знаний, чтобы писать простые программы. Точное и полное разъясйение понятий, Связанных даже с самым малым полным примером, потребовало бы многостраничных определений. Чтобы не превратить эту главу в справочник или обсуждение общих идей, в приведенных примерах даются только кратчайшие определения используемых; терминов. В последующих главах термины снова рассматриваются - тогда в/нашем распоряжении будет более широкий набор примеров, что поможет изложению. 1.1.1 Вывод Сначала давайтё напишем программу, выводящую строку: ttinclude <stream.h> main() j cout < < "Hello, world\n"; Строка #include <stream.h> сообщает компилятору включить Декларации (объявления) стандартных средств ввода и вывода, записанных 16
в файле stream.h. Без этих деклараций выражение cout < < "Hello, worldXn" не имело бы смысла. Оператор < < ("записать в") записывает свой второй аргумент в первый (в данном случае, строку "Hello, worldXn" в стандартный выводной поток cout) (Примечание: Программисты на С знают, что < < - оператор сдвига влево для целых чисел. Это применение < < не исчезло; однако, < < получил дополнительное определение для тех случаев, когда его левый операнд - выводной поток. Как это делается, описано в $1.8). Строка - это последовательность символов, заключенная в двойные кавычки. В строке обратная косая черта X и символ после нее обозначают один специальный символ; в данном случае, \п - это символ перехода на новую строку, так что будет написано Hello, world и переход на новую строку. Остальная часть программы main() { ... } обозначает функцию с именем main. В каждой программе должна быть функция, с именем main, и программа начинается с выполнения этой функции. 1.1.2 Компиляция Откуда взялись выходной поток cout и код, реализующий оператор < < ? Для получения выполнимого кода программу на C++ надо скомпилировать; процесс компиляции в сущности аналогичен компиляции языка С и использует большинство тех. же программ. Текст программы считывается и анализируется, если не обнаружены ошибки, генерируется код. Затем программа просматривается с целью обнаружить использованные, но не определенные имена и операторы (з нашем случае - cout и <<). После этого в программу пс возможности дсполн яются из библиотеки неопределенные имена (имеется стандартная библиотека, но можно пользоваться и своей собственной). В нашем случае cout и < < декларируются в stream, h; т.е. указываются их типы, но не даются подробности их реализации. В стандартной библиотеке имеется /спецификация и инициализирующий код для cout и код для <<. Естественно, в этой библиотеке есть и много других вещей, и .некоторые из них декларируются в stream.h, но к нашей программе для получения ее скомпилированного варианта добавляется только требующееся подмножество библиотеки. Команда компилятора C++ обычно называется СС. Используется она так же, как СС для программ на языке С; подробности см. в Вашем справочнике. Допустим, что программа, печатающая "Hello, world", хранится в файле с именем-hello-.^ тогда компилировать и запускать ее надо примерно так ($ - приглашение системы): $ СС hello.с $ a.out Hello, world $ a.out - это стандартное имя выполнимого результата компиляции; если Вы хотите дать своей программе имя, то используйте опцию -о 17
$ CC hello.с -о hello $ hello Hello, world $ 1.1.3 Ввод Ниже приведена программа конверсии (довольно многословная); она приглашает Вас ввести количество дюймов. После ввода она напечатает соответствующее количество сантиметров. main() int inch = 0; "inches = cout < < cin > > inch; cout < < inch; cout < < " in = "• cout < < inch“2.54; cout < < " cm\n"; } Первая строка main() декларирует целочисленную переменную inch. Ее значение считывается с помощью оператора > > ("взять из") из стандартного вводного потока cin. Декларации cin и >>, конечно, находятся в <stream.h>. После запуска на Вашем терминале будет примерно следующее: $ a.out inches = 12 12 in = 30.48 см $ В этом примере на каждый оператор вывода приходится по одному выражению; это необязательная многословность. Оператор вывода < < может работать со своим же результатом, так что четыре последних операции можно записать в одном выражении: cout << inch < < " in = " << inch’2.54 << "cm\n"; Ввод и вывод описаны более подробно в следующих разделах. Кстати, всю эту главу можно рассматривать как объяснение, как можно написать вышеприведенные программы на‘языке, не предоставляющем опреаторов ввода и вывода! На самом деле вышеприведенные программы написаны на C++, "расширенном" операциями ввода-вывода путем использования библиотек и файлов #include. Другими словами, описанный в справочнике язык C++ не определяет средств ввода и вывода; вместо этого операторы < < и > > определяются только с помощью средств, доступных каждому программисту. 18
1.2 КОММЕНТАРИИ \Нередко вставлять в текст программы текст, предназначенный только как комментарий для читателя и игнорируемый компилятором. В C+ + это можно делать двумя способами. Символы /’ открывают: комментарий, закрывающийся символами.;,’/. Вся такая последовательность эквивалентна игнорируемому символу (например, пробелу). Это' наиболее удобно для написания многострочных комментариев и для редактирования текста, однако, -отметим, . что комментарии типа /’ ’/ не вкладываются друг в друга. Символы // открывают комментарий, оканчивающийся b конце той же строки. Вся получившаяся последовательность опять же эквивалентна игнорируемому символу. Это наиболее удобно для написания .коротких комментариев. // Можно использовать для комментирования /’ или ’/, а /’ можно применять для комментирования //. 1.3 ТИПЫ И ДЕКЛАРАЦИИ (ОБЪЯВЛЕНИЯ) Всякое имя и всякое выражение имеет тип, определяющий, какие операции можно к нему применять. Например, декларация int inch; Определяет, что inch Й'Мёет тип int; т.е.. это целочисленная беременная. Декларация - это выражение, вводящее имя в программу. Декларация, задаёт тип этому имени. Тип /определяет правильное использование имени’ йл*и выражения. Для’целых йиёёл, например, определены операций +, -, и /. Если включен stream.К, то int можёт также быть вторым операндом оператора < <, если первый операнд - ostream. Тип объекта определяет не только, какие операции применимы к нему, но и значение этих операций. Например, выражение cout < < inch < < " in = " < < inch“2.54 < < "cm\n"; правильно и по-разному работаем с четырьмя выводимыми 'значениями. Строки печатаются в своем непосрдествённом виде, тогда как целое inch и’.&начение с* плавающей точкой'inch’2.54 преобразуются из их внутреннего представления в символьный’• вид’ Д’ля чтения человеком. В C++ есть несколько основных Типов и несколько способов создания новых типов. Простейшие формы типов в С+ + представлены в следующих разделах; наиболее интересные обсуждаются позднее. 1.3.1 Фундаментальные типы ,л;п' Фундаментальные типы, практически ■ ' прямо , соответствукЗщие аппаратным средствам, это: > r г 4 char short int long 1 float double Первые четыре типа используются для представления' целых чисел, последние два - для представления чиселс плавающей точкой. Переменная 19
иПа char имеет естественную длину для размещения символа на данном компьютере (обычно - байт), а переменная типа int имеет естественную длину Для размещения арифметического целого числа на11 д'анном компьютере (обычно - слово). Диапазон целых чисел, которые можно представить данным типом, зависит от размера. В C+ + .размерь! измеряются величинами, кратными размеру char, поэтому по Определению char имеет единичный размер. Отношения между фундаментальными типами можно записать так: \ > ... . л / s sizeof (char) < sizeofK(short) < sizeof (int) < sizeo( (long) sizeof (float) < sizeof (double), где sizeof - размер Вообще, неразумно было бы Делать5 какие-либо еще допущения о размерах фундаментальных типов. В частности, неверно, что на всех компьютерах в целом числе можно разместить указатель. Прилагательное const (константа) можно добавить к основному, типу, что даст тип с характеристиками, совпадающими с оригинальным' типом за исключением того, что значение переменных с типом const нельзя изменить после инициализации. const float pi = 3.14; const char plus = ' +'; Символ, заключенной в одиночные кавычки - это символьная константа. Отметим, что часто .константа, определенная подобным образом, не обязательно занимает память; ее значение может просто непосред¬ ственно использоваться там, где требу efti’ Константа должна' быть определена в том месте, где она декларируется. Для переменных инициализация необязательна, но настоятельно рекомендуется. Практически нет причин вводить локальную переменную, не t инициализируя ее. Арифметические операторы можно применять к любому сочетанию этих типов: + (плюс, унарный и бинарный) (плюс, унарный и бинарный) (умножение) / (деление) Аналогично логические операции: = = (равно) I= (не равно) < (меньше чем) > (больше чем) . <= (меньше или равно). > = (больше или равно) Учтите, что целое деление дает целый результат : 7/2 дает 3. Чтобы °лучить остаток можно использовать операцию % 7/2 дает 1. ч При ^Рисвоении и в арифметических операциях C++ производит все ^обходимые преобразования над базовыми типами, так чтобы они могли 20
использоваться совместно совершенно свободно : double d = 1 ; int i = 1 ; d = d + i; i = d + i ; 1.3.2. Производные типы Следующие операции образуют новые-типы из базовых .: указатель на “const & 8 постоянный указатель на - ссылка на массив функция Например: char’ р; char “const q; char v[1OJ; // указатель на символ // постоянный указатель на символ // массив из 10 символов Первый элемент массива имеет порядковый номер 0, так массив у состоит из 10 элементов v[0]...v[9]. Функции разъяснены в $1.5 , справочные сведения приведены в $1.9 справочного руководства. Указатель может содержать адрес объекта подходящего типа: char с ; //... р = &с ; // р points to с Унарный оператор & дает адрес элемента. 1.4 ВЫРАЖЕНИЯ И ОПЕРАТОРЫ C++ имеет широкий набор операций, которые используются в выражениях для задания и изменения значений переменных. Операторы составляют управляющий поток программы, и объявления используются для введения в программу имен переменных, констант и т.д. Учтите, что объявления являются операторами, поэтому они могут свободно использоваться совместно с другими операторами. 1.4.1 Выражения C++ имеет множество операторов, которые будут обленены там, где это будет необходимо. Однако, запомните, что операции -(дополнение) & (И) ~ (исключающее ИЛИ) 21
I (включающее ИЛИ) < < (логический сдвиг влево) > > (логический сдвиг Вправо) относятся к целым, и что не существует отдельных типов данных для логических операций. Смысл оператора меняется в зависимости от количества операндов; унарный & дает адрес , бинарный & это логическое И. Смысл оператора также зависит от типа его операндов; + в выражении а + b означает сложение чисел с плавающей точкой, если тип операторов float, и целое сложение для операндов типа inf. В $1.8 объясняется как операция, может быть введена для определенного пользователем типа без потери ее основного смысла для фундаментальных и производных типов. В C+ + оператор присваивания отличается по смыслу от аналогичного оператора в других языках программирования. Присвоение может появиться в неожиданном контексте; например, х sqrt.(a = Зхх). Следующий пример может быть полезен: а = Ь = с означает присвоение значения переменной с переменной Ь и затем переменной а. Кроме того, оператор присваивания может комбинироваться с большинством бинарных операций. Например, x[i + 3]M = 4 означает x[i + 3] = x[i + 3]“4 , только x[i + 3] вычисляется только один раз. Это в значительной степени увеличивает эффективность рабочего кода программы даже без оптимизирующего компилятора. К тому же это более изящно. В большинстве программ, написанных на 0+ + , широко используются указатели. Унарная операция " определяет указатель; так жр - это объект, на который указывает р. Такая операция называется обращение по ссылке. Например, пусть дано описание char "р ; тогда “р - это символ, на который указывает р. Оператор увеличения + + и уменьшения — часто полезны при работе с указателями; если р указывает на элемент массива, то после операции р + + р указывает на следующий элемент массира. 1.4.2 Выражения Наиболее распространенная форма оператора - это (минимальная часть программы, оканчивающаяся точкой Например, выражение с запятой) а = Ья3 + с; count < < "go go go"; lseek(fd,0,2); 1.4.3 Пустой оператор Простейший оператор - это пустой оператор: 22
Он ничего не делает. Однако, он может оказаться полезным, когда синтаксис требует наличия оператора, а Вам оператор не нужен. 1.4.4 Блоки Блок - это (возможно, пустой) список выражений, заключенный в фигурные скобки: »{ а = -Ь + 2; Ь + + ; } Блок позволяет Вам работать с несколькими выражениями так же, как с одним. Область ^ действия имени, объявленного в блоке, распространяется от точки Декларации до конца блока. Имя может скрыть, декларируя одно и то же имя во внутренних блоках. 1.4.5 Операторы условия if В нижеследующем примере выполняется конверсия как из дюймов в сантиметры, так и из сантиметров в дюймы; предполагается, что Вы указываете единицу измерения, добавляя i для дюймов и с для сантиметров: #include <stream.h> main() * A • { 'Ч ''- j const float fac Д.'/ 2.54; float x, in, cm; char ch = 0; cout < < "enter length: "; cin > > x > > ch; if (ch = = T) { in = x; cm = x’fac; } else if (ch = = 'c') { in = x/fac; cm = } else in = cm = 0; * cout < < in < < "in = ■ ! ' Обратите внимание, что условие в круглые скобки. ■■■' J А // введите длину // дюйм // сантиметр " < < ст < < " ст\п"; в операторе if должно заключаться 23
1.4.6 Оператор множественного выбора switch Оператор предыдущем в switch сравнивает значение с набором констант, примере можно было бы переписать так: Проверки switch (ch) { case 'i': in = x; cm = x’fac; break; case c': in = x/fac; cm = x; break; default: in = cm = 0; • break;' ) Операторы break используются для выхода из оператора switch. Константы для выбора должны быть заданы явным образом, и если сравниваемое значение не совпадает ни с одним из них, выбирается default. Оператор default необязателен. 1.4.7 Операторы цикла while Рассмотрим копирование строки, если даны: указатель р на ее первый символ и указатель q на результат. По соглашению строка заканчивается символом с целочисленным значением 0. while Ср ! = 0) { Mq = "р; // копировать символ q = q + 1; Р = р + 1; *q = 0; // конечный 0 не копировали * Условие после while должно быть заключено в скобки. Условие оценивается, и если его значение не равно нулю, то выполняется выражение, следующее непосредственно за while. Так повторяется до тех П0Р, пока значение условия не станет равно нулю. Приведенный пример довольно многословен. Для непосредственной при^еМеНТаЦИИ можно применять оператор + +, что упростит тестовый while (’р) Mq.+ + = *р + + ; "Я = 0; где р + + означает: "взять символ, на который указывает р, затем ИнкРементиРовать р". Пример можно еще сжать, поскольку в каждом цикле обращение к 24
значению р производится два раза. Копирование символов можно выполнят одновременно с проверкой условия: while (*q + + = "р + + )'• где берется символ, на который указывает р, инкрементируется р, симво копируется ев точку, на которую указывает q, затем инкрементируется < Если символ - ненулевой, то цикл повторяется. Так как все действи выполняется в теле условия, то отдельное выражение не требуется. Чтоб указать это, используется пустой оператор. C++ (как и язык ( одновременно и,, любят, и ненавидят за возможность столь лаконичны конструкций. 1.4.8. Операторы цикла for Рассмотрим копирование 10 элементов из одного вектора в другой: for (inf i = 0; i< 10; i+ +) q[i] = p[i); Это эквивалентно: inf i = 0; while (i<10) { q[i] = P[i]; i + + ; } но более читаемо, поскольку вся информация, управляющая циклом расположена в одном месте. Применительно к целочисленной переменной оператор инкрементации ++ просто прибавляет единицу. Первая часть оператора for не обязательно должна быть декларацией; это может быть любой оператор. Например: for (i = 0; i < 10; i++) q[i] = p[i]; эквивалентно предыдущему при условии, что ранее. 1.4.9 Деклараций (объявления) корректно декларирован Декларация - это выражение, вводящее имя в программу; кроме того^ она может инициализировать объект с этим именем. Декларация выполняется, т.-fe. оценивается инициализатор и осуществляется инициализация, тогда, когда управляющий поток доходит до декларации. Например: for (inf i = inf t vfM] v[i] = } 1; i<MAX; +) { f; 25
При раз, каждом выполнении выражения for i будет инициализироваться один а f - МАХ-1 раз. , 1.5 ФУНКЦИИ функция - это поименованная часть программы, которая может рваться из других частей программы сколь угодно часто. Рассмотрим программу, записывающую степени двух: extern float pow(float, int); //pow() определяется в другом месте main() ( for (int i=f); i< 10; i+ + ) cout << pow(2,i) << "\n"; ) Первая строка •- это декларация функции, которая определяет, что pow - это функция, берущая в качестве аргументов float и int и возвращающая float. Декларация функции производится для того, чтобы можно было обращаться к функции, определенной в другом месте. При вызове каждый аргумент функции проверяется на принадлежность к ожидаемому типу точно так же, как будто инициализируется переменная декларированного типа. Это обеспечивает правильную проверку тйпдв~”и конверсию типов. Например, вызов pow(12.3, "abed") вызывает у компилятора жалобы, поскольку "abed" - строка, а не int. При вызове pow(2, i) компилятор конвертирует целую . константу 2 в float, как предполагается в функции, pow можно определить так: float pow(float х, int n) * if (n < 0) error("sorry, negative exponent to pow()"); // извините, у pow() отрицательная степень switch (n) { case 0: return 1; case Г: return x; default: return x'pow(x,nH); Первая часть определения функции задает имя функции, тип возвращаемого значения (если оно есть) и типы и имена ее аргументов (если они есть). Ункция возвращает значение с помощью выражения retorn. Разл ичные функции обычно имеют различные имена, но для функций, выполняющих схожие задачи для различных типов объектов- иногда лучше 5? ть Этим функциям одинаковые имена. Когда у функций различаются типы РгУментов, компилятор всегда может их различить правильно выбрать, Функцию вызвать. Например, можно создать одну функцию т°ч^ния В степень для 4ель|Х чисел и другую - для чисел с плавающей overload pow; 26
int pow(int, int); double pow(double, double); / // ... x = pow(2, 10); у = pow(2.0, 10.0); Декларация overload pow; сообщает компилятору, что имя pow намеренно используется для боле? чем одной функции. Если функция не возвращает значения, ее нужно декларировать как void void swap(int" р, int" q) { int t = "p; ; 'p = "q; - i 1.6 СТРУКТУРА ПРОГРАММЫ Обычно программа на С + + состоит из многих исходных файлов, и в каждом есть последовательность деклараций типов, функций, переменных и констант. Если используемое имя должно обозначать одно и то же в двух разных исходных файлах, оно должно быть объявлено внешним ,(external). Например: extern double sqrt(double); extern istream cin; Наиболее популярный способ обеспечения согласованности между исходными файлами - помещать подобные декларации в отдельные файлы, называющиеся заголовочными файлами или хидер-файлами, а затем включать (include), т.е. копировать, эти'заголовочные файлы во все файлы, где нужны эти декларации. Например, если декларация функции sqrt хранится в заголовочном файле стандартных математических функций math.h, и Вам нужно извлечь квадратный корень из 4, то можно написать: #include <math.h> // . . х = sqrt(4).; u Поскольку обычный заголовочный файл включается во многие исходные файлы, он нек содержит деклараций, которые нельзя повторять. Например, тела функций даются только для функций с подстановкой вызова ($142); а инициализаторы - только для констант ($1.3.1). За исключением этих случаев, заголовочный файл - это хранилище информации о типах; он обеспечивает интерфейс между отдельно скомпилированными частями программы. 27
В директиве включения include имя файла, заключенное в угловые сКобки, как, например, <math.h>, относится к файлу с этим именем в стандзртном директории заголовочных файлов (часто это /usr/include/CC); Лайлы, расположенные в других директориях, обычно обозначаются именами в двойных кавычках. Например: #include "mathl.h" #include "/usr/bs/math2.h" означает включение файла mathl.h из текущего пользовательского директория и файла math2.h из директория /usr/bs. Ниже дается очень маленький пример, в котором строка определяется в одном файле, а печатается в другом. Файл header.h определяет необходимые типы:- // header.h extern char’ prog _ name; extern void f(); Файл main.c - это основная программа: // main.c #include "header.h" char’ prog _ name = "silly, but complete",//глупая, но завершенная main() { Ю; } а файл f.c печатает строку: // f.c #include <stream.h> #include* "header.h" void f() cout < < prog _ name << "\n"; Эту программу Вы можете скомпилировать и Запустить примерно так: $ СС main.c f.c -о silly $ silly silly, but complete $ 2 Зак. 1927 28
1.7 КЛАССЫ Посмотрим, как мы можем ■ определить тип ostream. Для упрощения задачи предположим, что для буферизации символов определен тип streambuf. В действительности тип streambuf определен в <stream.h>, где также находится настоящее определение ostream. Пожалуйста, не пытайтесь проверить на компьютере примеры с определением ostream в этом и следующих разделах; если только Вы не исключите <stream.h> полностью, компилятор будет жаловаться на переопределения. Определение типа, задаваемого пользователем (что называется в С + + классом - class) содержит спецификацию данных, требующихся для представления объекта этого типа, и набор операций; для работы с подобными объектами. Определение состоит из двух частей: личная часть, содержащая информацию, которую может использовать только исполнитель, и публичная часть - интерфейс для пользователей этого типа: class ostream { streambuf* but; int state; public: void put(char’); void put(long“); void put(double); } i Деклараций после метки public задают интерфейс: пользователь моэкет вызвать только три функции put(). Декларации до метки public определяют представление объекта класса ostream; имена but и state могут использоваться только функциями put(), объявленными в публичной части. Class определяет тип, а не объект, так что для использования ostream нам нужно его декларировать (так же, как мы объявляем переменные типа int): ostream my _ out; Если допустить, что my _ out правильно инициализирован (как объяснено в $1.10), то его можно использовать так: my _ out.put("Hello, world\n"); Оператор точки используется, чтобы уточнить элемент класса для данного объекта этого класса. В данном случае функция put() вызывается для объекта my __ out. Эту функцию можно определить так: void ostream::put(char’ р) while (*р) buf.sputc(*p + + ); где sputc() - это функция, которая помещает символ в streambuf. Префикс ostream необходим для того, чтобы отличать put() из ostream от других 29
функций с именем Put- ФУ Для того, чтобы вызвать функцию-элемент, необходимо указать объект иного класса. В функции-элементе такой объект может* быть указан Деявно, как сделано выше, в ostream: :put(); при каждом вызове but НтНосится к элементу buf того объекта, для которого вызывается функция. 0 Кроме того, на этот объект можно указывать явным образом - с оМощью указателя с именем this (этот). В функции-элементе класса X this неявно декларируется как X" (указатель на X) и инициализируется указателем на объект, для которого вызывается функция. Так, определение □stream::put() можно переписать и так: void ostream: :put(char" р) while ("р) this—> buf.sputc(“p + + ); } Оператор -> используется для указания йа элемент объекта с указателем. 1.8 ПЕРЕОПРЕДЕЛЕНИЕ ОПЕРАТОРОВ Реальный класс ostream определяет оператор < < для того, чтобы было удобно выводить различимые объекты одним выражением. Посмотрим, как это делается. Для того, чтобы определить @, где @ - любой оператор языка С + +, для типа, определяемого пользователем, Вам надо определить функцию с именем operator®), которая принимает аргументы соответствующего типа. Например: class ostream { // ... ostream operator< <(char"); ostream ostream: :operator << (char" p) while ("p) buf.sputc("p + + ); return "this; определяет, что оператор << есть элемент класса ostream, так что s<<p интерпретируется как s.operatorc <(р), когда s - ostream, и р - указатель на символ. Оператор << бинарен, но функция operator<<char") на первый взгляд принимает только один аргумент; однако, на самом деле, у нее есть и второй неявный стандартный аргумент this. Возврат ostream в качестве возвращаемого значения позволяет применить << к результату операции вывода. Например, s<<p<<q нтерПретируется как (s.operator< <(р)).operator< <(q). Таким образом еспечивается работа операций вывода для встроенных типов. С помощью набора операций, предоставляемых как публичные лементы класса ostream, Вы можете определить < < для такого типа, Ределяемого пользователем, как complex, не изменяя декларацию класса 2* 30
ostream: ostream operator << (ostream s, complex z) // complex имеет две части: real и imag // печатаем complex как (real, imag) return s < < < < z.real < < < < z.imag < < °)M; } Поскольку operator << (©stream, complex). - это не функция-элемент, то дщ того, чтобы она была бинарной, ей требуются два явных аргумента. Он, будет выводить значения в правильном порядке, поскольку < <, как i большинство операторов C++, группирует аргументы слева направо; т.е а< <Ь< <с значит (а< <Ь)< <с. При интерпретации операторов комцилято| понимает разницу между функциями-элементами и функциями-гнеэлементами Например, если z - это комплексная переменная, то s<<z будет расширено с помощью стандартного (без обращения к элементу) вызова функции operator < <(s,z). t ,9. ОБРАЩЕНИЯ ПО АДРЕСУ (ССЫЛКИ) К сожалению, последняя версия ostream. содержит серьезную ошибку и поэтому весьма неэффективна. Проблема состоит в том, что при каждом использовании < < ©stream копируется дважды: один раз в качестве аргумента и один раз как возвращаемое значение. В результате после каждого вызова state остается в прежнем состоянии. Требуется средство передачи указателя на ostream, а не передачи самого ostream. < Это можно сделать с помощью обращении по адресу иди ссылок. Ссылка работает, как имя объекта; Т& означает обращение по адресу Т. Обращение может быть инициализировано и становится альтернатИвнЫм именем для объекта, которым оно инициализирован. Например: ostream& si = my _ out; I ostream& s2 = cout; Теперь si и my _ out можно использовать одинаковым образом и с одинаковым значением. Например, присвоение si = s2; копирует объект, на адрес которого ссылается s2 (т.е. cout) в объект, на адрес которого ссылается s1 (т.е. my _ out). Элементы указываются с помощью оператора "точка" si .put("don't use ->"); и если применить к этому оператор адресации, то будет получен адрес объекта, к которому производилось обращение: &s1 = = &my_out 31
Первым, очевидным применением ссылок является обеспечение пер©Аачи Функций выводЬ адреса объекта, а не самого объекта (в некоторых языках это’ называется передача параметра по сеьмже): ostream& operator < <(ostream& s, complex z) { Feturn $ << "(h << z.real << << z.imag << 'У1; } Небезынтересно, что тело функции не меняется, но если бы значение присваивалось $, то изменен был бы сам объект, данный в качестве аргумента, а не его копия. В этом случае возврат адреса также повышает эффективность,, поскольку очевидный способ реализации ссылки - это указатель, а передавать указатель гораздо экономнее, чем большую структуру данных. Кроме того, обращения по адресу важны для определения входных потоков, поскольку оператор вврда получает в качестве операнда переменну»Р, в которую следует считывать. Если бы не применение обращений по адресу; пользователям пришлось бы передавать функциям ввода указатели в явном виде. class (stream { // ... inf state; public: istream& operator > >(char&); istream& operator > > (char*); istream& operator >>(int&); isf ream& operate r > > (I о n g &); // ... " Заметим, что для чтения в long и в Int используются две разные функции, тогда как для их печати потребовалась только одна. Это обычный случай, поскольку int конвертируется в long по стандартным неявным правилам конверсии ($с.6.6), что избавляет программиста от написания обеих Функций вывода. 1.10 КОНСТРУКТОРЫ В результате определения ostream как класса элементы данных стали личными. Только функция-элемент может подучить доступ к личным элементам, так что Вы должны предоставить такую функцию для инициализации. Подобная функция называется конструктором, и она отличается тем, что ее имя совпадает с именем класса: class ostream { // ... osiream($treambuf ’ astream(int size, char’ s); }; < ' .-v-.,. ■ 32
Здесь даны два конструктора. Один принимает вышеуказанный] streambuf для реального вывода; другой принимает размер и указатель на! символ для форматирования строки. В декларации список аргументов; необходимый конструктору, дописывается к имени. Теперь Вы можете объявлять такие потоки: ostream my - out(&some - stream _ buffer); char xx[256J; ostream xx — stream(256,xx); Декларация my _ out не только резервирует соответствующее количество памяти; она еще вызывает конструктор ostream: :ostream(streambuf’) для ^инициализации е^о аргументом &some_ stream — buffer, предположительно - указателем на подходящий объект класса streambuf. Так же обрабатывается декларация . хх_ slream, но она использует другой конструктор. Декларарирование конструкторов классу, не только дает способ инициализации объектов, но и гарантирует, что будут инициализированы,рее объекты этого класса. Если для класса декларированы конструкторы, то невозможно декларировать переменную этого класса, не вызвав конструктор. Если в классе есть конструктор, не принимающий аргументы, то этот конструктор будет вызываться, если в декларации не содержатся аргументы. 1.11 ВЕКТОРЫ ' Понятие вектора, встроенное в C++, дает максимальную эффективность при выполнении и минимальные расходы памяти. Кроме того, это весьма разностронний инструмент для создания средств высокого уровня, Особенно в комбинации с указателями, вам, однако, может не понравиться, что размер вектора надо задавать как константу, что отсутствует проверка границ вектора и т.д. Вот ответ на эти жалобы: "Это можете запрограммировать сами". Посмотрим, разумен ли такой ответ; другими словами, проверим силу абстрагирующих возможностей C++: попробуем создать эти средства для векторных типов нашего собственного проекта и рассмотрим встречающиеся при этом трудности, связанные с этим затраты и удобство пользования подуившимися векторными типами. class vector { int“ v; int sz; public: vector(int); // конструктор * yectorO; // деструктор int size() { return sz; } void set — size(int); int& operator[](inf); int& element i) { return v[i); } Функция size (размер) возвращает количество элементов вектора; т. е. индексы должны быть в диапазоне O...size()-1. Для изменения этого размера дается функция set — size (дать размер), функция elem дает доступ к 33
элементам, не проверяя индекс, а орега+ог[] дает доступ с проверкой гРаН Идея состоит в том, чтобы сделать сам класс структурой доступом к ктора с помощью оператора vector: :vector(int s) if (s<=0) error("bad vector size"); sz = s; v = new int[s]; Теперь векторы можно декларировать почти столь же изящно, как встроенные в сам язык векторы: vector v1(100); vector v2(nelem"2-4); Операцию доступа можно определить так: int& vector::operator[](int i) if (i<0 II sz< = i) error("vector index out of range"); return v[ij; • Оператор II (или) - это оператор "логическое или". Его правый операнд оценивается только в случае необходимости; т.е. если значение левого операнда равно Нулю. Возврат обращения по адресу гарантирует, что нотацию [] можно использовать с обеих сторон присвоения: vl [х] = v2[y]; Функция с забавным именем ~ vector - это деструктор, т.е. функция, объявленная для неявного вызова при выходе объекта класса из области действия. Деструктор класса С имеет имя ~ С. Если его определить так: vector:: ~ yectorQ delete v; То с помощью оператора delete он будет освобождать память, выделенную конструктором, так что когда vector выйдет из области действия, вся его память будет освобождена для возможного повторного использования. 1.12 ЗАМЕНА ВЫЗОВА ФУНКЦИИ Учитывая частоту вызова небольших функций, Вы, возможно, 34
беспокоитесь о затратах на вызовы функций. На вызов функции-элементу затрачивается не больше, чем на вызов функции-элемента с таким >к6 количеством аргументов (напомним, что у функции-элемента всегда есть один аргумент), а эффективность вызовов функций в С + + почти такая же, какой можно достичь в любом языке. Однако, с крайне малыми функциями может встать вопрос о затратах на вызов. В таком случае можно подумать, не стоит ли указать, что вызов функции может замещаться на ее тело. В случае положительного решения компилятор будет генерировать правильный код функции прямо в месте ее вызова. Семантика вызова не меняется. Например, если size() и elem() расширяются с заменой вызова: vector s(100); // ... ! i = s.size() х = elem(i-l); то генерируется код, эквивалентный следующему: // ... i = 100; х = s.v[i-1]; Обычно компилятор C++ достаточно умен, чтобы сгенерировать код не менее хороший, чем при прямом макрорасширении. Естественно, что для сохранения семантики компилятору иногда приходится пользоваться временными переменными и прочими маленькими хитростями. Для того, чтобы указать, что функция должна иметь замены вызова, перед ее определением нужно поставить ключевое слово inline, или в случае функции-элемента просто включить определение функции в декларацию класса, как сделано для size() и elem() в предыдущем примере. При правильном применении функции с inline одновременно увеличивают скорость выполнения и уменьшают размер объектного кода. В то же время, функции с подстановкой вызова засоряют декларации классов и могут замедлять компиляцию, поэтому не следует пользоваться ими без необходимости. Для того, чтобы замена вызова на тело функции давала значительный выигрыш в работе функции, функция должна быть очень мала. 1.13 ПРОИЗВОДНЫЕ КЛАССЫ Давайте теперь определим вектор, которому пользователь может задать границы индекса: class vec: public vector { int low, high; public: ; vec(int, int); int& elem(int); int& operator[](int); 35
0ПреДеление veG в качестве : public vector □„первых, означает, что vec - это вектор. Во-вторых тип vec имеет (наследует) все свойства типа vector в дополнение к свойствам, специально объявленным для него. Будем говорить, что класс vector -это базовый класс для vec, и наоборот, vec - производный от vector. А Класс vec модифицирует класс vector благодаря наличию иного конструктора, который требует от пользователя задать две границы индекса, а не размер, и благодаря собственным функциям доступа elem(int) и operator[](int). elem() класса vec легко' описать в терминах elem() класса vector: int& vec:: element i) return vector: :elem(i-low); } Оператор разрешения области действия :: используется, чтобы избежать бесконечной рекурсии при вызове vec::elem() из самого себя. Для ссылки на нелокальные имена можно применять унарный Разумно объявить vec::elem() как функцию с подстановкой вызова, поскольку вероятно, эффективность имеет значение, но необязательно, неразумно и невозможно писать ее так, чтобы она непосредственно использовала личный элемент v класса vector. У функций производного класса нет специального доступа к личным элементам их базового класса. Конструктор можно написать так: vec::vec(int lb, int hb) : (hb-lb + 1) { if (hb-lb<0) hb = lb; low = lb; high = hb; Конструкт : (hb-lb + 1) используется для задания списка аргументов, необходимого для конструктора базового класса vector: :vector(). Этот конструктор вы/ывается до тела vec::vec(): НиЖё приводится небольшой пример, который можно запустить, если его скомпилировать вместе с Другими декларациями для vector: #include <stream.h> void error(char’ p) cerr < < p < < "\n"; // cerr - это поток вывода ошибок exit(1); void vector::set_size(int) { /” пустой '/ } 36
int& vec::operator[](int i) if (iclow II highci) еггог("индекс vec за пределами диапазона")^ return elem(i); ’ } main() vector a(10); for (int i = 0; i<a.size(); i++) { a[i] = i; cotit < < a[i] < < " } cout < < "\n"; vec b(10,19); for (i = 0; i<b.size(); i++) b[i + 10] = a[i]; for (i = 0; i<b.size(); i + +) cout < < b[i + 10] < < " cout << "\n"; 5 ' ! При выполнении это дает 01 23 4 567 8 9 0 123 456789 В таком направлении векторный тип можно развивать и дальше. Очень просто создавать многомерные массивы, массивы, в которых количество измерений задается как аргумент конструктора, массивы вроде фортрановских, доступ к которым возможен в терминах как двух, так и трех измерений, и т.д. Подобный класс управляет доступом к некоторым данным. Поскольку любой доступ выполняется через интерфейс, предоставляемый public-частью класса, то представление данных можно изменить так, чтобы оно удовле¬ творяло потребностям исполнителя. Например, достаточно тривиально - заменить представление вектора на связный список. Обратная сторона этой медали состоит в том, что для конкретной реализации можно предоставить любой подходящий интерфейс. 1.14 ЕЩЕ ОБ ОПЕРАТОРАХ Другое направление развития векторов - это создание для них операций: class Vec : public vector { public: Vec(int s) : (s) {}. Vec(Vec&); ■ Vec() {} void operator = (Vec&); void operator’ = (Vec&); 37
void operator’ = (int); // ... }; Обратите внимание, как определяется передача (и только* она) аргумента конструктора производного класса, Vec::Vec(), конструктору базового класса, vector: :vector(). Это полезный пример. Оператор присваивания переопределяется, и его можно определить так: void Vec:: operator = (VecA a) * int s = size(); if (s! = a.size()) еггог("неправильный размер вектора для = "); for (int i = 0; i<s; i+ + ) elem(i) = a.elem(i); ’ } Теперь при присваивании Объектам Vec элементы действительно копируются, тогда как при присваивании объектам vector происходит простое копирование структуры, управляющей доступом к элементам. Последнее, однако, происходит и тогда, когда vector копируется без явного применения оператора присваивания: (1) когда vector инициализируется путем присваивания ему другого вектора; (2) когда vector передается в качестве аргумента; (3) когда vector передается как значение, возвращаемое из функции. Чтобы и в этих случаях получить управление для векторов Vec, Вы можете так определить конструктор Vec(vecA): Vec::Vec(VecA а) : (a.sizeQ) int sz = a.size(); ' for (int i = 0; i<sz; i + + ) elem(i) 1 = a.elem(i); Этот конструктор инициализирует Ved как копию другого Vec, и он вызывается в вышеуказанных случаях. Для операторов вроде = и + = выражение слева ясно определено, и по-видимому, естественно реализовать их как операции над объектом, обозначенным этим выражением. В частности, далее они могут изменить значение своего первого операнда. Для операторов вроде + и - левый операнд обычно не требует‘ особого внимания. Можно, напри'&ер/ пёредать оба аргумента по значению и все же получить правильную реализацию сложения векторов. Однако, векторы могут быть большими, поэтому чтобы избежать ненужного копирования, операнды + передаются оператору operator + () по ссылке: Vec operator + (VecA a, VecA b) int s = a.size()); if (s ! = b.sizeQ) еггог("неправильный размер вектора для +"); Vec sum(s); for (inf i = 0; i<s; i+ +) surri.elem(i) = a.elem(i) + b.elem(i); 38
return sum; Вот небольшой пример, который можно запустить, если его] скомпилировать .вместе с приведенными выше декларациями для vector: #include < stream.h > , t... void error(char“ p) { cerr < < p < < *\n"; exit( 1); } void vector::set _size(int) { /’ ... '/ } int& vec::operator(](int i) { /* ... 7 } .. mainQ { Vec a(10); . < Vec b(W); : for (int i = 0; i <a.size(); i + +) a[j] -= , i; b = a; . ./ ■ ,3, J Уес c = a + b; * for (i = Q; J<c.size(); i+ + ) cout ,< < c[i] < < ”\nn;. .. 1.15 ДРУЖЕСТВЕННЫЕ ФУНКЦИЙ , Функциональный operator + () не работает прямо с представлением вектора; и, естественно, не может, поскольку оно - не элемент; Иногда, однако, желательно дать функциям, не являющимся элементами;: доступ к личной части объекта класса. Например, ч если бы была , функция "с неконтролируемым доступом* vector: :elem(), то Вам пришлось бы трижды за каждый цикл проверять, соответствует ли индекс i границам вектора. В данном случае мы обошли эту проблему, однако, врзпикает ана часто, поэтому существует механизм, которым класс может дать функции-не- элементу доступ, к своей приватной части. Для этого в декларации класса помещается декларация функции, перед которой стрит ключевое слово friend. Например, если есть: ' class Vec; /Z Vec - это имя классаv . class vector { friend Vec operator + (Vec, Vet); . }; , ;‘<r то можно написать: Vec operator + (Vec a, Vpc b) / int s = a.size(); if (s ! =bvSizeQ) еггог(*'неправильный ррз^ер вектора для +’”); 39
Vec& sum = "new Vec(s); int" sp = sum.v; int* ap a.v; int’ bp = b.v; while (s—) ’sp + + = ’ap + + + "bp + + ; return sum; } Один особенно полезный аспект механизма friend состоит в том, что функция может быть другом двух и более классов. Для лучшего понимания этого рассмотрите определение vector и matrix (матрица), а затем — как определяется функция умножения ($с.8.8). 1.16 РОДОВЫЕ ВЕКТОРЫ "Пока все хорошо, - Вы можете сказать, - но мне нужен один такой вектор для типа matrix, который я сейчас определил". К сожалению, C+ + не дает возможностей определить вектор класса, имеющего тип элементов в качестве аргумента. Как один из способов преодоления этого можно предложить дублировать определение и класса и его функции-элементу. Это не идеальное, но нередко приемлемое решение. Для автоматизации этой задачи можно использовать макропроцессор ($4.7). Например, класс vector -это упрощенная версия клсса, который находится в стандартном заголовочном файле. Можно написать так: tfinclude <vector.h> declare( vector, int); main() vector(int) vv(10); w[2f = 3; } vv[10] = 4; // ошибка диапазона Файл vector.h так определяет макросы, что выражение aeclare(vector,int) расширяется до декларации класса vector практически так как было сделано выше, a implement(vector,int) расширяется до определений функций для этого класса. Поскольку implement(vector,int) расширяется до определений функций, его можно использовать в программе только один раз, тогда кек declare(vector,int) надо помещать в каждый файл, работающий с подобными целочисленными векторами. declare(vector,char); implement(vector,char); Даст (отдельный) тип "вектор символов**. Пример макроса^ реализующего Родовой вектор, приведен в $7.3.5. 40
1.17 ПОЛИМОРФНЫЕ ВЕКТОРЫ С другой стороны, векторный и прочие классы-хранилища можщ определить в терминах указателей на объекты некоторого класса: class common { // общий // ... }; class cvector { common” v; // ... public: cvector(int); common*& elem(int); common’& operator[](int); // ... }; Заметьте, что поскольку в этих векторах хранятся указатёли, а не сами объекты, один объект может одновременно "присутствовать" в нескольких таких векторах. Это очень полезное свойство для классов-хранилищ вроде векторов, связных списков, множеств и т.д. Более того, указателю: на производный класс может быть присвоен указатель на его базовый класс, тем самым cvector из вышеприведенного примера можно использовать для хранения указателей на объекты всех классов, производных от common. Например: class apple : public common { /" яблоко "/ } class orange : public common { /" апельсин ’/ } class apple __ vector : public cvector { public: cvector fruitbowl(IOO); // ваза с фруктами // ... apple aa; / orange oo; // ... fruitbowlfOl = &aa; fruitbowl[1] = &‘oo; Теперь, однако, компилятору уже неизвестен точный тип объекта, введенного в такой класс-хранилище. Например, в предыдущем Примере Вы знаете, что элемент вектора принадлежит к common, но яблоко это или апельсин (apple/orange)? Обычно возможно восстановить точный тип, чтобы обеспечить правильную работу с объектом. Для этого надо либо хранить какую-то информацию о типе объекта в нем самом, либо обеспечить, чтобы в данное хранилище попадали только объекты определенного тина. Последнее можно легко обеспечить с помощью производного класса. 41
НапримеР' можно создать вектор указателей на apple: class apple_vector : public cvector { public: apple“& element i) { return (apple“&) cvector::elem(i); }; конвертируя с помощью нотации конверсии типов - (тип)выражение - common’& (ссылку на указатель на common), возвращаемую из cvector::elem, в applet- Такое применение производных классов дает альтернативу родовым классам. Его немного труднее писать (если только не пользоваться макросами, которые, кстати, и используются для реализации родовых классов; см. $7.3.5), но его преимущество заключается в том, что все производные классы пользуются одним экземпляром функций базового класса. Для такого родового класса, как vector(type), каждый новый тип требует нового экземпляра этих функций (с помощью implement()). Альтернативный подход - хранение данных о типе в каждом объекте - дает нам стиль программирования, который часто называют объектно- ориентированным. 1.18 ВИРТУАЛЬНЫЕ ФУНКЦИИ Допустим, что надо написать программу, выводящую на экран геометрические фигуры. Общие атрибуты фигур представлены в классе shape (форма), частные атрибуты - в особых производных классах: class shape { point center; // центр ч color col; // цвет // ... public: void move(point to) {center = to; draw(); }// двигать point where() { return center; } // куда virtual void draw(); // чертить virtual void rotate(mt); // вращать }; " "■ Функции, которые можно определить без знания о конкретной форме (например, move и where), можно объявлять, как обычно. Остальные объявляются virtual (виртуальными), т.е. они будут определены в производном классе. Например: class circle: public shape { • int radius; public: void draw(); void rotatefint i) {} // ... 42
Если теперь shape _vec - это вектор геометрических' фигур, то можно написать: for (ini i = 0; i < no _ of _ shapes; i++) shape _ vec[i].roiate(45); для того, чтобы повернуть (и перечертить) все фигуры на 45 градусов. Такой стиль написания крайне полезен в интерактивных пррграммах, где объекты разнообразных типов обрабатываются сходным образом некоторым базовым программным обеспечением. В каком-то смысле, для пользователя обычная операция - это указать на объект и сказать ему: "Ты кто?", "Что ты за тип?" или "Делай, что тебе велено!", не давая при этом информации о типе. Эти данные программа может и должна вычислить сама.
7 "И.В.К." 105023 Москва, Мал. Семеновская, д. 5 Тел.: 936-50-67, 311-52-08 Факс: 203-93-55 ГЛАВА 2 ДЕКЛАРАЦИИ И КОНСТАНТЫ В этой главе описаны фундаментальные типы (char, inf, float и т.д.) и фундаментальные способы произведения of них новых типов (функций, векторов, указателей и т.д.). Имя вводится в программу декларацией (объявлением), задающей его тип и, возможно, начальное значение; даются понятия декларации, Определения, области действия имен, срока действия объектов и типа. Описаны обозначения констант в С + +, а также способы задания символьных констант. Примеры простр демонстрируют свойства языка. Более подробный и реальный пример, связанный с выражениями и операторами С + +, дается в следующей главе. Механизмы задания типов, определяемых пользователем, со связанными операциями даются в главах 4, 5 и 6, а здесь не рассматриваются. 2.1 ДЕКЛАРАЦИИ (ОБЪЯВЛЕНИЯ) Прежде чем использовать в программе на C++ какое-либо имя (идентификатор), его надо декларировать, т.е. следует задать его тип, что укажет компилятору, на какую группу сущностей ссылается данное имя. Вот несколько примеров, иллюстрирующих разнообразите деклараций: char ch; int count = 1; char" nameT = "Bjarne"; struct complex { float re, im; }; complex cvar; extern complex sqrt(complex); extern int error _ number; typedef complex point; float real(complex* p) { return p->re; }; const double pi = 3.1415926535897932385; struct user; Как можно видеть по этим примерам, декларацией можно сделать есколько больше, чем поосто ввязать тип с именем. Большинство этих переменной - столько памяти будет выделено. Для real - это 44
указанная функция. Для константы pi это знамение 3.1415926535897932385, Для complex такой объект - это новый тип. Для point - это тип complex, тем самым point становится синонимом для comp 1е/. Только декларации extern complex sqrt(complex); extern int error __ number; struct user; это не определения. Тем самым, объект, который они обозначают, должен быть определен в другом месте. Код (тело) функции sqrt должен быть задан какбй-то другой декларацией, память для целочисленной переменной' error _ number должна быть выделена какой-то другой декларацией error _ number, и ^акая-то другая декларация типа user должна определить, что представляет собой этот тип. В программе на С + + всегда должно быть ровно одно определение, но деклараций может быть много, и все они должны согласовываться по типу обозначаемого объекта, так что в нижеследующем фрагменте есть две ошибки: int count; int count; // ошибка: переопределение extern int error _ number; extern short error _ number; // ошибка: несовпадение типов а в это фрагменте ошибок нет (о применении extern см. $4.2): extern int error _ number; extern int error —number; Некоторые определения задают "значение" определяемых объектов: struct complex { float re, im; }; typedef complex point; float real(complex" p) { return p->re; }; const double pi = 3.1415926535897932385; У типов, функций и констант "значение" постоянно; у неконстантных типов данных первоначальное значение потом можно изменять: int count = 1; char" name = "Bjarne"; // ... count = 2; name = "Marian"; Из определений только char ch; не задает значение. Любая декларация, задающая значение, является определением. 2.1.1 Область действия Декларация вводит имя в область действия, т.е. имя может использоваться только в определенной части текста программы. Для имени, 45
пределенном в функции (часто его называют локальным именем), его °бласть действия распространяется от точки его декларации до конца блока, ° котором сделана эта декларация; для имени вне функции или класса ^часто его называют глобальным именем) его область действия распространяется от точки его декларации до конца файла, в' котором сделана эта декларация. Декларация имени в блоке может скрыть декларацию из включающего блока или глобального имени, т.е. внутри блока имя можнд переопределить и обозначить им иной объект. После выхода из блока это имя вновь получает свое предыдущее значение. Например: int х; // глобальный х К) { int х; х = 1; t } int х; х = 2; } X = 3; int“ р = &х; // локальный х скрывает глобальный х // присвоение локальному х // скрывает первый локальный х // присвоение второму локальному х // присвоение первому локальному х // берет адрес глобального х При написании больших программ без скрытия данных не обойтись. Однако, человек, читающий программу, скорее всего не заметит, что имя было скрыто в блоке, а вызванные этим ошибки очень трудно обнаружить, в основном, из-за их редкости. Следовательно, следует минимально пользоваться скрытием имен. Если Вы используете имена вроде i й х в качестве глобальных переменных или ^локальных переменных в крупной функции, то ждите неприятностей. ’ Скрытым глобальным именем можно пользоваться с помощью оператора разрешения области действия Например: int х; int х = 1; // скрыть гйббальный х } ::х = 2; // присвоить глобальному х Скрытым локальным именем пользоваться нельзя никак* Область действия имени начинается с точки его декларации; это значит, что имя можно использовать даже дЛя задания его собственного начального значения. Например: int х; 46
int x = x; // наоборот } Так писать - не недопустимо, просто глупо, и если Вы все же попробуете, то компилятор предупредит, что х "используется до присвоения значения". Тем не менее, можно сделать так, чтобы одно имя обозначало два разных объекта в блоке, и не пользоваться при этом оператором Например: int х = 11; ю { int у х; int х а 22; // наоборот // глобальный х // локальный х Переменная у инициализируется значением глобального х - 11 - а затем ей присваивается значение локальной переменной х - 22. Считается, что имена аргументов функций декларированы в самом внешнем блоке функции, так что f(int х) int х; // ошибка } является ошибкой* поскольку х дважды определяется в одной области действия. 2.1.2 Объекты и величины lvalue Можно резервировать память и использовать "переменные" без имен, и можно делать присваивание выражениям, имеющим странный вид (например, ’р(а+10] = 7). Следовательно, надо дать имя "чему-то в памяти". Вот что говорится об этом в справочнике по С + +: "Объект - это область памяти; lvalue (левое значение) - это выражение, ссылающееся на объект" ($с.5). Слово lvalue первоначально должно было значить "нечто, что может стоять слева от присваивания". Однако, не всякое "левое значение" может стоять слева от присваивания, lvalue может относиться и к константе (см. $2.4). 2.1.3. Время жизни Объект создается тогда, когда встречается его определение, и уничтожается, когда его имя выходит из области действия, если только программист не задаст иного. Объекты с глобальными именами создаются и инициализируются один раз (только) и "живут" до завершения программы. Таким же образом ведут себя объекты, в декларации которых есть ключевой слово static (статический). Например: 47
,На * Ь’ void К) . int b = 1; //, инициализируется под. каждом вызове f() static ini с. « I; 7/ инициализируется только одцн раз cout << м a s м << а* + . .. ”Ь =.. ," <<,Ь+ + , Л. • << < ”с” << C+ + << н\п"; } main() * white (а < 4) ф; } дает такой вывод: а = 1 b = 1 а = 2 b = 1 • а = 3 Ь- 1 с - 1 с = 2 С а 3<; Статическая переменная (static)» не инициализированная явно, неявнЪ инициализируется нулем ($2.43). , > Кроме того, с помощью операторов newи delete программист может создавать объекты с непосредственно управляемым сроком действия, см. $3.2.4. 2.2 ИМЕНА Имя (идентификатор) состоит из . последовательности букв и цифр. Первый символ •- обязательно буква. Символ подчеркивания _ считается буквой. C+ + не накладывает никаких ограничений на количество символов в имени, но некоторые компоненты конкретной реализации неподвластны автору компилятора (в частности, загрузчик), и у них, к сожалению, иногда бывают ограничения. Некоторые среды исполнительных систем также вынуждают расширять или ограничивать набор символов, разрешенных в составе идентификатора, причём расширения набора символов, (например, допущение символа $ в составе имени) ' приводят к непереносимости программ. В качестве имени нельзя использовать ключевые слова1 С. + + (см. К. 2.3). Примеры имен: hello this _ is i. a _ most _ unusually long _ name DEFINED foO bAr u^name HotseSense varO varl CLASS _ class Примеры последовательностей символов, которые нельзя использовать как идентификаторы: 4).; ; , 012 a fool $sys class Bvar pay.due toolbar .name if 48
Буквы верхнего и нижнего регистров различаются, так что Count и tount - разные имена, но неразумно выбирать имена, которые имеют очень мало различий. Имена, начинающиеся с подчеркивания, принято использовать в специальных целях в среде исполнительной системы, поэтому неразумно использовать такие имена в прикладных программах. При чтении программы компилятор всегда ищет самую длинную последовательность символов, которая может составить имя, поэтому varl (к это одно целое имя, а не имя var и после него число 10, так же как elseif - это одно имя, а не ключевое слово else и за ним - ключевое слово if. 2.3 ТИПЫ С каждым именем (идентификатором) в программе на С + + связан тип. Этот тип определяет, какие операции можно выполнять над этим именем (т.е. над объектом, обозначенным этим именем) и как интерпретируются эти операции. Например: inf error _ number; float real(complex" p); Поскольку error _ number объявлен как целое число (int), ему можно присваивать значения, употреблять его в арифметических выражениях и т.д. С другой стороны, ^функцию real моЗкно вызывать по адресу complex в качестве аргумента. Некоторые имена, вроде int и complex, это имена типов. Обычно имя типа используется, чтобы определить тип другого имени-в декларации. Кроме atorb над именем типа выполняются только операция sizeof (определяет количество памяти, необходимое для размещения объекта данного типа) и new (резервирует свободную память для объектов данного типа). Например: ‘ main() ' к int" р = hew int; cout < < "sizeof(int) = << siieof(iht) "\h"; } ' ■ Кроме того, именем >1типд можно пользоваться для указания на явную конверсию из одного тиггё в Другой ($3.2.4). Например: float f; chai*" р; // ... long II = long(p); // конвертировать p в long int i = int(f); // конвергировать f в int 2.3.1 Фундаментальные типы В C + + есть ряд фундаментальных типов, соответствующих наиболее частым фундаментальным единицам памяти компьютера и наиболее частых способов их использования: J 49
char short int int long int представляют целые числа различного размера, float double представляют числа с плавающей точкой, unsigned char unsigned short int unsigned int unsigned long int представляют целые числа без знака, логические значения, битовые вектора и т.д. Из соображений компактности записи в многословных сочетаниях можно без искажения смысла удалять слово int, тем самым, long значит long int, a unsigned значит unsigned int. Вообще, когда в декларации опущен тип, подразумевается int. Например: const а =1; static х; определяют объекты типа int. Целочисленный тип char - больше всего подходит для записи и обработки символов на данном компьютере, обычно, это байт из 8 бит. Размеры объектов в С.+ + выражаются в величинах, кратных размеру char, так что по определению sizeof(char) = 1. В зависимости от аппаратной части char - целое число со знаком, либо без знака. Тип unsigned char, естественно, всегда без знака, и его использование дает более переносимые программы, но из-за отказа от простого типа char эффективность работы программы может значительно снизиться. Множественность целочисленных типов, типов без знака и типов с плавающей точкой позволяет программисту полнее использовать характеристики аппаратуры. На многих ЭВМ для разных модификаций Фундаментальных типов имеются значительные различия по объему памяти, по времени доступа к памяти и по скорости вычислений. Зная характеристики ЭВМ, обычно легче выбрать, например, соответствующий Целочисленный тип для конкретной переменной. Труднее писать Действительно переносимые программы низкого уровня. Единственное, что можно точно утверждать о размерах фундаментальных типов, это: 1 = sizeof (char) < sizeof (short) < sizeof (int) < sizeof (long) sizeof (float) < sizeof (double) Однако, обычно бывает разумно предположить, что в char могут Размещаться целые числа от 0 до 127- (в этом типе всегда может 50
разместиться символ из набора символов ЭВМ), что short и int имеют минимальный размер 16 бит, что размер int соответствует целочисленной арифметике, и что размер long - минимум 24 бита. Предполагать большее опасно, и даже это практическое правило не универсально. В таблице $с.2.6 приводится таблица аппаратных характеристик для некоторых ЭВМ. Целочисленные типы unsigned представляют собой идеальное средство работы с памятью как с битовым вектором. А вот применять тип unsigned вместо int, чтобы получить еще один бит для представления целых положительных чисел, почти никогда не рекомендуется. Если Вы попытаетесь ограничить какие-то значения положительным диапазоном, объявив переменные unsigned, то как правило, это не удастся из-за правил неявной конверсии. Например; unsigned surprise = -1; не содержит ошибок (правда, компилятор выдаст соответствующее предупреждение). 2.3;2 Неявная конверсия типов В операциях присваивания и выражениях фундаментальные типы можно употреблять в любых комбинациях. Всегда, если возможно, выполняется конверсия значений с тем, чтобы не потерять информацию. Точное изложение правил см. в $с.6.6. В некоторых случаях информация может быть потеряна или даже искажена. Присваивание значения одного типа переменной другого типа, * в представлении которого меньше бит - это верный потенциальный источник неприятностей. Например, предположим, что следующий пример выполняется на ЭВМ с двоичным дополнением целых чисел и 8-битовыми символами: int il = 256 + 255; char ch = il; // ch = = 255 int i2 = ch; // i2 = = ? Один бит (самый значащий!) потерялся при присваивании ch = i1, и в ch будет находится битовая строка "все единицы" (т е. 8 единиц), и ни коим образом это не превратится в 511 при присваивании переменной i2! Но какое же значение окажется у i2? На ЭВМ DEC VAX, где char имеет знак, это будет -1; на ЭВМ AT&T ЗВ20, где char знака не имеет, это будет 255. В C++ нет механизма, встроенного в исполнительную систему, обнаруживающего подобные затруднения, а обнаружение oufti6oK при компиляции вообще вещь трудная, так что программист должен работать тщательно. 2.3.3 Производные типы От фундаментальных (и определяемых пользователем) типов можно произвести другие типы - с помощью операторов декларации “ указатель > & ссылка (обращение по адресу) 51
Г] вектор () функция и механизма определения структур. Например: int" а; float v[10J; char" р[20]; // вектор из 20 указателей на символы void f(int); struct str { short length; char" p; }; Правила составления типов с помощью этих операторов подробно описаны в $с.8.3-4. Основная идея состоит в том, что декларация производного типа отражает его использование. Например: int v[10J; // декларация вектора i = v[3]; // использование элемента вектора int" р; // декларация указателя i = "р; // использование указываемого объекта Суть понимания обозначений производных типов заключен в том, что " и & - это префиксные операторы, а [] и () - постфиксные,' поэтому для описания типов с нетипичными приоритетами операторов надо применять скобки. Например, поскольку у [] приоритет выше, чем у ": int" v[10]; // вектор указателей int ("р)[10];) // указатель на вектор Многие просто помнят, как записываются наиболее частые типы. Можно утомиться писать одну и ту же декларацию для каждого имени, которое надо ввести в программу, особенно, если типы у имен совпадают. Однако, можно объявить несколько имен в одной декларации; вместо одного имени декларация может просто содержать список имен, разделенных запятой. Например, можно объявить два цёлых таким образом: int х, у; // int х; int у; При декларации производных типов следует помнить, что операторы применяются только к отдельным именам (а не ко всем именам в этой Декларации). Например: . jnt" Р/ У»* // int’ р; int у; НЕ int" у; int х, “р; // int х; int" р; int v[10], "р; // int v.[10J; int" p; Считается, что подобные конструкции ухудшают читаемость программы и не Рекомендует их применять. 52
2.3.4 Пустой тип (void) С точки зрения синтаксиса тип void ведет себя как фундаментальный. Его, однако, можно использовать только как часть производного типа; не существует объектов типа void. Он используется в определении функции, не возвращающей значение, или как базовый тип для указателей на объекты неизвестного типа. void f(); // f не возвращает значение void" pv; // указатель на объект неизвестного типа Значение указателя дюбого типа можно присвоить переменной типа void. Сначала может показаться, что это не очень полезно, поскольку от void" нельзя сделать обратную ссылку, но в этом ограничении как раз и состоит полезность типа void". Его первоначальное назначение - передавать указатели функциям, которым не разрешено принимать тип объекта по умолчанию, и возвращать; из функций объекты без типа. Для того, чтобы использовать объект такого типа, требуется явное преобразование типов. Обычно подобными функциями пользуются на самом нижнем уровне системы при работе с реальными аппаратными средствами. Например: ' void’ allocate(int size); void deallocate(void"); Ю { int" pi = (int")allocate(10*sizeof(int)); char" pc = (char’)allocate(l 0); deallocate(pi); * deallocate(pc); 2.3.5 Указатели Для большинства типов Т: Т" - это указатель на Т. Иными словами, переменная типа Т" может содержать адрес объекта типа Т. Для указателей на векторы и указателей на функции, к сожалению, требуется более сложное обозначение: int’ pi; char"" срр; // указатель на указатель на char int ("vp)[10]; // указатель на вектор из 10 целых int ("fp)(char, char"); // указатель на функцию // принимающую аргументы (char, char") // и возвращающую int Основная операция над указателем - это обратная ссылка, т.е. обращение по адресу объекта, на который указывает указатель. Это операция называется также косвенная адресация. Оператор 'косвенной адресации - унарный (префиксный) ". Например: 53
char cl = 'a' ■.* char" p = &c1; // p содержит адрес cl char c2 = "p; // c2 = a Переменная, на которую указывает р, это cl, а значение, хранящееся в cl, тОн 'а, тек что значение "р, присвоенное с2, это 'а'. Над указателями можно выполнять некоторые арифметические операции. Вот, например, функция, подсчитывающая количество символов в строке (не учитывая конечный 0): int strlen(char" р) int i ,,f 0; while ("p + +) i + +; return i; } По-другому получить длину строки можно, сначала найдя конец строки и вычтя затем адрес начала строки из адреса ее конца: int strlen(char" р) char" q = р; while ("q + +) ; return q—p—1; ?f; Указатели на функции могут быть весьма полезны; они рассматриваются в $4.6.7. 2.3.6 Векторы Для типа Т: Tfsize] - это тип "вектор из size элементов типа Т". Элементы имеют номера оу 0 до sjze-1. Например: float v[3J; ,, // вектор из трех чисел с плавающей точкой: //v[0], v[1], v[2] int a[2][51; // два вектора из пяти целых чисел char" vpc[32]; // вектор из 32 указателей на символы Цикл вывода целочисленных значений букв нижнего регистра можно написать так: ч. j extern int strlen(char“); char alpha[] = "abcdefghijklmnopqrstuvwxyz"; !? main() < int sz ss . strlen(alpha); 54
for (int i = 0; i<sz; i + +) { char ch = alphafl]; cout << << chr(ch) << "T <<" = " « ch < < " * 0” << oct(ch) << " a Qx “ < < hex(ch) << "\n"; 1 } ‘ Функция chr() возвращает строковое представление небольшого целого числа; например, chr(80) это ЛР" на ЭВМ с набором символов стандарта ASCII. Функция oct() дает восьмеричное представление целого аргумента, а функция hex() дает шестнадцатиричное представление целого аргумента; chr(), octQ и. hex() декларированы в <slream.h>. Функция strlenQ использована для подсчета символов в alpha; вместо этого можно было воспользоваться размером alpha ($2.4.4). Если установлен стандарт ASCII, то вывод будет такой: а' = 97 = 0141 = 0x61 " ■ т- \ Ъ' = 98 = 0142 = 0x62 'с = 99 = 0143 = 0x63 Обратите внимание, что нет надобности указывать размер вектора alpha; компилятор подсчитывает количество символов в символьной строке, указанное в инициализаторе. Применение строк в качестве инициализатора вектора символов - это удобный, но к сожалению, уникальный 'способ использования строк. Аналогичного присваивания строки вектору* нет. Например: ; • ->л char v[9J; v = "a string"; • // ошибка • - это ’ ошибка, поскольку присваивание для векторов не определено/ Очевидно, что только строки подходят- для инициализации векторов символов; для других типов требуется более трудоемкий способ обозначения. Это обозначение9 можно применять и для символьных векторов. Например: . Г int vl П = { 1, 2, 3, 4}; int v2[j = { а', 'Ь', с', '<Г }; char v3[l = f 1, 2, 3, 4 }; 9 char v4[] = { a', 'b', 'с', d' }; Обратите внимание, что v4 - вектор из четырех (а не пяти) символов; он не завершается нулем, как того требуют соглашение и все библиотечные процедуры. Кроме того, обычно таким обозначением пользуются только для статических объектов. Многомерные массивы представлены векторами векторов, и использование запятой для указания границ массивов, что принято в некоторых других, языках, вызывает сообщения’ об ошибках при компиляции, поскольку запятая (,) - это оператор последовательности (см. $3.2.2). 55
Например, попробуйте написать так: int bad[5,2J; // ошибка и так: int v[5J[2]; int baa = v[4(t]; int good = v[4jt); // ошибка /7 правильно Декларация char v[2][5); объявляет вектор из двух элементов, каждый из которых - это вектор типа char[5]- В нижеприведенном примере первый из этих векторов инициализируется первыми пятью буквами, а второй - пятью первыми цифрами. ’ char v[2][5) » { 'а\ *b\ V» 'd‘, e\ ЧГ, T, '2'. 37 '4' }; main() { for (int i = 0; i<2; i+ +) { for (int j = 0; j<5; j+ +> cout << "v[" << i << ”F << i << "]=" << chr(v[i][j}) << " Л* cout < < u\n"; Это даст следующее: vrOHOJxa vFOimxb vF01J21 = c vpj[3]*d vf01f41 = e v[1][01-0 v(lj[2]«2 vM[3]»3 v[1I(4] = 4 2.3,7 Указатели и векторы В C++ указатели и векторы связаны очень тесно. Имя вектора можно использовать и как указатель на его первый элемент, так что пример с алфавитом можно переписать вот так: char alphaf} » “abcdefghijklmnopqrstuvvyxyz''; char1 p x alpha; char ch; < while (ch s= ’p + +) ; cout < < chc(ch) < < *' = 11 << ch << ” = O'* << oct(ch) < < “\nH; 56
Декларацию р также можно переписать char* р = &alpha[O]; Эта эквивалентность широко используется в вызовах функций, когда векторный аргумент всегда передается как указатель на первый элемент вектора; тем самым, в примере extern int strlen(char’); char v[] = "Annemarie"; char" p = v; strlen(p); strlen(v); '* в strlen передается одно и то же значение. Загвоздка в том, что это невозможно обойти; т.е, нет способа объявить функцию так, чтобы при ее вызове вектор v копировался ($4.6.3). Результат применения к указателям арифметических операторов + + + или — зависит от типа указываемого объекта. При применении арифметического оператора к указателю р на объект типа Т" предполагается, что р указывает на элемент вектора объектов типа Т; р + 1 относится к следующему элементу этого вектора, а р-1 - к предыдущему Элементу. При этом подразумевается, что величина р + 1 на sizeof(T) больше величины р. Например, при выполнении main() char cv[10]; int iv[10]; char" pc = cv; int" pi = iv; cout < < "char" " < < long(pc + l)-long(pc) << "\n"; ; } cout < < "inf " < < long(pc + 1}-long(pc) << "\n"; получается char" 1' int- 4 поскольку на ЭВМ на каждый символ отводится один байт, а на каждое целое число - по четыре байта. Перед вычитанием значения указателей были сконвертированы в тип long посредством явной конверсии типов ($3.2.5). Они конвертируются в long, а не в "очевидный" тип int, поскольку на некоторых ЭВМ указатель не размещается в типе int (т.е. sizeof(int) < sizeof(char")). Вычитание указателей определено только тогда, когда оба указателя указывают на элементы одного вектора (хотя в языке нет средств проверки, что это действительно так). При вычитании одного указателя из другого 57
езультатом является количество элементов вектора между - двумя Указателями (целое число). К указателю можно прибавить целое число или У-честь целое из него; в обоих случаях результат -это значение указателя, Если полученное значение не указывает на элемент того же вектора, что вектор-оригинал, то использование полученного значения приведет к непредсказуемым результатам. Например: int v1[10l; int v2[10J; int i = &v1[5]-&v1[3J; // 2 i = &v1[5]-&v2[3]; // результат неопределен int" p = v2 + 2; // p = = &v2[2] p = v2-2; // “p неопределено 2.3.8 Структуры Вектор - это совокупность элементов одного типа; struct (структура)- это совокупность элементов (почти) произвольных типов. Например: struct address { .... char“ name;' long number; char’ street; char’ town; char’ state[2]; int zip; // "Джим Денди" // 61 // "ул. Южная" // "Нью-Провиденс" // N' Т // 7974 определяет новый тип, называющийся address и состоящий из пунктов, которые нужно заполнить, чтобы; послать Кому-нибудь "''nMCwA’6'".-(h2i'dFess недостаточно обобщён, чтобы соответствовать всем возможным почтовым адресам, но как пример он достаточен). Обратите внимание на точку с запятой в конце, это один из немногих случаев в С + +, когда после фигурной скобки должна быть точка с запятой, и пользователи нередко это забывают. Переменные типа address . можно объявлять точно тёк же, как и Другие переменные, а доступ к отдельным их элементам осуществляется с помощью оператора точка (.). Например: address jd; jd.name = "Джим Денди"; jd.number = 61; Нотацию, использующуюся для инициализации векторов, можно применять Для инициализации переменных структурных типов. Например; - address jd = { "Джим Денди", 61, "ул. Южная", 58
Нью-Провиденс", {'N', 'J'}, 7974 }; Однако обычно лучше пользоваться конструктором ($5.2.4). Обрати^ внимание, что jd.state нельзя инициализировать строкой "NJ". Строци завершаются символом '\0', так что в "NJ" три символа, т.е. на один символ больше, чем помещается в jd.state. Доступ к структурным объектам часто выполняется с помощь^ оператора ->. Например: void print _ addr(address’ р) cout << p->name << "\n" << p-> number <<""<< p->street << "\n" < < p->town < < "\n" << chr(p->state[OJ) << chr(p->state[1 ]) j <<""<< p->zip << "\n"; Объекты структурных типов можно присваивать, передавать в качестве аргументов' функций и возвращать как результат функции. Например: address current; address set _ current(address next) address prev = current; ; current = next; return prev; Другие допустимые операции, как, например, сравнение (= = и ! =), не определены. Пользователь, однако, может сам определить эти операции, см. главу 6. Размер объекта структурного типа невозможно вычислить просто по сумме его размеров его элементов. Это объясняется тем, что на многих ЭВМ объекты определенных типов размещаются только в границах, зависящих от архитектуры (типичный пример: целое число может разместиться только с границы слова), или они обрабатываются гораздо эффективнее, если удовлетворяют этим условиям. Это приводит к. "дырам" в структурах. Например, (иногда) sizeof(address) - это 24, а не 22, как можно было ожидать. Обратите внимание, что именем типа можно пользоваться сразу же после его появления в программе, а не после завершения декларации. Например: struct link { link" previous; link" successor; };
ы вые объекты структурного типа нельзя объявлять до завершения декларации, так что struct’ no _ good { по_good member; }; ошибочна (компилятор не может определить размер no _ good). Для того, чтобы позволить двум структурным типам ссылаться друг на друга, можно просто объявить имя как имя структурного типа. Например: // определяется позже list; link { link" pre; link" sue; list" member _ of; struct struct }; struct list { link" head; не первая декларация list, декларация link вызвала бы Если бы синтаксическую ошибку. 2.3.9 Эквивалентность типов Два структурных Типа различны, даже если элементы. Например: они имеют одинаковые struct si { int а; |; struct s2 { int a; }; - это два разных типа, поэтому si х; s2 у = х; // ошибка: несоответствие типов Структурные типы отличаются к тому же от поэтому фундаментальных типов, si х; int i = х; // ошибка: несоответствие типов Можно, тем не менее, объявить новое имя типа и не вводить новый тип. декларация с ключевым словом typedef перед ней объявляет не новую временную данного типа, а новое имя типа. Например: typedef char" Pchar; Pchar pl, P2; char” p3 = pl; 3 Зак. 1927 60
2.3.10 Ссылки (обращения по адресу) Ссылка - это другое имя объекта. Первоначальная цель ссылок определять операции над типами» определяемыми пользователем - это рассматривается в главе 6. Ссылки удобны и как аргументы функций Обозначение Х& означает ссылку на X. Например: int i = 1; int& г = int X = г; г = 2; // г и i теперь ссылаются на один й тот же int // х ‘ = 1 // i = 2; Ссылку надо инициализировать (должно быть нечто, чему она служит именем). Обратите внимание, что инициализация ссылки - это вовсе не присваивание ссылке. Несмотря на внешний вид, никакие операторы не изменяют ссылок. Например: int ii = 0; int& rr = ii; rr+ +; // ii увеличивается на 1 правильно, но rr + + не увеличивает ссылку rr; + + относится скорее к int, которым в данном случае оказался ii. Следовательно, величину ссылки нельзя изменить после инициализации; она всегда ссылается на объекты, адресом которых она инициализирована. Чтобы получить указатель на объект, обозначенный ссылкой rr, надо написать &гг. Очевидная реализация ссылки - это (константный) указатель, от которого при каждом его использовании делается обратная ссылка. Тогда инициализация ссылки становится тривиальной задачей, если инициализатор - это lvalue-величина (объект, адрес которого можно получить, см. $с.5). Однако, инициализатор для Т& - необязательно должен быть lvalue- величиной или даже иметь тип Т. В таких случаях, нужно: [1] Во-первых, при необходимости применить преобразование типов (см. $с.6.6-8, $с.8.5.6); Г21 Затем поместить результат во временную переменную, и [3] Наконец, адрес этого можно использовать как значение инициализатора. Рассмотрим объявление doubled dr = 1; Ее интерпретацией будет: double’ drp; // ссылка, представленная указателем double temp; temp = double(l); 61
drp = fctemp; Гсылку можно применить для реализации функции, которая должна менять значение своего аргумента. int х = 1 ; void incr(int& аа) { аа + + ; } incr(x); // х = 2 Семантика передачи аргумента определена как семантика инициализации, поэтому при вызрве аргумент incr - аа - становится иным именем для х. Однако, по соображениям читаемости программы, лучше избегать функций, изменяющих свои аргументы. Желательно либо явно возвращать значение из функции, либо получать аргумент в виде указателя: int х = 1; int next(int р) { return р + 1; } х = next(x); // х = 2 void inc(int" p) { (’p) + +; } inc(Ax); // x =3 Ссылки также полезны для определения функций, которые можно употреблять как слева, так и справа от оператора присваивания. Здесь, опять же, много интересных случаев использования ссылок можно найти при создании нетривиальных типов, определяемых пользователем. Давайте в качестве примера определим простой ассоциативный массив. Для начала определим структуру pair таким образом: struct pair { char* name; int val; Основная иде^С состоит в том, что со строкой связано целое число. Легко определить функцию - find() - поддерживающую структуру данных из pair Для каждой новой, отличной строки, подающейся ей на вход. Для краткости приведем очень простую (и неэффективную) реализацию функции: large = 1024; pair vec[large + 1]; find(char’ поддерживает множество пар (pair) отыскивает р, возвращает его "пару", если находит, в противном случае возвращает неиспользуемую "пару" for (int i = 0; vec[i].name; i + +) if (strcmp(p,vec[i].name) = =0 return &vec[i]; pair' /“
if (i = = large) return &vec[large-1J; return &vec[ij; } Эту функцию можно использовать в функции value(), которая поддержив^ массив целых чисел, индексированных символьными строками (а н наоборот): irit& value(char" р) pair“ res = find(p); if (res-эмпате = = 0) { // раньше не видали: инициализировав res->name = new char[strlen(p) + 1]; strcopy(res-> name,p); res->val = 0; // начальное значение: 0 } return res->val; } Для данной строки аргумента value() находит соответствующи! целочисленный объект (а не значение соответствующего целого); затем ohi возвращает ссылку на него. Это можно использовать так: const МАХ = 256; // длиннее самого длинного слова main() // подсчитывает, сколько раз встречается каждое введенное слово char buf[MAXJ; while (cin>>buf) value(buf) + + ; for (int i = 0; vec[i].name; i + +) cout < < vec[i].name < < " << vec[i].val < < "\n"; При каждом выполнении цикла из стандартной строки ввода cin считывается одно слово в but (см. главу 8), а затем изменяется счетчик, связанный сс словом через find(). Наконец, печатается получившаяся таблица различны^ введенных слов с их частотами. Если, например, введено: аа bb ЬЬ аа аа ЬЬ аа аа то программа выдаст: аа: 5 ЬЬ: 3 Вышеприведенный пример легко оптимизировать для работы ? подлинным типом ассоциативного массива благодаря классу с переназначаемым оператором выбора [] ($6.7). 63
2.3.11 Регистры На многих ЭВМ с разной архитектурой доступ к (небольшим) объектам шолняется заметно быстрее, если они находятся в регистрах. В идеале омпилятор должен сам определять оптимальную стратегию использования егистров на ЭВМ, для которой компилируется программа. Это, однако, ^тривиальная задача, поэтому иногда имеет смысл программисту самому 'амекнуть об этом компилятору - объявив объект register. Например: register int i; register point cursor; register char" p; егистровыми декларациями надо пользоваться только в тех случаях, когда ффективность действительно важна. Объявление всех переменных register 1риведет в беспорядок текст программы и даже может увеличить размер ода и время выполнения (обычно для пересылки объекта в регистр и из регистра требуются команды). Адрес имени нельзя объявить register, кроме того, регистровая 1еременная не может быть глобальной. 2.4 КОНСТАНТЫ В C++- приняты нотации rfri* значений фундаментальных типов: имвольные константы, целые константы и константы с плавающей точкой, роме того, zero() можно применять как константу для указателя любого ипа, а символьные строки - это константы типа char[J. Можно задавать и имволические константы. Символическая константа - это имя, значение оторого нельзя изменить в его области действия. В С + + существуют три ида символических констант: (1) любой величине любого типа можно дать мя и использовать как константу, добавив к ее декларации ключевое слово onst; (2) множество целочисленных констант можно определить как еречисление; (3) имя любого вектора или функцйи - это константа. 2.4.1 Целочисленные константы Целочисленные константы встречаются в четырех видах: десятичные, осьмеричные, шестнадцатиричные и символьные. Десятичные константы ^пользуются чаще всего и имеют привычный вид: О 1234 976 12345678901234567890 ип десятичной константы — int при условии, что она умещается в int, в |ДадИВНОМ слУчае это Ьпд. Если для очень большой константы на данной “М нет соответствующего представления, компилятор обязан предупредить 0 этом. Константа, начинающаяся с нуля с х после него (Ох) - это встнадцатиричное число (по основанию 16), а константа, начинающаяся с УЛя с цифрой после него это восьмеричное число (по основанию 8). т примеры восьмеричных констант: 64
о 02 077 0123 их десятичные эквиваленты - 0, 2, 63 и 83. В шестнадцатиричной запис эти константы выглядят так: 0x0 0x2 Ox3f 0x53 Буквы а, b, с, d, е и f или их аналоги в верхнем регистре- представляют соответственно, 10, 11, 12, 13, 14 и 15.' Восьмеричными и шестнадц^ тиричными числами удобнее всего пользоваться для представления битовые строк; попытки выражать ими настоящие числа могут привести н недоразумениям. Например, на ЭВМ с представлением int как дополнен^ 2 16-битовое целое Oxffff - это отрицательное десятичное число -1; а если бы для представления це/ioro использовалось больше битов, то получилось бы 65535. 2.4.2 Константы с плавающей точкой Константы с плавающей точкой имеют тип double. В этом случае компилятор также обязан предупредить, если константы с плавающей точкой превышают максимальную представимую величину. Вот примеры некоторых констант с плавающей точкой: 1.23 .23 0.23 1. 1.0 1.2е10 1.23е-15 Обратите внимание, что в середине константы с плавающей точкой нельзя! вставлять пробел. Например, 65.43 е-21 - это не константа с плавающей] точкой, а четыре отдельных элемента языка j 65.43 е-21 ] i что вызовет синтаксическую ошибку. 1 Если Вам нужна константа с плавающей точкой типа float, ее можно задать так ($2.4.6): const float pi8 = 3.14159265; 2.4.3 Символьные константы Хотя в С + + и нет отдельного символьного типа данных, а есть целочисленный тип, в который можно записывать символы, в этом языке есть особая удобная нотация для записи символов. Символьная константа* это символ, заключенный в одинарные кавычки; например, 'а' и О- Подобные символьные константы - это на самом деле символические константы для целочисленных значений символов из набора символов той ЭВМ, на которой должна выполняться программа на C++ (что совсем необязательно совпадает с набором символов той ЭВМ, на которой программа компилируется). Поэтому, если на Вашей ЭВМ используете^ набор символов ASCII, то значение 0' - это 48, однако, если на Вашей ЭВМ принят код EBCDIC, то это 240. Используя символьные константы вместо их десятичных представлений, Вы получите более переносимые 65
программы котори* 8 . Кроме того, некоторые символы имеют стандартные имена,- в качестве управляющего символа используется обратная косая \Ь' возврат на шаг, забой \f перевод формата '\п' новая строка (перевод строки) z\r' возврат каретки z\f горизонтальная табуляция '\v' вертикальная табуляция Z\V обратная косая черта '\" одинарная кавычка (апостроф) '\"z двойные кавычки '\0' нулевой символ, целочисленное значение О Несмотря на внешний вид, это одинарные символы. Кроме того, символ можно представить как восьмеричное число из одной, двух или трех цифр (и \ перед ними) или как шестнадцатиричное число из одной, двух или трех цифр (и \х перед ними). Например: '\6' Лхб' 6 '\60' ЛхЗО' 48 z\137' \x05f 95 код АСК в ASCII О' в ASCII в ASCII Так можно представить любой символ из набора символов ЭВМ, в частности, вставить такой символ в символьную строку (см. следующий раздел). В результате использования числовой нотации для символов программа становится непереносимой на ЭВМ с другим набором символов. 2.4.4 Строки Строковая константа - это последовательность символов, заключенная в двойные кавычки: "это строка" Всякая строковая константа включает в себя на один символ больше, чем кажется; все строки завершаются нулевым символом '\0', имеющим значение 0. Например: 5; sizeof("asdf") ^ип строки - это "вектор соответствующего количества символов", так что сЬП ~ это char[5]. Пустая строка записывается как ”" (и тип ее - arU])- Обратите внимание, что для каждой строки s: strlen(s) = = sizeof(s)-1, °скольку s+rlen() не учитывает; завершающий 0. об Соглашение о представлении непечатаемых символов с помощью об*33™0^ кос°й черты распространяется и на символы внутри строки. Таким .Разом в СТРОКУ можно вставить и кавычки, и управляющий символ и Р?Тна* косая черта". Чаще всего этим пользуются для включения символа °и строки - \nz. Например: 66
cou+< < "писк в конце сообщения\007\п"; где 7 - это значение символа ASCII BEL ("звуковой сигнал"). В строку нельзя вставить "настоящий" символ перехода на нову* строку: "это не строка, а синтаксическая ошибка" Однако, в строке может быть обратная косая черта и за ней - переход н новую строку, и то и другое будет игнорировано. Например: cout << **вот это \ нормально" *5 напечатается так: вот это нормально Перевод строки и перед ним - обратная косая черта не приводит к тому; что в строковой константе появится символ перевода на новую строку; эт< просто нотационное удобство. В строку можно вставить и нулевой символ, но большинство программ не догадается, что после него еще есть символы. Например, строку "asdfXOOOhjkl" стандартные функции вроде strcpy() и strlenf) буду воспринимать и обрабатывать как "asdf". 5 При включении числовых констант в строку с восьмеричным илй шестнадцатиричным обозначением в числе лучше всегда писать три цифры. Числовая нотация и так достаточно трудна для восприятия и не стоит добавлять к ней заботы, как понять: символ после константы - это цифра или нет. См. примеры: char vl char v2 char v3 = ”a\x0fah\0129"; // a' '\xfa' 'h' '\12' '9' = "a\xfah\129"; // a \xfa' 'h' '\12' '9' = "a\xfad\127"; // a' '\xfad' '\\27' Обратите внимание, шестнадцатиричной нотации из двух цифр недостаточна на ЭВМ с 9-битовыми байтами. 2.4.5 Нуль Нуль (0) можно использовать как константу любого типа} целочисленного, с плавающей точкой или указателя. Ни один объект нЧ размещается по адресу 0. Тип нуля определяется по контексту. Обычно (но не обязательно) он представлен битовой строкой "все нули 1 соответствующего размера. d 2.4.6 Const ; i К декларации объекта можно добавить ключевое слово const,*и объеК! 67
сганеТ константой, а не переменной. Например: const int modet^= 145; const int v[] = { 1, 2, 3, 4 }; скольку такому объекту нельзя ничего присвоить, его надо П°иЦИализировать. Если нечто объявлено const, то это - гарантия, что его ИНдчение не изменится в его области действия: model = 165; model + +; // ошибка // ошибка Обратите внимание, что const модифицирует тип, т.е. const ограничивает возможности использования объекта, а не задает способ размещения константы в памяти. Например, весьмё разумно, а иногда и полезно, объявить функцию, которая возвращает const: const char" peek(int i) return private[i]; } функцию вроде этой можно использовать, чтобы разрешить чтение строки, которую нельзя изменить. Компилятор может несколькими способами воспользоваться тем обстоятельством, что объект является константой (правда, это зависит от качества компилятора). Самое очевидное: обычно компилятору не требуется отводить память для константы, поскольку ее значение уже известно. Далее, инициализатор константы - это часто (но не всегда) константное выражение; в таком случае его значение можно вычислить во время компиляции. Однако, вектору констант обычно приходится отвести память, поскольку в общем случае компилятор не может вычислить, к каким элементам вектора делается обращение из выражений. Тем не менее, на многих ЭВМ даже в этом случае можно повысить эффективность, разместив векторы констант в память, защищенную от записи. В работе с указателем участвуют два объекта: сам указатель и указываемый объект. Если перед декларацией указателя поставить const, то константой становится объект, а не указатель. Например: const char" pc = "asdf"; // указатель на константу рс[3] = а'; //ошибка pc = "ghjk"; // ошибки нет Объявить константой сам указатель, а не указываемый объект, можно с °мощью оператора "const. Например: char "const ср = "asdf"; // константный указатель СР[3] = 'а'; // ошибки нет СР = "ghjk"; // ошибка Объявить константами и указатель, и указываемый объект можно, объявив 68
их константами одновременно. Например: | const char ’const срс = "asdf"; // константный указатель 1 ср[3] = а'; // ошибка I ср = "ghjk"; // ошибка Объект, который при доступе через один указатель считается констант0| может быть переменной при доступе другим образом. Это особещ] полезно при работе с аргументами . функций. Благодаря декларацн аргумента-указателя const функция получает запрет на измене^ указываемого объекта. Например: char" strcpy(char“ р, const char" q);// запрет на изменение "q Адрес переменной можно присвоить указателю на константу: никакой бед от этого не случится. А вот адрес константы нельзя присвоил неограниченному указателю, поскольку это позволит изменять знамени объекта. Например: int а = 1; const с = 2; const" pl = &с; // ошибок нет const" р2 = &а; // ошибок нет int" рЗ = &с; // ошибка ■рЗ = 7; // изменяет значение с Как обычно, если в декларации опущен тип, то по умолчанию принимаете! тип int. 2.4.7 Перечисление Имеется другой способ определения целочисленных констант, которым^ нередко удобнее пользоваться, чем const. НаприМер: enum { ASM, AUTO, BREAK }; определяет три целых константы, называющихся перечислителями) (нумераторами), и присваивает им значения. Поскольку значений перечислителей приписываются с увеличением начиная по умолчанию с О» то это эквивалентно следующему: const ASM = 0; const AUTO = 1; const BREAK = 2; Перечислению можно дать имя. Например: enum keyword { ASM, AUTO, BREAK }; Имя перечисления становится синонимом int, а не нового типа. переменной имя keyword вместо int, можно дать понять и пользователю 69
илятору о предполагаемом использовании переменной. Например: КОМП keyword key; switch (key) { case ASM: // делает что-то break; case BREAK; // делает что-то break; } вызовет предупреждение компилятора/ поскольку в тексте обрабатываются только два из трех значений keyword. Присваивать значения перечислителям' можно и явным образом. Например: enum inti 6 { sign = 0100000, most _ significant = 040000, least _ significant = 1 ); Эти значения необязательно должны различаться, возрастать или быть положительными. 2.5 ЭКОНОМИЯ ПАМЯТИ При программировании нетривиальных прикладных задач неизбежно настает момент, когда требуется больше памяти, чем возможно или допустимо. Есть два способа получить больше памяти из наличной: [1] Размещать более одного малого объекта в байте; [2] Использовать для размещения разных объектов в памяти одну и ут же область памяти. ' Первый способ реализуется с помощью полей (field), а второй - с помощью объединений (union). Эти конструкты описываются в следующих Разделах. Поскольку обычно их роль состоит только в оптимизации программы, и чаще всего они непереносимы, то прежде чем воспользоваться этим средством, программисту надо как следует подумать, Часто лучшим решением оказывается изменение способа обработки данных; Например, больше работать с динамически отводимой памятью ($3.2.6), чем с заранее резервированной статической памятью. ^•5.1 Поля Довольно экстравагантно использовать char для представления °ичной переменной, например переключателя "включено/выключено", но 70
char - мельчайший объект, которому в C++ можно OTBecTJ самостоятельный фрагмент памяти. Однако, можно объединить нескольцЛ малых объектов в struct - как поля. Элемент определяется как поле, коГдЛ после его имени указывается, сколько бит он должен занимать. Допускаю^ неименованные поля, они не влияют на значение именованных полей, ими можно пользоваться для улучшения расположения полей в каком^. машинно-зависимом смысле: struct sreg { unsigned enable : 1; unsigned page : 3; unsigned : 1; // не используется unsigned mode : 2; unsigned : 4; // не используется unsigned access : 1; unsigned length : 1 ; unsigned non __ resident : 1; }; Кстати, это формат нулевого регистра состояний ЭВМ DEC PDP11/45 (при,) допущении, что в слове поля располагаются слева направо). Этот пример^ показывает и второе основное применение полей: именовать части внешнего^ чуждого формата. Поле должно иметь целочисленный тип и использоваться^ в программе так же, как и прочие целые числа за исключение^ невозможности получить адрес поля. В ядре операционной системы или в, отладчике, можно применять тип sreg таким образом: sreg’ srO = (sreg“)0777572; // ... if (srO-> access) { // неразрешенный доступ // разобраться с этим вопросом srO-> = 0; ) Однако, упаковка нескольких переменных в один байт необязательно экономит память. Это экономит память для данных, но при этом на большинстве ЭВМ увеличивается размер кода для работы с этими переменными. Известны случаи, когда программы значительно уменьшались в объеме после переноса двоичных переменных из битовых полей в символы! Более того, обычно можно гораздо быстрее получить доступ к char или int, чем к полю. Поля - это просто удобная сокращенная запись при применении логических операторов для извлечения информации из части слова и записи в него информации. 2.5.2 Объединения Рассмотрим проектирование таблицы символов, в которой каждая запись содержит имя и величину, и эта величина - либо строка, либо цело6 число: struct entry { 71
char" name; char type; • ‘ char" string— value; // используется, если type= = 's' int int _ value; // используется, если type = = i' -> }; void print _ entry (entry* p) switch (p->type) { case 's': cout << p-> string _ value; break; case 'i': cout <<p-> int —value; break; default: cerr < < "неверный тип\п"; . break; } ) Поскольку одновременно string —value и int —value никогда не могут использоваться, то ясно, что память расходуется впустую. Это легко можно исправить, указав, что обе переменных - элементы union (объединения), вот так: struct entry { char’ name; char type; union { char" string— value; // используется, если type = = s' int int —value; // используется, если type= = i' Таким образом, вся программа работы с entry не меняется, но при этом обеспечивается, что при выделении памяти для entry string —value и •nt-value расположатся по одному и тому же адресу. При этом подразумевается, что что в совокупности все элементы объединения занимают столько же памяти, сколько и самый большой его элемент. Работать с объединением так, чтобы значение считывалось посредством того элемента, через который оно было записано - это чистая оптимизация. Однако в крупных программах нелегко обеспечить, чтобы объединение ^пользовалось только так, и в результате неправильного применения могут возникнуть малозаметные ошибки. Объединение можно заключить в блок, так что будет гарантировано правильное соответствие между полем типа и типами элементов объединения ($5.4.6). Иногда объединения применяют для "конверсии типов" (чаще всего тим занимаются программисты с опытом работы на языках без средств у^®еРсии типов, где приходится обманывать компилятор). Например, на АХ этот фрагмент "конвертирует" int в int’, просто полагаясь на побитную ЭКвивалентность: 72
struct fudge { union { int i; int’ p; }; }; fudge a; a.i = 4096; int" p = a.p; // неудачное применение Это, между прочим, и не настоящая конверсия: на некоторых ЭВМ i.nt и inf различаются по размерам, а на других ЭВМ целое число не может располагаться по нечетному адресу. Подобное применение объединения непереносимо, а конверсия типов задается явным и переносимым образом ($3.2.5). Иногда объединения намеренно применяют во избежание конверсии типов.Например, кто-то может использовать fudge, чтобы создать представление нулевого указателя: fudge.р = 0; . 1 int i = fudge.i; // i - это необязательно 0 г Кроме того, объединение можно именовать, т.е. дать ему полноправный тип. Например, fudge можно объявить так: union fudge { int i; int" p; и, как и выше, (неправильно) использовать. Тем не менее, существуют Оправданные применения именованных объединений, см. $5.4.6. 2.6 УПРАЖНЕНИЯ 1. ("1) Запустите программу "Hello, world" ($1.1.1). 2. ("1) Для каждой декларации из $2.1 сделайте следующее: если декларация - это не определение, напишите для нее определение. Если декларация - это определение, напишите такую декларацию, которая не была бы определением. 3. ("1) Напишите декларации для следующих типов: указатель на символ,вектор из 10 целых чисел, ссылка на вектор из 10 целых чисел, указатель на вектор из символьных строк, указатель на указатель на символ, целочисленная константа, указатель на целочисленную константу, константный указатель на целое число. Проинициализируйте каждый из них. 4. (“1.5) Напишите программу, которая печатает размеры фундамен¬ тальных типов и типа указателя. Воспользуйтесь оператором sizeof. 5. ("1-5) Напишите программу, которая печатает буквы ицифры 'О'...'9' и их целочисленные значения. Выполните то же для прочих 73
печатаемых символов. Вы полните то же, используя шестнадцатиричную нотацию. г (“1) Распечатайте битовое представление указателя 0 на Вашей ЭВМ. Указание: см. $2.5.2. 7 ("1.5) Напишите функцию, которая печатает порядок и мантиссу аргумента double. > q ("2) Укажите минимальные и максимальные значения следующих типов на Вашей ЭВМ: char, short, int, long, float, double, unsigned, char’,int" и void". Есть ли какие-нибудь еще ограничения на их значения? Например, может ли int" иметь нечетное значение? С каким выравниванием располагаются они в памяти? Например, может ли int располагаться по нечетному адресу? 9. ("1) Укажите максимальную длину локального имени, которое можно) использовать в программе на C++ на Вашей ЭВМ? Укажите максимальную длину внешнего имени, которое можно использовать в программе на С + + на Вашей ЭВМ? Есть ли ограничения по составу символов, которые можно использовать, в имени? Ю. ("2) Определите one так: const one = 1; Попробуйте изменить значение one на 2. Определите num так: const num[] = {1,2}; ■ Попробуйте изменить значение num[1] на 2. 11. (*1) Напишите функцию, которая выполняет обмен значении между двумя целочисленными переменными. В качестве типа аргумента возьмите int". Напишите другую функцию обмена; используя тип аргумента int&. 12. ("1) Какой размер имеет вектор str в следующем примере: char str[] = "краткая строка"; Какую длину имеет строка "краткая строка"? 13. ("1.5) Задайте таблицу названий месяцев в году и количества дней в них. Запишите эту таблицу. Выполните это двумя способами: сначала с помощью вектора названий и вектора количества дней, а затем с помощью вектора структур, где в каждой структуре содержится название месяца и количество дней в нем. 14- (“1) С помощью typedef дайте определения типам: символ без знака, символьная константа без знака, указатель на целое число, указатель на указатель на символ, указатель на векторы символов, вектор из 7 указателей на целые числа, указатель на вектор из 7 указателей на целые числа и указатель на 8 векторов из 7 указателей на целые числа.
’’И.В.К.” 105023 Москва, Мал. Семеновская, д. 5 Тел.: 936-50-67, 311-52-08 Факс: 203-93-55 ГЛАВА 3 ^ВЫРАЖЕНИЯ И ОПЕРАТОРЫ В C++ есть небольшой, но гибкий набор типов операторов для управления ходом выполнения программы и богатый набор операторов для обработки данных. В одном полном примере даются всеобщеупотреби¬ тельные средства. После этого дается резюме по выражениям и относительно подробно рассматриваются явное преобразование типов и использование свободной памяти. Затем дается резюме по операторам и, наконец, рассматривается стиль оформления отступов и комментариев в тексте программы. 3.1 НАСТОЛЬНЫЙ КАЛЬКУЛЯТОР Будем рассматривать выражения и операторы на примере программы настольного калькулятора, выполняющего четыре стандартных арифмети¬ ческих действия (в виде инфиксных операторов) над числами с плавающей точкой. Пользователь может, кроме того, задавать переменные. Например, при вводе г = 2.5 area = pi’r’r (pi задается заранее) программа-калькулятор напишет: 2.5 19.635 гДе 2.5 - это результат первой строки ввода, а 19.635 - результат второй строки. Калькулятор состоит из четырех основных частей: анализатора, функции вода, таблицы символов и драйвера. На самом деле это миниатюрный компилятор, где анализатор выполняет синтаксический анализ, функция гввода обрабатывает ввод и выполняет лексический анализ, таблица символов содержит временную информацию, а драйвер выполняет инициализацию, вывод и обработку ошибок. К этому калькулятору можно добавить еще много полезных качеств, но длина программы и так немалая (200 строк), а новые качества, скорее всего, только увеличат длину программы и не улучшат понимания применения С + + . 75
3.1.1 Анализатор Ниже приведена грамматика языка, принимаемого калькулятором: * program: END // END - это конец ввода \ ехрг_ list END expr_list: expression PRINT // PRINT - это \n' либо expression PRINT expr_ list expression: expression + term expression - term term term: term / primary term " primary primary primary: NUMBER // число с плавающей точкой в С + + NAME // имя в C + + кроме ' NAME = expression - primary ( expression ) Другими словами, программа - это последовательность строк; каждая строка состоит из одного или более выражений, разделенных точкой с запятой. Основные элементы выражения - это числа, имена и операторы *, /, +, - (унарный и бинарный) и -. Необязательно объявлять имена перед использованием. Тип примененного синтаксического анализа обычно называется рекурсивным анализом сверху вниз; это распространенный и понятный метод. К тому же в языке вроде C++, где затраты на вызов функции относительно низки, этот метод эффективен. Для каждой продукции грамматики существует функция, вызывающая другие функции. Терминальные символы (например, END, NUMBER, + и -) распознаются лексическим анализатором - get_ token() - а нетерминальные символы распознаются Функциями синтаксического анализа - expr(), term() и prim(). Когда опознаны оба операнда в (под)выражении, оно вычисляется; в реальном же компиляторе на этом этапе генерируется код. Для ввода анализатор пользуется функцией get _token(). Результат последнего вызова функции get_token() находится в переменной currj.tok. Значение curr_tok ограничено перечислением token _ value: enum token _ value { NAME, NUMBER, END, PLUS = ' + ', MINUS = MUL = DIV = 7', 76
PRINT = ASSIGN = ' = LP ='(', RP = J token _ value curr _tok; Каждая функция анализатора предполагает, что уже была вызвана функция get__token() и в curr-tok содержится следующая лексема, которую надо анализировать. Это позволяет анализатору заглядывать на одну лексему вперед и заставляет каждую функцию анализатора считывать всегда на одну лексему больше, чем требуется обрабатываемой продукции. Каждая функция анализатора оценивает "свое" выражение и возвращает значение. Функция ехрг() обрабатывает сложение и вычитание, она состоит из одного цикла поиска терминов, которые надо складывать или вычитать: double ехрг() double left = term(); for(;;) switch (curr-tok) { pase PLUS: get —tokeh(); left + = term(); break; case MINUS: get —,token(); left *= term(); break; default: return left; // сложение и вычитание { // "вечный" цикл // прочитать ' V // прочитать Эта функция сама по себе не так уж много делает. Как принято в большинстве функций высокого уровня в составе большой программы, для работы она вызывает другие функции. Обратите внимание, что выражение вроде 2-3 + 4 оценивается как (2-3)+ 4, как задается грамматикой. Странное обозначение for(;;) - это стандартный способ создать бесконечный цикл; его можно назвать "вечным". Это вырожденный случай оператора for; по-другому то же можно написать кдк while(l). Оператор switch выполняется повторно до > тех пор, пока не встретится + или а по умолчанию выполняется оператор return. Операторы + = и -= используются для выполнения сложения и вычитания; без изменения смысла программы можно было бы написать left = left + term() и left = left-term(). Однако форма left + = term() и left- = term() не только короче, но и явно выражает желаемую операцию. Для бинарного оператора @ выражение х@ = у означает х = х@у с той разницей, что значение х вычисляется только один раз; это справедливо в отношении бинарных операторов + / % А I << >> поэтому возможны следующие операторы присваивания: 77
= + = -= “= /= %= &= 1= ' = <<- >> = Каждый из них ~ целая отдельная лексема, так что а + 1; это синтаксическая ошибка, так как между + и = есть пробел. (% * это оператор деления по модулю; &, I и л - это побитовые логические операторы И, ИЛИ и исключающее ИЛИ; << и >> - это операторы левого сдвига и пра-вого сдвига.) Функции term() и get_token() следует объявить до ехрг(). В главе 4 рассматривается, как организовать программу в виде набора файлов. С одним исключением декларации в этом примере настольного калькулятора могут стоять в таком порядке, что все будет объявлено только один раз и точно перед использованием. Исключением является ехрг(), которая вызывает term(), которая вызывает prim(), которая в свою очередь вызывает ехрг(). Этот цикл надо где-то прервать; декларация double ехрг(); // без этого не обойтись до определения prim() прекрасно подойдет. функция term() обрабатывает умножение и деление сходным образом: double term() { double left =■ prim(); // умножение и деление } for(;;) switch (curr_tok) { case MUL: get_token(); // left " = prim(); break; case DIV: get_token(); // double d = prim(); if (d = = 0) return left / = d; - break; default: return left; } прочитать прочитать 7Z еггог("деление на 0"); Проверка на отсутствие деления на нуль необходима, так как результат такого деления неолределен и обычно приводит к катастрофическим пЬследствиям. Функция error{char") описана ниже. Переменная d. вводится в программу там, где она нужна, и она сразу же инициализируется. Во многих языках декларация может находиться только в начале блока. Такое ограничение может создавать весьма неприятные искажения стиля программирования и/или ненужные ошибки. Чаще всего неинициализи¬ рованная локальная переменная - это просто признак плохого стиля программирования; исключение составляют переменные, которые будут инициализироваться операторами ввода, и переменные векторного и •78
структурного типа/ для которых нет удобных отдельных операторов присваивания. Обратите внимание, что = - это оператор присваивания, = = - это оператор сравнения. Фуйкция prim, обрабатывающая первичное выражение, выглядит примерно так же, с тем исключением, что так как в иерархии вызовов мы спускаемся ниже, выполняется уже некоторая настоящая работа и не требуется цикл: double prim() // обработка первичных выражений { > switch (curr_fok) { case NUMBER; // константа с плавающей точкой get _ tolcen(); / return number _ value; case NAME: if (get_token() = = ASSIGN) { name" n = insert(name_ string); get _token(); n-> value = expr(); return n-> value; return look(name _ string)-> value; case MINUS: // унарный минус get _ token(); return -prim; case LP: get _ token (); double e = expr(); if (curr_tok != RP) геЬигп("ожидается )"); get_token(); return e; case END; return 1; default: return еггог("ожидается первичное выражение"); Как только встречается NUMBER (т.е. константа с плавающей точкой), возвращается его значение. Процедура ввода get_token() помещает значение в глобальную переменную number _ value. Использование в программе глобальной переменной - нередко признак не очень четкой структуры, для которой требуется провести некоторую оптимизацию. Так же и здесь: в идеале лексема обычно состоит из двух частей: величина, определяющая вид лексемы (в данной программе - token _ value), и (при необходимости) значение лексемьь В нашем случае есть только простая переменная curr_tok, так что необходима глобальная переменная number _ value для хранения значения последнего считанного числа (NUMBER). Это работает только потому, что в вычислениях калькулятор всегда работает с Одним числом прежде чем вводит еще одно число. Точно так же, как величина 79
последнего встреченного NUMBER кранится в number _ value, символьная строка, представляющая последнее встреченное NAME (имя), хранится в name string. Прежде чем как-либо обработать имя, калькулятор должен заглянуть вперед и определить, присваивается ли что-нибудь именц, или оно просто используется. В обоих случаях надо справиться в таблице символов. Сама таблица описана в $3.1.3, здесь она проверяется на содержание имен вида: struct name { char’ string; name" next; double value; }; где next используется только функциями работы с таблицей: name" 1оок(сЬат"); name" insert(char’); Обе функции возвращают указатель на name, соответствующее аргументу "символьная строка"; look() сообщает, если имя не определено. Это значит, что в калькуляторе имена можно использовать без предварительной декларации, но впервые имя должно стоять слева от оператора присваивания. 3.1.2 Функция «вода Нередко считывание ввода - это самая запутанная часть программы. Причина этого состоит в том, что- программа должна общаться с человеком, она должна справляться с человеческими капризами, соглашениями и произвольными, на первый взгляд, ошибками. Попытки заставить человека поступать так, как это устраивает компьютер, часто (и справедливо) считаются оскорбительными. Задача процедуры ввода низкого уровня - читать символы по одному и составлять из них лексемы высокого уровня. Затем эти лексемы становятся элементами ввода для процедур высокого уровня. В нашем примере ввод низкого уровня выполняется функцией get __tokenf). К счастью, писать процедуры ввода низкого уровня приходится не каждый день; в хорошей системе для этого имеются стандартные функции. Правила ввода в калькулятор были намеренно выбраны так, чтобы потоковым функциям было неудобно с ними работать; некоторыми изменениями в определениях лексем можно было бы получить обманчивое упрощение функции^ get etoken(). Первая проблема состоит в том, что символа новой строки '\п' имеет значение для калькулятора, однако функции потокового ввода считают его игнорируемым символом. То есть, для этих функций '\п' важен только как конец лексемы. Чтобы решить эту задачу, надо исследовать игнорируемые символы (пробел, табуляция и т.д.): char ch; 80
do { // пропускать все игнорируемые символы кроме '\п' if(!cin.get(ch)) return curr_tok = END; } while (ch! = \n' && isspace(ch)); Вызов cin.get(ch) считывает один символ из стандартного вводного потока в ch. Проверка if (lcin.get(ch)) дает неуспех, если из cin невозможно прочитать символ; в этом случае возвращается END, что завершает сеанс работы с калькулятором. Используется оператор ’(NOT), поскольку get() возвращает ненулевое значение в случае успеха. Функция (с подстановкой тела) isspaceQ из <ctvpe.h>’ выполняет стандартную проверку на игнорируемый символ ($8.4.1); isspace(c) возвращает ненулевое значение, если с - игнорируемый символ, и нуль в противном случае. Проверка производится просмотром таблицы, поэтому использовать isspaceQ гораздо быстрее, чем проверять на конкретные игнорируемые символы; то же относится и к функциям isalpha(), isdigit() и isalnumQ, использующихся в get_token(). После пропуска игнорируемого символа анализируется следующий символ, чтобы определить, какого рода лексема поступает. Рассмотрим по отдельности некоторые случаи, прежде чем дать полную функцию. Символы завершения выражений '\п' и обрабатываются так: switch (ch) { case case \n': cin >> WS; // пропустить игнорируемый символ return curr _tok = PRINT; Пропуск игнорируемого символа (заново) необязателен, но если его делать, это предотвратит повторные вызовы get _token(). WS - это стандартный игнорируемый объект, объявленный в <stream.h>; единственное его применение - игнорировать такой объект. Ошибка ввода или конец ввода не будут обнаружены до следующего вызова get_token(). Обратите внимание на то, как несколько меток вариантов case можно использовать в одной последовательности операторов, обрабатывающих эти варианты. Лексема PRINT возвращается и помещается в curr_tok в обоих случаях. Числа обрабатываются таким образом: case О': case Г: case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case cin.putback(ch); cin >> number _ value; return curr _tok = NUMBER; Располагать ме^ки вариантов case, горизонтально, а не вертикально обычно не очень хорошо, поскольку читать это намного труднее, но скучно отводить по одной строке на каждую цифру. Поскольку оператор > > уже определен для чтения констант с плавающей точкой в double, код тривиален: во-первых начальный символ (цифра или точка) возвращается в cin, а затем константу можно читать в number _ value. Имя, т.е. лексема NAME, определяется как буква, после которой, 81
возможно, следуют несколько букв или цифр: е is (isalpha(ch)) { char" р = name _ string; "p + + = ch; 1 while (cin.get(ch) && isalnum(ch)) "p + + = ch; cin.putback(ch); ‘P = 0; return curr _ tok = NAME; } Это дает строку с завершающим нулем в name_ string. Функции isalpha() и isalnum() содержатся в <ctype.h>; isalnum(c) не равна нулю, если с - буква или цифра, и нулю в противном случае. Вот, наконец, полный текст функции: token _ value get_token() char ch; do { // пропускать все игнорируемые символы кроме'Хп' if(!cin.get(ch)) return curr _tok = END; } while (ch! = '\n' && isspace(ch)); switch (ch) { case case '\n': cin > > WS; // пропустить игнорируемый символ return curr _ tok = PRINT; case case '/': case ' + case case T- > case ): case ' = ': return curr __tok = ch; case 'O': case 'Г: case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case cin.putback(ch); cin >> number __ value; return curr _ tok = NUMBER; default: is (isalpha(ch)) { char’ p = name_ string; "p + + = ch; while (cin.get(ch) && isalnum(ch)) "p + + = ch; cin.putback(ch); ■p = 0; return curr _ tok = NAME; 82
еггог("неверная лексема"); return curr_tok = PRINT; Поскольку для любого оператора token _ value определена как целочисленное значение оператора, все варианты операторов обрабатываются тривиально. 3.1.3 Таблица символов Доступ к таблице символов выполняет одна функция: name’ look(char" р, int ins = 0); Ее второй аргумент означает, подразумевается ли, что строка символов раньше была туда записана. Инициализатор = 0 задает аргумент по умолчанию для тех случаев, когда look вызывается с одним аргументом. Это дает нотационное удобство: Iook("sqrt2") означает Iook("sqrt2",0), т.е. только поиск без записи. Чтобы получить такое же нотационное удобство для записи, определим вторую функцию: inline name’ insert(char’ s) { return look(s,1); } Как указано выше, записи 'Таблицы имеют тип: struct name { char" string; name’ next; double value; }; Элемент next используется для связи имен в таблице. Сама таблица - это просто вектор указателей на объекты типа name: const TBLSZ = 23; name’ table[TBLSZJ; Поскольку по умолчанию все статические объекты инициализируются нулём, такая тривиальная декларация table гарантирует правильную ини¬ циализацию. Для поиск имени в таблице link() использует простое хеширование (имена с одинаковым хеш-кодом связываются вместе): int ii = 0; // хеширование char’ рр = р; while ("рр) ii = ii<<1 Л "рр + +; г if (ii < 0) ii = -ii; Л ’■ ii % = TBLSZ; 83
Это значит, что каждый символ строки ввода р "добавляете»" к ii ("сумме" предыдущих символов) посредством исключающего ИЛИ- В хлу бит устанавливается в единицу тогда и только тогда, когда соответствующие биты операндов х и у различаются. До выполнения исключающего ИЛИ над символом ii сдвигается влево на один бит, чтобы исключить использование только одного его байта. Это же можно выразить и так: ii < < = 1; ii = “рр + + ; Применение л несколько лучше и быстрее, чем +. В обоих случаях сдвиг необходим для получения разумного хеш-кода. Операторы if (ii < 0) ii = -ii; if % = TBLSZ; гарантируют, что ii находится в интервале 0...TBLSZ; % - оператор деления по модулю. Вот полная функция: extern int strlen(const char"); extern int strcmp(const char“, const char’); extern char" strcpy(char“, const char"); name" look(char" p, int ins = 0) int ii = 0; // хеширование char" pp = p; while ("pp) ii = ii < < 1 ~ "pp + + ; if (ii < 0) ir = -ii; ii %= TBLSZ; for (name" n = table[ii]; n; n = n->next) // поиск if (strcmp(p,n-> string) = = 0) return n; if (ins = = 0) еггог("имя не найдено"); name" nn = new name; nn-> string = new char[strlen(p) + 1 ]; strcpy(nn-> string,p); nn-> value = 1; nn->next = tablefii]; tablefii] = nn; return nn; } После вычисления хеш-кода ii имя отыскивается простым просмотром полей next. Каждое name сравнивается с помощью стандартной функции сравнения строк strempf). Если строка отыскивается, возвращается ее имя, в противном случае добавляется новое имя. Добавление нового name включает создание нового объекта name в 84
свободной памяти с помощью оператора new (см. $3.2.6), инициализацию объекта и добавление его к списку имен. Последнее выполняется записью нового имени в начало списка, поскольку это можно сделать, даже не проверяя, есть ли уже список. Символьную строку с именем надо также записать в свободную память. С помощью функции strlen() определяется, сколько требуется свободной памяти, с помощью new память резервируется, a strcp,y() копирует строку в эту память. 3.1.4 Обработка ошибок Данная программа проста, поэтому обработка ошибок в ней - небольшая проблема. Функция обработки ошибок просто подсчитывает ошибки, выводит сообщение об ошибке и делает возврат: int no _ of _ errors; double error(char“ s) { cerr < < "error " < < s < < "\n"; no __ of _ errors + Ч-; return 1; } Значение возвращается потому, что обычно ошибки возникают в ходе процесса вычисления выражения, так что надо либо полностью прервать процесс вычисления, либо возвратить значение, которое в дальнейшем вряд ли вызовет ошибки. Для нашего простого калькулятора подходит последнее - простое решение; Если бы get_token() хранила информацию о номерах строк, то еггог() могла бы сообщать пользователю, где приблизительно произошла ошибка. Это было бы полезно, если калькулятор использовать в неинтерактивном режиме. Часто программу следует завершить после возникновения ошибки, поскольку неизвестно разумного способа ее продолжить. Это можно сделать, вызвав exit(), которая сначала очищает выводные потоки ($8.3.2), а затем завершает программу и возвращает ее аргумент. Более жесткий способ завершения программы - это вызов abort(), которая завершает программу немедленно или немедленно после записи информации для отладчика {разгрузка памяти), подробности можно узнать в Вашем справочнике. 3.1.5 Драйвер Теперь, когда у нас есть все части программы, нужен только драйвер- для инициализации и запуска. В данном простом примере main() можно написать так: int main() { // запись заранее определенных имен: insert("pi")-> value = 3.1415926535897932385; insert("e")-> value = 2.7182818284590452354; 85
while (cin) { 4 , get^token(); - if(curr_tok == END) break; x if (curr_tok = = PRINT) continue; } cout << expr() << "\n"; return no _ of _ errors; } Обычно main() возвращает нуль, если программа завершается нормально, и ненулевое значение в противном случае, поэтому возврат количества ошибок дает прекрасный конец. Когда это происходит, единствен¬ ное, что надо сделать - это записать в таблицу символов заранее определенные имена. Первичная задача главного цикла - читать выражения и записывать ответ. Это выполняется строкой: cout < < ехрг() < < "\п”; Проверка cin в каждом проходе цикла гарантирует, что программа завершится, если что-то случилось с потоком ввода, а проверка END гарантирует, что производится корректный выход из цикла, когда get _token () обнаружит конец файла. Оператор break выполняет выход из ближайшего оператора switch или цикла, в который он погружен (т.е. оператора for, оператора while или оператора do). Проверка PRINT (т.е. на '\п' и освобождает ехрг() от ответственности за обработку пустых выражений, оператор continue эквивалентен переходу к самому концу цикла, так что в этом случае • ' while {cin) { // ... if (curr_tok = = PRINT) continue; cout < < expr() < < "\n"; эквивалентно следующему while (cin) { // ... if (curr_tok = = PRINT) goto end_of__loop; cout << expr() << "\n"; end __ of _ loop: ; Подробнее циклы описаны в $с.9. 3.1.6 Аргументы командной строки Если Вы написали и протестировали программу, то можете заметить, 86
что печатать выражения через стандартный ввод довольно утомительно, поскольку часто нужно бывает подсчитать только одно выражение. Если бы можно было вводить это выражение/ как аргумент командной строки, удалось бы сэкономить множество нажатий клавиш; Как сказано выше, программа начинается с вызова main(). После этого main() получает два аргумента: задающих количество аргументов, обычно под именем argc, и вектор аргументов, обычно под именем argv. Аргументы - это символьные строки, так что тип argv - char’fargc]. Имя программы передается как argv[0], так что argc всегда не менее 1. Например, в команде de 150/1.1934 у аргументов будут такие значения: argc 2 argv[01 "de" argv[1] "150/1.1934" Получить доступ к аргументам командной строки нетрудно; проблема состоит в том, как это использовать без перепрограммирования. В таком случае оказывается, что это тривиально, поскольку вводной поток может перенаправлен на символьную строку вместо файла ($8.5). Например, cin может читать символы-и-з строки, а не из стандартного ввода: int main(int argc, char’ argv[]) switch (argc) { case 1: // чтение из стандартного ввода break; case 2: .// чтение из строки аргументов cin = “new istream(strlen(argv[1]),argv[1]); break; default: еггог("слишком много аргументов"); return 1; } //'как и раньше } За исключением добавления аргументов к main() и их обработки оператором switch программа не изменилась. Можно легко модифицировать main() так, чтобы программа принимала несколько аргументов командной строки, но представляется, что это необязательно, особенно учитывая, что несколько выражений можно передать как единый аргумент: de "скорость = 1Л934; 150/скорость; 19.75/скорость;217/скорость" Кавычки здесь необходимы, поскольку ; - это разделитель системных команд в системе UNIX. 87
3.2 СИНТАКСИС C+ + Операторы языка C++ полно и систематично описаны $с.7, прочтите, пожалуйста, этот раздел. Здесь, тем не менее, дается обзор и некоторые примеры; После каждого оператора дается одно или несколько имен, которые ему присвоены и примеры использования. В этих примерах имя—класса - это имя класса, элемент - это имя элемента, объект - это выражение, дающее объект - класс, указатель * это выражение, дающее указатель, выражение - это выражение, и lvalue - это выражение, обозначающее непостоянный объект. Тип может быть именем .полностью общего типа (с ’, () и т.д.) только, если оно заключено в скобки, в остальных случаях имеются ограничения. Унарные операторы и операторы присваивания ассоциативны справа; все прочие ассоциативны слева. То есть а = b = с значит а = (Ь = с), а + Ь + с значит (а + Ь) + с, а "р + + значит "(р + +)» а не (*р) + +. В каждой части таблицы содержатся операторы одного приоритета. У оператора из верхней части таблицы приоритет выше, чем у операторов из нижних частей таблицы. Например: а + Ь“с значит а + (Ь“с), поскольку у " приоритет выше, чем у +, и а + b-с значит (а + Ь)-с, поскольку + и - имеют одинаковый приоритет (и поскольку + ассоциативен слева). Операторы (часть 1) разрешение области действия глобальное имя_класса::элемент : :имя - > выбор элемента указатель-> элемент [] индексация указатель [выражение] О вызов функции выражение(список_выраж) О создание значения тип (список^выраж). sizeof размера-объекта sizeof выражение sizeof размер \типа sizeof (тип) ++ пост-инкрементация lvalue++ ++ пре-инкрементация ++lvalue пост-декрементация lvalue-- - - цре-декрементация --lvalue дополнение "выражение i не ’выражение - унарный минус -выражение + унарный плюс +выражение & адрес ^lvalue * обращение по адресу ‘выражение new создать (выделить память) new тип delete уничтожить (освободить память.) delete указатель delete[J уничтожить вектор delete [выраж] уХазат. () преобразование типов (тип) выражение *. умножение выражение * выражение / деление выражение / выражение % деление по модулю выражение % выражение + сложение (плюс) выражение + выражение вычитание (минус) выражение - выражение 88
3.2.1 Скобки Скобками в синтаксисе C+ + пользуются очень часто; использовать их надо в очень большом числе случаев, что создает путаницу: они окружают аргументы в вызовах функций, тип при преобразовании типов; в именах типов для обозначения функций, а также для разрешения коллизий приоритетов. К счастью, последне требуется нечасто, поскольку благодаря уровням приоритетов и правилам ассоциативности выражения "работают без сюрппризов" (т.е. соответствуют наиболее распространенной практике). Операторы (часть 2) < < > > сдвиг влево сдвиг вправо выражение выражение < < выражение > > выражение < меньше чем выражение < выражение <= меньше чем или равно выражение <= выражение > больше чем выражение > выражение >= больше чем или, равно. выражение >= выражение = равно выражение = выражение ! = не равно выражение ’= выражение & побитовое И выражение & выражение - побитовое исключающее ИЛИ выражение выражение 1 Побитовое включающее ИЛИ выражение 1 выражение && логическое И выражение && выражение 11 логическое включающее ИЛИ выражение 11 выражение ? : арифметическое если выраж. ? выраж. : выраж. простое присваивание lvalue = выражение *= умножить и присвоить lvalue *= выражение /= делить и присвоить lvalue /= выражение %= делить по модулю и присвоить lvalue %= выражение += прибавить и присвоить lvalue += выражение - = вычесть и присвоить lvalue -= выражение < <= сдвинуть влево и присвоить lvalue < <= выражение > >= сдвинуть вправо и присвоить lvalue >>= выражение <&= И и присвоить lvalue &= выражение 1 = включающее ИЛИ и присвоить lvalue 1= выражение • = исключающее ИЛИ и Присвоить lvalue ‘= выражение У запятая (последовательность) выражение , выражение Например: if (i<=0 I Imax < i) // ... 89
иМеет очевидный смысл. Тем не менее, если программист не уверен, как выполнятся эти правила, надо применять скобки, и некоторые < й программисты предпочитают более длинную и менее изящную форму if ( (i<=0) II (max<i) ) // ... С возрастанием сложности подвыражений, скобки используются, чаще, но сложные подвыражения - источник ошибок, так что если Вы чувствуете потребность в скобках, стоит призадуматься, не надо ли разбить выражение, использовав лишнюю переменную. Кроме того, бывают случаи, когда приоритеты операторов дают "неожиданный” результат. Например: if (i&mask = = 0) // ... не выполняет логическое сложение i с mask, а затем не проверяет, равен ли результат нулю. Так как у = = приоритет выше, чем у &, это выражение интерпретируется ка| i&(mas.k = =0). В данном случае скобки имеют значение: if ((i&mask) = = 0) // ... Следует также отметить, что следующий пример не работает так, как того ожидал бы наивный пользователь: if (0 <= а <= 99) // ... Ошибок это не содержит, но интерпретируется как (0 < = а) , = 99, где результат первого сравнения равен 0 или Г, но не а (если только а не равно 1). Для проверки, находится ли а в диапазоне 0...99, надо написать: if (0 < = а && а < = 99) // ... 3.2.2 Последовательность вычисления Последовательность вычисления подвыражений в пределах выражения неопределен. Например: int i = 1; v[i] = i + + ; может вычисляться либо как v[1]= 1, либр как v[2] = 1. Лучший код будет сгенерирован в отсутствии ограничений tja последовательность вычисления выражений. Хорошо, если компилятор будет предупреждать о подобных неоднозначностях, ио большинство компиляторов этого не делают. Операторы && II [арантиру Например ют, что их левый операнд вычисляется до правого операнда. , b = (а = 2,а + 1) присваивает b число 3. Примеры использования && II даются в $3.3.1. Обратите внимание, что оператор последовательности (запятая) логически отличается от запятой, которая используется для 90
разделения аргументов в вызовах функций. Сравните: i],i + + ); 7/ Два аргумента v[i],i + + ) ); // один аргумент В вызове fl два аргумента: v[i] и i + + , а порядок вычисления выражений аргументов неопределен. Программа, зависящая от последовательности вычисления выражений аргументов, написана плохим стилем и она непереносима. В вызове f2 один аргумент: .выражение с запятой (v[i],i + + ), оно эквивалентно i + +. Нельзя пользоваться скобками для вмешательства в последовательность вычислений. Например, а"(Ь/с) может вычисляться в порядке (а"Ь)/ с, поскольку “ и / имеют одинаковый приоритет. Если порядок вычислений важен, следует применить дополнительную (временную) переменную; например, (t=b/c,a"t). 3.2.3 Инкрементация и декрементация Оператор + + используется для прямого увеличения (инкрементации), вместо того, чтобы сделать то же не прямо - сочетанием сложения и присваивания. По определению + + lvalue значит lvalue + = 1, что в свою очередь значит lvalue = lvalue + 1, если только lvalue не дает побочных эффектов. Выражение, обозначающее инкрментируемый объект, вычисляется (оценивается) один раз (только). Уменьшение (декрементация) выражается сходным образом оператором —. Операторы + + и — могут использоваться1 как в префиксной, так и постфиксной форме. Значением + + х является новое (т.е. увеличенное) значение х. Например, у = + + х эквивалентно у = (х + = 1). Однако, значение х + + - это прежнее значение х. Например, у = х+ + эквивалентно y = (t = x,x + = 1,t), где t -переменная того же типа, что и х. Операторы инкрементации особенно полезны при инкрементации и декрементации переменных в циклах. Например, вот так можно копировать строку с завершающим нулем: inline void cpy(char" р, const char’ q) while (“p + + = “q + + ); Напомним, что инкрементация и декрементация указателей, как и сложение и вычитание указателей, работают с элементами векторов, указуемых указателем; после р + + р указывает на следующий элемент. Для указателя р типа V следующее справедливо по определению: long(p + 1) == long(p) + sizeof(T); 3.2.4 Побитовые логические операторы Побитовые логические операторы & I л - > > < < 91
применяются к целым числам, т.е. к объектам типа char, short/ inf, long и их аналогам с unsigned, полученные результаты - также целые чЬсла. Типичное использование побитовых логических операторов -реализация понятия небольшого множества (битового вектора). В этом случае каждый бит целого числа без знака представляет один элемент множества, а количество бит ограничивает количество элементов множества. Бинарный оператор & интерпретируется как пересечение* I - как объединение и 74 - как разница. Для именования элементов множества можно воспользоваться перечислением. Вот небольшой пример, заимствованный из реализации (а не пользовательского интерфейса) <stream.h>: enum state _ value { _good = 0, _eof=1, _fail = 2, _bad = 4 }; __ good определять необязательно; просто дано соответствующее имя состоянию, когда проблем нет. Состояние потока можно восстановить так: cout.state = ..good; Проверить, нет ли искажений потока или неудачного завершения операции, можно так: if (cout.state&( _ bad I _ fail)) // плохо дело Дополнительные скобки необходимы, поскольку & имеет приоритет выше чем у I. Функция, достигшая конца ввода, может сообщить об этом так: cin.state I = _eof; Используется оператор I =, поскольку поток уже мог быть искажен (т.е. state = = _bad), поэтому выражение cin.state = _ eof сняло бы этот признак. Понять, чем различаются состояния двух потоков, можно таким' образом: sfate_ value diff = cin.state 74 cout.state; Для типа stream _ state эта разница не столь полезна, однако для Других аналогичных типов это очень удобно. Например, рассмотрим сравнение битового вектора, представляющего множество обрабатывающихся прерываний, с другим вектором, представляющим множество прерываний, ожидающих обработки. Обратите внимание, что использование полей ($2.5.1) - Действительно удобная и короткая замена сдвига и маскирования для извлечения битовых полей из слова. Это, конечно, можно сделать и с помощью побитовых логических операторов. Например, вот как можно извлечь средние 16 бит из 32-битового int: unsigned short middle(int a) {return (a> >8)&0xffff; } 4 Зак. 1927 92
Не путайте побитовые логические операторы с логическими операторами: && 11 1 Последние возвращают либо 0, либо 1, и они в первую очередь применяются в проверках в операторах if, while или for ($3.3.1). Например, 10 (не нуль) имеет значение 1, тогда как ~0 (дополнение нуля) -это битовая строка "все единицы", и «ее значение - обычно -1. 3.2.5 Преобразование типов Иногда требуется явным образом преобразовать величину одного типа в величину другого типа. Явное преобразование типов дает величину одного типа, получив величину другого типа. Например: float г = float(l); преобразует целое число 1 в величину с плавающей точкой 1.0 до выполнения присваивания. Результат преобразования типов - не левое значение, так что ему нельзя сделать присваивания (если только это не тип обращения по адресу). Для явного преобразования типов имеются две нотации: традиционная из языка С (double) а и функциональная нотация double(a). Функциональную нотацию нельзя применять к типам, не имеющим простого имени. Например, для преобразования значения в тип указателя надо либо воспользоваться традиционной нотацией char" р = (char“)0777; либо определить новое имя типа: typedef char" Pchar; char’ p = Pchar(0777); В нетривиальных случаях предпочтительнее функциональная нотация. Сравните два этих эквивалентных примера: Pname n2 = Pbase(n1->tp)-> b _ name; // функциональная нотация Pname пЗ = ((Pbase)n2->tp)-> b _ name;// традиционная нотация Поскольку у оператора -> более высокий приоритет, чем у преобразования типов, последнее выражение интерпретируется так: " ((Pbase)(n2->tp))->b _ name Применяя явное преобразование типов к типам указателей, можно притвориться, что объект имеет любой тип. Например: any _ type" р = (any _ type")&some __ object; 93
и обращении через р позволит работать с some _ object, как с тиром апу_+УРе- 7 Не следует без необходимости применять преобразование ти/Гов. программы, использующие многочисленные явные преобразования тийов, труднее для понимания, чем программы, в которых их мало. Однако, такие! программы все же легче понять, чем программы вовсе не использующие типы для представления понятий высокого уровня (например, программа, работающая с регистром устройства с помощью сдвигов и маскирования целых чисел, а не определяющая соответствующую структуру и работающая с ней, см. $2.5.2). Более того, правильность явного преобразования типов часто критически зависит от понимания программистом способов обработки объектов различных типов в языке, а также очень часто - от деталей реализации. Например: int i=1; char* pc = "asdf"; int“ pi = &i; i = (int)pc; pc = (char’)i; pi = (inf)pc; pc = (char')pi; // внимание: значение pc может измениться // на некоторых ЭВМ // sizeof(int)<sizeof(char") // внимание: значение рс может измениться // на некоторых ЭВМ char" имеет представление, // отличное от представления int" На многих ЭВМ никакой беды не произойдет, но на других результаты этого будут плачевными. В лучшем случае, такая программа непереносима. Kart правило, надежнее предположить, что указатели на разные структуры имеют одинаковое представление. Более того, любой указатель может быть присвоен (без явного преобразования типов) типу "void, a "void можно явно преобразовать в указатель любого типа. В С+ + во многих случаях не требуется явное преобразование типов, когда языку С (и другим языкам) оно потребовалось бы. Во многих программах можно полностью обойтись без явного преобразования типов, а во многих других программах его можно ограничить несколькими процедурами. 3.2.6 Свободная память Именованный объект может быть либо статическим, либо Динамическим ($2.1.3). Статический объект получает память в момент старта программы и существует все время выполнения программы. Автоматический объект получает память всякий раз, когда начинает выполняться блок, в который он входит, и существует до тех пор, пока не выполнится выход из Рлока. Тем не менее, бывает полезно создавать новые объекты, которые существуют до тех пор, пока в них не отпадет необходимость. В частности, НеРедко бывает полезно создать объект, который можно использовать и после выхода из функции, в которой создан объект. Такие объекты создаются оператором new (новый), а оператором delete (уничтожить) их 4* 94
можно потом уничтожить. Об объектах, созданных оператором new, говорЯт что они находятся в свободной памяти. Обычно такие объекты - это узЛь' дерева или элементы связного списка, составляющие часть больше структуры данных, размер которой невозможно установить во вреМя компиляции. Рассмотрим, как можно написать компилятор в стиле написания нашего настольного калькулятора. Функции синтаксического анализа могут создать представление выражений вида дерева, которое используется генератором кода. Например: struct enode { token _ value oper; enode" left; enode" right; }; enode" expr() { enode" left = term(); for (;;) switch(curr _tok) { case PLUS: case MINUS: get-tokenO; enode" n = new enode; n->oper = curr._tpk; n—> left = left; n-> right = term(); left = n; break; default: return left; Генератор кода может следующим образом использовать получившееся дерево: void generate(enode" n) switch (n->oper) { case PLUS: /7 сделать что-то нужное delete n; Объект, созданный new, существует до тех пор, пока не будет явно уничтожен оператором delete, после ?того память, которую он занимал, может снова использоваться оператором new. Не существует специального "сборщика мусора", который отыскивает объекты без ссылок и 95
педоставляет их оператору new для повторного использования. Операурр delete можно применять только к указателям, возвращенным от new^ или нулю. Выполнение delete над нулем ничего не изменяет. ' С помощью new можно создавать векторы объектов. Например: char" save_ string(char" р) char" s = new char[strlen(p) + 1]; strcpy(s,p); return s; } Обратите внимание, что для того, чтобы освободить память, выделенную new, delete надо дать возможность определить размер обрабатываемого объекта. Например: i int main(int argc, char’ argv[]) if (argc < 2) exit(1); char" p = save —string(arg[1 ]); delete p; } При этом подразумевается, что объект, получивший память от new, занимает чуть больше памяти, чем статический объект (как правило, на одно слово больше). Кроме того, размер вектора длЗ бКерации уничтожения можно задавать явно. Например: int main(int argc, char’ argv[]) if (argc < 2) exit(1); int size = strlen(argv[11) + 1; char" p = save_string(argv[1]); deletefsize] p; Размер вектора, заданный пользователем, игнорируется за исключением некоторых типов, определяемых пользователем ($5.5.5). Операторы свободной памяти реализуются функциями ($с.7.2.3): void" operator new(long); void operator delete(long); В стандартной реализации new не инициализирует возвращаемый объект. Что произойдет, если new не сможет найти и выделить свободную память? Поскольку даже виртуальная память конечна, когда-нибудь это Должно случиться; запрос вроде char" р = new char[ 100000000]; 96
как правило, создаст какие-нибудь неприятности. При неуспехе new эт0 оператор вызывает функцию, на которую указывает указатель _ new _ handler (указатели на функции рассматриваются в $ 4.6.9)? Значение этого указателя можно задать непосредственно или же воспользоваться функцией set_ new _ handlerQ. Например: #include <stream.h> void out _ of _ store() cerr < < "отказ оператора new: память исчерпана\п"; exi+(1); > typedef void ("PF)(); // указатель на тип функции extern PF set _ new _ handler(PF); main() < set _ new _ handler(&out _ of _ store); char" p = new char[ 100000000]; cout << "сделано, p = " << long(p) << "\n"; обычно никогда не дойдет до сообщение "сделано", а сообщит отказ оператора new: память исчерпана _ new _ handler (обработчик new) может делать кое-что более разумное, чем просто прекратить программу. Если Вы знаете, как работают new и delete, например, если Вы подставили свои operator new() и operator deleteO, то обработчик new может попытаться найти какое-то количество памяти для возврата в new. Другими словами, пользователь может дать свой сборщик мусора, и таким образом, использование delete станет необязательным. Заметим, правда, что такая задача абсолютно не для начинающих. По причинам исторического порядка new просто возвращает указатель 0, если он не может найти ддстаточно памяти и не указан _ new _ handler. Например: #include <stream.h> main() char" p = new char[ 100000000]; { cout << "сделано, p = " long(p) << "\n"; } ■ ’ г ’ даст сделано, p = 0 97
Вас предупредили! Учтите, что специфицировав _ new _ handler, ВьГ тем самым берете на себя ответственность за проверку на исчерпание памяти прИ каждом использовании new в программе ( за исключением случаев, когда пользователь задал отдельные процедуры обработки резервирования памяти для объектов особых типов, определяемых пользователем, см. $5.5.6>. 3.3 СЕМАНТИКА C+ + Операторы с точки зрения семантики C++ полностью и системно изложены в $с.9; пожалуйста, прочтите 3tOT раздел. Тем не менее,' здесь дается обзор и несколько примеров. Операторы оператор: декларация [ список_операторов ] выражение ; if ( выражение ) оператор if ( выражение ) оператор else оператор switch ( выражение ) оператор while ( выражение ) оператор do оператор while ( выражение ) ; for ( оператор выражение ; выражение ) оператор case выражение-константа : оператор default •: оператор break ; continue ; return выражение ; goto идентификатор ; идентификатор : оператор список_операторов оператор оператор список операторов Обратите внимание, что декларация - это оператор и что нет оператора присваивания или оператора вызова процедуры; присваивания и вызовы функций обрабатываются как выражения. 3.3.1 Проверки Проверить значение можно либо оператором if, либо оператором switch: if ( выражение ) оператор 98
if ( выражение ) оператор else оператор switch ( выражение ) оператор В С + + нет особого булева типа. Операторы сравнения ' = =!=<<=>> = возвращают целую 1, если проверка оказалось истинной, и Q в противном случае. Нередко можно встретить макроопределения: TRUE (истина) - 1 и FALSE (ложь) - 0. В операторе if первый (или единственный) оператор выполняется, если выражение не равно нулю, а второй оператор (если он задан) выполняется в противном случае. При этом подразумевается, что в качестве условия можно использовать любое целочисленное выражение. В частности, если а - целое, то if (а) // ... эквивалентно if (а ! = 0) // ... Логические операторы && 11 ’ чаще всего используются в условиях. Операторы && и II не вычисляют без необходимости свой второй аргумент. Например: if (р && 1<р-> count) // ... сначала проверяет;, равно ли р не-нулю, и только если оно равно не-нулю, проверяет 1 <р-> count. Некоторые простые случаи оператора if удобно заменяются выражениями условной операции ("if арифметический"). Например: if (а < = Ь) max = b; else max = а; лучше выразить так: max = (а < = b) ? Ь : а; Скобки вокруг условия необязательны, но кажется, что с ними программу легче читать. Для некоторых простых случаев оператора switch можно воспользоваться набором операторов if. Например: switch (val) { * 99
case 1: case 2: f(); break; g(); default: break; h(); break; можно по-другому записать так: if (val = = 1) else h(); 2) Здесь смысл тот же, но первая версия (со switch) предпочтительнее, поскольку в этом случае явно указан характер операции (сравнение величины с набором констант). В нетривиальных случаях это свойство обеспечивает лучшую читаемость оператора switch. Позаботьтесь о том, чтобы вариант (case) оператора switch завершился каким-либо образом, если только Вам не нужно продолжить выполнение и следующего варианта. Например: switch (vai) { // осторожно case 1: cout << "вариант 1\n"; case 2: cout < < "вариант 2\n"; default: cout < < "выход по умолчанию: варианты не найдены\п"; при val = =1 напечатает: вариант 1 вариант 2 выход по умолчанию: варианты не найдены к великому изумлению непосвященных. Самый распространенный способ закончить вариант - использовать оператор break, однако иногда полезен return , а бывает, что можно применить и goto. Например: switch (val) { case 0: cout < < "вариант 0\n"; case 1: 100
cout < < "вариант 1\n"; return; case 2: cout < < "вариант 2\n"; goto easel; default: cout < < "выход по умолчанию: варианты не найдены\п"; return; } При значении vaK= = 2, это - дает вариант 2 вариант 1 Обратите внимание, что метку варианта нельзя использовать в операторе goto: goto case 1; // синтаксическая ошибка 3.3.2 Оператор перехода goto ч В С + + имеется пресловутый оператор goto. goto идентификатор ; идентификатор : оператор В общем программировании высокого уровйя он редко используется, но может быть весьма полезен, если программа на С + + генерируется другой программой, а не пишется непосредственно человеком’; например, goto можно использовать в анализаторе, созданном генератором анализаторов по некоторой грамматике. Оператор goto может понадобиться в тех случаях, когда требуется достичь максимальной эффективности, например, во внутренних циклах некоторой прикладной задачи, работающей в реальном времени. Один из немногих разумных способов применения goto - выход из вложенного цикла или оператора-переключателя switch (break выходит только из самого внутреннего цикла или блока переключателя). Например for (int i = 0; i <n; i + + ) for (int j = 0; j < m i + + ) if (nm[i][j] - a) goto found; // не найдено // ... found: // найдено // nm[i][j] = = a Кроме того, есть еще оператор continue, который на самом деле переходит в конец оператора цикла, как показано в $3.1.5. 101
3.4 КОММЕНТАРИИ И ОТСТУПЫ v Разумное использование комментариев и последовательное применение отступов значительно облегчают и упрощают задачу чтения и понимания программы. Существует несколько практических последовательных методов применения отступов. Нет серьезных причин предпочесть какой-то один из нИх (хотя у большинства программистов есть свои привязанности). То же относится к стилю написания комментариев. Неправильное использование комментариев может серьезно ухудшить читаемость программы во многих аспектах. Компилятор никак не мо^ет вникнуть в суть комментариев, поэтому он не сможет гарантировать, что комментарий... 11 имеет смысл; '2' описывает программу; 3] своевременен. В большинстве программ комментарии непонятны, неоднозначны и попросту неверны. Плохой комментарий может быть хуже отсутствия комментария. Если что-то можно выразить непосредственно на языке программирования, то так и надо сделать, а не просто упомянуть об этом в комментариях Имеются в виду комментарии Вроде // переменную "v" надо инициализировать. // переменную "v" может использовать только функция //.перед вызовом любой функции в этом файле // вызови функцию "initQ". // в конце своей программы вызови функцию "cleanupf)". // не применяй функцию nweird()". // функция "f()" принимает два аргумента. Подобные аргументы часто могут стать ненужными, если правильно работать на С + +. Например, благодаря применению правил согласования ($4.2) и правил видимости, инициализации и очистки для классов (см. $5.5.2) исчезнет необходимость в вышеприведенных примерах. Если один раз что-то было ясно сформулировано на языке программирования, то не следует повторять это же в комментарии. Например: а = b + с; //а становится b + с counts- +; // увеличить счетчик (counter) Подобные комментарии хуже, чем излишни: они увеличивают объем текста, который надо прочитать человеку, нередко они затуманивают структуру Программы, и они, возможно, неверны. Предпочититэльно: 102
[1] Комментарии для каждого исходного файла, где сказано, что общего у деклараций в этом файле, ссылки на справочники, общие указания по ведению программы и т.д. [2] Комментарий для каждой нетривиальной функции, указывающий ее цель, используемый алгоритм (если только он не очевиден) и, возможно, несколько слов о допущениях относительно среды функции [3] Краткие комментарии там, где текст программы не Очевиден и/или непереносим. [4] Как можно меньше сверх того. Например: * // +Ы.с: Реализация таблицы символов. /’ Gaussian elimination with partial pivoting See Ralston: "A first course " p. 411 '/ /" .......... Copyright (c) 1984 AT&T, Ink. All rights reserved Хорошо выбранная и хорошо написанная последовательность комментариев - это существенная часть программы. Написать хорошие комментарии может оказаться так же трудно, как и написать саму программу. Обратите также внимание, что если в функции используются исключительно комментарии вида // , то любую часть этой функции можно исключить с помощью комментариев вида /" "/ , и наоборот. 3.5 УПРАЖНЕНИЯ 1. (в1) Перепишите следующий оператор for в форме эквивалентного оператора while: for (i = 0;, i < max _ length; i+ + ) if (input _ line[i] = = ?) quest _ count + +; Перепишите это с использованием указателя как управляемой переменной, т.е. так чтобы проверка имела вид ’р = = ? . 2. (“1) Полностью разбейте скобками следующие выражения:’ a = b + c"d<<2&8 а & 077 != 3 а==ЬНа==с .&& с < 5 с = х != 0 0 < = i < 7 103
f(1,2) + 3 а = - 1 + + b - - 5 а = Ь = = C+ + а = Ь = с = 0 а[4][2] ■= ■ a-b,c = d Ь ? с : ’ d ■ 2 ("2) Найдите 5 различных конструктов C++, для которых неопределено значение. 4. ("2) Найдите 10 различных примеров непереносимых программных фрагментов на С + + . 5. ("1) Что происходит, если на Вашей ЭВМ Вы выполните деление на нуль? Что происходит при переполнении и исчезновении значащих разрядов? 6. (*1) Полностью разбейте скобками следующие выражения: + + а— (int")p->m “р.т ■a[i] 7. (*2) Напишите функции: strlen() - возвращает длину строки, strcpyQ - копирует одну строку в другую, strcmp() - сравнивает две строки. Обдумайте, какими должны быть типы аргументов и типы возвращаемых значений, а затем сравните свои функции со стандартными, которые объявлены в <string.h> и описаны в Вашем справочнике. 8. (а1) Посмотрите, как Ваш компьютер реагирует на такие ошибки: а : = b + 1; if (а = 3) // ... if (а&077 = = 0) // ... Придумайте ошибки попроще и посмотрите, как на них реагирует компьютер. 9- (*2) Напишите функцию caf(), которая принимает два строковых аргумента и возвращает строку - конкатенацию аргументов. Для получения памяти, в которой разместить результат, воспользуйтесь оператором new. Напишите функцию rev(), которая берет строковый аргумент и переставляет его символы в обратном порядке. То есть после выполнения rev(p) последний символ р должен стать первым 1 и т.д. Ю. (’2) Что делает этот пример? void send(register" to, register" from, register count) // Изобретение Даффа. Пояснительные комментарии // намеренно стерты. register n = (count + 7)/8; 104
switch (count%8) { case 0: do { ’to + + - “from + + case 7: ’to + + — “from + + case 6: ’to + + = “from + + case 5: “to + + = “from + + case 4: “to + + = “from + + case 3: “to + + - “from + + case 2: “to + + = “from + +* case 1: “to + + = "from + + } while (—n>0); } } Зачем кому-либо понадобилось писать такое? 11. (“2) Напишите функцию atoi(), которая берет строку, содержащую цифры, и возвращает соответствующее целое число int. Например, atoi("123”) дает 123. Модифицируйте atoi() так, чтобы помимо простых десятичных чисел она обрабатывала числа в восьмиричной и шестнадцатиричной нотации C++. Модифицируйте atoi() так, чтобы она обрабатывала нотацию символьных констант С + +. Напишите функцию itoa(), которая дает строковое представление целого аргумента. 12. (’2) Перепишите функцию get_token ($3.1.2) так, чтобы за каждый раз она считывала в буфер по одной строке, а затем составляла лексемы, читая символы из буфера. 13. (“2) К настольному калькулятору из $3.1 добавьте функции квадратного корня - sqrt(), log() и sin(). Указание: заранее определите имена и вызывайте функции посредством вектора указателей на функции. Не Забывайте проверять аргументы в вызовах функций. 14. (’3) Обеспечьте, чтобы в настольном калькуляторе пользователь мог определять функции. Указание: Определите функцию как последовательность операторов точно так же, как их ввел бы пользователь. Такую последовательность можно хранить либо как символьную строку, либо как список лексем. Затем, когда функция вызвана, читайте и выполняйте эти операции. Если Вы хотите, чтобы функция, определенная пользователем, принимала аргументы, Вам придется придумать для этого нотацию. 15. (“1.5) Преобразуйте настольный калькулятор так, чтобы в нём вместо статических переменных name _ string и number value использовалась структура symbol: struct symbol { token _value tok; union { double number _ value; char’ name_string; 16. ('2.5) Напишите программу, удаляющую комментарии из программь* на C++. То есть надо читать из cin и удалять комментарии // и / 105
-/ а результат записывать в cout. Не беспокойтесь о внешнем виде результата (зто будет другая задача, потруднее). Не беспокойтесь о неверных программах. Позаботьтесь о // , Г и "/ в комментариях, строках и символьных константах. (*2) Просмотрите различные программы, чтобы проникнуться идеями различных практических стилей написания комментариев и применения отступов.
И.В.К." 105023 Москва, Мал. Семеновская, д. 5 Тел.: 936-50-67, 311-52-08 Факс: 203-93-55 ГЛАВА 4 > ФУНКЦИИ И ФАЙЛЫ Все нетривиальные программы составляются из нескольких отдельно скомпилированных элементов (обычно, их называют просто файлами). В данной главе описано, как функции, скомпилированные по отдельности, могут вызывать друг друга и совместно пользоваться данными, и как обеспечивается совместимость типов, использующихся в различных частях программы. Подробно обсуждаются функции, включая передачу аргументов, аргументы/ по умолчанию, переназначение имен функций, указатели на функции и, конечно, объявление и определение функций. Наконец, описываются макросы. 4.1 ВВЕДЕНИЕ Обычно невозможно держать всю программу в одном файле, поскольку программные тексты для работы со стандартными библиотеками и операционной системой находятся в другом месте. Более того, держать текст всей пользовательской программы в одном файле обычно непрактично и неудобно. Способ разбиения программы на отдельные файлы может помочь читателю понять общую структуру программы и позволит компилятору улучшить эту. структуру. Поскольку единица компиляции - файл, то приходится перекомпилировать весь файл, если он изменился (пусть и совсем немного). Даже для программы умеренной длины можно значительно сократить время перекомпиляции, если разбить ее на файлы разумной длины. Рассмотрим пример с калькулятором. Он был сделан в виде единого исходного файла. Если Вы его вводили, то у Вас наверняка были трудности с расположением всех деклараций (объявлений) в правильном порядке,* и пришлось воспользоваться по крайней мере одной "фальшивой" декларацией, чтобы компилятор смог обработать взаимно рекурсивные функции expr(), term() и prim(). В книге было сказано, что в программе четыре части (лексический анализатор, синтаксический анализатор, таблица символов и драйвер), но сам текст программы это никак не отражал. На самом деле, калькулятор писался не так. Его просто не так пишут; даже если при составлении этой "бросовой" программы отбросить все соображения программной методологии и поддержки и эффективности компиляции, автор все равно разделил бы эту 200-строчную программу на несколько файлов просто для того, чтобы программировать было приятнее. 107 ,.Г
Программа, состоящая из нескольких отдельно скомпилированных мастей, должна быть непротиворечивой в смысле использования имрн и типов таким же образом, что и программа, состоящая из единого исходнрго файла. В принципе это может обеспечить линкер. Линкер (другое русское название - редактор связей. Прим, пере в.) - это программа^ связывающая воедино отдельно скомпилированные части. Иногда линкер называют загрузчиком (по ошибке); в системе UNIX линкер называется Id (от английского loader - загрузчик. Прим, перев.). Однако в большинстве операционных систем линкеры слабо обеспечивают проверку непротиво¬ речивости. Недостаточную поддержку линкера программист может компенсировать, предоставив дополнительную информацию о типах (декларации). После этого в программе будет обеспечиваться непротиворечивость, благодаря проверке непротиворечивости деклараций в отдельно компилируемых файлах. Ваша операционная система должны иметь обеспечивающие это средства. Архитектура С'+ + поддерживает подобное явное линкование1. 4.2 ЛИНКОВАНИЕ ' Имя, не являющееся локальным по отношению к функции ил^ классу, должно, если не указано иное, относиться к одному и тому же типу, значению, функции или объекту в. любой скомпилированной части программы. То есть некоторое имя может быть дано только одному нелокальному типу, значению, функции или объекту. Рассмотрим, например, два файла: // filel.c: int а = 1; int f() {../"• что-то делает ’/ } // file2.c: extern int a; int f(); void g() { a = f(); } а и f(), используемые g() в file2.c - это а и f(), определенные в filel.c. Ключевое слово extern указывает на то, что декларация а в file2.c - это (только) декларация, а не определение. Если бы а инициализировалось, то компилятор проигнорировал бы extern, поскольку декларация с инициализацией - это всегда определение. Объект должен определяться только один раз в программе. Объявлять его можно несколько раз, но при этом типы должны в точности совпадать.Например: // filel.c: 1 Архитектура C++ в большинстве случаев поддерживает неявное линкирование. Однако сфера использования языка С существенно расширилась, так что теперь случаи Неявного линкирования составляют меньшинство. 108
int а = 1; int b = 1; extern int c; // file2.c: int a; extern double b; extern int c; Здесь есть три ошибки: а определено дважды (int а; - это определение со значением int а = 0;), b объявляется дважды, причем с разными типами, с объявляется дважды, но не определяется. Ошибки такого рода (ошибки линкования) компилятор, обрабатывающий каждый раз по одному файлу, не может обнаружить. Они, однако, обнаруживаются линкером. Следующая программа написана не на С + + (а на языке С): // filel.c: int а; ; int f() { return a; } // file2.c: int a; int g() { return f(); } Во-первых, file2.c написан не на С + +, поскольку f() надо было объявить, так что компилятор C+ + обратит на это внимание. Во-вторых, (если устранить проблемы в file2.c) программа не будет редактироваться, так как а определено дважды. Имя можно сделать локальным в файле, объявив его static. Например: // filel.c: static int а = 6; static int f() { /’. ... "/. } // file2.c: static int a = 7; static int f() { /" ... 7 } Поскольку во всех случаях а й f объявлены статически, полученная программа будет правильной. В каждом файле будет свое а и свое f(). Если переменные и функции явно объявлются static, то данный фрагмент программы становится легче ронимать, (не приходится заглядывать куда-то еще). Применение static для функций еще и уменьшает потери на вызовы функций, поскольку оптимизирующему компилятору легче работать. Рассмотрим два этих файла: // filel.c: const а = 7; inline int f() { /’ ... V } struct s { int a,b; '/ ); 109
// file2.c: const a = 7; inline int f() { Г ... "/ } J f ' struct s { int a,b; "/ }; ' ~ Если правило "только одно определение" применять и к константам, функциям с подстановкой тела (inline-функциям) и определениям типов, как это делается с функциями и переменными, то filel.c и file2.c не могут быть частями одной программы на С + +. Но если это так, то как могут два файла использовать одни и те же типы и константы? Коротко ответить можно так: типы, константы и т.п. можно определять столько раз, сколько понадобится, при условии идентичности определений. Полный ответ будет посложнее (рассмотрим его в следующем разделе). 4.3 ЗАГОЛОВОЧНЫЕ ФАЙЛЫ Во всех декларациях одного и того же объекта его типы должны быть непротиворечивы. Один из способов добиться этого - снабдить линкер средствами проверки типов, но большинство линкеров имеют архитектуру 50-х годов и по практическим соображениям изменять их нельзя2. Другой подход состоит в обеспечении того, чтобы исходный текст, поданный на вход компилятора, либо был непротиворечивым, либо включал такие сведения, которые позволяют компилятору заметить противоречивость. Один из таких - несовершенных, но простых - методов обеспечения непротиворечивости деклараций в разных файлах: включать заголовочные файлы (header file) с интерфейсной информацией в исходные файлы программ и/или определений данных. Механизм #include (включения) крайне простое средство обработки текста, собирающее фрагменты исходной программы в единый элемент (файл) для компиляции. Директива #include "что _включить" заменяет строку, в которой стоит #include, на содержимое файла "что_включить". Содержимое должно быть также написано на C++, поскольку после его включения компилятор переходит к ’ обработке содержимого. Нередко процедуру включения выполняет отдельная программа, называемая препроцессор С, которую вызывает СС для преобразования исходного файла из вида, представленного программистом, в вид без директив включения, и только после этого начинается собственно компиляция. Однопроходный же компилятор, наоборот, обрабатывает эти Директивы по мере их появления в исходном тексте. Если программист хочет увидеть действие директив включения, то можно использовать команду СС -Е file.c 2Один линкер легко программа, зависящая от изменить, но после этого будет ли оптимизированного линкера? переносимой но
для запуска препроцессора так же, как это делает СС перед собственно компиляцией. Если надо включать файлы из стандартной библиотеки заголовочных файлов, то вместо кавычек используются угловые скобки и >. Например: #include <stream.h> // из стандартной библиотеки заголовочных // файлов #include "myheader.h" // из текущей директории В использовании < > есть то преимущество, что действительное имя стандартной директории заголовочных файлов не встраивается в программу (часто сначала поиск ведется в /usr/include/СС, а потом - в /usr/include). К сожалению, пробёл в директивах включения не игнорируется: #include < stream.h > // не найдет <stream.h> Может показаться экстравагантным: перекомпилировать файл всякий раз, когда он куда-нибудь включается, однако время компиляции подобного файла обычно ненамного отличается от времени чтения некоторой заранее скомпилированной версии этого же файла. Причина такого решения: текст программы - это весьма компактное представление программы, а включаемые, файлы обычно содержат только декларации, а не код, который требует тщательного компиляционного анализа. Следующие практические правила, что включать и что не включать в заголовочные файлы - это не требования языка, а просто разумные советы по применению механизма #include. Заголовочный файд может включать: Определения типов Декларации функций Определения функций с подстановкой тела Декларации данных Определения констант Перечисления Директивы включения Макроопределения Комментарии struct point { int х, у; ];. extern int strlen(const char*); inline char get() [ return ‘p++; ] extern int a; const float pi = 3.141593; enum bool { false, true' ]; #include <signal.h> #define Case break;case /* проверка на конец файла */ Но никогда не надо включать Определения обычных функций Определения данных Определения множеств констант char getО { return *р++; ) int а; const tbl[] = [ /* ... */ ]; В системе UNIX у заголовочных файлов есть удобный суффикс _.h. Файлы, содержащие определения функций* или данных, должны иметь суффикс .с. Соответственно, их часто называют ".h-файлы" и ".с-файлы" Макросы рассматриваются в $4.7. Обратите внимание, что в С + + макросы менее полезны, чем в языке С, поскольку в С + + есть такие конструкты, 111
как const - для определения констант, и inline - для устранения потерь на вызовы функций. Причина, по которой в заголовочных файлах допускается определение простых констант, и не рекомендуется определение множеств констант, чисто практическая. В принципе, проблема заключается только в той, “чтобы разрешить повторяющееся определение переменных (даже определения функций могут повторяться). Тем не менее, старомодным линкерам трудно убедиться в идентичности нетривиальных констант и удалить ненужные дубли. Более того, простые случаи встречаются гораздо чаще, и поэтому важнее; чтобы для них генерировался хороший код. 4i.3.1 Единый заголовочный файл Самое простое решение проблемы разделения программы на несколько файлов - разместить функции й определения данных в разумном количестве исходных файлов и объявить все нужные им для взаимодействия типы в едином заголовочном файле, который будет включаться во^ все файлы. Для программы-калькулятора можно представить себе четыре .с- файла: lex.с, syn.c, table.с и main.с и заголовочный файл dc.h, содержащий декоарации всякого имени, которое встречается больше чем в одном .с- файле: 4 J/ dc.h: общие декларации для калькулятора ^include <stream.h> enum token _ valiie { NAME, NUMBER, PLUS=' + ', MINUS = PRINT = ASSIGN = ' = END, MUL = '*', DIV = 7Z, LP ='(', RP =')' extern int no _ of _ errors; extern double error(char* s); ’ extern token _ value get_ token(); extern token _ value curr_tok; extern double number _ value; extern char name_string[256]; extern double expr(); extern double term(); extern double prim(); struct name { char" string; name“next; double value; }; extern name’ look(char" p, int ins = 0); inline name" insert(char” s) { return look(s,1); } 112
Оставив в стороне реальный текст, посмотрим, как будет выглядеть lex.c: // lex.c: ввод и лексический анализ #include "dc.h" /. #include < ctype.h > < token _ value curr.tok; double number _ value; char name _ string[256); token _ value get_^token() { /’ ... 7 } i Обратите внимание на то, что такой спосрб использования заголовочного файла гарантирует^ что всякая декларация объекта, определяемого пользователем в заголовочном файле, в какой-тр момент будет ^в^лючена в файл, в котором она .определена. ДЧапример, при компиляции lex.c компилятор получит на вход следующее: v V extern token _ value get^token(); // ... 1 * 1': token „value get_ token() { /’ ..; 7 } Это гарантирует, что компилятор обнаружит все противоречия в типах, заданных для имени. Например, если бы в декларации ^et^tokenj) было указано, что кона возвращает token _ value, но в определении указано, что она возвращает int, то компиляция lex.c дала бы ошибку несоответствия типов. - ( Файл syn.c будет иметь такой вид: // syn.c: синтаксический анализ и вычисление , #include "dc.h" double prim() { /’ ... 7 } \ double term() { /' ... 7 } i( double expr() { /’ ... 7 } $ Файл table.с будет иметь такой вид: // table.с: таблица символов и поиск #include "dc.h" extern char’ strcmp(const char’, const char’); extern char" strcpy(char", const char’); extern int strlen(const char’); const TBLSZ = 23; name" table[TBLSZJ; name" look(char“ p, int ins) { /" ... 7 } 113
Обратите внимание на то, что в самом файле table.с объявлены стандартные функции работы со строками, поэтому для них не производится проверка на непротиворечивость. Почти всегда лучше включить заголовочный файл, чем объявлять имя внешним (extern) в .с-файле. Из-за Этого, возможно, придется включать "слишком много", но как правило, это серьезно не повлияет на время компиляции, и в итоге сэкономит время программиста. Для примера посмотрите, как функция strlen() переобъявлена в main.с (ниже). Это - клавиши, нажатые впустую, и потенциальный источник ошибок, поскольку компилятор не может проверить непротиворечивость этих двух объявлений. Естественно, эту проблему можно было бы обойти, если бы в dc.h были записаны все внешние (extern) декларации, как мы и собирались. • Эту "неряшливость" в программе мы оставили нарочно, поскольку подобное часто встречается в программах на языке С. Это очень соблазнительно для программиста, и чаще всего приводит к ошибкам, которые трудно обнаружить,-: и создает программы, которые трудно поддерживать. Вас предупредили! Наконец, файл main.с будет иметь такой вид: // main.с: инициализация, главный цикл и обработка ошибок #include "dc.h" int no _ of _ errors; double error(char’ s) { Г ... 7 } extern int strlen(const char’); maih(int argc, char’ argvf]) { /" ... V } Есть один важный случай, когда размер заголовочных файлов становится серьезной проблемой. С помощью набора заголовочных файлов и библиотеки можно расширить набор общих и прикладных типов (см. главы 5 - 8). В этих случаях заголовочные файлы зачастую содержат тысячи строк, и они читаются всякий раз в начале каждой компиляции. Содержимое этих файлов, как правило, фиксировано и изменяется очень редко. Здесь бы очень пригодился метод запуска компилятора, заранее загруженного содержимым этих заголовочных файлов, ведь в некотором смысле создается специальный язык со своим собственным компилятором. Пока еще не существует стандартной процедуры создания подобного заранее загружённого компилятора. 4.3.2 Множественные заголовочные файлы Метод создания единого заголовочного файла очень полезен, когда программа имеет небольшой объем, и Вы не собираетесь использовать ее составные части по отдельности. В этом случае нетрудно понять, по какой причине те или иные декларации помещены в заголовочный файл. Здесь могут помочь комментарии. Вместо этого можно дать каждой части программы свой заголовочный файл, определяющий средства и инструменты, содержащиеся в этой части. Для каждого .с-файла есть соответствующий h-файл, и каждый .с-файл включает свой .h-файл (определяющий, что дает 114
.с-файл) и, возможно, другие .h-файлы (определяющие, что требуется файлу). Рассматривая организацию программы-калькулятора, видно, что функция еггог() используется почти во всех функциях программы, сама она использует только <stream.h>. Это обычная ситуация с функциями обработки ошибок, и как следствие, еггог() должна быть, отделена от main(): // error.h: обработка ошибок extern int no __ of _ errors; extern double erbor(char“ s); // error.c #include < stream.h> #include "error.h" int no _ of _ errors; double error(char" s) { /’ ... ’/ } При таком подходе к использованию заголовочных файлов, .h-файл и связанный с ним .с-файл можно рассматривать как модуль, в котором .h- файл задает интерфейс, а .с-файл определяет реализацию. Таблица символов не зависит от остальных частей калькулятора за исключением использования функции обработки ошибок. Это можно выразить явным образом: // table.h: декларации таблицы символов struct name { char" string; t name’ next; double value; }; extern name’ look(char“ p, int ins = 0); inline name" insert(char’ s) { return look(s,1); } // table.с: определения таблицы символов #iriclude "error.h" #include <string.h> #include "table.h" const TBLSZ = 23; name" table[TBLSZJ; name" look(char" p, int ins) { /’ ... "/ } 115
Обратите внимание на то, что теперь декларации функций обработки сТрок включаются из <string.h>. Так устраняется еще один потенциальный источник ошибок. “ 'Л // lex.h: декларации для ввода и лексического анализа enum token —value { NAME, NUMBER, END, PLUS = ' + ', MINUS = MUL = '"', DIV = 7', PRINT = ASSIGN = ' = ', LP = 'C, RP = ')' }; extern token_ value curr_ tok; extern double number _ value; extern char name_ string[256]; extern token —value get —token(); Это весьма небрежный интерфейс с лексическим анализатором. Отсутствие правильного типа для лексемы проявляется в необходимости представлять пользователю функции get_token() ее реальные лексические буферы number —value и name_ string. // lex.с: определения ввода и лексического анализа #include <stream.h> # include <ctype.h> #include "error.h" #include "lex.h" token —value curr_ tok; double number— value; char name_string[256]; token —value get_token() { /“ ... 7 } Интерфейс с синтаксическим анализатором особенно прозрачен: // syn.h: декларации для синтаксического анализа и вычислений extern double expr(); extern double term(); extern double prim(); // syn.c: определения для синтаксического анализа и вычислений #include "error.h" #include "lex.h" #include "syn.h" double prim() { /“ ... 7 } double term() { /’ ... 7 } 116
double expr() { /' ... '/ } i Программа main, как всегда, тривиальна: // main.с: главная программа #include <stream.h> #include "error.h" #include "lex.h" ^include "syn.h" „ * tfinclude "table.h" * #include <string.h> main(int argc, char" argv[]) { /“ ... "/ } Сколько заголовочных файлов используется в программе - зависит от многих факторов. Многие из этих факторов больше Связаны со способом обработки файлов Вашей операционной системой, а не с С + + . Например, если Ваш текстовый редактор не может одновременно работать с несколькимй файлами, то становится не так приятно работать с несколькими заголовочными файлами. Аналогично, если чтение 10 файлов по 50 строк занимает заметно больше времени, чем чтение одного файла в 500 строк, следует все хорошо взвесить, прежде чем принять подход множественных заголовочных файлов в небольшом проекте. Один с!овет: набором из 10 заголовочных файлов вместе со стандартными заголовочными файлами обычно легко управлять. С другой стороны, -если для большой программы Вы разобьете декларации на логически минимальные заголовочные файлы (каждая декларация структуры - в своем файле и т.п.), то у Вас запросто получится неуправляемая каша из сотен файлов. 4.3.3 Скрытие данных С помощью заголовочных файлов пользователь может определять явные интерфейсы, которые гарантируют непротиворечивость использования типов в программе. Однако, интерфейс, содержащийся в заголовочном файле, можно обойти благодаря использованию extern (внешних) деклараций в .с-файлах. Обратите внимание, что подобный стиль линкования не рекомендуется: // filel.c: // "extern" не используется int а = 7; const с = 8; void f(long) { /" ... "/ } // file2.c: // "extern" в .с-файле extern int a; extern const c; extern f(int); int g() { return f(a + c); } Поскольку внешние декларации файла file2.c не включены вместе с 117
определениями в файл filel.c, компилятор не может проверить кепроГиворечивость этой программы. Следовательно, если только загрузчик не будет намного сообразительнее среднего уровня, две ошибки в этой программе придется отыскивать программисту. Пользователь может предохранить свой файл от подобных' 'Л неупорядоченных связей, объявив имена, не предназначенные для общего пользования, static, так что их областью действия будет данный файл, и они будут скрыты1 от прочих частей программы. Например: // tabte.c: определения таблицы символов #include "error.h" #include <string.h> #include htable.n" const TBLSZ = 23; static name’ table[TBLSZj; name" look(char" p, int ins) { /" ... "/ } Таким образом, гарантируется, что доступ к таблице символов всегда идет через 1бок(). Константу TBLSZ "скрывать • необязательно. ’ 4.4 ФАЙЛЫ КАК МОДУЛИ В предыдущем разделе .с-файл и .h-файл вместе определяют программу. '.Ь-файл - это интерфейс, который используют другие части программы; ,<>файл задает реализацию. Подобный элемен? часто называют модулем* Доступны только имена, необходимые пользователю, остальное скрыто. Такое свойство называется скрытием данных, хотя данные - это не все, что можно скрыть. Модули такого типа обеспечивают большую гибкость. Например, реализация программы может состоять из одного или нескольких .с-файлов, и дается несколько интерфейсов в виде .h-файлов. Информацию, которую пользователю знать необязательно, надежно скрыта в ''.«-файлах. Если важно не дать пользователю информацию, что конкретно содержат ,с-файлы, можно не предоставлять ;йх в форме исходных текстов. Достаточно дать эквивалентные о-файлы - результаты работы компилятора. Иногдёврзникают проблемы из-за того, йто такая гибкость достигается без формёлЬной структуры. Сам язык не счйтаёт модуль своим элементом, и у компилятора нет; средств отличать .Ь-файль^, опрёделякэщиё имена, используемые другими модулями (экспорт), от .h-файлов, определяющих имена из других модулей (импорт). < Также могут возникать проблемы из-за того, что модуль определяет мнбжество объектов, а не новый тип. Например, модуль таблицы определяет одну таблицу; если нужйы две таблицы, то исходя из идеи модулей не существует тривиального способа это сделать; решение этой задами дается в главе 5. ?' Всякий'объект, которому выделена статическая, память, по умолчанию инициализируется нулём, а другие (постоянные) значения может задать программист. Такой вид инициализаций - сёмый ^примитивный. К счастью, благодаря классам можно указать, какой Ьрограм?мный фрагмент надо lie
выполнить при инициализации и до любого использования модуля и/или какой программный фрагмент надо выполнить для очистки после последнего использования модуля, см. $5.5.2. 4.5 КАК СОЗДАТЬ БИБЛИОТЕКУ Выражения вроде "записать в библиотеку" и "найден в библиотеке" часто встречаются (в этой книге и в других источниках), но; что же это значит для программы на С + + ? К сожалению, ответ зависит от того, какая операционная система используется; в данном разделе рассказывается, как создать библиотеку средствами 8т-го издания операционной системы UNIX. В других системах есть аналогичные средства. В основном, библиотека представляет собой множество, .о-файлов, полученных компиляцией соответствующего множества .с-фёйлрв. Обычно имеется одий или несколько .h-файлов с декларациям**, которые необходимы для использования этих .h-файлов. Рассмотрим для, примера требование предоставить удобный набор математических функций для некоторого неопределенного множества пользователей. Заголовочный файл мог бы выглядеть так: extern double sqrf(double); // подмножество <;math.h> extern double sin(double); extern double cos(double); extern double exp(double); extern double log(dOuble); В таком случае Определения этих функций хранились бы, соответственно, в файлах sqrt.c, sin.c, pos.c, ехр.с и log.c. V Библиотеку с именем nftath.a можно создать таким образом: $ СС ,-с sqrf.c sin.c cos.c exp.c log.c $ ar cr math.a sqtt.o sin.о cos.o exp.o log.p . $ ran lib math.a Сначала исходные файлы компилируются,, что. дает соответствующие объектные фрйлы. Затем дается команда аг, п6 которой создаётся архив с именем math.a. Затем в этом.архиве создается индекс Для обеспечения быстрого доступа к нему. Если в Вашей, системе нет команды ranlib, то вероятно, она Вам и не нужна; пожалуйста, посмотрите, ^сказано в справочнике по Ващей системе об аг. Использовать библиотеку модкно так: $ СС myprog.c math.a i Ну и какое же преимущество дает использование библиотеки перед непосредственным использованием .,о-файлов| Например; $ СС mypTog.c sqrt.o sin.о cos.o exp.o log.о Для большинства, программ нетривиально указать правильный набор .о-файлов. В вышеуказанном примере включались все файлы, но если бы функции из myprog.c вызывали только sqrt() и cos(), то кажется, достаточно 119
было бы писать так: $ СС myprog.c sqrt.o cos.o ' < чТо неверно, поскольку qos.c использует sin.с. Линкер, который вызывается командой СС для обработки .а-файла (в данном случае - math.a), знает, как из множества .о-файлов, составляющих a-файл, извлечь только необходимые .о-файлы. Другими словами, с помощью библиотеки можно включить много определений, используя одно имя (включая определения функций и переменных, требующихся внутренним функциям и никогда не доступных пользователю), и в то же время иметь гарантию, что в программу-результат попадет только минимальное количество определений. 4.6 ФУНКЦИИ Обычный способ что-нибудь выполнить в программе на С + + - это вызвать функцию, выполняющую это. Определить функцию - значит указать, как должна выполняться операция. Функцию нельзя вызвать до тех чпор, пока она не определена. 4.6.1 Декларации функций Декларация функции дает имя функции, тип; значения, возвращаемого (если оно есть) функцией, и количество и типы аргументов, которые надо дать в вызове функции. Например: extern double sqrt(double); extern elem" next_elem(); extern char’ strcpy(char’ to, const char’ from); extern void exit(int); Семантика передачи параметров идентична йсёмантике инициализации. Типы аргументов проверяются и в случае необходимости выполняемся неявное преобразование типов. Например, с учетом предыдущих деклараций double sr2 = sqrt(2); Даст правильный вызов функции sqrt() с аргументом с плавающей точкой - 2.0. Значение подобной проверки й преобразования типов трудно переоценить. Декларация функции может содержать имена аргументов. Это может быть удобно читателям, но компилятор просто игнорирует такие ймена. 4.6.2 Определения функций Всякая функция, вызванная в программе, должна быть где-то определена (только один раз). Определение функции - это декларация Функции, в которой представлено тело функции. Например: extern void swap(int", int“); // декларация 120
void swap(int" p, int" q); // определение int t = "p; ? = > q = +; Во избежание потерь на вызов функции функцию можно объявить inline (функцией с подстановкой тела), см. $1.12, а ее аргументы, можно объявить register (регистровыми) для обеспечения более быстрого доступа к ним ($2.3.11). Обоими средствами можно злоупотребить, поэтому ими не следует пользоваться в случае сомнений в их целесообразности. 4.6.3 Передача аргументов Когда функция вызывается, то каждому ее формальному аргументу отводится память и каждый формальный аргумент инициализируется значением реального аргумента. Семантика передачи аргументов аналогична семантике инициализации. В частности, тип реального аргумента сравнивается с типом соответствующего формального аргумента, и выполняется преобразование типов - стандартных и определяемых пользователем. Имеются особые правила передачи векторов ($4.6.5), средство передачи аргументов без проверки ($4.6.8.) и средство задания аргументов по умолчанию ($4.6.6). Сравните: void f(int val, int& ref) val + +; ref + +; } При вызове f() val + + увеличивает локальную копию первого реального аргумента, a ref + + увеличивает второй реальный аргумент. Например, int i = 1; int j = 1; увеличит j, а не i. Первый аргумент, i, передается по значению, второй аргумент, j, передается по адресу. Как сказано в $2.3.10, использование функций, модифицирующих аргументы, вызванные по адресу, может затруднить чтение программ, и как правило, этого следует избегать (см., однако $6.5 и $8.4). Тем не менее, бывает гораздо эффективнее передать крупный объект по адресу, а не по значению. В этом случае аргумент можно объявить const, что будет означать, что адрес используется только по соображениям эффективности, а не для того, чтобы дать возможность вызванной функции изменить значение объекта: void f(const large& arg) о 121
// значение "arg" изменить нельзя } * диалогично, объявление аргумента-?указателя const информирует читателя 6 том, что значение объекта, на который указывает аргумент, функцией не изменяется. Например: extern int strlen(const char"); // из <string.h> extern char" strcpy(char" to, const char" from); , extern int strcmp(const char", const char"); Необходимость в подобной практике повышается с ростом размера программы. Обратите внимание, что семантика передачи аргументов отличается от семантики присваивания. Это важно при работе с аргументами типа const, адресными аргументами и аргументами некоторых типов, определяемых пользователем ($6.6). # 4.6.4 Возврат значения Значение может (и должно) быть возвращено из функции, которая не объявлена void. Возвращаемое значение задается оператором возврата (return). Например: int fac(int n) { return (n>1) ? n’fac(n-l) : 1; } В функции может быть более одного оператора возврата: int fac(int n) { if (n > 1) return n’fac(n-l) else return 1 ; Как и семантика передачи аргументов, семантика возврата значения из функции идентична семантике инициализации. Считается, что оператор возврата инициализирует переменную возвращаемого типа. /Тип возвращаемого выражения сравнивается с типом возвращаемого типа и выполняются преобразования всех стандартных и определяемых пользователем типов. Например: double f() { // ... return 1; // неявно преобразуется в double(l) } Всякий раз при вызове функции в памяти создается новая копия ее аргументов и автоматических переменных. После возврата из функции эта память используется заново, поэтому неразумно возвращать указатель на 122
локальную переменную. Содержимое указуемой ячейки непредсказуемо изменяется: inf f() { inf local = 1; // ... return &local; // так делать нельзя } Эта ошибка - более редкая, чем эквивалентная ей ошибка с использованием адресов: int& f() { int local = 1; // ... return local; // так делать нельзя } К счастью, компилятор предупреждает о подобных возвращаемых значениях. Вот еще пример: int& f() { return 1; } // так делать нельзя 4.6.5 Векторные аргументы Если в качестве аргумента функции используется вектор, передается указатель на его первый элемент. Например: int strlen(const char"); void f() < char v[l = "вектор"; strlen(v); strlen("HnKonac"); }/ Другими словами, при передаче в качестве аргумента аргумент типа Т[] преобразуется в тип Т". При этом подразумевается, что присваивание элементу векторного аргумента изменяет значение элемента вектора, указуемого аргументом. Иначе говоря, векторы отличаются от аргументов других типор тем, что их нельзя передать по значению. Размер вектора недоступен вызванной функции. Это может мешать в работе, но существуют различные способы это преодолеть. Строки завершаются нулями, поэтому их размер легко подсчитать. Для прочих векторов можно передавать размер во втором аргументе, или можно вместо простого вектора передавать тип, состоящий из указателя и индикатора длины (см. $1.11). Например: void compute1(int" vec_ptr, int vec —size); // один способ struct vec { // другой способ
int" ptr; int size; }; void compute2(vec v); С многомерными массивами все сложнее, но нередко вместо них можно пользоваться векторами указателей, а для них специальная обработка не требуется. Например: char" day[] = { "ни" "пт” ,,Г'Г\П "чт" "r-l-r" //„-И ПН , ВТ , ср , ЧТ , пт , со , ВС }; Рассмотрим, однако, как можно определить функцию обработки двумерной матрицы. Если во время компиляции ее размерности известны, то проблем нет: void print _m34(int m[3][4]) for int i = 0; i<3; i+ +) { for int j = 0; j<4; j + + ) cout <<""<< m[i][j]; cout << "\n"; } * 1 Конечно, эта матрица передается как указатель, так что размерности используются только для удобства нотации; Первая размерность массива не связана с задачей найти расположение элемента ($2.3.6). Следовательно, его можно передавать как аргумент: void print__mi4(int m[][4], int diml) for int i = 0; i<dim1; i+ + ) { for int j = 0; j<4; j,+ +) cout <<""<< m[i][j]; cout << "\n”; Трудности возникают, если надо передать обе размерности. "Очевидное решение" попросту не работает: void print_mij(int m[][], int diml, int dim2) // ошибка for int i = 0; i<dim1; i+ + ) { for int j = 0; j<dim2; j+ + ) cout << " " << m[i][j]; // сюрприз! cout < < "\n"; } . 124 0 Зак. 1927
Во-первых, декларация аргумента т[][] неверна, поскольку чтобы найтц расположение элемента, необходимо знать вторую размерность многомерном массиве. Во-вторых, выражение интерпретируется как "(ж(т + i) + j), но это вряд ли то, что программист. Вот правильное решение: (правильно* имел в виду void print _ mij(int” m, int diml, int dim2) for int i = 0; i<dim1; i+ + ) { for int j = 0; j<dim2; j+ + ) cout << " " << ((int’)m[i’dim2 + j]; //непонятно cout < < "\n"; } } . Выражение для доступа к элементам эквивалентно выражению, которое генерирует компилятор, когда ему известна последняя размерность. Чтобы сделать текст программы чуть понятнее, можно ввести дополнительную переменную: int’ v = (int’)m; // ... v[i’dim2 + j] 4.6.6 Аргументы по умолчанию Нередко функции в самом общем случае требуется больше аргументов, чем в самом Простом и, обычно, самом частом. Так, например, библиотека потоковых функций содержит функцию hex(), которая выдает строку, содержащую шестнадцатиричное представление целого числа. Второй аргумент используется для указания, в скольких символах можно представить первый аргумент. Если количество символов мало для представления целого числа, выполняется усечение; если оно слишком велико, строка дополняется пробелами. Нередко программисту все равно, сколько символов требуется для представления целого,^если их достаточно, поэтому нуль во втором аргументе означает "использовать точно необходимое количество символов". Чтобы избежать засорения программы вызовами вроде hex(i,0), функция объявлена так: extern char" hex(long, int =0); Инициализатор второго аргумента - это аргумент по умолчанию. Это значит, что если в вызове есть только один аргумент, то вместо второго используется аргумент по умолчанию. Например: cout << """" << hex(31) << hex(32,3) << интерпретируется как 125
cout << """ « hex(31,0) << hex(32,3) << и будет напечатано: “If 20“' ~ ' Тип аргумента по умолчанию проверяется в момент объявления функции, и сам аргумент вычисляется в момент вызова. По умолчанию можно объявлять только конечные аргументы, так что: int f(int, int =0, char" =0); // правильно int g(int =0, int =0, char"); // ошибка int h(int =0, int, char" =0); // ошибка Обратите внимание, что в данном контексте пробел между "и = имеет значение (* = - это оператор присваивания):, int nasty (char ” =-0);. // синтаксическая ошибка 4.6.7 Переназначаемые (совмещаемые) имена функций Очень часто бывает полезно давать различным функциям различные имена, но если какие-то функции выполняют одну и ту же работу с разными типами, то удобнее давать им одинаковое имя. Использование одного и того.же имени для разных операций с разными типами называется переназначением > или совмещением. . Этот /детод уже применяется к основным операциям в С + + : сложение имеет только одно имя, +, но оно применяется для сложения целых чисел, чисел с плавающей точкой и указателей. Эта идея легко распространяется на обработку операций, определяемых программистом, то есть на функции. Чтобы защитить программиста от случайного повторного использования имени, существует ограничение, что имя может именовать более че?м одну функцию, только если оно было объявлено переназначаемым (Overload). Например: overload print; void print(int); void print(char'); Что касается компилятора, то единственное, что есть общего у Функций с одинаковым именем - это имя. Конечно, подразумевается, что эти функции в каком-то смысле схожи, но язык ни ограничивает, ни помогает программисту. Таким образом, функции с переназначаемым именем -это в первую очередь удобство обозначения. Это удобство имеет значение для функций с общепринятыми именами вроде sqrt,4 print и open, ^ли имя семантически значимо, как, например, операторы +, " и << ($6.2), и в случае конструкторов ($5.2.4 и $6.3.1) такое удобство становится существенным. При вызове функции с переназначаемым именем f Компилятор должен понять, какую из функций с именем f надо вызвать. '7Т° делается путем сравнения типов реальных аргументов с типами Формальных аргументов всех функций с ’именем f. Поиск вызываемой Функции ведется в три отдельных этапа: 5* 126
[11 Поиск точного совпадения и вызов найденной функции; [2J Поиск совпадения с учетом встроенных преобразований и вызов любой найденной функции; [3] Поиск совпадения с учетом преобразований, определяемых пользователем ($6.3), и если найден уникальный набор преобразований вызов найденной функции. Например: overload print(double), print(int); void f() { print(l); print(I.O); Правило точного совпадения обеспечивает, что f напечатает Г как целое и 1.0 как число с плавающей точкой; Нуль, char или short дают точное совпадение с аргументом int. Точно так же, float дает точное совпадение с double. К функциям С переназначаемым именем стандартные правила преобразования типов C++ применимы лишь частично ($с.6.6). Не выполняются преобразования, которые могут разрушить информацию, соответственно, int преобразуется в long, int преобразуется в double, нуль преобразуется в long, нуль преобразуется в double, и преобразования указателей: нуль преобразуется в указатель, указатель преобразуется в void1, указатель на производный класс преобразуется в указатель на базовый класс ($7.2. 4). Вот пример, когда преобразование необходимо: overload print(double), print(long); void f(int a) { print(a); Здесь а можно .напечатать только как double или long. Такую неясность можно устранить явным преобразованием типов (либо print(long(a)), либо print(double(a))). С учетом этих правип' можно обеспечить, чтобы при зйачительных различиях эффективности или точности вычислений для обрабатываемых типов применялся самый простой алгоритм (функция). Напримёр: overload pow; int pow(int, int); double pow(double, double); // из <math.h> complex pow(double, complex); // из <complex.h> complex pow(complex, int); complex pow(complex, double); 127
complex pow(complex, complex); g ходе поиска совпадения типов игнорируются unsigned и const. 4.6.8 Неопределенное число аргументов Для некоторых функций невозможно указать количество и типы аргументов, ожидаемых в вызове. При объявлении такой функции в конце декларации Списка аргументов ставится эллипсис (...), что значит "и может быть еще аргументы". Например: int printf(char" ...); Это указывает, что в вызове printf должен быть указан, по-крайней мере, один аргумент — char"; кроме того, функция може+ иметь или не иметь дополнительные аргументы. Например: printf("Hello, worldXn"); рппН("Меня зовут %s %s\n", first _ name, second _ name); printf("%d + %d = %d\n", 2,3,5); Такой функции^ри интерпретации списка аргументов приходится полагаться на информацию, недоступную компилятору. В случае printf() первый аргумент - это форматная строка, содержащая специальные последовательности символов, Позволяющие функции printf() правильно обрабатывать свои аргументы; %s значит "жди аргумента типа char"", a %d значит "жди аргумента типа int". Тем не менее, компилятору это неизвестно, поэтому он не может гарантировать, что ожидаемые аргументы действительно присутствуют, или что аргументы имеют праййНьные типы. Например: рппН("Меня зовут %s %s\n", 2); ^компилируется и (в лучшем случае) напечатает нечто странное. Понятно, что если аргумент не объявлен, то у компилятора нет необходимой информации для выполнения стандартной проверки типа и преобразования типа. В этом случае char или short передаются как int, а float передается как double. Это вовсе необязательно то, что ожидал пользователь. Вырожденный случай эллипсиса, как, например в wild(...), полностью отключает проверку аргументов и оставляет программиста наедине с тьмой проблем, так хорошо знакомых программирующим на языке С. В хорошо продуманной программе может потребоваться самое большее несколько Функций, аргументы которых не полностью определены. В большинстве случаев из соображений проверки типов можно пользоваться функциями с переназначаемым именем и функциями с аргументами по умолчанию вместо т°го, чтобы совсем не определять типы аргументов. Только если и количество аргументов, и тип аргументов могут изменяться, необходимо применять эллипсис. Самое частое применение эллипсиса - задать интерфейс к библиотекам функций на языке С, которые были определены в то время, Когда других возможностей не было: 128
extern int fprintf(FILE", char" ...); extern int execl(char" extern int abort(...); // из <stdio.h> //из <sysent.h> // из <libc.h> Стандартный набор макросов, с помощью которых можно работать с неопределенными аргументами в подобных функциях, можно взять из заголовочного файла <stdargs.h>. Рассмотрим создание функции обработки ошибок, которая принимает один целочисленный аргумент^ соответствующий серьезности ошибки, а после него - произвольное количество символьных строк. Идея заключается в составлении сообщения об ошибке, передавая каждое его слово как отдельный строковый аргумент: <1г void error(int ...); main(int argc, char“ argv[]) switch (argc) { case 1: error(0,argv[0],0); break; case 2: error(0,argv[0],argv[ 1 ],0); break; default: error(1 ,argv[0]/'c",dec(argc-1 ),"аргументамч",О); Функцию обработки ошибок можно определить так: #include <stdargs.h void error(int n ...) /’"n" и после него список символьных строк char", завершающихся нулями “/ f va_ list ар; vastart(ap,n); // // начало аргументов for (;;) { char* p = va_arg(ap,char“); if (p = = 0) break; cerr < < p < < " и и. va end(ap); // очистка аргументов cerr < < "\n"; if (n) exit(n); 129
Сначала определяется va_list, и инициализируется вызовом va,_ start(). /Макрос va_ start принимает в качестве аргументов имя va _ list и имя последнего формального аргумента. Макрос va_arg() используется для поочередной обработки неименованных аргументов. В каждом вызове программист должен предоставлять тип; va_arg() предполагает, что передан реальный аргумент этого типа, но* обычно не существует способа удостовериться в этом. Перед возвратом из функции, в которой использовался макрос va_start(), необходимо вызвать va__end(). Причина этого заключается в том, что va_ start может так изменить стек, что будет невозможно успешно выполнить возврат; va_end() ликвидирует все эти изменения. 4.6.9 Указатель на функцию С функцией можно сделать только две вещи: вызвать ее и получить ее адрес. Указатель, полученный от адреса функции, затем можно использовать для вызова функции. Например: void error(char“ р) { /' ... ’/ } void (“efct)(char" р); void f() efct = &error; ("efct)("error"); } // указатель на функцию // efct указывает на error // вызов error посредством efct Для вызова функции по указателю, например, efct, надо выполнить операцию обратной ссылки, “efct. Поскольку у оператора вызова функции () приоритет выше/ чем у оператора обратной ссылки “, то нельзя просто написать "efct("error"), это значило бы “(efct("error")), что есть ошибка несовпадения типов. То же относится к синтаксису деклараций (см. также Обратите внимание, что у указателей на функции есть типы аргументов, объявленные точно так же, как и сами функции. При присваивании указателям типы функций должны полностью совпадать. Например: void (’pfXchar’); void fl (char’); int f2(char“); void f3(int“); void f() pf =-&f1; pf = &f2; pf = &f3; // указатель на void(char’) // void(char’); // int(char'); // void(int“); // правильно // ошибка: неверный тип возврата //. ошибка: неверный тип аргумента (“pf)("asdf"); // правильно 130
(“pf)(1); // ошибка: неверный тип аргумента int i = ("pf)("qwer"); // ошибка: void присваивается int } Правила передачи аргументов одинаковы для непосредственных вызовов функций и для вызовов функций по указателю. Нередко бывает удобно определить имя типа "указатель на функцию", чтобы потом пользоваться более очевидным синтаксисом. Например: typedef int ("SIG_TYP)(); // из <signal.h> typedef void ("SIG_ ARG_TYP)(); SIG-TYP signal(ijnt, SIG_ ARG.TYP); Иногда удобен вектор указателей на функцию. Например, система меню для моёго текстового редактора, работающего с мышью, реализовано на основе векторов указателей на функции, соответствующих операциям. Подробно систему не стоит описывать, но вот общая идея: typedef void ("PF)(); PF edit__ops[] = { // операции редактирования cut, paste, snarf, search // вырезать, наклеить, форматировать, искать }» PF file_ops[] = { // работа с файлами open, reshape, close, write // открыть, переформировать, закрыть, записать Затем определяем и инициализируем указатели, соответствующие выбранным в меню действиям и связанные с кнопками мыши: PF" button2 = edit _ops; PF" button3 = file-Ops; В полной реализации для определения каждой точки меню требуется больше информации. Например, где-то надо хранить строку с выводимым текстом. При использовании этой системы значение кнопок мыши часто меняется в зависимости от контекста. Эти изменения можно (частично) реализовать, изменяя значения "кнопочных" указателей. Когда пользователь выбирает точку в меню, например, точку 3 кнопки 2, выполняется соответствующая операция: Cbutton2[3])(); Оценить выразительную силу указателей на функции можно, попробовав написать такую программу без них. Меню можно менять во время выполнения, вставляя новые функции в таблицу операторов. Также легко создавать новые меню во время выполнения. Указатели на функции можно применять для создания полиморфных 131
процедур» т е- процедур, применимых к объектам различных типов: typedef int (BCFT)(charB,charB); int Г sort(charB base, unsigned h, int sz, CFT cmp) ; Сортировка "n" элементов вектора "base” в порядке возрастания с помощью функции сравнения, на которую указывает "стр”. Размер элементов = "sz” { Очень неэффективный алгоритм: пузырьковая сортировка ’7 for (int i = 0; i < n—1; i+ + ) for (int j = n—1; i<j; j—) { char” pj = base + j’sz // b[j] char’ pjl = pj-sz // b[j-1] if (Ccmp)(pj,pj1) <0) поменять местами b[j] и b[i-1 ] (int k = 0; k<sz; k+ +) { char temp = pj[k]; pj[k] = pj1[k] = temp; // for } Процедура сортировки не знает типов сортируемых объектов, только количество элементов (размер вектора), размер каждого элемента, и какой функцией производить сравнение. Тип sort() был выбран такой же, что и у стандартной библиотечной функции сортировки языка С - qsort(). Настоящие программы используют qsortQ. Поскольку sort() не возвращает значения, ее надо объявить void, но когда определялась qsort(), в языке С не было типа void. Аналогично, в качестве типа аргумента было бы честнее использовать void" вместо char*. Подобной функцией сортировки можно упорядочить таблицу вроде этой: struct user { char" name; char* id; int dqpt; // имя // идентификатор // отдел typedef user’ Puser; user heads[] = { "McIlroy M.D.", "Aho A.V.", "Weinberger P.J.", "Schryer N.L.", "Schryer N.L.", "doug", 11271, "ava", 11272, "pjw", 11273, "nls", 11274, "nls", 11275, 132
"Kernighan B.W.", "bwk", 11276 void print _ id(Puser v, int n) for (int i = 0; i<n; i+ + ) cout . < < v[i].name << "\t" < < vfil.id < < "\t" < < vfij.dept < < ”\n"; } Перед началом сортировки надо сначала определить Функции сравнения. Функция сравнения должна возвращать отрицательное значение, если ее первый аргумент меньше второго, нуль - если они равны, и положительное значение в противных случаях: int cmp1(char“ р, char* q) // Сравнение строк с именами return strcmp(Puser(p)->name, Puser(q)-> name); int cmp2(char’ p, char" q) // Сравнение номеров отделов return strcmp(Puser(p)-> dept, Puser(q)-> dept); d } Следующая программа сортирует и распечатывает результаты: main() { й sort((char')heads,6,sizeof(user),cmp1); print. id(heads,6); // в алфавитном порядке cout, < < "\п";: sort((char")heads,6,sizeof(user),cmp2); print id(heads,6); // в порядке, номеров отделов Можно пользоваться адресом функции с подстановкой тела и функции с переназначением имени. 4.9 МАКРОСЫ Макросы *определяются в $с.11. В языке С они имеют большое значение, но в C+ + используются гораздо меньше. Вот первое правило по макросам: без необходимости не применяйте. Замечено, что почти каждый макрос сигнализирует о дефекте либо язьн$а программирования, либо программы. Если Вы хотите применять макросы, то, пожалуйста, сначала очень внимательно прочтите руководство по используемому Вами препроцессору языка С. Простой макрос определяется так: #define имя остальная часть строки 133
Когда встречается "имя" (как лексема), то оно заменяется на "остальную часть строки". Например: Л ' named = имя будет расширено вот во что: named = остальная часть строки Можно определить макрос, принимающий аргументы. Например: #define mac(a,b) arguments a argumen+2: b При использовании mac указываются две строки аргументов. Во время расширения они заменят а и Ь. Например: expanded = mac(foo bar, yuk yuk) будет расширено вот во что: expanded argument!: too bar argument2: yuk yuk Макросы работают co строками, и им мало что известно о синтаксисе C++ и ничего не известно о типах C++ или правилах области действия. Компилятор же получает только уже расширенный макрос, а не его определение. Это приводит к очень непонятным сообщениям об ошибках. Вот некоторые допустимые макросы: #define Case break;case #define nl <<"\n" #define forever for(;;) #define MIN(a,b) ((a)<(b))?(a):(b)) Вот некоторые совершенно ненужные макросы: #define PI 3.141593 #define BEGIN { #define END } Вот некоторые опасные макросы: ' #define SQUARE(a) а’а #define INCR_xx (xx) + + #define DISP = 4 Чтобы понять, чем они опасны, попробуйте выполнить расширения нижеследующего: int хх = 0; // глобальный счетчик void f() { 134
int xx = 0; //• локальная переменная хх = SQUARE(xx + 2); // xx = xx + 2"xx + 2 INCR_xx; // увеличивает локальную xx if (a-DISP= = b) {// a -= 4 = =b // ... Если Вам пришлось применить макрос, то при обращении к глобальным именам воспользуйтесь оператором разрешения области действия :: ($2.1.1), и по возможности заключайте в скобки все имена- аргументы макросов (см. выше - MIN). Обратите внимание на различные результаты расширения этих двух макросов: #define ml (a) something(a) #define m2(a) something(a) // глубокомысленный комментарий /" глубокомысленный комментарий "/ Например, int а = ml (1) + 2; int b = m2(1) + 2; расширяются вот во что: int а = . something(1) int b = something(l) ■' 7 // глубокомысленный комментарий + 2 /" глубокомысленный' комментарий"/+ 2; С помощью макросов можно создать свой личный язык; он,Скорее всего, будет непонятен другим. Более того, препроцессор языка С - это очень простой макропроцессор. Если Вы попробуете сделать на нем что- нибудь нетривиальное, то убедитесь, что это либо невозможно, либо неоправданно трудно (см., однако, $7.3.5). 4.8 УПРАЖНЕНИЯ 1. ("1) Напишите декларации для: функции, принимающей аргументы типа указателя на символ и ссылки на целое число и не возвращающей значения; указателя на эту функцию; функции, аргументом которой является такой указатель; функции, возвращающей такой указатель. Напишите определение функции, принимающей такой указатель в качестве аргумента и возвращающей этот ^аргумент. Указание: воспользуйтесь typedef. 2. ("1) Что бы это значило? Для чего это может пригодиться? typedef int (rifii&) (int, int); 3. ("1.5) Напишите программу вроде "Hello, world", которая принимает имя как аргумент командной строки и пишет "Hello, имя". Измените программу так, чтобы она принимала любое количество имен в качестве аргументов, и поприветствуйте всех. 135
4. 6. 7. 8. 9. 10. 11. 13. 14. 15. U. (’1.5) Напишите программу, которая читает произвольное количество файлов, имена которых указаны в аргументах командной строки, и выводит их по очереди в cout. Поскольку эта программа соединяет (конкатенирует) аргументы при выводе, ее можно назвать cat. 5. (’2) Преобразуйте небольшую программу на языке С в язык С +.. +. Измените заголовочные файлы так, чтобы в них объявлялись все вызываемые функции и типы всех аргументов. По возможности заменитевсе операторы #define на enum const или inline. (’2) Создайте функцию sort() ($4.6.9) на основе более эффективного алгоритма сортировки. (’2) Рассмотрите определение struct tnode в $с.8.5. Напишите функцию ввода новых слов в дерево из узлов типа tnode. Напишите функцию печати дерева таких узлов. Напишите функцию печати дерева таких узлов, где слова следуют в алфавитном порядке. Измените tnode так, чтобы в нем хранился (только) указатель на слово произвольной длины, расположенное в свободной памяти оператором new. Измените написанные функции так, чтобы они работали с новым определением tnode. (*2) Напишите "модуль”, работающий со стеком, .h-файл должен объявлять функции push(), рор() и любые нужные для этого функции (и только их), .с-файл определяет функции и данные, необходимые для работы со стеком. ("2) Изучите свои стандартные заголовочные файлы. Просмотрите файлы в директориях /usr/include и /usiy include/CC (или там, где стандартные файлы хранятся в Вашей системе). Почитайте те, что покажутся Вам интересными. ("2) Напишите функцию инвертирования двумерного массива. (*2) Напишите программу кодирования, которая читает из cin и записывает закодированные символы в cout. Можно воспользоваться такой простой схемой кодирования: закодированный вид символа с это с^кеур], где key (ключ) - это строка, принятая как аргумент командной строки. Программа использует символы key циклически до тех пор, пока не считает весь входной поток. Декодирование закодированного текста с тем же ключом дает исходный текст. Если ключа нет (или он пуст), то кодирование не производится. 12. ("3) Напишите программу, помогающую расшифровать сообщения, закодированные описанным выше способом при неизвестном ключе. Указание: см. David Kahn: The Code-breakers, Macmillan, 1967, New York, pp. 207-213. ("3) Напишите функцию обработки ошибок error, которая принимает форматную строку вроде printf, содержащую директивы %s, %с и %d и произвольное количество аргументов. printf() не применяйте. Загляните в $8.2.4, если Вы не знаете, что значит %s и т.п. Воспользуйтесь <stdargs.h>. ("1) Какие имена Вы выберете для типов указателя на функцию,определенных с помощью typedef. ("2) Просмотрите несколько программ, чтобы прочувствовать разнообразие стилей реально используемых имен. Как применяются заглавные буквы? Как используется подчеркивание? Где встречаются короткие имена вроде i и х? (’1) Что неправильно в этих макроопределениях? 136
tfdefine PI = 3.141593; #define MAX(a,b) a>b?a:b #define fac(a) (a)"fac((a)-1) 17. ("3) Напишите простой макропроцессор, позволяющий определять и расширять простые макросы (как это делает препроцессор языка С) Читайте из cin и записывайте в cout. Сначала йё беритесь за макрос^ с аргументами. Указание: в настольном калькуляторе ($3.1) есть таблица символов и лексический анализатор - можете их изменять.
(• "И.В.К." 105023 Москва, Мал. Семеновская, д. 5 Тел.: 936-50-67, 311-52-08 Факс: 203-93-55 ГЛАВА 5 КЛАССЫ В данной главе описаны средства C++ по определению новых типов оступом к данным, разрешенным только определенному множеству кций. Рассматриваются способы защиты, инициализации, доступа и, >нец, очистки структуры данных. В примерах разбираются простые классы работы с таблицей символов, со стеком, с множествами и для создания шчающего ("надежного") объединения. Следующие две главы завершают :ание средств C++ по созданию новых типов и содержат новые пресные примеры. 5.1 ВВЕДЕНИЕ И ОБЗОР Цель понятия класса в С + +, как описано в этой и двух следующих ах - дать программисту инструмент создания для новых типов, которыми :но пользоваться так же удобно, как и встроенными типами. В идеале определенный пользователем,, не должен ничем отличаться от оенных типов в Смысле использования, но только по способу создания. Тип - это конкретное представление некоторой идеи (понятия). >имер, тип С+ + . float и его операции +, -, " и т.д. дают ниченную, но конкретную версию математического понятия ветвенного числа. Основанием для создания нового типа является мление дать конкретное и четкое определение понятию, для которого непосредственного и очевидного представителя из встроенных типов. >имер, в программе, связанной с телефонией, можно определить тип module (модуль канала связи), а в программе обработки текста - тип of—paragraphs (список абзацев). Программу, в которой созданные типы ко совпадают с понятиями прикладной задачи, обычно легче понять и ^фицировать, чем программу, в которой этого нет. Хорошо подобранный р типов, определенных пользователем, сокращает программу, кроме он позволяет компилятору обнаруживать случаи неразрешенного льзования объектов, что иначе осталось бы незамеченным до этапа фования программы. Главное в идее определения нового типа - это отделить частные ли реализации (например, структуру данных, используемую для хранения кта этого типа) от тех его характеристик, которые присущи объекту при ильном его использовании (например, полный список функций, которые от право доступа к этим данным). Это разделение можно реализовать ролем за всеми обращениями к структуре данных и внутренними 138
вспомогательными процедурами, работающими через специальный интерфейс. Эта глава состоит из четырех довольно независимых глав: $5.2 Классы и элементы. В этом разделе вводится базовое понятие типа, определяемого пользователем класс (class). Доступ к объектам класса может быть разрешен только набору функций, объявленных как часть класса; эти функции называются функциями-элементами. Объекты класса создаются и инициализируются функциями-элементами, специально объявленными для этой цели, эти функции* называются конструкторами. Можно объявить особую функцию-элемент для очистки всякого объекта данного класса при уничтожении объекта, такая функция называется деструктором. $5.3 Интерфейсы и реализации. В этом разделе даны два примера конструирования, реализации и использования классов. $5.4 Дружественные функции и объединения. В этом разделе даётся много дополнительных подробностей о классах. Показано, как осуществляется доступ к личным (приватным) частям класса функцией, которая не . является элементом данного класса. Такая функция называется дружественной (friend). В этом разделе, кроме того, показано как создать различающее объединение. $5.5 Конструкторы и деструкторы. Объект можно создать в автоматической, статической или свободной памяти. Кроме того, объект может быть элементом некоторого множества (типа вектора или класса), которое, в свою очередь, может размещаться одним из этих трех способов. Довольно подробно разбирается применение конструкторов и деструкторов. 5.2 КЛАССЫ И ЭЛЕМЕНТЫ Класс (class) - это тип, определяемый пользователем. В данном разделе даются основные средства определения класса, создания объектов класса, работы с этими объектами и, наконец, очистки этих объектов после использования. 5.2.1 Функции-элементы Рассмотрим реализацию понятия даты с использованием struct для определения представления date (дата) и набора функций, работающих с переменными этого типа: struct date (int month, day, year; }; date today; void set_ date(date", int, int, inf); void next —dafe(date’); void print _ date(date’); // ... Между функциями и типом данных явной связи нет. Эту связь можно установить, если функций объявить элементами: struct date { 139
int month, day, year; void set(int, int, int); void get(int“, int*, int’j; void next(); void print(); }; функции, .объявленные таким образом, называются функциями-элементами и йх можно вызывать только для работы с определенными переменными соответствующего типа, используя стандартный синтаксис доступа к элементам структуры. Например: date today; date my _ birthday; void f() my _ birthday.set(30,12,1950); today .set( 18,1,1985); my _ birthday.print(); today.next(); Поскольку л у разных структур могут быть функции-элементы с одинаковыми именами, то при определении функции-элемента необходимо указывать имя структуры: void date::next() if ( + + day > 28 ) { // сделать трудное дело } } В самой функции-элементе имена элементов можно использовать без явной ссылки на объект. В этом случае имя будет относиться к тому элементу объекта, для которого она была вызвана. 5.2.2 Классы В декларации date в предыдущем подразделе дан набор функций работы с date, но там не указано, что только эти функции могут иметь доступ к объектам типа date. Такое ограничение можно ввести, применив class вместо struct: class date { int month, day, year; public: void set(int, int, int); 140
void get(int", int", int"); void next(); void print(); }; Метка public разделяет тело класса на две части. Имена в первой - приватной - части могут использоваться только функциями-элементами. Вторая - публичная или общая (public) - часть составляет интерфейс к объектам этого класса. Структура (struct) - это просто класс (class), все элементы которого общие, поэтому функции-элементы определяются как и раньше. Например: . void date::print() // печать в американском формате cout << month << < < day << '7" << year ; } Однакр, функции, не являющиеся функциями-элементами не могут получить доступ к личным элементам класса date. Например: void backdate() • today.day—; // ошибка Разрешение доступа к структуре данных только функциям явно объявленного списка несет в себе несколько достоинств. Любая ошибка, в результате которой date принимает неверное значение (например, 36 декабря 1985 г.), вызвана, видимо, ошибкой в функции-элементе, пбэто'му первый этап отладки - локализация ошибки - выполняется даже до запуска программы. Это частный случай общего замечания о том, что любое изменение поведения типа date может и должно осуществляться посредством изменения его элементов. Другое достоинство состоит в том, что ’для того, чтобы научиться работать с этим типом, потенциальному пользователю этого типа надо лишь изучить определение "функций- элементов. Защита личлых данных зависит от ограничения на использование имен элементов класса. Следовательно, ее можно обойти путем манипуляции с адресами и явного преобразования типов, но это уже хитрости. 5.2.3 Обращение к самому себе В функции-элементе можно обращаться непосредственно к элементам объекта, для которого была вызвана функция-элемент. Например: class х { int m; public: int readm() { return m; } }; x aa; 141
x bb; void f() int a = aa.readm(); int b = bb.readm(); П(>и первом вызове элемента readm() m относится к aa.m, а при втором - к' bb.m. Указатель на объект для которого вызывается функция-элемент, образует скрытый аргумент функции. На этот неявный аргумент можно делать явную ссылку как на this ("этот")* В каждой функции класса х указатель this неявно объявляется как х" this; и инициализируется указателем на объект, для которого была вызвана функция-элемент. Поскольку this - это ключевое слово, то явно объявить его нельзя. Класс х можно объявить эквивалентным образом тёк: class х { int m; public: int readm() { return this->m; } V- При обращении к элементам необязательно использовать this; в основном, this применяется при написании функций-элементов, которые непосредственно работают с указателями. Типичный пример: функция вставки связи в двунаправленный список: class dlink { dlink" pre; // предыдущий dlink" sue; //следующий public: void append(dlink"); }; " ■" void } dlink::append(dlink" p) p->suc = sue; p->pre this; suc->pre = p; sue = p; // то есть, p->suc = this->suc // явное использование "this" // то есть, this->suc->pre = p // то есть, this—> sue = p dlink" list _ head; 14$
void f(dlink" a, dlink" b) // ... list _ head-> append(a); list _ head-> append(b); } Связи такого характера составляют основу для списковых классов, описанных в главе 7. Для вставки связи в список необходимо .изменить объекты, на которых указывают this, pre, и sue.. Все они имеют тип dlink, поэтому функция-элемент dlink: :append() может к ним обращаться. ВС+ + единицей защиты является class, а не отдельный объект класса. 5.2.4 Инициализация Использование функций вроде set_date() для инициализации объектов класса неэлегантно и порождает ошибки. Поскольку нигде не сказано, что объект нужно инициализировать, программист может забыть это сделать или (с не менее разрушительными результатами) сделать это дважды. Удачнее будет позволить программисту объявить функцию с явной целью инициализировать объекты. Так как эта функция конструирует объекты данного типа, она называется конструктором. Конструктор распознается по тому, что его имя совпадает с именем класса. Например: class date { // ... date(int, int, int); }; Если у класса есть конструктор, то все объекты класса будут инициализироваться. Если конструктор требует аргументы, их следует указать: date today = date(23,6,1983); date xmas(25,12,0); // сокращенная форма date my _ birthday; // неверно, отсутствует инициализатор Нередко бывает полезно обеспечить несколько способов инициализации объектов класса. Это можно сделать созданием нескольких конструкторов. Например: class date { int month, day, year; public: // ... date(int, int, int); date(char’); date(int); date(); }; // день месяц год // дата в строковом представлении // день, текущий месяц и год // дата по умолчанию: сегодня 143
Конструкторы подчиняются тем же правилам типов аргументов что и функции с переназначением имени ($4.6.7). Поскольку конструкторы значительно различаются по типам своих аргументов, компилятор сможет в каждом случае выбрать правильный конструктор: date today(4); date july4("July 4, 1983"); date guy("5 Nov"); date now; // инициализация значением по умолчанию 4 Обратите внимание на то, что у функций-элементов имена могут переназначаться без явного использования ключевого слова overload. Поскольку полный список функций-элементов находится в декларации класса и, как правило, короток, нет необходимости использовать слово overload для защиты of случайного повторного использования имени. Множественность конструкторов в примере с date типична. При разработке класса всегда есть соблазн задать в нем "все", поскольку кажется, что легче предусмотреть некоторое свойство на случай, если оно кому-нибудь понадобится, или просто потому, что хорошо смотрится, чем решить, что же действительно необходимо. В последнем случае приходится больше думать, но в результате обычно получаются более компактные и понятные программы. Один из способов сократить количество родственных функций - применение аргументов по умолчанию. В случае date каждому аргументу можно присвоить значение по умолчанию, интерпретирующееся как "возьми значение по умолчанию: today (сегодня)". class date { intK month, day, year; public: // ... date(int d =0, int m =0, int у =0); date(char“); // дата в строковом представлении }; date: :date(int d, int m, int y._ day = d ? d : today.day; „ month = m ? m : today.month; year = у ? у : today .year; // проверка на правильность даты // ... } При использовании значения аргумента "возьми по умолчанию" выбранное значение должно быть за пределами множества возможных значений аргумента. Для day и month это ясно, а вот для year - неочевидно. К счастью, в европейском календаре нет нуля: первый год нашей эры (уеаг= =1) следует сразу же за первым годом до нашей эры (year = =-1), однако это слишком уж тонко для настоящей программы. Объект класса без конструкторов можно инициализировать, присвоив ему другой объект этого класса. Можно этим пользоваться и при наличии 144
конструкторов. Например: date d = today; // инициализация присваиванием В сущности, имеется конструктор по умолчанию, определенный как побитовая копия объектов одинакового класса Если такой конструктор по умолчанию нежелателен для класса X, его можно переопределить конструктором с именем Х(Х&). Рассмотрим это ниже, в $6.6. 5.2.5 Очистка Чаще всего у типа, определённого пользователем, естьеконструктор, гарантирующий правильную инициализацию. Для многих типов требуется обратная операция - деструктор - для обеспечения правильной очистки объектов данного типа. Имя деструктора для класса X - это ~ Х() ("дополнение к конструктору"). В частности, многие классы используют фрагменты свободной памяти (см. $3.2.6), которая выделяется конструктором и освобождается деструктором. Ниже, например, приведен обычный стековый тип, из которого для краткости выбросили обработку всех ошибок: class char _ stack { int size; char’ top; char’ s; public: . char _ stack(int sz) { top = s = new char[size = sz]; } ~ char _ stack() { delete s; } // деструктор void push(char c) { ’top + + = c; } char pop() { return '—top; } } i Когда char_ stack выходит из области действия, вызывается деструктор: void f() char _ stack si (100); char_stack s2(200); si .push('a'); s2.push(s1 .popO); char ch = s2.pop(); } cout << chr(ch) << "\n"; При вызове f() вызовется конструктор char_ stack и выделит si вектор из 100 символов, a s2 - вектор из 200 символов; при возврате из f() память, занятая этими двумя векторами, будет снова освобождена. зо? 5.2.6 Подстановка тела (inline) При программировании с использованием классов очень часто используется множество небольших функций. В сущности, функции 145
используются там, где в традиционно организованной программе просто использовалась бы одна из разновидностей структур данных; что' раньше было соглашением - теперь стандарт, учитываемый компилятором.< Это может привести к огромным потерям эффективности из-за того, что затраты на вызовы функций (правда, не таких уж больших, как в других языках) пока еще больше, чем пара ссылок в памяти, требующихся для тела тривиальной функции. Для решения этой проблемы было разработано средство подстановки тела функции - inline. Считается, что функция-элемент, определенная (а не просто объявленная) в декларации класса - это функция с подстановкой тела. Это, например, значит, что код, сгенерированный для рассмотренных выше функций, использующих char _ stack, не содержит вызовов функций, кроме вызовов для. выполнения операций вывода! Другими словами, при разработке класса не надо принимать в расчет даже минимальные потери при выполнении; даже мельчайшая операция может быть эффективно выполнена. Это замечание опровергает популярное обоснование применения публичных элементов данных. Функцию-элемент также можно объявить inline вне декларации класса. Например: class char _ stack { int size; char" top; char" s; public: char pop(); // ... }; inline char char _ stack::pop() return "—top; 5.3 ИНТЕРФЕЙСЫ И РЕАЛИЗАЦИЯ Что составляет хороший класс? Это нечто, имеющее небольшой и хорошо определенный набор операций. Это нечто, что можно считать "черным ящиком", с которым работают только посредством этого набора операций. Это нечто, позволяющее изменить его реальное представление без влияния на способы использования этого множества операций. Это нечто, с чем хочется работать. Очевидные примеры дают классы-контейнеры: таблицы, множества, списки, векторы, словари и т.д. В таком классе есть операция вставки, обычно в нем также есть операции проверки, имеется ли уже некий номер в контейнере, возможно, есть операции сортировки элементов, возможно, есть операции просмотра элементов в некотором порядке и, наконец, возможно, есть операция уничтожения элемента. Обычно у классов- контейнеров есть конструкторы и деструкторы. Скрытие данных и хорошо определенный интерфейс можно обеспечить и модульным подходом (см., например, $4.4: файлы как модули). Однако, 146
класс - это тип; чтобы пользоваться классом, надо создавать объекты этого класса, и таких объектов можно создать столько, сколько потребуется. Модуль есть объект сам по себе; чтобы им пользоваться, его надо инициализировать, и такой объект всего один. 5.3.1 Другие возможности реализации При условии, что декларация общей части класса и декларация функций-элементов остаются неизменными, реализацию класса можно менять, не влияя на его пользователей. Рассмотрим таблицу символов вроде той, что использовалась в примере с настольным калькулятором в главе 3. Это таблица имен: «г struct name { * f char" string; name" next; double value; }; Вот вариант класса table (таблица): // файл table.h: class table { name" tbl; public: table() { tbl = 0; } name" look(char", int = 0); name’ insert(char“ s) { return look(s,1); } Отличие этой таблицы от приведенной в главе 3 - в том, что она имеет правильный тип. Можно объявить больше одной таблицы (table), Можно установить указатель на таблицу и т.д. Например: #include "table.h" table globals; table keywords; table" locals; main() { locals = new table; // ... ’ '■ } Ниже приведена реализация функции table: :look(), использующая последовательный поиск в связанном списке имен (name) в таблице: $iriclude <string.h> 147
name" table: :look(char" p, int ins) " { for (name’ n = tbl; n; n=:n->next) - if (strcmp(p,n-> string) = = 0) return n; if (ins = = 0) еггог("имя не найдено"); name" nn = new name; nn-> string = new cbar[strlen(p) + 1 ]; strcpy(nn-> string,p); nn-> value = 1; nn->next = tbl; tbl = nn; return nn; } Теперь рассмотрим возможность оптимизации класса table путем применения хешированного поиска, как в примере с настольным калькулятором. Сделать это тем более трудно из-за ограничения, что программные фрагменты, написанные с использованием предыдущей версии класса table, должны работать правильно без дополнительных изменений: class table { name"’ tbl; int size; public: table(int sz = 15); - table(); name" look(char“, int = 0); name" insert(char" s) { return look(s,1); } }» Вследствие требований, конкретного размера таблицы при применении хеширования пришлось изменить структуру данных и конструктор. Наличие в конструкторе аргумента по умолчанию гарантирует, что тексты ранних программ, в которых не указан размер таблицы, по-прежнему верны. Аргументы по умолчанию очень полезны в ситуациях, когда приходится изменять класс, не влияя на прежние программы. Теперь конструктор и деструктор обрабатывают создание и удаление хеш-таблиц: table: :table(int sz) if (sz < 0) еггог("отрицательный размер таблицы"); tbl = new name"[size = sz]; for (int i = 0; i<sz; i+ + 0 tbl[i] = 0; table:: ~ table() for (int i = 0; i<size; i+ + ) { 148
name’ nx; for (name’ n = tbl[i]; -n; n = nx) { nx = n-> next; ■ delete n-> string; delete n; } delete tbl; } Можно сделать более простую и понятную версию table:: * table(), объявив деструктор для класса name. Функция поиска практически идентична функции, созданной в примере с настольным калькулятором ($3.1.3): name" table: :look(char’ р, int ins) { int ii = 0; char" pp = p; while ('pp) ii = ii< < 1 Л "pp + + ; if (ii < 0) ii = -ii; ii % = size; for (name" n = tbl[ii]; n; n = n->next) if (strcmp(p,n-> string) = = 0) return n; if (ins = = 0) еггог("имя не найдено"); name' nn = new name; nn-> string = new char[strlen(p) + 1 ]; strcpy(nn-> string,p); nn-> value = 1; nn->next = tblfii]; tbl[ii] = nn; return nn; } Ясно, что когда изменяется объявление класса, необходимо перекомпилировать функции-элементы класса. В идеале подобные изменения абсолютно не повлияют на пользователей класса. К сожалению, это не так. Чтобы зарезервировать память для переменной типа некоторого Класса, компилятору должен быть известен размер объекта класса. При изменении размера этих объектов необходимо перекомпилировать файлы, содержащие обращения к классу. Можно создать программное обеспечение, определяющее (минимальный) набор файлов, которые необходимо перекомпилировать при изменениях декларации класса, и такие программы созданы, но еще не получили широкого распространения. Как. же так, спросите Вы, C++ построен так, что приходится перекомпилировать пользовательские программы, работающие с классом, при изменении личной части класса? И зачем вообще нужна личная часть в декларации класса? Иными словами, если пользователи класса лишены доступа к личным элементам, зачем нужно вставлять их декларации в 149
заголовочные файлы, которые должен читать пользователь? Ответ: , для эффективности. Во многих системах удается значительно упростить процесс компиляции м последовательность операций, выполняющих вызов функции, если известен размер автоматических объектов (объектов в стеке). Эту проблему можно обойти, если вместо каждого объекта класса представлять его указателем на "реальный" объект. Поскольку 'у всех таких указателей одинаковый размер, выделение памяти для "реальных" объектов можно определить в файле, содержащем и личную часть класса- таким образом эту задачу можно решить. Это решение, однако, требует лишней ссылки в памяти для доступа к элементам класса и, что хуже, на каждый вызов функции над автоматическим объектом класса приходится по меньшей мере по одному вызову процедур резервирования и, освобождения свободной памяти. Кроме того, будет невозможно осуществить подстановку тела функций-элементов, работающих с личными данными. Более того, такие изменения не позволят совместное линкование фрагментов программ на языках С и С + + (поскольку компилятор С обрабатывает struct иначе, чем компилятор С + +). Поэтому такое решение было сочтено неподходящим для С + + . 5.3.2 Полный класс При программировании без скрытия данных (с помощью структур) требуется меньше продумывания, чём при программировании с использованием скрытия данных (и классов). Структуру можно определить, не особенно задумываясь, как предполагается ее использовать, но определяя класс, главное - задать полный набор операций с новым типом; это - важное смещение акцентов. Время, затраченное на разработку нового класса, обычно с лихвой окупается при разработке и тестировании программы. Вот пример полного типа - intset - обозначающего понятие "множество целых чисел". class intset .{ int cursize, maxsize; int “x; public: intset(int m, int n); • intsetQ; // максимум m элементов int в 1...n int member(int t); void insert(,int t); // "+" - элемент? // добавить "t" к множеству Vbid iterate(int& i) int ok(int& i) int next(int& i) ) I — w, /, { return i< cursize; } ( return x[i + +]; } Для проверки этого класса мо&йо создать и затем распечатать множество случайных целых чисел. Такое множество может представлять тираж лотереи. Кроме того, это простое множество можно применять Для проверки последовательности целых чисел на дубли, однако для большинства прикладных задач тип этого множества надо разработать 150
получше. Как всегда возможны ошибки: #include <stream.h> void error(char “s) cerr < < "множество: " < < s < < "\n"; exit(1); } . Класс intset используется функцией main(), ожидающей два целочисленных аргумента. Первый аргумент задает требующееся количество случайных чисел. Второй аргумент задает диапазон, в котором должны находиться случайные Целые числа: main(int argc, char" argv[]) if (argc ! = 3) еггог("ожидаются два аргумента”); int count = 0; int m = atoi(argv[1 ]); // количество элементов' множества int n = atoi(argv[2J); // в диапазоне 1...n intset s(m,n); while (count<m) { int t = randint(n); if (s.member(t) = = 0) { s.insert(t); count + + ; print _ in _ order(&s); // печатать по порядку Счетчик аргументов - argc - должен быть равен 3, если программе требуются два аргумента, поскольку в argy[0] всегда передается имя программы. Функция extern int atoi(char’); - это стандартная библиотечная функция, преобразующая целое число из символьного вида в его внутреннее (двоичное) представление. Случайные числа генерируются с помощью стандартной функции rarid(): extern int rand(); // осторожно,' они не очень-то случайные int randint(int u) // в диапазоне 1...u . int г i randf); if (r < 0) r = -r; return 1 + r%u; } 151
Пользователю малоинтересны подробности реализации класса,; но, тем не менее, здесь представлены функции-элементы. Конструктор выделяет память вектору целых чисел указанного максимального размера множёства, а деструктор освобождает эту память: intset: :intset(int m, int n) // максимум m элементов int в 1...n if (m<1 II n<m) еггог("неверный размер intset"); cursize = 0; maxsize = m; x = new int[maxsize]; intset:: ~ intset() delete x; } Целые числа добавляются во множество таким образом, что располагается там в порядке возрастания: void intset::insert(int t) if (++cursize > maxsize) еггог("слишком много элементов"); int i = cursize-1; x[i] = t; while (i>0 && x[i- 1]>x[i]) { int t = x[ij; // поменять местами x[i] и x[i-1] Для поиска элемента применяется просюй бинарный поиск: int intset::member(int t) // бинарный поиск int I = 0; int u = cursize-1; while (I < = u) { int m = (I + u)/2; if (t < x[m]) u = m-1; else if (t > x[mj) I = m + 1; else return 1; // нашли } 152
return 0; } // не нашли Наконец, поскольку представление intset скрыто от пользователя, мы должны дать набор операций, позволяющий пользователю просматривать множество в некотором порядке. Множество организовано не слишком сложно, поэтому можно просто дать способ доступа к вектору (я, может быть, завтра решу переделать intset в связный список). Даются три функции: iterate() - для инициализации просмртра, ок() - для проверки на существование следующего элемента й next() - для перехода к следующему элементу: clasd intset { t, // ... void iterate(int& i) int ok(int& i) int next(int& i) }; { i = о } { return iccursize; } { return x[i + + ]; } Чтобы обеспечить взаимодействие трех этих функций и знать, да какого места дошел просмотр, пользователь должен указать целочисленный аргумент. Поскольку элементы хранятся в упорядоченном списке, реализация функций тривиальна. Теперь можно определить функцию print _ in _ order (печатать по порядку): L void print _ in _ order(intset“ set) int var; set-> iterate(var); while (set-> ok(var)) cout << set-> next(var) << "\n"; } Другой способ реализации просмотра описан в $6.8. 5.4 ДРУЖЕСТВЕННЫЕ ФУНКЦИИ И ОБЪЕДИНЕНИЯ В данном разделе описываются новые свойства классов. Показано, как обеспечить доступ к приватным элементам дляч функции, не являющейся функцией-элементом. Рассматриваются способы разрешения конфликтов имен элементов, вложенные декларации классов и способы устранения нежелательных вложенностей. Кроме того, показаны возможности совместного использования элементов данных объектами класса и способы применения указателей на элементы. НакЬй’ёц; приводится пример написания различающего ("надежного") объединения. 5.4.1 Дружественные функции Допустим, Вы определили два класса: vector (вектор) и matrix (матрица). Каждый класс скрывает себе представление и предоставляет полный набор операций для работы с объектами данного типа. Определим теперь функцию умножения матрицы на вектор. Для простоты допустим, что 153
вектор состоит из четырех элементов с номерами 0 ... 3, а матрица - из четырех векторов с номерами 0 ... 3. Допустим также, что доступ к элементам вектора выполняется функцией elem(), проверяющей номер элемента, и что у класса matrix есть такая же функция. Вот один из подходов к определению глобальной функции multipIyO ("умножить"): vector multiply(matrixA m, vectorA v); vector r; for (int i = 0; i<3; i+ + ) { // r[i] = m[i] ” v; r.elem(i) = 0; 1 for (int j = 0; j<3; j + +) r.elem(i) + = m.elem(i,j) “ v.elem(j); return r; } Это в некотором смысле "естественный" способ выполнения операции, но он очень неэффективен. На каждый вызов multiply() приходится 4”(1 +4’3) вызовов elem(). Если же мы сделаем multipIyO элементом класса vector, мы решим задачу проверки номера при доступе к элементу вектора, а если мы сделаем multipIyO элементом класса matrix, мы решим задачу проверки номера при доступе к элементу матрицы. Однако функция не может быть элементом двух классов. Здесь нужен такой конструкт языка, который дал бы функции доступ к приватной части класса. Функция, не являющаяся функцией-элементрм, получающая доступ к приватной части класса, называется дружественной (friend) классу. Функция становится дружественной классу, если в классе она объявлена Как friend. Например: class matrix; class vector { float v[4); friend vector multiply(matrixA, vectorA); }; class matrix { vector v[4]; // ... friend vector multiply(matnxA, vectorA); }; В friend-функции нет ничего особенного за исключением права доступа к приватной части класса. В частности, у friend-функции нет указателя this (если только она не полноправная функция-элемент). Декларация friend- функции - это действительная декларация. Она вводит имя функции в самую широкую область действия в программе и проверяет другие Декларации этого же имени. Декларацию дружественной функции можно поместить и в личной, и в общей части декларации класса, это неважно. 154
Теперь можно переписать функцию умножения с непосредственным использованием элементов векторов и матрицы: vector multiply(matrix& m, vector& v) vector r; for (int i = 0; i<3; i+ +) { // r[i] = m[i] “ v; r.v[i] = 0; for (int j = 0; j <3; j + + ) r.y[i] + = m.v[i][j] ‘ V-v[j]; } return r; } С задачей повышения эффективности всегда можно справиться и без механизма friend (например, можно определить функцию умножения векторов и через нее определить multiplyf)). При этом, однако, возникает множество проблем, и снять их легче всего, если дать функции, не являющейся функцией-элементом класса, возможность доступа к приватной части этого класса. В главе 6 приведено много примеров использования friend. Относительные достоинства дружественных функций и функций- элементов рассматриваются ниже. Функция-элемент одного класса может быть дружественной иному классу. Например: class х { // ... void f(), >; class у { // ... friend void x::f(); }; Нередко все функции одного класса - дружественны другому классу. Для этого есть даже сокращенная запись: class х { friend class у; // ... }; По этой "дружеской" декларации все функции-элементы класса у становятся дружественными классу х. 5.4.2 Уточнение имени элемента Иногда полезно явно различить имена элементов класса и прочие имена. Для этого можно применить оператор разрешения области действия: 155
class x { int m; public: int readm() { return x::m; } void setm(int m) { x::m = m; } } i - В x::setm() имя аргумента m скрывает элемент m, так что на этот .элемент можно ссылаться только пр его уточненному имени, х::т. Левым операндом :: должно быть имЛ? класса. Имя, Рёред которым 'стоит (только) :: , должно быть глобальным. Это особенно важно, поскольку позволяет использовать в качестве имен функций-элементов популярные имена вроде read, put и open, не теряя при этом возмо^кности, обращаться к их аналогам *■ не-элементам. Например: class my __ file { //... . public: " • inf opeh(char“, char");. }; 1 int my _ fflev: rbpen(char" name, char" spec) ' // ... if (::open(rtame,flag)) // ... /7 ... } . , . J 5.4.3 Вложенные классы { // применить open из UNIX(2) .! * Декларации классов могут вкладываться друг в друга. Например: class set { к Л t< struct setmem { . > > ? * int mem; < ? я - z, setmem" next; f * Asetmem(int m, setmem" n) {mem = m; next =? n; } / k setmem" first; public: set() { first = 0; } insert(int m) { first = new setmem(m,fii$t); } // ’ }; 3 a исключением крайне простых классов, подобные декларации запутанны. Более того, вложенность классов - это, в крайнем случае, • Удобство обозначения, поскольку вложенный класс не находится в области Действия включающего его класса: 6 Зак. 1927 156
class set { struct setmem { int mem; setmem’ next; setmem(int m, setmem" n) setmem: :setmem(int m, setmem" n) { mem s m; next =n; } setmem mT(1,0); < : v г » ' Конструкты вроде set:: setmem: :setmem() и ненужны,' ^ неверны. Единственный способ- скрыть имя класса - это применить подход "файлы как модули" ($4.4). Наиболее нетривиальные классы лучше всего объявлять по отдельности: class setmem { ; friend class set; // доступ только ддя элементов из set int mem; setmem" next; ° setmem(int m, setmem" n) (mem = m; next-=-n; ф }; class set { setmem" first; public: set() { first = 0; } insert(int m) { first = new setmem(m,first); } // ... }; / -7 5.4.4 Статические элементы ; ' Класс - это тип, а не объект данных, и для каждого объекта класса существует своя копия элементов данных класса. Тем Йе лХенёё, некоторые типы изящнее всего реализуются в том случае,; ёбли^ёсе объекты этого типа совместно пользуются некоторыми данными. Желательно объявлять совместно используемые данные частью класса. НаприМёр, для управления задачами в операционной системе или ее эмуляции часто полезно работать со списком всех задач: class task { // ... task" next; static task" task _ chain; void schedule(int); void wait(event); // ... }; 157
Декларация static task . chain гарантирует, что будет существовать только один экземпляр этой переменной, а не по экземпляру на каждый объект task. Тем не менее она находится в области действия класса task, и "извне" к ней можно обратиться, лишь объявив ее public- В таком случае ее имя надо уточнять именем класса: task:: task, chain В функции-элементе ее можно именовать просто task .chain. Использование статических (static) элементов классов может значительно сократить потребности в глобальных переменных. 5.4.5 Указатели на элементы Можнополучить адрес элемента класса. Получение адреса фуНкции- элемента ... нередко бывает полезно, поскольку методы и причины использования указателей на функции, приведенные в $4.6,9, в равной степени относятся к указателям на функции-элементьь Однако, в настоящее вре^ля вязыке>С+ + -,естьне достаток: невозможно выразить -тил указателя, полученный такой операцией. к Следовательно, требуется уловка, использующая причуды нынешней реализации.. Не гарантируется, что следующий пример будет работать, и следует ограничить применение этой уловки, так что программу можно будет вернуть к использованию правильного конструкта языка, когда он появится. Хитрость вот в чем: воспользоваться тем, что в настоящее время this реализован как (скрытый) первый аргумент функции-элемента^ #include; <stream.h> ; struct cl char" vaJ; void print(int y) { cout << val < < x < < "\n"; }; cl(char" v) { val , = v; } }; // "поддельный" тип для функций-элементов: J typedef void ("PRQC)(Yoid’, int); mainO { cl Zl("z1 ")’ cl z2("z2/); PROC pfl = PROC(&z1.print); . uPRQC pf2 ₽ROG(&z2.print); • ■ zl.print(l); Cpf1)(&z1,2); z2.print(3); ("pf2)(&z2,4); 6* 158
Во многих случаях вместо указателей на функции можно применять виртуальные функции (см. главу 7). 5.4.6 Структуры и объединения Структура (struct) - это по определению просто класс, все элементы которого типа public, то есть struct s { ... это просто краткая форма class s { public: ... тех случаях; когда сокрытие данных определяется как структуру (struct), в одинаковый адрес ч(см. $с.&5.13). Если Структуры применяются в нецелесообразно. ■* Именованное объединение которой все элементы имеют известно;1 что в каждый момент времени значимым »является т:олько один элемент структуры,, то можно создать объединение- это экономит) память. Вот как» например, молено определить объединение для хранения лексем в^ компиляторе языка С: union tok_ val { char" p; char v[8]; long i; double d; ■ с. .<> V. г..//> строка к •<&. -ч. ' // идентификатор (максимум 8 символов) // целочисленные значения ' // значения с плавающей, тонкой,1 гЛ }; Возникает проблема: в каким элементом работают в типов невозможна. Например* void { и 2» . :1V общем случае компилятору не известно, с какой момент, поэтому правильная проверка 4 .. г: . . 3 и ' ’ • И' • strange(int i) tok_ valx; if (i) else X P “ x.d = sqrt(x.d); .' <■ \ t 2; // ошибка, если х ! } Q :/'• . \: ,л х . ( ... - эсSi- Более того, определенное так объединение невозможно инициализировать. Например, неверно следующее: с L tok_val curr_va1 = 12; // ошибка: int приписывается tok_ val А вот к такому объединению можно применять конструкторы: 159
union long i tok_val { Л char" р; // i char v[8]; // i // i double d; // : tok_ valjfchar'); . J // tok_ valfint ii) { i iitdk valfdouble dd) d строка ; идентификатор (максиму)* 8 символов) целочисленные значения - значения с плавающей точкой }; ' надо разобраться, р или v i = ii; { d = dd; } Так можно работать в Ждить по правилам^'Для Например: void f() ' { тех случаях, когда типы элементов можно переназначения имен функции* (См< ‘$4.6.7 и ■ -. S-V ’ ■ .-. .и’.' tok_val а = 10; tok_ val b = 10.0; // a.i = 10 // b.d = 10.0 •'6 Г Если это невозможно (например, Если это невозможно (например, для типов char' и char[8], int и char и т.п.), то узнать', какой элемент нужен, можно только ай’аййзом^ инициализатора при выполнении программы Или с помощью дополнительного аргумента. Например: tok_ val::+ok_ val(char' pp) I if (strlen(pp) < = 8) strncpy(v,pp,8); // короткая строка else P = pp; } J" Вообще говоря, следует1 избегать таких случаев. 4 Применение конструкторов не гарантирует От случайного неправильного применения tok.val в результате присваивания этому объекту значения одного типа м последующего использования этфю значения как значения другого типа. ^Для решения этой проблемы' в1 классможно включить объединение, которое будет следить за тем, величина какого типа запоминаемся: , •ц • '■ class tok; val { ; chfiC'tag; urflort 'Char' p; char v[8); Iona •; ' doable* d; int check(char t, char' s) ■г т N ■' 3 ' 160
{ if(tag! = t) { error(s); return 0; } feturn 1; public: tok_val(char" pp); tok_val(long ii); <{ i = ii; tag = T; } tok_ val(double dd ) { d = dd; tag = D'; } , f }; long& ival() double& fval() char"& sval() char" id() { check('i',"ival"); return i; }f { check('D',"fval"); return d; }\. {’ check('S',"sval"); return p; } \ { check('N',"if"); return v; } ' Конструктор,- принимающий аргумент .вида" /'Ымвольн^ строка", использует для копирования коротких строк стандартную функцию sfrn4cpy(). Функция strncpyO похожа на функцию strcpyQ, за исключением того; .что она использует третий аргумент, описывающий количество символов, которое нужно скопировать: ( ' tok _ val: :tok _ val(char \ pp) > }; if (strlen(pp) < = 8) | tag .=, 'N'; ?trncpy(v,pp;,8); [ // короткая строка // копируются 8 символов о п. else { // длинная строка ’ tag = 'S'; p = pp; // указатель просто запоминается . Тип tok __ val можно использовать таким образом: • void f() ' { tok _ val tl ("short"); ., r // присваивание у tok_ val t2("long4 string"); j/ присваивание p char s[8]; strncpy(s,t1 .id(.),8); // все в порядке stmcpy(&,t2.id(y,8); // check() не сработает 5.5 КОНСТРУКТОРЫ И ДЕСТРУКТОРЫ Если для класса определен конструктор, то он рызьц^тся каждый раз при создании объекта этого класса. Если для кд^а определен деструктор, то он вызывается каждый раз при разрушений объекта этого класса. Объекты могут создаваться как: [1] Автоматический объект: создается каждый ра£ г}ри ’объявлении его в , ходе исполнения программы и уничтожается при'выходе из блока, в котором он используется; 161
Статический объект: создается один раз при запуске программы и уничтожается один раз при терминации программы; Объект, сохраняемый в свободной памяти: создается с помощью оператора new и уничтожается с помощью оператора delete; Объект-элемент: элемент другого класса или векторный элемент. Объект может быть также сконструирован явным использованием конструктора в выражении (см. $6.4). В этом случае он Является автоматическим объектом. В последующих разделах предполагается, что объекты принадлежат к классу с конструктором и деструктором. В качестве примера используется класс table (см. $5.3). 5.5.1. Предупреждение Если х и у являются объектами класса cl, то выражение х t= у по умолчанию означает побитовое копирование у > х (см. $2.3.8). При использовании объектов, для которых определены ксжструктор й деструктор, присваивание, интерпретируемое подобным образом, может Давать неожиданный (и обычно нежелательный) эффект. Например: class char __ stack {, int size; char" top; char" s; public: char __ stack(int sz) * char _ stack() 4 void push(char c) char pop() top = s = new char[size = sz]; } delete s; } // деструктор "top + + = c; } return "—top; } void h() char _ stack s1(100); char_stack $2.j? si; // ошибка char _ stack s3(99); s3 = s2; // ошибка } В этом примере конструктор char•_stack::char_stack() вызывается • дважды: для si и s3. Для s2 он не вызывается, поскольку эта переменная инициализируется с помощью присваивания. Однако, деструктор char_stack:: ~ char_stack() вызывается трижды: для si, s2 й s3! Более того, по умолчанию присваивание интерпретируется как побитовое копирование, так .что в конце исполнения h() si, s2 и s3 должны содержать указатели на векторы символов, размещенные в области памяти, которая была свободна в, момент Создания si. Указателя на вектор символов, помещенного в память при соЗдайии $3, не остается. Подобных аномалий можно избежать: см. главу 6. 162
5.5.2. Статическая память Рассмотрим следующий пример: table tbll(100); void f() { static table tb!2(200); j - -- ' ■ - main() 1 Ю; } В данном случае конструктор table: :table() (как это определено в 5.3.1) вызывается Дважды: один раз для tbll и один раз для tb 12. Деструктор ~ table: :table() также будет вызван дважды: для уничтожения tblT^H tbl2 после выхода из функции main(), Конструкторы для глобальных старческих объектов исполняются в порядке появления их объявлений; деструкторы вызываются в обратном порядке. При отсутствии функции, в которой объявлен локальный статический объект, вызов соответствующего конструктора йе определен. Если вызывается конструктор для локального статического объекта, то вызов происходит после того', как вызваны конструкторы для лексически предшествующих глобальных статических объектов. ; ■ Аргументы конструкторов для статических объектов должньс быть константными выражениями: / void g(int а) static table t(a); // ошибка По традиции, исполнение функции main() рассматривалось как исполнение программы. Это неверно, даже в С, однако только использование размещения статических объектов класса с конструкторами и/или деструкторами даёт программисту простую и очевидную возможность создания кода, который исполняется перед и/или после вызова функции main. ' ■' ' Вызов конструкторов и деструкторов для статических объектов играет исключительно важную роль в C++. Этс^дает возможность обеспечить правильную инициализацию и очистку структур данных в библиотеках. Рассмотри А заголовочный’файл <stream.h>. Откуда* берутся cin, cout и cerr? Где они инициализируются? И, что наиболее важно, поскольку выходные потоки сбдёржат внутренние' буферы для Символов, каким образов они очищаются? Простой и очевидный ответ: все это проделывается с помощью соответствующих конструкторов и деструкторов до и после исполнения функции main. Существуют методы для инициализации и очистки библиотечных средств, альтернативные использованию конструкторов и деструкторов. Эти методь! либо узко специализированы либо исключительно 163
уродливы. Если программа терминируется с помощью функций exit(), вызываются деструкторы для статических объектов. Нр если программа терминируется с помощью функции abort(), то деструкторы не вызываются, Обратите внимание: из этого следует, что функция exit() не прерывает программу немедленно". Вызов функции exit() в деструкторе может привести к бесконечной рекурсии. Иногда, если Вы создаете библиотеку, необходимо или просто удобно придумать тип с конструктором й деструктором с единственной целью: инициализация и очистка. Этот тип будет использоваться лищь однажды: для размещения статического объекта так, чтобы можно было вызвать конструктор и деструктор. 5.5.3, Свободная память Рассмотрим пример: main() { ' ' table" р = new table(tUu); ; f table" q ±= new table(200): " ' ....... delete p; delete p; // возможно, ошибка Конструктор table: :table() будет вызван дважды, так же, как и деструктор table:: “ table(). Следует отметить, что C++ не гарантирует того, что деструктор всегда вызывается для объекта, созданного' с помощью оператора new. В предыдущей программе q никогда не будет уничтожен; а р будет уничтожено дважды! В зависимости от типа р и q, программист может считать или не считать это ошибкой. Невозможность уничтожить объект, как правило, не является ошибкой - это приводит лишь к излишней затрате адресного Пространства. Однако "двойное" уничтожение р обычно Является серьезной ошйбкой. Типичный результат двукратного применения оператора delete к одному и тому же указателю - образование бесконечного Цикла в программе, обслуживающей свободную память. Однако поведение системы в» таком случае не описывается в определении язьЖа и зависит от реализации. Пользователь может определить новую реализацию операторов new и delete (см. $3.2.6). Можно также определить способ взаимодействия конструктора и деструктора с операторами new и delete (см. $5.5.6). 5.5.4. Объекты класса как элементы ” ’’ Рассмотрим пример: z class classdef { table members; , / int no _ of _ members; // ... classdef(int size); ; * ~ classdef(); 164
}; " Идея ясна: classdef должен содержать таблицу элементов размера size. Проблема заключается в тОм^ чтобы получить конструктор table::table()f вызываемый с аргументом size. Это можно сделать т£ким образом': classdef::classdef(int size) : members(size) no _ of _ members = size; // ... } Аргументы для конструктора элемента (в данном случае table::table ()) помещаются в определение (но не в объявление) конструктора класса, который содержит этот элемент (в данном случае - cjassdef::classdef()). После этого конструктор элемента вызывается перед ienoM конструктора, в котором определен список его аргументов. i Если элементов, для которых необходим список аргументов для конструктора, больше, их можно описать аналогично. Например: class classdef { table members; table friends; int no — of _ members; //... .V. dM$def(ipt size); * classdef; '* г Списки аргументов для элементов разделяются запятыми (а не двоеточием); списки инициализирующих величин могут быть расположены в любом порядке: , "" Л Л-. classdef: :classdef(int size) : frjends(size),members(size) no _ of — members & size; -у.Л- // .... } Порядок, в котором вызываются конструкторы, не определен, поэтому не рекомендуется использовать списки аргументов с возможными побочными эффектами: classdef::classdef(int size) :friends(size = size/2),members(size) // плохой стиль { ' . no — of members = size; // - } 165
Если для конструктора элемента аргументы не нужны, то и> список аргументов описывать не нужно. Например, поскольку tabiejtabfe был определен с аргументом 15 по умолчанию, то верно следующее: classdef: :classdef(int sizfe) :members(size) no __ of _ members = size; // ... ' -v ■: Реймер таблицы friends давен 15. Когда уничтожается оф»ект класса, содержащий объекты класса (например, classdef), вначале исполняется тело собственного деструктора объекта, а затем исполняются деструкторы элементов. Рассмотрим обычную альтернативу использованию объектов класса как элементов? использование элементов-указателей и инициализацию их в конструкторе: class classdef { table" members; table" friends; int no _ of_ members; // ... classdef(int size); -classdbfO; — ■' ид- '' ■' • K- .1 H ... . ..*• <. ■?:. -:m- da^sdef::tlassdef(ftit siz£) « тетЬёгз = new table(size); friends - new table; //размер таблицы по умолчанию no _ of _ members = size; 7/ ... } , Z; Поскольку Таблицы были созданы с помощью оператора new, они должны уничтожаться оператором delete: classdef:: ~ cla$sdef() ' А ’Г: i'! ГГ ... delete members; •'* delete friends; > Объекты, создаваемые отдельно (подобно описанным ’выше), могут оказаться полезными, однако обратите внимание на то, что members и friends указывают на различные объекты. Для каждого из них нужно проводить Операции размещения и освобождения. Более того, указатель и объект трёб'уют больше памяти, чем Ьбъёкт элемента. 166
5.5.5. Векторы объектов классе Чтобы можно было объявить вектор объектов класса с конструктором, этот класс должен иметь конструктор, который можно вызвать без списка аргументов. Нельзя использовать даже списки аргументов пр умолчанию. Например, table tblvec[1Oj; является ошибкой, поскольку для table: :table() требуется целый аргумент. Аргументы для конструктора в объявлении вектора описать нельзя. Чтобы можно было объявить векторы таблиц, объявление класса table (5^.1) можно было бы изменить тац; «i, с class table { // ... void init(int szj^s, , v // public: ' Z table(int sz) // { init (sz); } table» // {ynit(li); ) }; •• ''J ; d " 1 подобно старому конструктору I ........ как и раньше, но без умолчания по умолчанию - Ъ При уничтожении вектора для каждого его элемента нужно вызвать деструктор. Для векторов, размещенных без использования оператора new, это делается неявно. Однако, для векторов, размещенных7 в свободной памяти, неявное исполнение невозможно, прскольку компилятор не в состоянии отличить указатель на одиночный объект от указателя на первый элемент вектора объектов. Например: void to ' table" tl = new table; table" t2 = new tableflO]; delete tl; // оди^ объект tabl© -..nOkl delete t2; 7/ ошибка: 10 объектов table } В этом случае программист должен указать размер вектора: у void g(int sz) { ■■ table" t[ = new table; = new table[sz]; ... .у рТ’Л ' •> ■'■ ■ ■■■ . «j. <-: ■• ... > Но почему компилятор( не может, вычислить количество элем^ОДВ, исходя • ■ ■ к delete t1( deletefszl 167
из отведенного объема памяти? Потому что распределитель свободной памяти не является частью языка; его может написать сам программист. 5.5.6. Небольшие объекты Если Вы используете много небольших объектов, размещаемых в свободной памяти, Вы можете обнаружить, что в Вашей программе размещение и перемещение объектов занимает много времени. Одно из решений - сконструЙрбва’гь улучшенный распределитель памяти Общего назначения. Другой вариант - создать класс для управления свободным пространством памяти для объектов данного класса через определение соответствующих конструкторов и деструкторов. Рассмотрим класс name, использованный в примерах table. Его можно было бы определить так: ' struct п’ёте { / char’ 0rjhg; riSme" Anext; doftfete value; nemefthar", doubFe, пате"); 1 ~ name(); }» Прбгреммйст Может воспользоваться тем, что размещение и перемещение в памяти объектов известного типа можно провести/ гораздо более эффективно (как по времени, так и по занимаемой памяти), чем с помощью общей реализации через операторы new и delete. Общая идея заключается в предварительном размещении "толстых кусков" объектов name и связывании их воедино, сведя тем самым размещение и перемещение в памяти К операциям с простым связанным списком. Переменная nfree - это дескриптор списка неиспользованных names. const NALL = 126; патё" nfree; и Распределитель памяти, используемый оператором neW запоминает размер объекта вместе с объектом (это необходимо для нормального функционирования оператора delete). Такого перерасхода памяти легко избежать, если использовать типоспецифйчный распределитель памяти. Например, распределитель, приведенный ниже, использует на моем компьютере 16 байт дДя хранения name, в' то время^ как стандартный распределитель'' свободной памяти отводит для этого 20 байт. Дот как эя> делается: name: :name(char" s; double v, name" n) register name" p = nfree; // первое размещение » (p) nfree = p-> next; 168
else { // размещение и связывание name" q = (name')new char[ NALL’sizeof(name) ]; for(p = nfree = &q[NALL-1]; q<p; >p—) p->next =* p-1; (p + 1)->next = 0; } this = p;. string = s; value = v; next = n; // затем инициализация * ‘ : V ■ 1 ‘r ■ ' .. Присваивание this ^информирует компилятор о том, что .программист принял управление и что не следует использовать механизм, принятый по умолчанию для распределения памяти. Конструктор name::name() действует в случае, если память тдля name распределяется только оператором new, однако для многих типов это всегда так. В разделе 5.5.6 разъясняется, как цаписать конструктор, который будет обрабатывать как св^одную память, так и другие типы распределения. \ Заметьте, что память нельзя распределить просто ^ак: : . name" q = new name[NALL]; 3 поскольку это приведет к бесконечной рекурсии при вызове оператором new name::riame(). , - _ < .. •J ,<■ ?»>.!• Исключение из памяти тривиально: ■ name:: ~ name() { ■ next = nfree; ' nfree = this; this = 0; } Присваивание в деструкторе переменной this значения 0 гарантирует, что для исключения объект^из памяти не будет использоваться стандартное устройство. v 5.5.7. Предупреждение 9 При п^йсвёивании значения переменной this в конструкторе величина this не определена до момента присваивания. Следовательно, обращение к элементу дЬ проведения присваивания не определено и, вероятно, приведет к беде. Существующий компилятор не проверяет, присваивается ,лй this значение при всех возможных вариантах исполнения: mytype::mytype(int i) if (i) this = mytype _ alloc(); // присваивание элементам , ,t i •• ’ w }; 169
Это будет скомпилировано, и при i i’= 0 память для объёкта отведена не будет. Конструктор может определить, вызван ли он оператором new или как-нибудь иначе. Если он вызывался через new, указатель this имеет входную величину 0/'в ином случае this указывает на пространство, уже занятое объектом (например, на стек). Таким образом, легко написать конструктор, который отводит память только (и если только) он был вызван оператором-new. Например: mytype::rnytype(?ht i) г . - if (this = = 0) this = mytype _ аИос(); //присваивание элементам " *т ДлЙ Деструктора аналогичной Особенности, позволяющей определить, создавало! Яй егО^об'ьект С: помощью оператора new, нет. Невозможно и определить, был ли деструктор задействован через оператор delete, или тем, что объект вышел из области видимости. Если это важно, пользователь может хранить соответствующую информацию где-нибудь так, чтобы деструктор м,ог ее прочесть. Альтернативный вариант - пользователь может обеспечить правильное размещение объектов только данного класса. Если первый подход ^реализуется, то второй неинтересен: 9 Если реализатор' класса является и его единственным' пользователем, то разумно упростить класс на основе некоторых допущений по его использованию. Если же класс создается для более (Широкого пользования, подобных допущений, как правило, следует избегать. л > 5.5.8с Объекты переменного размера Управляя размещением в памяти и изъятием из памяти, пользователь моНсет также создавать объекты, размер которых йё определен во время компиляцию В предыдущих примерах классы vector? stack/1!nt^et/и table были реализованы как структуры фиксированного^ размера, содержащие указатели на действительные местам хранения. Это означает, что для создания подобных объектов в свободной памяти Необходимы две операции размещения, и что каясдое обращение к сохраняемой информации включает в себя дополнительную адресацию. Например: 4 class char _ stack { int size; char’ top; char' s; ’ public: char _ stack(int sz) | top = s = new char[size = sz]; } слёг _ stack() { delete s; } //деструктор void push(char c) I 'top + + = c; } char p0pO { return ’—top; } }; . < Если каждый объект класса размещен в свободной памяти, эта 170
операция не нужна. Вот альтернативный вариант/ . „ class char stack { Г; iir- " ' У int size; char" top; • • v. я char s[1 ]; ' -• 'a"T' public: . ■ >, char _ steck( int sz); void push(char c) j "top + + = c; } return '—top; } char pop() { }; , •* ’V-- char _ stack::char _ stack(int sz) 4 if(this) error ("стек расположен не в свободном пространстве"); ,.v.- Jf(sz < 1) еггрг("размерч£;дека < 1") W < q v v this =; (char _ stack" )new ch;ar[sizeof(char _steck>i*sz*4<]; / x size = sz; , л * ■ ,' / / top == ’$;.••• v- • ' t a-- } ' . 4., . . . ■: .• <■ ■ Обратите внимание на то, что деструктор больше не нужен,’поскольку оператор delete может освобождать .память, исполвзовамиуюсЬаг_ stack, безо всякой помощи £© стороны программиста, л, > ? т t. 1. 2. 3. 4. 5. 5.6. УПРАЖНЕМИЯ Л ("1) Модифицируйте калькулятор из главы 3, используя класс table. ("1) Постройте tnode ($с.8.5) как классу.? с конструкторами, деструкторами и т.д. Определите дерево из tnode как класс с конструкторами, деструкторами и ьд. , ("1) Модифицируйте,, класс intset ($5 3.2) в набор символьных строк. (“1); Модифицируйте класс intset в -набор, состоящим из блоков node, где node - структура, определенная Вампф <-.у ■ ; СЗ) Определите класс для- анализа, ^запоминания; вычисления и распечатки простых арифметических выражений, состоящих из-целых констант и операторов -ь,. - \ и /. Общий интерфейс должен выглядеть так: ■> class expr { т // ... public: expr(char’); ■.> h int eval(); ; у > 1 void print(); 4 };. -;x. Аргумент "строка символов" для конструктора ехрг::ехрг() является выражением. Функция expr::eval() возвращает значение выражения, а expr::print() распечатывает представление выражения в cout. Программа может выглядеть следующим образом: 171
6. 7. 8. 9. expr x("123/4+ 123’4-3"); CQut < < "x = " < < x.evalf) < < "\n"; x.printO; Определите Шсс expr дважды: представления -Связанного списке -г--- - ...i: один раз с использованием для спискё’ узлов, а второй - с использованием для ^Представления цепочки символов. Поэкспериментируйте с разными Способами распечатки выражения: полностью заключенное, в скобки, постфиксная нотация, ассемблерный код и т.д. (’1) Определите класс char_ queue, так, чтобы общий интерфейс не зависел от представления. Реализуйте char _ queue (1) как связанный список й (2) ■'как вектор. О согласованности не беспокойтесь. (“2) Определите класс histogram (гистограмма), который подсчитывает числа в определенных интервалах, описанных как аргументы конструктора. histogram. Напишите, функции для .. распечатки гистЬграмАлЬ1. Предусмотрите обработку значений, выходящие за прёделЬ1рг1ределенной области. Подсказка: <fask.h>. ("2J Определите несколько классов, для получения случайных нисел 6пределёнйЬ1М распределением. Каждый класс дрджён содержать 7 ксййструйтор, ОНисывающий параметры распределения и функцию draw (рисовать), которая возвращает "следующее" значение Подсказка; <4ask.h>. См также класс intset. ч (’2J Перепишите пример date ($5.2.2), пример char_ stack ($J>.2. 5) и ' ihfs^t ($5.3.2), не Используя фуйкций элемента (включая конструкторы и деструкторы). Используйте только class и friend. Проверьте каждую новую версию. Сравните их с версиями, которые используют функции элемента. (*3) Сконструируйте класс таблицы символов и класс для ввода лЙбл и цы символов для некоторого язык^Прсмотритркомпилятррэтого языка,для того, чтобы понять, как на самрАА деде выглядит таблица си лАволдв: 1 i ; ; . ...... .. .;.... ("^'’ Мрди<$^^ класс выражения/из упражнения §/*тдк, . чтобы лАОжно было работать переменные и оператор присваивания =. Используйте класс таблицы символов из Упражнения 10. (*1^аЙалпрргралАма: > - к # include <strdam.h^ ‘ •»". р х- ч. n&ih() \ cput < < "ПривртХп" Модифицируйте ее таким образом, чтобы она печатала на выходе 10. It. 12. < к ‘its Инициализация Привет Очистка Ни в коем случае не меняйте main().
- J V' "И.В.К." г— 105023 Москва, 1- - А Мал. Семеновская,, д. 5 Тел.: 936-50*67, 311^2-08 Iwl^= л факс: 203-93-55 ГЛАВА 6 . .• •< . : ;.|Г > ; - ПСРЕОП^Д^СНИЕ ОПЕРАТОРОВ ? J В настоящей главе ЙЬисан механизм переопределения операторов, заложенный g C++. Программист может определитьзначениеоператоров, применяемых к данному классу; в дополнение к^арЙфмётическим и логическим операторамг и Операторам отношения можно определить операторы вьГЗОва () и индёк'саёии [Ха^также переопределит ьрператоры присваивания и ’инициализаций.' Можно определить явное и ндявное преобразование типов между базовыми типами и типами, определяемыми пользователем. Показано, как определить класс, объект которого Hpf'может быть скопирован 'Йн и уничтожен иначе, как с помощью функций, определенных пользователем. '■*Т 6.1 ВВЕДЕНИЕ J 5 X Программы часто имеют дало с объектами, которые являются конкретными представлениями абстрактным койцёлцмй. Найр и мер, тин данных языка С+ + mt, соемйстис^с опсраторемн +г, / и т.д.,)Э обеспечивает (ограниченную) реализацию матемеенч^Ыой концепции церых, чисел. Такие концепции обычно включают всебя набор операторов, , представляющих основные операции йодббъектемк в кратком, удобном и общепринятом виде. К Сожалению,0 Лишь очень■ нСЙогогй^’ концепции могут непосредственно поддерживаться языком программирования. Например, такие концепции, как арифметика комплексных чисел, мтричиея armafl^a, логические сигналы и работа с цепочками не поддерживаются налрямуф йеыком C + + . Классы дают возможность описывать представления нелримитивных объектов в C++ (наряду с набором операций, которые можно проводить с данными объектами). Определение операторов для работы с объектами класса иногда дает программисту возможность получить более удобную и стандартную нотацию для работы с объектами класса по сравнению с основной функциональной нотацией. Например: ’it* ?. ■ • , class complex { double re, im; public: complex (double r, double i) { re = r; imc i } friend complex operator + (complex, complex); friend complex operator"(complex, complex); 173
определяет простую реализацию концепции комплексных чисел, в*которой число представляется парой чисел с плавающей точкой, вычисляемых с двойной точностью. РЗ^ота с ними проводится с помощью операторов + и " (исключительно). Программист описывает значение + и определив функции operator* и operator". Например, если даны комплексные числа b и с, Ь + с (по определению) означает operator * (Ь,с). Теперь можно подойти к общепринятой интерпретации комплексных выражений. Например: void f() complex а = complex(1, 3.1); cbmpleX b = complex(1.2, 2); complex c = b; a '=='■ b + c; • b = b + c“a; c = a’b * complex (1,2); v , . ■■ Соблюдаются обычные правила старшинства, так что второй оператор означает b = Ь +(с"а), а не b = (b + c)“a. 6.2. ФУНКЦИИ ОПЕРАТОРА Можно объявить функции, определяющие значения для следующих операторов: + ' / " % & 1 1 ’ =.. < > , += _ = * = /= %= ‘= 1= < < > > > >= < <= = != <= >= && | | ++ — [] Ч) new delete Последние четыре оператора - это операторы индексации (6.7), функция вызова (6.8), функция распределения свободной памяти И функция очистки своб.одной памяти (3.2.6). Нельзя менять старшинство этих операторов, а также менять синтаксис выражений. Например, нельзя определить унарйую операцию % или бйнарнукИ. Нельзя определить новые лексемы операторов, но Вы можете использовать нотацию вызова функции, если Данный набор операторов не адекватен. Например, используйте pow(), а не ■ Эти ограничения могут показаться драконовскими, однако более гибкие правила легко могут привести к неоднозначностям. Например, определение оператора "" как оператора возведения в степень, на первый взгляд, может показаться очевидной Нелегкой задачей, но подумайте еще раз. Дрлжен ли оператор "" ставиться слева (как в Фортране) или справа (й^к в Алголе)? СледуёХ* ли выражение а^р интерпретировать* как а’Ср) или как (а)""(р)? Название функций чтйёратора - это -ключевое слово operator, за которым следует сам оператор, например, operator<<. Функция оператора объявляется и вызывается в дальнейшем 'как любая другая функция; Использование оператора - Это лишь сокращенная запись для явного вызова функции оператора. НапрйМёр:' 7 ■ 174
с vpid f(complex a, complex Ц) complex c = a + b; // сокращенная запись complex d я operator+ (e»b);// явный вызов При условии, что complex объявлен раньше, оба .йн^адузатора.’ являются синонимами. ' ; 6.2.1. Бинарные и унарные операторы Бинарный oifepaTOp может быть определен либо, к^н функция-элемент с одним аргументом, либо как дружественная функция с двумя аргументами. Таким' образом, для любого бинарного оператора @ аа@ЬЬ может интерпретироваться либо как aa.operator@(bb) либо как operafor@(aa,bb). Если определены оба, то aa@bb , является ошибкой. Унарный оператор, префиксный или постфиксный, может быть определен либо как функция-элемейт без аргументов, либо как дружественная функция с одним аргументом. Следовательно, для любого унарного оператора @ как аа@, так и @аа может интерпретироваться либо как aa.operator@(), либо как operator@(aa). Если определены оба, то аа@ и @ра являЮтФ» ошибками. Рассмотрим следующие примеры: class X { , // дружественные функции: friend X operator-(X); friend X operator-(X,X); friend X operator-^); frien d X оperator-( X, X, X); // унарный минус // бинарный минус ( // ошибка: нет операнда4 // ошибка: тернарный // элементы (с неявно заданным первым аргументом: this): Х“ operator&(); // унарный & (адрес) X operator&(X); s // бинарный & (или) , \ X operator&(X,X); ч // ошиока:7^тернарный < ... \ . <4- . Если переопределяются операторы ++ и постфиксная и префиксная формы не различаются. р,г .. •> 6.2.2. Предварительно определенные значения операторов . Никаких предположений о значении операторов; определенных пользователем, не делается. В частности, поскольку переопределенный оператор = не предполагает неявного присваивания значения своему первому операнду, проверки на то, является . ли этот операнд lvalue- выражением, не делается, (гмб). " ль Значения некоторых 'Встроенных операторов определяются как эквивалентные некоторым комбинациям других- операторов с теми же аргументами. Например* если а является целым, -+ +а означает а+ =1, что, >175
в свою очередь, означает а = а + 1. Такие соотношения не сохраняются для операторов, определенных пользователем, если только' пользователь «случайно не определил их таким образом. Например, определение qperator + = () для типа complex не может быть выведено из определений comp lex Г: operator 4-() и complex:: operator = (). Исторически прожилось так, что операторы = и & имеют предварительно определенные зйачения, если они примёняются к объектам класса. Элегантного сдрсоба "разопределить" эти два оператора нет. Однако, для класса X их ^мфжно отключить. Можно, например, объявить X::operator&(), не дав0я, ему определения. Если а кйком-то месте будет использован адрес X, редактор связей обнаружит недостающее определение1. Альтернативный вариант,- можно определить X::operator6^(), чтобы вызвать ошибку во время работы программы^ 6.2.3. Операторы и типы, определяемые пользователем . Функция оператора должна быть элементом, либо иметь, по крайней мере, один аргумент объекта класса (для функций, переопре¬ деленных операторами new и delete, это необязательна)* Это правило запрещает пользователю изменять значение любого выражения, которое не включает в себя тип данных, определяемый пользователем. В частности, нельзя определить функцию оператора, оперирующую исключительно с указателями. Функция оператора, которая должна использовать в качестве первого операнда базовый тип, не может быть ■функцией-элементбм; Например, рассмотрим сложение комплексной переменной аа с целым числом 2: аа + 2 может интерпретироваться соответственно объявленной функцией-элементом как аа.operator + (2), но 2,+ аа нё может интерпретироваться таким образом, поскольку Класса int, для которого определение + озирает 2.operator + (аа), нет. Даже если бы такой класс существовал, то для того, чтобы справиться с 2 + аа и аа + 2, понадобились бы две различных функции- элемента. Поскольку компилятор "не знает" значения ч■,определенного пользователем, он нё может предполагать, что это действие коммутативно, и интерпретировать 2 + аа как аа + 2. Этот пример тривиально решается с помощью дружественных функций. Все функции оператора, по определению, переопределяемы. Функция оператора придает новое значение оператору в дополнение й встроенному определению. Может быть несколько функции оператора с одинаковым именем, поскольку они различаются по типам аргумента; atq чпЬз^ол^%т компилятору различать их (см. $4.6.7). 6.3. ПРЕОБРАЗОВАНИЕ ТИПОВ, ОПРЕДЕЛЯЕМЫХ ПОЛЬЗОВАТЕЛЕМ Реализация комплексных чисел,, представленная во введении, слишком ограниченна для того, чтобы удовлётворйть кого бы тё‘ ни бйГло И должна быть расширена. В основном, это обычное повторение методики, представ- !В некоторых системах редактор связей настолько "находчив", что "жалуется'' даже в случае, если не определена не используемая функция. В таких системах этот метод не может использоваться. < , z, < 176
ленно^ дыше. Например: ; class complex { double re, im; complex^(double redouble i) { re = r; irn^i } friend complex operator + (complex, comple-k); friend complex rperator+(complex, double); friend complex operator + (double, compld#); ' friend complex operatoHctfmplex, comple^ ?1’ 4 friend complex Uperator-(coVnplex7 double); " friend complex d.perator-(double, complex); complex operatdr-(); // унарный - ,ч friend complex bpet.afor"(cbmplex, complex); /4riend complex ope7ator“(complex, double); friend Comdex oberator’fdouble. Comdex); b =, c"2 0"c; c = (d+ei> } Однако, Описание функций для каждой комби нац^ии^готр lex и double, как -то сделано выше для operator’Q нелыноси/Ао скучйй. БоУгее того, настоящий инструмент для комплексной арифметики должен содержать, по Крайней мере, десяток таких функций; см., например, тип complex, объявленный в <complex.h>. 6.3.1. Конструкторы Альтернативой использованию нёскольких (Совмещенных) функций является объявление конструктора, который создает данные типа complex на основе данных типа double. Например: \ * class complex { // - L complex(doublr г) { re = r; im = 0 } }; Конструктор, использующий один аргумент, необязательно вызывать явно: 157
complex zl = complex(23); complex z2 = 23; Как zl, так и z2 будут инициализироваться через вызов complex(23, 0). Конструктор -это предписание для создания величины данного типа. Если при вызове ожидается значение данного типа и если конструктор может создать тако^ зЛЬчение с использованием для присваивания некоторой вё&йчины, то конструктор будет использоваться. Например, класс complex можно было бы объявить так: class complex { double re, im; < public: . complex(double r, double i =0) { re = r; im = i; } friend complex operator + (complex, complex); friend complex operator’(cp,mplex, complex); }; После такого объявления разрешаются операции, включающие в себя обработку переменных типа complex и целочисленные константы. Целочисленная константа будет интерпретироваться как константа типа complex с мнимой частью, равной 0. Например, а = Ь*2 означает а = operator"(b, complex( double(2), double(O) ) ) Преобразования, определенные пользователем, применяется неявно лишь в случае, если оно уникально (6.3.3). Объект, созданный явным или неявным использованием конструктора, является автоматическим и уничтожается при первой же возможности, как правило, немедленно после вызова оператора, в котором он был создан. 6.3.2. Операторы преобразований Использование конструкторов для описания типа преобразования весьма удобно, но имеет подтексты, которые могут оказаться нежелательными. [1] Запрещены неявные преобразования от типов, определенный пользователем, к базовым типам (поскольку базовые типы не являются классами; [2] Нельзя описать преобразование от нового типа к Старому, не модифицируя описание старого типд; [3] Нельзя получать конструктор с одним аргументом так, чтобы при его применении не проводилось никаких преобразований.. Последнее, по-видимому, не представляет собой серьезной проблемы, в то время как с первыми двумя можно справиться, определив оператор Преобразования для исходного типа. Функция-элемент X ^operator Т(), где Т - имя типа, определяет преобразование от X k J. Например, можно определить тип tiny (очень маленький), к которому могут принадлежать 178
только целые величины в интервале 0..63; однако эти величины могут свободно использоваться совместно с обычными цёль/ми в арифметических операциях: class tiny { char v; int assign(irtt i) { return v = public: tiny^int i) tiny(tiny& t) int operator ₽ (tiny& t) inf operator = (int i) operator int() (i& 63) Г (еггог("ошйбка иД¥ёрвала"),О.: i; J f assign(i); } v = > return v , = t.v; } return assign(i); } '’ return v; ) //сЗ ± 60 1 ‘ // интервал’ нё проверяется - не Нужно // i > ’ :' // ошибка интервала: с1 = 0 (не 66) // бшйбка йнтервала: с2 = 0 // интервал не проверяется не нужно Интервал проверяется каждый рёз, когда tiny инициализируется типом int и каждый раз, когдё ему присвёйваётся значение типаint. Одно значение типа tiny можно присвоить, другому без проверки интервала. Для того, чтобы дать возможность производить с переменными типа tiny обычные арифметические операций,' определен tiny::operator int() ^ неявное преобразование от tiny к int. Каждый раз, когДа там; где ' ?Д^лжен использоваться тип int, появляется tiny, исполЬзуёт^Я соотвётствующёё int. Например: " - void main () . { tiny tl -= c2; s tiny c2 = 62; tiny c3 = c2 - cl ; tiny c4 = c3; int i = cl + c2; cl = c2 +2 “ cl; c2 = cl - i; c3 = c2; > По-видимому, был бы удобнее тип вектор tiny , поскольку при этом также экономится память; чтобы такой тип было удобно использоват^ можно применить оператор индексации (]. п ij- Еще одно применение операторов' преобразования, определенных пользователем - это объявление типов для нестандартных представлений чисел (арифметика по основанию 100, арифметика! чисел с фиксированной точкой, двоично-десятичные представления чисел и т.д.); обычно такие объявления включают в себя переопределение таких операторов, йёк + и *. Функции преобразования, по-видимому, ’особенно удобны для работы со структурами данных; В этих случаях считывание данных (реализуемое оператором преобразования) тривиально, в то время, как операции присваивания и инициализации далеко' Не столь тривиальны. Типы, istrearn и ostream основаны На такой функции преобразования, которая разрешает операторы вида ' 179
while (cin> >x) cout< <x; дная операция cin>>x, приведенная выше, возвращает istream&. Эта цчина неявно преобразуется в величину, описывающую состояние cin и значение в дальнейшем можно проверить Оператором while (8.4.2). 4эко, в целом, определение неявного' преобразования от одного типа к тому так, что при преобразовании теряется информация - плохая иДёя. 6.3.3. Неоднозначности Присваивание объекту класса X (или инициализация его) разрешены ю тогда, когда присваиваемая величина имеет тип X, либо если >еделено уникальное преобразование присваиваемой величины к типу X. В некоторых случаях величину желаемого типа можно сконструировать iTOpHbiM использованием конструкторов или операторов преобразования. > должно быть сделано явным образом; разрешается только одйн уровень юных преобразований, определяемых пользователем. В некоторых случаях, 1ичину требуемого Типа можно 'сконструировать несколькими способами, ие реализации запрещены. Например: class х { /" ..? 7 x(int); x(char'), }; class у { /’ ... 7 y(int); }; class z { /- ... 7 z(x); }; J ; overload :f; ' 1 ■ b x f(x); у f(y); // запрещено: неоднозначность - f(x(1))t или f(y(1)) // запрещено: \g(z(k('/asdf"))) нё проверяется z g(z); Преобразования, определенные пользователем, принимаются во иманйё только в случае, ёсли вызсй функции без них невозможен, пример: ‘ class х I /' ... 7 x(iti+J;; 'overload h(doub1r), h(k;)- h(1); - "J' I30B может интерпретироваться либо как h(double(1), либо как h(x(1)) и* гласно правилу'уникальности, представляется запрещенным. Однако, первая терпретацйя его использует только стандартное преобразование и, в ответствии с правилами из 4.6.7, будет выбрана именно она. Правила для преобразований нельзя рекомендовать ни как наиболее остые ‘ для реализации или документирования, ни как наиболее общие, осмотрим требование уникальности разрешенного преобразования. Более 180
простой подход предоставил бы компилятору возможность выбирать любое найденное им преобразование; следовательно, не надо было бы просматривать все возможные преобразования для того, чтобы разрешить объявленное выражение. К сожалению, это означало бы, что результат действия программы зависит от того, какое преобразование обнаружено. Таким образом, действие программы в какой-то степени зависело бы от порядка объявлений преобразований. Поскольку это нередко свойственно разным исходным файлам (написанным разными-программистами), действие программы зависело бы от порядка, в котором объединялись ее. части. Альтернативно, неявные преобразования можно вообще запретить. Нет ничего проще, однако введение такого правила привело бы либо к тому, что программист был бы вынужден писать исключительно неэлегантные программы, либо к взрывному увеличению количества совмещаемых функций, как это видно из примера класса complex в предыдущем разделе. При наиболее общем подходе следовало бы учитывать всю возможную информацию о типах и рассматривать все возможные преобразования. Например, при использовании предыдущих объявлений, с aa = ff(1) можно было бы оперировать, поскольку тип аа определяет единственную интерпретацию. Если аа - это х, то f(x(1)) - единственная ^функция, в которой в выражении присваивания используется х; если аа -г это у, то вместо нее будет использоваться f(y(1)). Наиболее общий подход ^правился бы и с g("asdf"), поскольку g(z(x("asdf"))) - это единственно вдрможная интерпретация. Проблема, связанная с реализацией такого подхода - необходимость расширенного анализа полных выражений для определения способа интерпретации каждого оператора и вызова функции. Это приводит к замедлению компиляции, а также к неожиданным интерпретациям и сообщениям об ошибках, по мере того, как компилятор рассматривает преобразования, определенные в библиотеках и т.д. При использовании такого подхода компилятор учитывает больше информации, чем это предполагает программист, пишущий исходный код! 6.4. КОНСТАНТЫ Нельзя определить константы типа класса в том смысле, что 1.2 и 123 являются константами типа double. Однако, вместо этого часто можно использовать константы базовых типов; это можно сделать, ^если для интерпретации их используются функции-элементы класса. Общий механизм для этого обеспечивают конструкторы, использующие один аргумент. Если конструкторы просты и допускают inline-замену (замену вызова их на замещение телом конструктора), то вполне резонно считать результатом вызова конструкторов константу. Например, учитывая объявление класса complex в <complex.h>, при вычислении выражения zz1“3 + zz2'complex(1,2) функции будут вызваны два раза, а нё пять. Для выполнения двух операций потребуются действительные вызовы функций, однако операция + и вызов конструктора для создания complex(3) и complex(1,2) будут обеспечены ini ine^-pacm и рением. 6.5. БОЛЬШИЕ ОБЪЕКТЫ При каждом использовании бинарного оператора complex, объявленного так, как это описано ранее, в качестве аргумента функции, реализующей 181
оператор, используется копия каждого операнда. Издержки, связанные с копированием двух величин типа double заметны, но, в общем, вполне приемлемы. К сожалению, не "все классы имеют достаточно небольшое представление. Чтобы избежать избыточного копирования, можно объявить функцию, использующую аргументы - обращения по адресу. Например: class matrix { double m[4][4]; public: matrix(); • friend matrix operator + (matrixA, matrixA); •?-’ friend matrix operator"(matrixA, matrixA); Обращения по адресу позволяют использовать выражения, включающие в себя обычные арифметические операторы без дополнительного копирования. Указатели использовать нельзя, поскольку значение оператора, примененного к указателю, нельзя переопределить. Оператор сложения можно было бы определить так: matrix operator + (matrixA argl, matrixA arg2) matrix sum; for (int i = 0; i<4; i + + ) for (tnt j = 0; j<4; j+ +) a sum.m[i][j] = arg1.m[i][j] + arg2.m[i][j]; return sum; Такой оператор + () принимает операнды + через обращения по адресу, но возвращает значение объекта. Возврат обращения, по-видимому, был бы более эффективным. class matrix { // ... friend matrixA operator + (matrixA, matrixA); friend matrixA operator"(matrixA, matrixA); Такая конструкция разрешена, но при этом возникает проблема распределения памяти. Поскольку обращение к результату будет передаваться за пределы функции как обращение к возвращаемой величине, она не может быть автоматической переменной. Поскольку часто оператор используется в выражении несколько раз, результат не может быть локальной переменной класса static. Как правило, он будет размещаться в свободной памяти. Копирование возвращаемой величины часто обходится Дешевле (в плане времени выполнения программы и адресного пространства, занимаемого кодом и данными); кроме того, упрощается программирование. 182
6.6. ПРИСВАИВАНИЕ И ИНИЦИАЛИЗАЦИЯ Рассмотри# одень простой клас< string: struct string { char" p; int size; // размер вектора, на ^о^р^ыйуказывает р string(intsz) { р new char[size = sz]; } ! ~ sfoingO { delete р; } г. String... - это структура данных, состоящая из указателя на вектор с^мволов4 и размера, этого вектора. Вектор создается конструктором и уничтожается деструктором. Однако, как показано в 5.10, это может привебти к О1^0кам. Например: vbid f() ' ' 0 ' '< { ”■ . • ф'; ' string si(10); / string s2(20); ' ’■ si = разместит в памяти ..два вектора, однако присваивание si = s2 уничтожит указатель на один из/ нйх и скопирует второй. При выходе ^з. функции f() для si и s2 будет вызван деструктор, который затем дважды уничтожит один и тот же вектор с предсказуемым .катастрофическим результатом. Это затруднение г разрешается соответствующим определением присваивания объектов ’’к/йЦа string. ' 11 - struct string { char" р; Jnt size; // размер вектора, на которой 4уКазЬ<йЬёт р string(int sz) { р pew char[?ize = sz]; } : \ ~ string() { delete .p; }1 : void operator = (stringA); ,ч I' •• 4 i '. У11 -Ч- * ' - > ; A voi^ tiring::c$per£tor = (stringfi a) ts- if (this = = &a) returft /7'защй^4’ot s ±= s; ' ’ •' delete pf ■' ■’ p' new charfsize = a.size]; . *^РУ(Р. a.p); „ • St' * , *'.< . Такое определение string гарантирует, что программа, приведенная в предыдущем примере, сработает так, как надо. Однако, незначйтельная модификация функции f() приведет к тому, что проблема предстанет в 183
ином облмЧйи: void Ю { '•!7 strihg $1(10); string s2 = s1; Обратите внимание на то, что создается лишь одна переменная string, в то время, как уничтожаются две. Оператор присваивания! определенный пользователем, неприменим к неинициализированному ' объекту. Поверхностный взгляд на string::operator = () показывает, почему это так: указатель р содержит неопределенную и совершенно случайнук> величину. Оператор присваивания часто рассчитан на то, что его аргументы инициализированы. Для инициализации, приведенной выше, этр> не так по определению. Следовательно, для того, чтобы справиться с инициализацией, нужно написать аналогичную, .но отдельную функцию: ‘ struct string { г /ч • char’ р; ■ int size; // размер вектора, на который указывает р . •■ь- string(int sz)s{ р = new char[size= sz]; } ~ string() { delete p; } void operators (string&); string(string&); void string::dtring(string& a) ' p = newchar[size = a.size]; < strcpyfp, a,p); . . ; . .■> }. : :.*< : . Для типа. X конструктор Х(Х&) следит за инициализацией объектом тогоже типа X* . Еще ;.;раз следует подчеркнут!», что присваизание и инициализация - это разные операции. Это особенно аажно. учцтыватьпри применении деструктора. Если класс X содержит деструктор, который решает нетривиальную задачу (например, перераспределение памяти), очень вероятно, что потребуется полное дополнение функций ддя того, чтобы гарантированно избежать побитового копирования объектов: class X { // ... Х(что-нибудь); // конструктор: создает объекты Х(Х&); //конструктор: копирование дрк инициализации operator = (Х&); // присваивание: очистка и копирование ~ Х(); // деструктор: очистка L Объект копируется еще в двух случаях: если он используется как аргумент 184
функции и как величина, возвращаемая функцией. Если передается аргумент, то инициализируется переменная, до сих пор не инициализированная формальный аргумент. Семантические правила идентичны правилам для других инициализаций. То же самое происходит при возврате функции, хотя это менее очевидно. В обоих случаях будет использоваться Х(Х&) (если он определен). string g(string arg) { return arg; } main ()   } "asdf" String s = "asdf1 Ясно, что после вызова g() значением s должно быть {'asdf". Копирование значения s в аргумент arg не сложно; для этого нужно вызвать функцию $trihg(string&). Для того, чтобы передать копию этого значения за пределы функции д() нужно еще раз вызвать функцию string(string&); на этот раз инициализируемая переменная - это временная переменная, значение которой затем присваивается s. Такие временные переменные, конечно, нужно уничтожить как мржно быстрее с помощью вызова функции string:: ~ stririg(). 6.7. ИНДЕКСАЦИЯ Для придания индексам значений классов можно использовать функцию operator^]. Второй аргумент (индекс) функции operator[] может быть любогр типа. Это дает возможность'определять ассоциативные массивы и т.д. В качестве примера перекодируем пример из 2.3J0, в котором для написания небольшой программы, подсчитывающей частоту встречаемости слов в файле, будет испЪлъЭовай ассоциативный массив. В примере и! ^.3.10 для этогё использовалась функция. Вот как определяется<чсоответствующий ассоциативный Массив: ' struct pair { * char" name; int val; }; class assoc { pair’ vec; int max; : ' int 1гёё; public: T assoc(int); int& pperator[](char“); vbid print—aH(); 185
}; В assoc содержится вектор переменных pair размера max. Индекс первого неиспользованного элемента вектора хранится в free. Конструк тор выглядит так: ■> assoc: :assoc(int s) • . {.1Н .Ju . i max = (s<16) ? s : 16; free = 0; — vec = new pair[max]; . В реализации используется столь же тривиальный и неэффективный метод поиска, что и в 2.3.10. Однако, при переполнении assoc растет: #include < string.h> int& assoc: :operator[](char" p); ! > обработка набора pair:/ проводится ПОИСК p, возвращается обращение к целой части его создается новая "pair", если/p не найдёно ■ ■ .register* pair' pp; for(ppi = &vec[free-1 ]; уес< = рр; рр—) ;/ if (strcmp(p,pp-> name) = = 0) return pp->val; if (free s = max) { // переполнение: расширить вектор pair" nvec s new рагг(тах2]; . Fbr(int i = 0; i<max; i+ +) nvec[i] = vec[i); delete vec; vec » nvec; max = 2’max; } pp = &vec[free + + ); pp->name = new char[strlen(p) + 1]; strcpy(pp-> name,p); pp->val = 0; // начальное значение: 0 return pp->val; л.’* ' ■■ Поскольку представление assoc скрыто, нужно найти спрсоб напечатать его. В следующем разделе будет показало, как можно определить подходящий итератор. Здесь мы используем простую функцию для печати: void assoc::print^аЩ) 186
{ for (int i ч 0; i<free; i + +) cout* vec[i].name < < " *«<?< vec[i].val < <■' "\n"; И, наконец, мы можем написать тривиальную основную программ^:/ main()// подсчитывает частоту встречаемости каждого слова нё входе { const MAX = 256; // больше/ чем сёмое большое слово char buffMAX]; assoc vec(512); Awhile (ci»T> i>buf) veC[bufj + + ; vec.prin+__ а1Г(); ' } 6.8. ВЫЗОВ ФУНКЦИИ Вызов функции, то есть, запись выражение(список выражений), можно интерпретировахк» как бинарную операцию, и оператдр вызову () можно переназначить, (совместить) так же, как и другие операторы,, Список аргументов для функции opferator() вычисляется и проверяётся в соответствии с обычными правилами передачи аргументов. По-видимому, переназначение вызова функции целесообразно прежде всегр для определенных /типов с единственно возможной операций или для типов, в которых однё операция используется столь часто, что остальными в большинстве контекстов можно пренебречь. Для ассоциативного массива типа assoc мы не опредёДиди; итератор. Это можно было бы сделать, определив класс assdc iterator, который должен представлять элементы класса assoc в некотором порядке. Итератор должен иметь доступ к Данным, хранящимися в assoc; следовательно, его нужно сделать дружественным (friend). class assoc { friend class assoc _ iterator; pair" vec; ? int max; int free; public: j assoc(int); Л V/ int&operator[](char*); чг 4 } f ,, Итератор можно определить как: Class assoc^it0tbtor { f : / assoc* ts; // текущей массив assoc int i; // текущий индекс public: assoc _iterator(assoc& s) { cs = As; i 0; } 187
pair" operator ()() { return (i<cs->free)? &cs->vec[i + + ] : 0; } . } i Итератор assoc _ iterator должен быть инициализирован для массива assoc. Он будет возвращать указатель на (новую) pair из этого массива каждый раз, когда он активируется использованием оператора (). Когда итератор доходит до конца массива, он возвращает 0: main()// подсчитывает частоту встречаемости каждого слова на входе { const МАХ = 256; // больше, чем самое большое слово char buf[MAX]; assoc vec(512); while (cin >> but) vec[buf] + +; assoc _ iterator next(vec); pair" p; while ( p = next() ) cout << p->name << ” << p->val << "\n"; Итератор типа, подобного этому, имеет преимущество перед набором функций, делающих то же самое: он пользуется своими собственными частными данными для того, чтобы следить за процессом итерации. Как правило, важно и то, что одновременно можно задействовать несколько итераторов такого типа. Естественно, что такое использование объектов для представления итераторов не должно как-то особенно влиять на переопределяемый оператор. Многие любят итераторы *с такими операциями, как first(), next() и last(). 6.9. СТРОКОВЫЙ КЛАСС Ниже представлен более реалистичный строковый класс (string). Он подсчитывает обращений к строке для того, чтобы свести к минимуму копирование и использует стандартные символьные строки C++ как константы. #include <stream.h> #include <string.h> class string { struct srep { char' s; int n; }; srep 'p; // указатель на данные // счетчик обращений public: string(char '); // string x = "abc" 7 Зак. 1927 188
string(); // string x; string(string &); // string x = string ... string& operator = (char "); string& operator = (strimg &); - string(); char& operator[](int i); friend ostream& operator < <(ostream&, string&); friend istream& operator> >(istream&, string&); friend int operator = = (string &x, char “s) J return strcmp(x.p->s, s) = = 0; } friend int operator = = (string &x, string &y); { return strcmp(x.p->s, y.p->s) = = "0; } friend in^operator! = (string &x, char ’s) { return strcmp(x.p->s, s) ! = 0; } friend int operator! = (string &x, string &y); { return strcmp(x.p->s, y.p->s) ! = 0; } Конструкторы и деструктор, как обычно, тривиальны: string: :string() р = newsrep; p->s = 0; p->ri = 1; string::string(char“ s) p = new srep; p->s = new char[strlen(s) + 1 ]; strcpy(p— > s, s); p->n = 1; } string::string(string& x) x.p->n + + ; P = x.p; } string:: ~ string() if (—p->n = = 0 { delete p->s; delete p; 189
Как обычно, операторы присваивания очень похожи на конструкторы. Они дружны обеспечивать стирание своего первого (левого) операнда: string& string::operator = (char" s) if(p->n > 1) { // отключает сам себя p->n—; p = new srep; else if (p->n = = 1) delete p->s; p->s = new char[ strlen(s) +1 ]; strcpy(p-> s, s); p->n = 1; return “this; Разумно обеспечить правильную работу программы для случая присваивания значения объекта самому объекту. * string& string::operator = (string& x) x.p- >n+ + ; if (—p—> n = = 0) { delete p->s; delete p; P = x.p; return “this; } Предназначение оператора вывода . - продемонстрировать использование счетчика обращений. Он выводит Каждую входную строку, используя оператор <<, определенный ниже: ostream& operator < <(ostream£ s, string& x) return s << x.p->s << " [" <<x.p->n << "]\n”; Bo входной операции используется стандартная функция ввода символьной строки (8.4.1): istream& operator> >(istream& s, string& x) char buf[256J; s > > buf; 7% 190
Xcout << "echo: " << x << "W; return s; I Оператор символам. void { доступа к отдельным Индекс проверяется. error(char‘ p) cerr << P << "Xn"; exit(1); } char& s+ring.:operator[](int i) ( if (i<0 II strlen(p->s<0 error ("индекс вне границ интервала"); return p->s[i]; ) Основная программа просто "вод^их ^"символвнь^строки, a ж ж S.S£"rX №■» "»₽■»«• - p,6“’v main() { string х[100]; int n; cout for(n < < "НачалиХп"; = 0; cin> >x[nj; n + + 1 ?(n9=yJ 100) еггогС-слишком много строк"); cout < < (у = *[nty if (у = = "done") break; << "теперь пойдем o^pariwXn"; for (inf i = n-1;0<=i; «-) cout << x[i], ФУНКЦИИ И ФУНКЦИИ-ЭЛЕМЕНТЫ Нак»».ц — ‘ ?р:хв' 1‘" j”! - — } 6.10. ДРУЖЕСТВЕННЫЕ 191
выбора. * w v Рассмотрим простои класс л: class X { 7/ ... X(int); int m(); friend int f(X&); } .... На первый взгляд нет никаких причин для того, чтобы при^проведении „,р„*й еРо6»к™» класс. X W созданного неявным преобразованием типа. Например. void g() 1 .m(); ЦП; { // ошибка // f(X(1)); } Таким образом, операция, изменяющая состояние объекта класса. Таким образом, операция, изменяющий ~е - б |уа)ие_ должна быть элементом, а не другом. , ' + + и т.д.), для типов, операнда для фундаментальных типов ( , определить как определенных пользователем наиб°лее есДции желательно иметь элементы. Напротив, если «XXия ^ализующаРя его, должна быть Неявное пР®?б₽®зовеаНэлёме^ом Часто именно так обстоит дело для дружественной, а не элементо . ТРебующие lvalue-операндов при функций, реализующих операторы, не треоующ применении к фундаментальным типа (, , яются то по-видимому, нет Р Если преобразования типов не определяются,^в^ной функции, явных причин для пРеА"°чт®£“ ения по адресу (или наоборот). В использующей аргумент для обр щ предпочесть один синтаксис некоторых случаях программист мож Р по-Видимому предпочтет для вызова^ другому. Например инверсии матрицы запись inv(m) ал Р е_ саму матрицу, а не просто если функция inv() действительно Р РУ жна быть, элементом, возвращает новую матрицу, обратну , элемент Не всегда возможно Н При прочих равных условиях, В^ИР^?Л дькогдХ^ибудь оператор заранее определить, не определит не потребуют ли будущие преобразования. Не всегда можно пред » Синтаксис вызова модификации программы X- что функции-элемента совершенно опреде _ обпашения гораздо . менее объект можно модифицировать; Р' У могут быть заметно короче, „ ТОГО, выражения е> ал^е-еуХГтУвТенная Функция должна то время, как элемент может неявно элементов обычно короче, чем и/*ена только не используется переопределение. аргумент обращения очевиден. Более того, выражения чем эквивалентные выражения в использовать явный аргумент, использовать this. Имена Дружественных функций, если 192
6.11. ПРЕДУПРЕЖДЕНИЕ Как и большинством особенностей языка программирования оператором переопределения можно злоупотребить. В частности, можно использовать возможность определения новых значений для старых операторов так, что написанная программа будет абсолютно непонятна. Например, представьте себе проблемы, с которыми сталкивается читающий программу, в которой оператор + переопределен так, что он обозначает вычитание. Представленный механизм должен защитить программиста и читателя от эксцессов, возникающих при переопределении. Программист не может изменить значения операторов для базовых типов данных. Сохраняется синтаксис выражений и старшинство операторов. Пожалуй, будет V разумно использовать переопределение операторов прежде всего для имитации общепринятой формы использования операторов. Если такой общепринятой формы нет, или если набор операторов, разрешенных для переопределения в С + + не подходит для имитации общепринятой формы использования операторов, можно использовать нотацию вызова функции. 6.12. УПРАЖНЕНИЯ 1. (“2) Определите итератор для класса string. Определит^ оператор соединения + и оператор 4- = "присоединить к концу". Кйкие еще операции Вы хотели бы использовать с классом string? \ 2. (*1.5) Напишите через переопределение субстроковый оператор для строкового класса. 3. ("3) Сконструируйте класс string так, чтобы в левой части; оператора присваивания можно было бы использовать субстроковый- Оператор. Вначале напишите версию, в которой можно присвоить строку субстроке той же длины, а затем версию, в которой длины строк могут различаться. 4. ("2) Сконструируйте класс string так, чтобы чтобы он имел семантику значений для присваивания, передачи аргументов и т.д.; то есть, класс, в котором копируется представление строки, а не просто управляющая структура данных в классе string. 5. ("3) Модифицируйте класс string из предыдущего примера таким образом, чтобы строки копировались только тогда, когДа это необходимо. Это значит, что совместное использование двух строк должно сохраняться до тех пор, пока одна из строк не будет изменена. Не пытайтесь создать субстроковый оператор, который в то же время можно использовать в левой части выражения. 6. (’4) Сконструируйте класс string с семантикой значений, задерживаемым копированием и субстроковым оператором, который можно использовать в левой части выражения. 7. ("2) Какие преобразования используются в каждом выражении программы, приведенной ниже? struct X { int i; X(int); > operator + (int); 193
}; struct Y { int i; Y(X); operator + (X); operator int(); X operator" (X,Y); x; 2; + 10; + 10; + Ю " y; + у + i; ' x + i; t(7); f(y); y+ y; Определите X и Y как целые типы. Модифицируйте программу так, чтобы она запускалась и печатала при этом Значения каждого разрешенного выражения. 8. ("2) Определите класс INT, являющийся полным аналогом int. Подсказка: определите INT::operator int(). 9. ("1) Определите класс RINT, являющийся аналогом int, но для которого разрешены только операции + (унарная и бинарная), - (унарная и бинарная), ", / и %. Подсказка: не определяйте RlNT::operator int(). 10. ("3) Определите класс, LINT, являющийся аналогом RINT за исключением того, что точность вычислений его составляет, по крайней мере, 64 бита. 11. ("4) Определите класс, реализующий арифметику произвольной точности. Подсказка: Вам придется оперировать свободной памятью так же, как Вы делали это для класса string. 12. ("2) Напишите программу, которая оказалась нечитаемой • из-за использования переназначенных операторов и макросов. Идея: определите + . для обозначения - и наоборот для чисел типа INT; затем используйте макрокоманду для определения int как INT. Переопределение популярных функций, использование аргументов типа "обращение" и несколько комментариев, вводящих в заблуждение, также могут хорошо запутать читателя. 13. ("3) Обменяйтесь с приятелем результатами предыдущего упражнения. 194
Попытайтесь понять, что делает программа Вашего приятеля (не запуская ее). Когда Вы закончите это упражнение, Вы поймете, чего следует избегать при написании программ. 14. (’2) Перепишите пример complex ($6.3.1), пример tiny ($6.3.2) и пример string ($6.9) так, чтобы в них не использовались функции friend. Используйте только функции-элементы. Проверьте каждую из новых версий. Сравните их с версиями, использующими функции friend. Еще раз просмотрите упражнение 5.3. 15. (“2) Определите тип vec4 как вектор из четырех элементов-типа float. Определите operator[] для vec4. Определите операторы +, ’, /( = , +=,-=,’=,/= для комбинаций векторов и чисел с плавающей точкой. 16. (’3) Определите tfnacc mat4 как вектор из четырех vec4. Определите для mat4 operator[], возвращающий vec4. Определите для этого типа обычные матричные операции. Определите функцию, которая производит с mat4 Гауссово исключение. 17. ("2) Определите клас£ vector, аналогичный vec4, размер которого передается как аргумент конструктору vector: :vector(int). 18. (“3) Определите класс matrix, аналогичный mat4, размеры которого передаются как аргументы конструктору matrix::matrix(int,int).\
И.В.К." 105023 Москва, Мал. Семеновская, д. 5 Тел.: 936-50-67, 311-52-08 Факс: 203-93-55 ГЛАВА 7 ПРОИЗВОДНЫЕ КЛАССЫ В этой главе описана концепция производных классов языка С + +. Производные классы предоставляют простой, гибкий и эффективный механизм для описания альтернативных интерфейсов класса и для определения класса добавлением своих средств к существующим классам, без их перепрограммирования или перекомпиляции. Используя производные классы, можно получить также общий интерфейс для нескольких различных классов, так что можно будет работать с объектами этих классов одинаковым образом из различных частей программы. Как правило, такой интерфейс включает в себя размещение в каждом объекте информации о типе, так что такие объекты можно соответствующим образом использовать в контекстах, в которых их тип в момент компиляции неизвестен. Для того, чтобы надежно и элегантно работать с объектами, тип которых зависит от хода выполнения программы, разработана концепция виртуальных функций. В принципе, производные классы существуют для того, чтобы облегчить программисту реализацию общности. 7.1. ВВЕДЕНИЕ Предположим, что мы составляем некоторое общее устройство (например, тип цепных списков, таблицу символов, или планировщик для моделирующей системы), предназначенный для использования разными людьми в различных контекстах. Очевидно, что никакого недостатка в кандидатах для такого устройства нет, и выгода от стандартизации их огромна. Любой опытный программист, по-видимому, писал (и отлаживал) десяток вариантов наборов типов, хэш-таблиц, функций сортировки и т.д., но каждый программист (и каждая программа) имеет, по-видимому, свое представление об этих концепциях. Это делает программы такого рода трудночитаемыми, трудноотлаживаемыми и трудноизменяемыми. Более того, в большой программе вполне может оказаться несколько копий (почти) идентичных кодов для работы с такими фундаментальными Концепциями. Первопричина такого хаоса заключается частично в том, что концептуально затруднительно представить такие общецелевые устройства в языке программирования, частично в том, что достаточно общий устройства слишком накладны в плане занимаемого места и/или времени. Поэтому их неудобно применять в качестве простейших и наиболее интенсивно используемых устройств (связные списки, векторы и т.д.), хотя именно в этом качестве они были бы наиболее Полезны. Концепция производных 196
классов G + +, представленная в 7.2, не дает общего решения всех этих задач, однако она дает возможность справляться с реализацией некоторых важных частных случаев. Например, будет показано, как определить эффективный общий класс связного списка так, чтобы все версии его использовали код совместно. Написание общецелевых устройств - вещь нетривиальная, и основные акценты в конструкции часто несколько* отличаются от акцентов в программах специального назначения. Очевидно, что четкой разделительной линии между общецелевыми и специальными программами провести нельзя, и можно считать, что методы и средства языка, представленные в настоящей главе, будут тем полезнее, чем больше объем и сложность составляемых программ. 7.2. ПРОИЗВОДНЫЕ КЛАССЫ Для того, чтобы разделить задачи понимания механизмов языка и методов их использования, концепция производных классов представляется в три этапа. Прежде всего, на небольших примерах (которые не претендуют на то, чтобы быть реально действующими программами) будут отписаны собственно особенности языка (нотация и семантика). После этого будут продемонстрированы некоторые нетривиальные примеры использования производных классов, и, наконец, будет представлена полная программа. *' •• $ 7.2.1. Образование производных классов * Рассмотрим построение программы, которая ведет учет служащих компании. Такая программа может включать в себя следующую структуру данных: struct employee { сЬаг’Жате; short age; short department; int salary; employee" next; Поле next будет использоваться в Теперь попробуем определить struct manager { employee emp; employee" group; // ... класс // // }; качестве связки в списке служащих, manager Хуправляющий): запись управляющего-служащего те, кем руководит управляющий Управляющий - это тоже служащий; данные employee хранятся в элементе emp объекта manager. Для читателя-человека это очевидно, но для компилятора в элементе emp нет ничего особенного. Указатель на управляющего (manager*) не является указателем на служащего (employee"). 197
тэк что просто использовать один из них там, где требуется другой,- нельзя В частности, нельзя поместить управляющего в список служащих, не написав специального кода. Можно либо явно использовать преобразование типа к manager*, либо поместить в список служащих адрес элемента етр; оДнако, и тот, и \ другой способ неэлегантны и могут оказаться весьма невразумительными. Правильный подход - установить, что управляющий действительно является служащим и добавить некоторую информацию: struct manager : employee { employee" group; }; Класс manager является производным от employee и, напротив, employee является базовым классом для manager. Класс, manager, в добавление к элементу group, содержит элементы класса employee (name, age и т.д.). * Используя такое определение классов employee и managar, мы можем теперь составить список служащих, некоторые из которых являются управляющими. Например: void f() * manager ml, m2; employee el, e2; employee" elist; elist = Am1; // размещает ml, el, m2 и e2 в elist ml.next = &e1; el.next = &m2; m2.next = &e2; e2.next = 0; } •4» Поскольку .управляющий использовать как employee*, управляющий, так что employee* это объясняется в 7.2.4. является служащим, manager" можно Однако, служащий - это необязательно нельзя использовать как manager*. Детально 7.2.2. Функции-элементы Простые структуры данных, такие, как employee и manager, на самом деле, неинтересны и используются не особенно широко. Поэтому посмотрим, как к ним добавить функции. Например: class employee { char" name; // ... public: employee* next; void print(); // ... 198
class manager : public employee { И ... public: void рппЦ); // ... Нужно“г *г™„ии вйХо водного класса manager класса employee могут использовать employee? Какие элементы базового. а ?Р Какие элементы базового функции-элементы "Р0"3^"0™ являющаяся элементом, использовать субъектом 'типа та^еЛаким' образом программист может использовать СотЙты“на эти вопросы в прикладной программе? Рассмотрим следующее: void { manager: :print() cout << " имя " << name << "\n”; // ... <[ } Элемент базового объекта. Предполагается^ не"будет скомпилирована; ' . _ ж ж 14 *ЧП А него недоступно. Для многих элементам своего' базгжого класса. . элемента становится бессмысленной, возможность доступа к i созданием производного класса, лйнатгжить все случаи использования просмотром функций, объявленных как ; поисках производных классов, — - классах, затем найти каждь^^"а^ механизм 'friend^ описанный в 5.3. Например: eCTbl 663 °ПИСа--- i, что на этот использовать общее (Public) ижля своего то есть, без описания , объект"у*азь1вает this, так что name "„•ocS'k “о.™ ба!“ого и„н ато м°“<е' п®,леиент и^еегИЯДпступ’ к М°примгкь1» *Sa В “том случае “ХТ.л-"^”^ npocMoipvm vfz7.™... - иашпый исходный файл всей программ классу. Придется пРос“а^”®®ТЬ проверить кХдую функцию в эти, поисках производных классов, з ПОоИЗВОдный от этих классов и т.д. Е классах, затем наити каждый кла , Р о нерезультативно, лучшем случае, это утомительно, и, как ^ра^ло, доступа либо отдельны* другой стороны, для разре |/Пасса можно использоват! ; либо любой функции отдельного класса, можно ' . . . х _ С 7 Ндппимео. class employee { friend void manager: :рппЦ); // ... }; решает задачу для manager: :print(), а 199
class Employee { friend Vlass manager; делает любой элемент класса employee доступным для любой функции класса manager. В частности, name становится доступным для manager: :print(). Альтернативное и иногда более понятное решение заключается в использовании |в производном классе только общих элементов его базового класса. Например: * void manager::prinf() employee: :print(); // распечатка информации из employee // ... // распечатка информации из manager Обратите внимание на то, что нужно использовать ::, поскольку функция print() была переопределена в manager. Такое повторное использование имен является типичным. Можно было бы неосторожно написать: void manager::print() ^rint(); } // распечатка информации из employee // распечатка информации из manager И обнаружить, что программа:, зацикливается на неожиданной последовательности рекурсивных вызовов при вызове manager::print(). 7.2.3. Видимость Класс employee был сделан общим базовым класс класса manager с помощью объявления class manager : public employee { Это означает, что общий элемент в классе employee также является общим элементом в классе manager. Например: void clear(manager" р) { } p~>next = 0; будет компилироваться, поскольку next является общим элементом как в employee, так и в manager. С другой стороны, можно объявить частный базовый класс, просто опустив слово public в объявлении класса: 200
class manager : employee { // ... k I Это означает, что общий элемент в классе employee является приватным элементом в классе manager. То есть, функции-элементы класса manager, как и раньше, могут использовать общие элементы из класса employee, но такие элементы недоступны для объектов, использующих класс manager. В частности, при использовании приведенного объявления manager функция clear() компилироваться не будет. Дружественные функции производного класса имеют такой же доступ к элементам базового класса, что и функции-элементы. ’ Поскольку базовые классы объявляются общими чаще, чем приватными, жаль, что объявление общего базового класса длиннее, ^ем объявление приватного. Кроме того, это является источником ошибок, смущающих новичка. Если объявляется производная структура (struct), то ее базовый класс по умолчанию имеет тип public (общий). То есть, struct D : В { ... s означает class D : public В { public: ... Это значит, что если Вам не нужно "скрывать” данные (такую возможность дает использование типов class, public и friend), Вьг;можете просто не использовать эти ключевые слова и придерживаться типа struct. Такие средства языка, как функции-элементы, конструкторы и переопределение оператора не зависят от механизма скрывания данных. Можно также объявить общими элементами производного класса лишь некоторые, а не все общие элементы базового класса. Например: class manager : employee { public: П ... employee: :name; employee: department; }; Ъ Нотация имя класса :: имя элемента ; не вводит новый элемент, но просто делает общий элемент базового класса общим для производного класса. Теперь, name и department можно использовать для manager, a salary и аде - нет. Естественно, что сделать приватный элемент базового кдасса общим элементом производного класса нельзя. Нельзя также, используя такую нотацию, сделать общими переопределенные имена. 201
В заключение можно отметить, что, дополняя хвозможности, заложенные! в базовый класс, производный класс может дааать свои средства (имена) недоступными для использования. Иными^ словами, производный класс может обеспечивать полный или почти полный доступ к своему базовому классу, или вообще запретить доступ. 7.2.4. Указатели Если кл^сс derived имеет общий базовый класс base, то значение указателя на derived можно присвоить переменной типа "указатель" без использования явного преобразования типа. Обратное преобразование, от указателя на base к указателю на base, должно быть явным. Например: class base { /* ... */ }; class derived : public base { /’ ... */ }; derived m; base* pb = &m; // неявное преобразование derived" pt = pb; // ошибка: base" не является derived" ' pd = (derived’)pb; // явное преобразование Иными словами, объект производного класса можно обрабатывать как объект его базового класса с использованием указателей. Обратное неверно. Если бы base был приватным базовым классом для derived, то неявное преобразование derived" к base* не производилось бы. Неявное преобразование в данном случае не производится потому, что к общему элементу класса base имеется доступ через указатель на base, но не через указатель на derived: class base { int ml; public: int m2; // m2 - общий элемент класса base class derived : base { // m2 HE является общим элементом для derived }; derived d; d.m2, = 2; // ошибка: m2 принадлежит приватному базовому классу base" pb * = &d; // ошибка (приватный класс base) pb->m2 = 2; // все в порядке pb = (base")£d; // все в порядке: явное преобразование. pb->m2 = 2; // все в порядке Помимо всег0 прочего, в этом примере показано, что, используя явное преобразование типа, Вы можете нарушать правила защиты. Понятно, что делать Это не рекомендуется, и обычно программист получает за это ’’награду". К сожалению, безнаказанное использотание явного преобра¬ зования типа устраивает адскую жизнь невинным жертвам, обслуживающим программы, в которых оно содержится. К счастью, преобразования, 202
позволяющего использовать приватное имя ml, не существуе^. Приватный элемент класса могут использовать только элементы и дружественные этомъ классу функции. | J.2.5. Иерархии классов / Производный класс сам может быть базовым классом/ Например: class employee { ... }; i class secretary : employee J • class manager : employee { ... }; class temporary : employee { ... }; class consultant : temporary { ... }; class director :4,,manager { ... }; class vice _ president : manager { ... }; #- class president : vice-president { ... }; Такой набор связанных классов традиционно называется иерархией классов. Поскольку произвести класс можно только из одного базового класса, такая иерархия является деревом и не может иметь более общую структуру графа. Например: ? class temporary { ... }; class employee { ... }; class secretary : employee { ... }; // не в языке С + +: class temporary _secretary : temporary : secretary { ... }; 7 9 class consultant : temporary : employee {...}; Это обидно, поскольку направленный ациклический граф производных классов мог бы быть весьма полезен. Такие структуры объявлять нельзя; их нужно моделировать, используя элементы соответствующих типов. Например: class temporary { ... }; class employee { ... }; class secretary : employee { ... }; I/ Альтернатива: class temporary - secretary : secretary { temporary temp; ... }; class consultant : employee {temporary temp; ... }; Это не элегантно и страдает именно теми недостатками, для преодоления которых были придуманы производные классы. Например! поскольку consultant не является производным от temporary, consultant не может быть помещен в список служащих temporary; для этого нужно написать специальный код. Однако, такой подход успешно применялся во многих полезных программах. 203
7.2J6 Конструкторы и деструкторы Дпя\ некоторых производных классов требуются конструкторы. Если базовый курсе имеет конструктор, то этот конструктор должен вызываться. Если конструктор базового класса использует аргументы, то эти аргументы должны предоставляться. Например: class base { // ... public: base(char" n, short t); r base(); } i class derived : public base { base m; public: derived(char” n); • derived(); Аргументы для конструктора базового класса описаны в определении конструктора производного класса. В этом отношении базовый класс выступает в той же роли, что безымянный элемент производного класса (см. $5.5.4). Например: derived: :derived(char? n) : (nJO), mf'member", 123) / ... } Объекты класса создаются снизу вверх: вначале основание, затем элементы и, наконец, собственно производный класс. Уничтожаются они в Обратном порядке: вначале сам производный класс, затем элементы и основание. 7.2,7. Поля типов Для того, чтобы использовать производные классы не только для сокращения длины определения, нужно решить следующую задачу. Если имеется указатель типа base", то к какому производному типу в действительности принадлежит объект, на который он указывает? Есть три фундаментальных решения этой проблемы. [1] Гарантировать, что указатель может указывать только, на объекты одного типа (7.3.3). [21 Разместить в базовом классе поле типа для проверяющих функций. [3] Использовать виртуальные функции (7.2.8). Указатели на базовые классы используются обычно для конструи¬ рования классов-контейнеров, таких, как наборы, векторы и списки. В таком 204
случае, вариант 1 приводит к созданию однородных списков,/ то есть списков объектов одного типа. Варианты 2 и 3 можно использовать дл построения неоднородных списков, то есть, списков объектов (ил указателей) разных типов. Вариант 3 - это специальная версия варианта ; гарантирующая сохранение типа. I Вначале рассмотрим простой вариант с полем типа, то есть, вариан 2. Пример управляющий/служащий можно переписать так: ' enum empl__type { М, Е }; struct employee { empl_type type; employee’ next; char’ name; short department; // ... ■I; struct manager : employee { employee" group; short level; ? // ... J; Используя эти объявления, мы можем теперь написать функцию, Котора распечатывает информацию о каждом служащем: void print_ employee(employее" е) { * switch e->type { case Е: cout << е->name << "\t" <<e->department << "\n"; // ... break; case M: cout << e->name << "\t" << e->department << "\n'V // ... manager’ p = (manager’)e; cout << " level " < < p-> level << "\n”; // ... break; и использовать ее для распечатки списка управляющих таким образом: void f(employee’ II) for (; II; II = ll->next) print_ employee(ll); 205
Эта( программа работает вполне успешно, особенно в небольших программах, написанных одним человеком. Однако в ней имеется принципиальный недостаток - ее действие зависит от типов, с которыми работает программист, причем эта зависимость не может быть проверена компилятором. Как правило, это приводит к двум видам ошибок в больших программах^. Во-первых - это сбои при проверке поля типа. Во-вторых - это невозможность разместить в операторе switch все возможные варианты case, как это сделано в предыдущем примере. Этих ошибок достаточно легко избежать при написании программы, однако они практически неизбежны при модификации нетривиальной программы, особенно, большой программы, написанной другими. Ситуация усугубляется тем, что такие функции, как print() часто организуются так, чтобы унифицировать включаемые в них классы. Например: void print _ employeefemployee" е) cout << e->name << "\t" << e-> department << "\n"; // ... if (e->type = = m) { manager" p = (manager’)e; cout << " level " << p->level << "\ri"; // ... } Поиск всех подобных выражений if, спрятанных в большой функции, обслуживающей много производных классов, может оказаться весьма затруднительным. Даже если удастся найти все такие выражения, разобраться в программе будет сложно. 7.2.8. Виртуальные функции Использование виртуальных функций позволяет решить проблемы, связанные с использованием поля типа - программист может объявить функцию в базовом классе так, что она будет переопределяться в каждом производном классе. Компилятор и загрузчик гарантируют правильное соответствие объектов и функций, которые применяются к объектам. Например: struct employee { employee" next; char’ name; short department; // ... virtual void print(); Ключевое слово virtual означает, что функция print() может иметь различные версии в различных производных классах и что найти соответствующую версию для каждого вызова функции print() - это задача компилятора. Тип функции объявляется в базовом классе и не может быть 206
переобъявлен в производном классе. Виртуальная функция должна быть определена для класса, в котором она впервые объявлена. Например: void employee::print() cout < < e->name << "\t" << e-> department << "\n"; // ... } Таким образом, виртуальную функцию можно использовать даже в том случае, если от ее класса не создано ни одного производного класса; в производном классе, для которого не нужна специальная версия виртуальной функции, ее можно не упоминать. При создании производного класса, в котором нужно использовать виртуальную функцию, достаточно просто предусмотреть ее. Например: struct manager : employee { employee" group; short level; // ... void print(); voud manager::print() employee: :print(); > cout < < " \tlevel " < < level < < "\n"; ; // ... } Функция print —employeeQ теперь не нужна, поскольку функции-элементы prirtt() заняли свои места и список служащих можно напечатать таким образом: void f(employee“ II) c_r for(; II; ll = ll->next) II->print(); Информация о каждом служащем будет распечатана в соответствии с его "типом". Например: main() < employee е; e.name = "J.Brown"; e.department = 1234; e.next = 0; manager m; m.name = "J.Smith"; m.department = 1234; 207
m. level = 2; m.next = &e; f(£m); } даст на выходе J.Smith 1234 level 2 J.Brown 1234 Обратите снимание на то, что эта программа будет работать даже в том случае, если функция f() была написана и скомпилирована до того, как указанный производный класс manager был вообще задуман! Ясно, что при реализации такого подхода в каждом объекте класса employee должна храниться некоторая информация о типах. Занимаемое пространство (в данной реализации) достаточно велико для того, чтобы содержать указатель. Дополнительное адресное пространство занимается только в объектах класса с виртуальной функцией, а не во всех объектах класса и даже не в каждом объекте производного класса. Вы расплачиваетесь только за классы, для которых Вы объявили виртуальные функции. При вызове функции с использованием оператора разрешения :: (как это было сделано в manager::print()), виртуальный механизм гарантированно не используется. В противном случае, вызов manager: :print() позволял бы бесконечную рекурсию. Применение имени ограниченного использования имеет и другой положительный эффект: если функция virtual является также и inline (что встречается достаточно часто), то там, где в вызове используется ::, можно использовать замещение телом функции. Это предоставляет программисту эффективный способ для реализации некоторых важных особых задач, в которых для одного и того же объекта одна виртуальная функция вызывает другую. Поскольку тип объекта определяется в вызове первой виртуальной функции, часто нет необходимости в динамическом определении типа при втором вызове для того же объекта. 7.3. АЛЬТЕРНАТИВНЫЕ ИНТЕРФЕЙСЫ После представления средств языка, имеющих отношение к производным классам, вернемся к проблемам, ради решения которых они созданы. Фундаментальная идея классов, описанных в настоящем разделе, заключается в том, что классы пишутся один раз и используются позже программистами, которые не могут изменить их определение. Заголовочный файл будет размещен где-то в другом месте; программист может использовать его копию с помощью директивы #include. Файлы, описывающие определение, обычно компилируются, а затем размещаются в библиотеке. 7.3.1. Интерфейс Рассмотрим составление класса slist для отдельных цепных списков. Класс должен быть составлен так, чтобы его можно было использовать как 208
ent get(); void clear(); slistQ slist(ent a) • slistQ основание для получения как неоднородных, так и однородных списков объектов, типы которых будут определены позже. Прежде всего, определим тип ent: typedef void" ent; Истинная природа типа ent не имеет значения, но он должен допускать существование указателя на него. Теперь определим тип slink: class slink { friend class slist; friend class slist _ iterator; slink" next; ent e; slink(ent a, slink" p) { e = a; next=p; } Звено может содержать единственный ent; оно используется для реализации класса slist: class slist { friend class slist _ iterator; ) slink" last; // last->next - начало списка public: int insert(ent a); // добавление к началу списка int append(ent а);// добавление к концу списка // возврат и удаление начала списка // удаление всех звеньев г last = 0; } last = new slink(a,0); last-> next = last; } dear(); } fl Хотя ясно, что список реализуется как цепной список, реализацию можно изменить так, чтобы использовать вектор из ent без изменений кода пользователем. Это значит, что способ использования slinks не рпизан в объявлениях общих функций slist; он определяется лишь в частных фрагментах и определениях функций. 7.3.2. Реализация Реализация функций sljst, в принципе, проста. Единственно существенная проблема - это что делать в случае ошибок, например, что делать, если пользователь пытается удалить что-нибудь из пустого списка. Этот вопрос будет обсужден в 7.3.4. В этом разделе приведены определения элементов slist. Обратите внимание на то, как запоминание указателя на последний элемент циклического списка позволяет просто реализовать как операцию append(), так и операцию insert(): int slist: :insert(&nt a) 209
if (last) lash->next = new slink(a,last-> next); else { last = new slink(a,O); last->,next = last; , i. * return 0; * } int slist::append(ent. a) it (last) last = last->next = new slink(a,last^> next); else { last = new slink(a,0):; lasf-> next = last; r } return 0; } '■ " ... ent slist: :get() < J if(last = = 0) slist_ ЬапсЛег("удаление из пустого slist"); slink’ f = Jast->next; J ent r = f->e; if (f = = last) last = 0; < v else ■ ■ ц ■ :'/y. Iast->next = f->next; ? delete f; » ' ' ' i , return r; ' V ti } < - Обратите внимание на то, как вызывается slist_ hanpier (описание приведено в 7.3.4). Этот указатель на имя функции используется точно так же, как если бы это было имя функции. Это.' сокращенная нотация более явного вызова функции: (“slist_ Ьапд1ег)("удаление из пустого списк.а")^ Наконец, s1ist::clear() удаляет из списка все элементы: void, slist::clear() { slink’ I - last; v 1 if(| = = 0) return; ’ do { slink’ II = I; I = I-> next; delete II;. } while (I! = last); 210
Класс slist не предоставляет средств для "заглядывания" внутрь списка, только средства для включения новых элементов и исключения Старых. Однако, как класс slist, так и класс slink объявляют класс slist _ iterator как friend (дружественный), так что мы можем объявить подходящий итератор. Вот этот итератор, написанный в стиле, представленном в 6.8: class slist—iterator { slink" се; slist’ cs; public: slist - iteratgr(slist& s) { cs =t &s; ce = cs->Jast; } ent operator()() { // для обозначения конца итерации возвращает О // не универсален по отношению ко всем типам // особенно хорош для указателей ent ret = се ? се = се-> next_->e : 0; if (Се == cs- .last) се = 0; '1 return ret; 7.3.3. Как его использовать Класс slist фактически бесполезен в том виде, в каком он^есть. В конце концов, для чего можно использовать список указателей тйЙё void*? Хитрость заключается в том^ чтобц| произвести от класса slist класс, получив тем самым список объектов типа, необходимого в конкретной программе. Рассмотрим компилятор для такого языка, как С + +. В этом разделе будут широко использоваться списки имен (name); name может выглядеть следующим образом: struct name { char" string; // .;. В списках будут размещаться указатели на имена объектов, а не сами поименованные объекты. Этр дает возможность использовать небольшое информационное поле е из списка slist; кроме того, при таком подходе одно и то же имя может одновременно размещаться в разных списках. Ниже дано определение класса nlist, тривиально произведенного от класса slist: ' ■ ' #include "slist.h" #include "name.h" struct nlist- : Slist. { void insertfname" a) { slist: :insert(a); } 211
void append(name" a) { slist: :append(a); } name" get() { return (name")slist::get(); } nlist() { } nlist(name" a) : (a) { } Функции нового класса либо непосредственно производятся из slist, либо не делают ничего, кроме преобразования типа. Класс nlist - это не что иное, как альтернативный интерфейс для класса slist. Поскольку на самом деле ent имеет тип void", явное преобразование указателей name", использующих фактические аргументы, не требуется (2.3.4). Списки, имен можно использовать в классе, который представляет собой определение класса, следующим образом: struct classdef { nlist friends; nlist constructors; nlist destructors; nlist members; nlist operators; nlist virtuals; // ... void add _ name(name'); classdefQ * classdefQ; }; К этим спискам можно добавлять имена. Это делается так: void classdef::add_name(name" n) if (n-> is __ friendQ) { if (find(&friends,n)) еггог("повторное объявление friend"); else if (find(&members,n)) error("friend переобъявлен как элемент"); else friends.append(n); if(n— > is _ operatorQ) operators.append(n); // ... } Здесь is _ operatorQ и is __ friendQ - это функции элементы класса name. Функцию findQ можно написать так: int find(nlist" Н, name" n) { slist __ iteratorff(“(slist’)ll); ent p; While ( p = ff() ) if (p = = n) return 1; 212
return 0; Для того, чтобы применить slist _ iterator для nlist, здесь использованс явное преобразование типа. Лучшее решение - создать итератор для клэссое nlist - приведено в 7.3.5. Можно распечатать nlist с помощью такой функции: void print _ list(nlist" II, char" list.;name) slist — iterator count("(slist")II); name" p; int n = Of . while ( count() ) n++; cout << list _ name < < "\n" << n << " элементы\п"; slist _ iterator print("(srist')ll); while ( p = (name’)printO ) cout < < p->. string <<"\n"; 7.3.4. Обработка ошибок Есть четыре ответа на вопрос: "Что делать, если при использовании общецелевого средства (такого, как slist) возникает ошибка в ходе выполнения программы?" (в языке C++ отсутствуют средства языка, специально предназначенные для обработки ошибок, возникающих в ходе выполнения программы/: ' [1] Возвратить запрещенную величину и предоставить пользователю возможность проверить ее. [2] Возвратить дополнительное значение статусу и дать пользователю возможность проверить его. [3] Вызвать функцию обработки ошибки, предусмотренную в части классе slist. [4] Вызвать функцию/ обработки ошибки, которую, как предполагается, напишет пользователь. Для небольших программ, написанных самим пользователем, нет особых причин предпочтения какого-либо варианта. ОднакЬ, для общецелевых средств ситуация совершенно иная. Первый вариант возврах' запрещенной величины - невыполним. Общего способа определить, будет ли данная величина запрещена во всех вариантах использования slist, не существует. Второй подход - возврат значения статуса - в некоторых случаях можно использовать (разновидность такой схемы используется для стандартных потоков ввода/вывода istream и ostream - см. $8.4.2). Однако, он страдает серьезным недостатком: если только ошибки не возникают часто, у пользователя редко возникает потребность проверить статус. Более того, средство может быть использовано в сотнях и тысячах мест программы. Проверка статуса в каждом месте делает программу крайне трудночитаемой.. В третьем подходе - предоставлении, функции обработки ошибок - 213
отсутствует гибкость. Реализатор средства общего назначения не знает, как пользователь хотел бы обрабатывать ошибки. Например, пользователь может пожелать, чтобы сообщения об ошибках выдавались на датском или венгерском языке. Четвертый подход - предоставление пользователю возможность самому написать функцию обработки ошибок - в некоторой степени привлекателен, при условии, что реализатор представит класс как библиотеку ($4.5), в которой будут содержаться применяемые по умолчанию версии функций, обрабатывающих ошибки. Варианты 3 и 4 можно сделать более гибкими (и, в принципе, эквивалентными), описав указатель на функцию, а не саму функцию. Это дает возможность разработчику средства (такого, как slist) предоставить функцию обработки ошибок, действующую^ по умолчанию; гфи этом программист, использующий списки, может (если .это понадобится) легко написать собственную функцию. Наример: Ч ■ typeclef void ("PFC)(char“); // указатель на тип функции extern rPFC slist _ handler; extern PFC set_ slist —handler (PFC); Функция set — slist — handlerQ дает пользователю возможность заменить функцию, вызываемую по умолчанию. Традиционная реализация предостав¬ ляет функцию обработки ошибок, вызываемую по умолчанию; эта функция вначале записывает сообщение в сегг, а затем терминирует программу, используя extt(): #include "slist.h" #include < stream.h> void default —error(char“ s) cerr << s << "\n"; j exit(1); Она также объявляет указатель на функцию обработки Ошибок и, для удобств^ записи, функцию для его установки: А PFC slist —handler = default-error; ’ PFC set-slist-hand1er(PFC handler) { PFC rr = slist _ handler; slist _ handler = handler; ; return rr; ' ’ } .. > - Обратите внимание на то, что sef_ slist _ handler() возвращает предыдущий slist _ handler. Это удобно для пользователя, поскольку дает ему возможность устанавливать и сбрасывать манипуляторы по принципу стека. Это особенно удобно в больших программах, в которых slist может быть использован в нескольких Контекстах, каждый из которых может 214
предоставлять свои собственные программы обработки ошибок. Например: PFC old = set _ slist-handler(my_ handler); г. • // код, в котором , в случае ошибок в slist // будет использоваться my - handler set _ slist _ handler(old); // сброс } Чтобь| реализовать еще более тонкий контроль, slist—«handler мог бы быть элементом кддсса slist; это позволило бы использовать разные манипуляторы ошибками в разных хписках. 7.3.5. Родовые классы Очевидно, что можно определить и списки других типов (classdef", int, char", и т.д.) тем же способом, что и nlist: простым образованием производного класса от класса slist. Процесс определения таких новых типов весьма ; утомителен (и, следовательно, велик риск появления ошибок); однако, * его можно < "механизировать", используя макрокоманды. К сожалению, при использовании стандартного препроцессора С (4.7 и г11.1), эхо также весьма болезненная процедура. Однако, составленные в результате макрокоманды очень легко использовать. Вот пример.того, как можно представить родовой класс slist, названный glist, как макрокоманду. Прежде всего, для создания такой макрокоманды нужно использовать некоторые инструменты из файла <generic.h>: ' у" #include "slist.h" #ifndef GENERICH #include <generic.h> #endif Обратите внимание на то, как #ifndef используется для того», чтобы гарантировать строго однократное включение файла < generic.h> ф£одну и ту же компиляцию дважды. GENERICH определена в файле <generic.h>. После этого определяются имена для новых родовых классов. Для этого используется функция name2() - макрокоманда для соединения имен из <generic.h>: #define gskist(type) name2(type,gslist) #define gslist —iterator(type) name2(type, gslist —iterator) Наконец, можно написать классы gslist(type) и gslist _ iterator(type): #define gslistdeclare(type) \ struct gsTist(type) : slist {, \ int insert(type a) \ { return slist::insert( ent (a), ); } \ 215
int append(type a) { return slist::append( ent a) ); } type get() { return type( slist::get() ); } gslist(type) () { } gslist(type)(tvpe a) : (ent(a) ) (} * gslist(type)() ( clear(); } struct gslist _ iterator(type): :slist _ iterator { gslist _ iterator (type) : gslist(type& s ) : ( slist&)s ) {} type operator()() }; { return type( slist_ iterator::operator()() ); } \ \ \ \ \ \ \ \ \ \ \ \ \ \ Замыкающий символ \ показывает, что следующая строка - это часть определяемой макрокоманды. С использованием этой макрокоманды, список указателей на name, подобный классу nlist, описанному ранее, можно определить следующим образом: #include "name.h" typedef name’ Pname; declare(gslist, Pname); // объявляется класс gslist(Pname) gslist(Pname) nl; // объявляется gslist(Pname) Макрокоманда declare определена в файле <generic.h>. Она соединяет свои аргументы и вызывает макрокоманду с этим именем, в данном случае, gslistdeclare, определенную выше. Тип имени аргумента для declare должен быть простым именем. Использованный метод расширения макрокоманды не может применяться к такому имени типа, как name"; следовательно, здесь необходим оператор typedef. Использование "словообразования" гарантирует, что во всех случаях применения родового класса будет использоваться совместный код. Этот метод можно применять только для создания классов объектов того же размера (или меньшего), чем размер базового класса^ использованного в макрокоманде. Однако, такой метод идеален для создания списков указателей. Класс gslist использован в $7.6.2. 7.3.6. Ограниченные интерфейсы Класс slist - это класс очень общего характера. Иногда такая всеобщность не нужна или даже нежелательна. Ограниченные формы списков, такие, как стеки и очереди, используются гораздо чаще, чем сами общие списки. Вы можете создавать подобные структуры данных, не объявляя базовый класс общим. Например, можно определить очередь целых чисел так: #include "slist.h" 216
class iqueue : slist { public: // в предположении, что // sizeof(int)< = sizeof(void") }; void put(int a) int get() iqueueQ { slist: :append((vord“) a); } { return int(slist::get(); } Такое словообразование производит две логически независимых операции: концепция списка ограничивается до концепции очереди и, для того, чтобы ограничить концепцию очереди до очереди данных целого типа, iqueue, указывается тип int. Альтернативный вариант - отдельное выполнение этих операций. В "следующем примере вначале приведен список, ограниченный так, чтобы его можно было бы использовать только в качестве стека: #include "slist.h" class stack : slist { public: slist:: insert,• slist:: get; stack() {} stack(ent a) z (a) { } }; Этот список можно использовать в дальнейшем для создания типа "стек указателей на символы": #include "stack.h" class cpstack : stack { public: void push(char* a) { slist: :insert(a); } 7.4. ДОБАВЛЕНИЕ К КЛАССУ }; В предыдущих примерах производный класс ничего не добавлял к базовому классу. Для производного класса определялись лишь функции преобразования типа. Каждый производный класс просто обеспечивал альтернативный интерфейс для общего набора программ. Это важный специальный случай, однако, наиболее распространенная причина определения нового класса как производного класса, состоит в том, что пользователь хочет иметь в своем распоряжении все, что может дать базовый класс, плюс еще что-то. Для производного класса можно определить новые элементы данных и функции-элементы, дополняющие те, что произведены от его базового класса. Это является альтернативной стратегией для получения цепных 217
списков. Обратите внимание на то, что когда в slist, определенный так, как описано выше, помещается новый элемент, создается slink, содержащий два указателя. Этот процесс занимает некоторое время. Можно обойтись без одного из указателей при условии, что в любой момент объект должен находиться только в одном списке; таким образом, указатель next можно разместить в самом объекте, а не в отдельном объекте slink. Идея заключается в том, что нужно создать класс olink, который содержит только поле next, и класс olist, который может работать с указателями на эти звенья; таким образом, класс olist сможет манипулировать объектами любого класса, произведенного от olink. Буква "о”, в именах должна напоминать Вам, что в любой момент объект может находиться только в одном olist. struct olink { olink’ next; }; Класс olist очень похож на класс sllst. Различие между ними , состоит в том, что пользователь класса olist работает непосредственно с объектами класса olink: class olist { olink’ last; public: void insert(olink* p); void append(olink’ p); olink" get(); // ... }; Теперь можно произвести от класса olink класс name: г clasps name : public olink { // ... }; Теперь составление списка объектов name, который можно использовать без затрат времени и памяти на размещение, становится тривиальной задачей. Объекты, помещаемые в olist, теряют свой тип. Это значит, что компилятор знает лишь, что что они имеют тип olink Истинный тип можно восстановить, используя явное преобразование типа объектов, извлекаемых из olist. Например: void f() olist II; name nn; ll.insert(&nn); l' // тип &nn утерян, name’ pn = (name")ll.get(); // и восстановлен } Альтернативно, тип можно восстановить, создав еще один класс 218
производный от olist, и предназначенный для выполнения преобразования типа: class onlist : public olist { // ... name" get() { return (name“)olist::get(); } }; Объект name в любой момент может находиться только в одном списка. Для объектов name такое положение может оказаться неприемлемым, однако нет недостатка в классах, для которых это вполне приемлемо. Например, класс shape (фигура) использует для работы со списком всех фигур именно этот подход. Обратите внимание на то, что slist можно было бы определить как класс, производный от olist, объединив тем самым обе концепции. Однако, использование базовых и производных классов на столь микроскопическом уровне программирования может привести к крайне искаженному коду, 7.5. НЕОДНОРОДНЫЕ СПИСКИ Предыдущие списки были однородными. Это означает, что в списки помещались объекты только одного типа; для этого использовался механизм производного класса. Однако, списки необязательно должны быть однородными. Список, описанный в терминах указателей на класс, может содержать объекты любого класса, произведенного от этого класса, то есть, он может быть неоднордным. Возможно, что это важнейший и полезнейший аспект применения производных классов; это неотъемлемая часть- стиля программирования, представленного в следующем примере. Такой стилк программирования часто называют основанным на объектах или объектно- ориентированным программированием; он основан на операциях, применяемых единым образом к объектам, находящимся в неоднородных списках. Значение каждой операции зависит от действительного типа объектов в списке (тип становится известным только в ходе выполнения программы), а не просто от типа элементов списка (известного компилятору). 7.6. ПОЛНАЯ ПРОГРАММА Рассмотрим процесс создания программы, которая рисует на экране геометрические фигуры. Она естественным, образом разбивается на три части: [1] Программа управления экраном: программы низкого уровня и структуры данных, определяющих состояние экрана; она имеет дело только с точками и прямыми линиями; [2] Библиотека фигур: набор определений основных фигур, таких, как прямоугольник и круг и стандартные программы манипулирования этими фигурами; [3] Прикладная программа: набор определений, специфических для конкретных случаев и код для их использования. 219
Скорее всего, эти три части будут написаны разными людьми (в разных организациях, в разное время). Как правило, эти части пишутся в J-дМ порядке, в каком они представлены; ситуация усложняется тем, что создатели программ низшего уровня не имеют точного представления о т$м, для чего в дальнейшем будет использоваться написанный ими код.- gce это отражено в нижеследующем примере. Чтобы пример был достаточно короток, в библиотеке фигур содержится лишь несколько простых средств, а прикладная программа тривиальна. Для работы с экраном использована исключительно простая концепция, так что читатель сможет опробовать эту программу даже в случае, если он не располагает графичес¬ кими средствами. Графическую часть программы легко заменить чем-нибудь более подходящим, не затрагивая при этом код библиотеки фигур или прикладной программы. 7.6.1. Программа управления экраном Программа управления экраном написана на языке С (а не С+ + ) для того, чтобы подчеркнуть разделение уровней реализации. Это оказалось весьма утомительным занятием, поэтому мы пришли к компромиссу: используется Стиль языка С (без функций-элементов, виртуальных функций, операторов, определенных пользователем и т.д.), однако применяются конструкторы, аргументы функций определяются и проверяются соответствующим образом и т.д. Ретроспективно, программа управления экраном очень похожа на программу С, модифицированную так, чтобы использовать преимущества особенностей языка C++ без полной переделки ирфграммы. Экран представляется как двумерный массив символов, с которым работают функции put_point() и put—line(); эти функции при обращении к экрану используют структуру point: // файл screen.h const ХМАХ = 4Q, YMAX = 24; struct point { int x,y; point() { } point(int a, int b) { x = a; у = b } . —41 overload put __ point; extern void put _ point(int a, int b); inline void put _ point(point p) { put —point(p.x, p.y); } overload put — line; extern void put_ line(int, int, int, int); inline void put _line(point a, point b} { put_ line(a.x, a.y, b.x, b.y; } extern void screen _ init(); extern void screen _ refreshf); extern void screen _ clear(); #include <stream.h> 8 Зак. 1927 220
Перед первым применением функции вывода, экран должен б: инициализирован функцией screen _ а изменения в структуре дан»,г.., экрана отражаются на экране только после вызова функции screen _ refreHv) (обновление_ экрана). Читатель может видеть, что "обновлен производится просто распечаткой новой копии массива экрана лове предыдущей версии. Ниже приведены определения функций и данных ? экрана: #include. "screen.h” #include <stream.h> enum color {black white = ' ' }; char screen[XMAX][YMAX]; voidscreen _ init() e < for (int у = 0; y<YMAX; y+’+) / for (int x = 0; x<XMAX;x + +) < screen[x][y] = white; ■ } Точки записываются только в том случае, если они находятся з пределах экрана inline int on _ screen(int a, int b) e return 0<=a && a<XMAX &&0< = b && b<YMAX; . } ! * void put __ point(int a, int b) if (on _ screen(a.b)) screen[a][b] - black; Для вычерчивания прямых используется функция put_line(): void put_line(int x0, int yO, int xl, int yl) /“ Строится прямая линия от (х0, уО) до (xl, yl). Уравнение строящейся прямой - Ь(х-хО) + а(у-уО) = 0 минимизируется abs(eps), где epsj= 2“(Ь(х-хО) + а(у-уО). См. Newman and Sproull: "Principles of Interactive Computer Graphics" McGraw-Hill, New York, 1979. pp 33-44. ’/ register dx- 1; int a = xl - x0; if (a < 0) dx = -1, a = -a; register dy = 1; int b = yl - yO; 221
»j Ь V <,. и у ' — -С-, и* t-A-? а т- »п* xcrit ~ ~Ь + t'-Q .а; Р- ;;<4;;) { put ..... nohtbO- Л) И(,Ф - ii - yi) b;'c.ak; if(eps < if(eps> a } *’dt) xO + = *' a-."_b) /Г dx, eps ..b; ? d -':ps V-VO,. . a. r < и обновления ?-феМв ПГ 3 screen .... clear()' t screen ,.._ init(): L g ; s cj een „ refresh]) for (int y = VMA/ V I ; 6 <: = ; у for (int х = 0: жХМАХ; к -;■) -пев©1 ••isnpaRo ;xut.p:;a($CJ 32л[х|[у]. coiJt.pi.it('\n'|; 5 Дг.*; расгхчаАкн символов как сил-аолсг используем с я фу:-?.и;-.чя osl/eam: put(); фу у. ostream: - operator < <() распечатывает символа. ?;а;; небольшие смелые, Те^Е..,. *г>>- можете представить себе; •••••<••; $'г-< Оиргде.игикл существуют /г^иь как извлеченные компилятором из библиотеки, которую Вы не можете М=’:-.Г; - у ;.; л..(ровать. Ф64- Библиотека $wyp ААы должны определить общую концепцию фигуры. Это надо сделать так, \то6ы ее можно было бы использовать (как базовый класс shape) дле всех конкретных фигур (например, круги и квадраты) и так, чтобы с любой фигурой можно было бы работать исключите ?ыю через интерфейс,, представляемый классом shape (фигура). struct shape { shape() { shape _ list.append(this); } virtual virtual virtual ’irtual virtual point north() point south}) point east() point neastf) point seastQ { return point(0,0); ) { return point(O.O): ) { return point(0,0), }• { return point(OfO); } { return point(0,0); } virtual void draw() { }; virtual void move (int. int) { }; 8* 222
Идея СОСТОИТ В ТОМ, ЧТО фигуры размещаются функцией Iiiuve^ и выводятся на экран функцией draw(). Фигуры размещаются друг относительно друга в соответствии с концепцией точек ориентирования, а именно, по румбам компаса. Каждая отдельная фигура определяет для себя значения этих точек, и каждая определяет, как ее нужно рисовать. ДЛя экономии места, в настоящем примере реально определены лишь те румбы, которые используются в нем. Конструктор shape::shape() включает описание фигуры в список фигур shape „ list. Этот список представляет собой список gslist, то есть, вариант родового цепного списка (как это определено в $7.3.5. Список и подходящий итератор были составлены таким образом: typedef shape" sp; declare(gslist, sp); typedef gslisf(sp) shape „1st; typedef gslist _ iterator(sp) si „iterator; На основании этого, список shape „list можно объявить так: shape „1st shape „list; Прямую можно построить либо исходя из двух точек, либо исходя из точки и целого числа. В последнем случае, строится горизонталь, длина которой задается целым числом. Знак числа показывает, является ли точка девой или правой концевой точкой. Вот определение конструкции: I class line : public shape { прямая строится от "w" до "е" north() (север) определяется как "вверх>от центра до самой северной (самой верхней) точки" ■/ -V point w,e; о public: point north() { return point(w.x + e.x)/2, e.y <w.y?w.y:e.y; } point south() { return point(w.x + e.x)/2, e.y<w.y?e.y:w.y; } void move(int a, int b) { w.x + = a; w.y + = b; e.x + = a; e.y + = b; } void draw() { put„ line(w,e); } line(point a, point b) { w = a; e = b; } line(point a, int I) { w = point(a.x + 1-1, a.y); e = a; } }; Аналогично определяется rectangle (прямоугольник): 223
7 class rectangle : public shape { nw n ne I I I I w c e I I I I sw s se (Обозначения на схеме: nw - северо-запад, n - север, ne - северо- восток и т.д.) V point sw, ne; public: point north() point south() point neast() point sweasf() void move(int a, { return { return { return { return int b) { sw.x + = a; sw.y void draw() rectangle(point, point); point((sw„x + ne.x)/2,ne.y); } point((sw.x + ne.x)/2,sw.y); } ne; } sw; } + = b; ne.x + = a; ne.y + b; } Прямоугольник (rectangle) строится по двум точкам. Код усложняется тем, что нужно описать относительное расположение этих точек: rectangle::rectangle(point a, point -b) if (a.x < = b.x) { if (а У < = by- < sw = a; ne = b; > else { sw = point(a.x, b.y); ■ ! ne = point(b.x, a.y); } else { if (a.t < = b.y) { sw = point(b.x, a.y); ne = point(a.x, b.y); ) else sw = b; ne = a; } } } 224
’ ^ЗОСрбЗНТЬ -I ,p£/*i.--7 • X-Ki. p :'»Z. ■■ ■& - :.> •. C :--r'G d> •.; u. . . ■ -•■■=>';: jlf ± i «'0 { p.-vint п”л*>"г. rs; r point se(rie.x,. sw.y); put _ line(nw,ne); put _ line(ne,se); put_ line(se,sw); put _ line(sw,riw); } Помимо определений фигур, библиотека фигур содержи г фуик 4 пеботы с ними, Например: vc-c зЬзре . refresh'O; ‘// рисует все фигуры stacWshape" р, shape" qh >/ фзге* nv у •.■>.■■...■ хИ г.> . кЛ ейу) должна справазпъся с на?.ш:м безыскугнч-..м ‘ .и u'./Cji-ccBbifcaer все фигуры. Обратите внимание ьз го/ чъ: функция совершенно не представляет себе, какую фигуру она-сибу* shope.. гелтезКО ?<;-ее.:6 _ ctear(); si... iterator nextfshape .... list): *y-} shape p; while ( p = next() ) p->draw(); screen.... refrr<sh(j; > Наконец.- ниже приведена лстинна?. фу?-гчцч'т ут-.-лиi ?: -тго р?.•?/•• г. •одну фигуру над другой, указывая, что so'uthf) (юг) одной из них .< находится непосредственно над »югНт() ц.е&ером> другой void stackfshape’ q, shape p) // размещает у сверху •? q point n = p~ >north(); point s =. q->south(), q-> move(n.x-s.x,x.y-s.y * 1\ } Теперь представьте себе, что эта библиотека является собствен?-•:с.:.- некоторой компании, торгующей программным обеспечением Эта комча продае Вам только заголовочный файл, содержащий определения фк - скомпилироабяную версию определений • функций. При згсм, вь а-/< определить новые фигуры и использовать функции-у»плиты для г.ъ собственных фигур. 225
7.6.3. Прикладная программа Прикладная программа исключительно проста. Определяется новая фигура myshape, в распечатанном виде несколько напоминающая лицо, а затем пишется главная программа (main), которая рисует это лицо в шляпе. Прежде всего, определение myshape: #include "shape.h" class myshape : public rectangle { line" l_eye; // левый глаз line" r_ eye; // правый глаз line" mouth; // рот public: myshape(point, point); void draw(); void move(int, int); }; Глаза и рот - это отдельные и независимые объекты, созданные конструктором myshape: myshape::myshape(point a, point b) : (a,b) int II = neast().x-swest().x + 1; int hh = neast().y-swest().y + 1; I —eye = new line( point(swest().x + 2,swest().y + hh"3/4),2); r_eye = new line( point(swest().x + ll-4,swest().y + hh"3/4),2); mouth = new line( point(swest().x + 2,swest().y + hh/4), II—4); Объекты "глаза" и "рот" обновляются раздельно с помощью функции shape _ refresh и, в принципе, с ними можно работать независимо от объекта my _ shape, к которому они принадлежат. Это один способ определения фигур для иерархично сконструированных объектов, таких, как myshape. Другой способ иллюстрируется на примере объекта "нос". Этот объект не определяется, он просто добавляется к рисунку с помощью функции draw(): void myshape::draw() rectangle: :draw(); put _ point(point( swest().x + neast().x)/2,swest().y + neast().y)/2)); myshape перемещается, по экрану передвижением основного прямоугольника rectangle и вторичных объектов 1_еуе, г_еуе и mouth: 226
void myshape::move(int a, int b) rectangle: :move(a,b); l_eye->move(a,b); r _ eye-> move(a,b); mouth-> move(a,b); } Наконец, можно сконструировать новые фигуры и немного подвигать их: main() shape’ pl = new rectang1e(point(0,0),point( 10,10)); shape’ p2^ = new line(point(0,15),17); shape’ p3 - new myshape(point(15,10),point(27,18)); shape _ refresh(); ' p3->move(-10,-10); '''■ stack(p2,p3); ? stack(p1,p2); return 0; } Еще раз обратите внимание на то, как такие функции, как shape _ refresh() и stack() работают с объектами типов, которые были определены намного позже того, как эти функции были написаны (и, возможно, скомпилированы). 7.7. СВОБОДНАЯ ПАМЯТЬ При использовании класса slist, Вы можете обнаружить, что Ваша программа тратит слишком много времени на размещение и удаление из памяти объектов класса slink. Класс slink - это важнейший пример класса, 227
который предоставляет программисту возможность управлять распределением свободной памяти. Метод оптимизации, описанный в 5.5.6, идеально подходит для объектов такого рода. Поскольку каждый класс slink создается с помощью оператора new и уничтожается с помощью оператора delete элементами класса slist, другие методы распределения памяти не используются. Если производный класс присваивает значение указателю this, то конструктор для его базового класса будет вызываться только после того, как будет произведено это присваивание, а значение this в . конструкторе базового класса будет значением, присвоенным конструктором производного класса. Если значение указателя this определяется в базовом классе, то конструктор производного класса использует именно это значение. Например: #include < stream.h> struct base { base(); }; struct derived : base (derived(); }; bse::base() cout < < "\tbase 1: this =" < < int(this) << "\n"; if (this = = 0) this = (base*)27; cout < < "\tbase 2: this =" << int(this) << "\n"; ) derived: :derived() cout << "\tderived 1: this = " << int(this) << "\n"; this = (this = =0) ? (derived')43 : this; cout < < "\tderived 2: this =" << int(this) << "\n"; } ' main() cout < < "base b;\n"; (основание b) base b; cout < < "new base;\n"; (новое основание) new base; cout < < "derived d;\n"; (производный d) derived d; cout < < "new derived;\n"; (новый производный) new derived; cout < < "at the end\n"; (конец) } 228
даст на выходе base Ь: base 1: this = 2147478307 base 2: this = 2147478307 new base: base 1: this = O base 2: this = 27 derived d; derived 1: this = 2147478306 base 1: this = 2147478306 base 2: this = 2147478306 derived 2: "this = 2147478306 new derived: derived 1: this = O base 1: this = 43 base 2; this = 43 derived 2: this = 43 at the end Если значение указателю this присваивается деструктором для производного класса, то присвоенное значение - это значение, прочитанное деструктором для его базового класса. Если значение this присваивается в конструкторе, то важно иметь в виду, что присваивание значения указателю this происходит при каждом проходе через конструктор1. 7.8. УПРАЖНЕНИЯ , 1. (“1) Дано: class base { public: virtual void iam() { cout << "base\n"; } Создайте два производных класса от класса base и для каждого из них определите функцию iam() так, чтобы она распечатывала имя класса. Создайте объекты этих классов и вызовите для них функцию iam(). Присвойте указателям base* значение адреса объектов производных классов и вызовите через эти указатели функцию iam(). 2. (”2) Реализуйте примитивы экрана ($7.6.1) подходящим для Вашей системы способом. 3. Определите класс triangle (треугольник) и circle (круг). ХК сожалению, о таком присваивании легко забыть. Например, в раннем варианте этой книги вторая строка конструктора derived: :derived() выглядела так: if (this = = 0) this = (derived")43; Соответственно, для d конструктор базового класса не вызывался. Программа была вполне корректной, но ясно, что она не делала того, что хотел автор. 229
(м2) Опреде/юте функцию, история вычерчивает пинию, соединяющую две фжуры, обыскивая две ближайшие " точки ориентирования' и соединяя их. (’2) Моднфч'д’фуйте нрл.Аэр shape так, чтобы Вче про-извсди'лсу. rectangle и наоборот. С 2) Сконструируйте и реализуйте двояко связанный список, который .можно использовать без итератора. ( 2) Сконструируйте и реализуйте двояко связанный список, который можно было бы использовать только -через итератор. Итератор должен содержать операции для перемещения вперед и назад, операции для ясдал и ул*•••.-..г;:! з сгу.с/е ’••! -.-л'о с о б -.тун а . ■; ег,у ■ ;< ..у элементу. (■'?) Создайте рг.довую ^ер-гию двояко связанного сине*'* (4) Создайте сг.исск, в который включаются и из которого исключаются сами объекты (а не указатели на них). Пусть он работает для класса X, в котором определены Х::Х(Х&), X:: ' Х() и X:: operator - (Х&). (’5) С'мжструсручю г еа.шзу йч е библио.ску д>:ч ■. моделирования. Подсказка: <task.h>. Однако, это старая програм/ла и Вы можете создать лучшую версию. Должен быъ класс task (задача). Объект клзсса task должен запоминать свое состояние и иметь возможность восстановить его (Вы можете определить task; js3ve() и 'ask:irer-'o: е()) так, что он может работать как сопрограмма. Отдельные задачи можно определить как объекты хяас/А производно! о от класса task. Программа, вызыв?>-..• агл:- классом task, может быть описана как виртуальная функция. Нужно предусмотреть возможность передачи аргументов hobomv ул-?..—у task как apt улленты- его конструкторафв}. Должен быть ор; знмзозан планировщик, реализующий концепцию виртуального времени. Напишчте функцию task::de!ay(!ong), которая "расходует" вир-туспы-юе время. Будет ли планировщик частью класса task или он будет существовать отдельно -это одно из главных стратегических г. аще-ц-щ разработки. Задачи должны "общаться" Друг с другом. .Сконструируйте для этого класс queue (очередь). Придумайте способ, ??.<-.•-з-итч Лэ.-к. ам.чдагь or :• щредщ рбрс-" ---..у' -... г.; < • к ■унмфгшмроеанным способом. Как бы Вы отлаживал!', программы, г. такой бибг ’-.стакг-
"И.В.К." 105023 Москва, Мал. Семеновская, д. 5 Тел.: 936-50-67, 311-52-08 Факс: 203-93-55 ГЛАВА 8 ПОТОКИ Язык C++ не предоставляет средств для ввода и вывода. Это и не нужно; такого рода средства /ложно просто и элегантно создать, используя сам язык. Библиотека стандартного потока ввода/вывода, описанная ; обеспечивает гарантирующий сохранность типа, гибкий и эффективней метод для обработки символьных входных и выходных целых, чисел с плавающей точкой и символьных строк, а также простую модель для расширения их для обработки типов, определенных пользователем. Их пользовательск- интерфейсы находятся в <stream.h>. В этой главе представлена самэ библиотека потока, некоторые, способы ее использования и методы, используемые для ее реализации. 8.1. ВВЕДЕНИЕ Конструирование и реализация стандартных средств ввода/вывода для языка программирования пользуется печальной известностью сложней проблемы. Традиционно средства ввода/вывода создавались исключителе е для обработки нескольких встроенных типов данных. Однако, в нетривиальных программах на C++ обычно используется много типов, определенных пользователем; для ввода/вывода значений этих типов также должны предоставляться средства. Ясно, что средства ввода/вывода должны быть простыми, удобными и безопасными в использовании и, помимо всего, полными. Никто не может дать решение, удовлетворяющее веет; следовательно, пользователь должен иметь возможность создавать альтернативные средства ввода/вывода и расширять стандартные средства для специальных прикладных программ. Язык C++ построен так, что он дает пользователю возможность определять новые типы, которые столь же эффективны и удобны, что и встроенные типы. Следовательно, разумным требованием будет требований того, чтобы средства ввода/вывода для C++ предоставлялись в языче только с использованием средств, доступных любому программиста. Средства потоков ввода/вывода, представленные ниже, являются результат: л усилий,, приложенных к решению этих задач. Средства ввода/вывода в <stream.h> связаны исключительно с проце с¬ сом преобразования типов объектов в последовательность символов и наоборот. Есть и другие модели для ввода/вывода, однако эта модель является фундаментальной в системе UNIX; большая часть видов бинарного ввода/вывода обрабатывается таким образом, что символы рассматриваются 231
просто как битовые комбинации, а общепринятое соответствие их алфавитным символам игнорируется. Таким образом, ключевая проблема для программиста - описать соответствие типированного объекта и нетипированной цепочки. Типо-безопасную (сохраняющую тип) и единообразную обработку -как встроенных типов, так и типов, определенных пользователем, можно обеспечить использованием одного имени совмещенной функции для набора функций вывода. Например: put(cerr,"x - "); //сегг - выходной поток ошибки put(cerr,x); put(cerr,"\n"); Тип аргумента определяет, какая функция будет задействована для каждого аргумента. Такое решение использовалось в нескольких языках. Однако, оно многословно. Переназначение оператора < < для того, чтобы он означал "put .to" (вывести на) приводит к лучшей нотации и позволяет программисту определить вывод последовательности объектов с помощью одной строки. Например: сегг < < "х = " < < х < < "\п"; где сегг - стандартный выходной поток ошибки. Так, если х имеет тип int и значение его равно 123, такой оператор напечатает в стандартном выходном потоке ошибки х = 123 и символ новой строки. Аналогично, если х имеет тип complex, определенный пользователем, и значение х равно (1,2.4), вышеприведенный оператор напечатает в сегг х = (1,2.4) Такой метод можно использовать лишь если х принадлежит к типу, для которого определен оператор < <; пользователь может легко, определить оператор < < и для нового типа. 8.2. ВЫВОД В настоящем разделе вначале будут обсуждены средства как для форматированного, так и неформатированного вывода встроенных типов. Затем будет представлен стандартный способ описания операций вывода для типов, определенных пользователем. 8.2.1. Вывод встроенных типов Определяется класс ostream; наряду с этим определяется оператор < < (вывести на). С помощью этих определений обрабатывается выходной поток для встроенных типов: 232
class ostream ( /7 public: ostrearnA орегакм л -c (char*); ostreamA operator << (int i) ; return ‘this < < long(i); } ostream& operator < <(long); ostreamA operator < < (double), osireamA puf(chai); } Функция operator<< возвращает обращение по адресу ostream. которого она бьща ^вызвана таг- т -■ •- ■■■*” мох-но г;р«: - 'hv. оператор ostrearn. Например: сегг < < "х = к; где х имеет тиг; inf, будд; t р л-.о ;ься г-д. 1 д' (cerr.operator<<~ HV'?pemfcr < <(х): \ В частности, это подразумеваем что если с помощью одного orjep? вывода печатаются несколько эге.м-.'^оз, они будут ра<. м-^;аг.. ожидаемом порядке - слева направо. Иметь operator <<, котос-''h ! -о=?-ч-»ует тип ,нИ\ изгг.с..; .у;, int может неявно преобрэзогь.е-аться в long. Однако, он мих преобразовываться и в double Чаvти? определения ostrearn::operate - f позволяет исключить эту £?еоднс-зн:а ..юсть. Функция estreat: :ри/..... предназначена для печатания символов как символов; функция с stream: .operator << (hit) печатаем целочисленные значения с-.:.£лтог. 8.Х?. Вмкзд гиязз,- оп£/*д««₽-нн!Ь1Х пользователем Рассмотрим тип. -ними пользователем: class complex { % double ;е, im, public: compbxfdouble r - 0, double ? ~ 0) ( re = r; im - i } friend double rsa!(complex& a) ( return a.re; } friend double hnag(complexA a) { return a.irn; } friend complex operator г (complex, complex); friend complex operator-(complex, complex); friend complex opsrafor’fr'rnplex, complex); friend complex operator/(complex, complex), / / }; Пдя тПДТУ. ОЛЬТ; < < i/ Cd’/; mJ: • 2.13
ostream& operator < <(ostream&s , complex z) } return s << '<< ?ea!(z) << << imag(z) << ")";. и использовать его точно так же, как и встроенный тип: complex х(1,2); // ... cout < < "х = " < < х даст на выходе X = (1,2) Определение операции вывода для типа, определенного пользователем, не требует ни изменений в объявлении класса ostream, ни доступа к структуре данных (скрытой), которую он обслуживает. Первое обстоятельство благоприятно, поскольку объявление класса ostream размещено в стандартных заголовочных файлах, для которых пользователь, в общем, не должен создавать специальный код доступа. Второй момент также важен, поскольку он защищает структуру данных от случайного повреждения. Можно также изменить реализацию ostream, не меняя при этом реализацию программ пользователя. 8=2.3= Некоторые детали конструкции Оператор вывода использовался для того, чтобы избежать многословия, связанного с использованием функции вывода. Но почему <<? Новую лексему создать нельзя (см. $6.2). И для ввода, и для вывода можно было бы использовать оператор присваивания, однако, по-видимому, большинство программистов предпочитает иметь разные обозначения для операторов эвода и вывода. Более того, оператор = связывает операнды неправильно, го есть, cout = а = Ь означает cout = (а = Ь). Опробовались операторы < и >, однако их значения "меньше, чем" и "больше, чем" настолько прочно въелись в сознание, что новые операторы ввода/вывода оказывались практически нечитаемыми. Помимо этого, в большинстве клавиатур знак "<" печатается той же клавишей, что знак в результате нередко получаются такие операторы: cout < х , у , z; Цля таких операторов трудно придумать подходящее сообщение об ошибке. Операторы << и >>, по-видимому, подобных проблем не вызывают. Они асимметричны, то есть, могут показывать направление "в" и "из". Старшинство < < и > > достаточно низкое, так что они позволяют выполнять арифметические действия без применения круглых скобок. Например: cout << "а’Ь + с = " << аяЬ + с << "\п"; 234
Естественно, что для написания выражений, содержащих операторы низшего старшинства, надо использовать круглые скобки. Например: cout << "а^Ысз" << (аЛЫс) << ”\п"; В выражения вывода можно использовать и оператор сдвига влево: cout << "a<<b = ” << (а< <Ь) << "\п"; Выражения в C++ не имеют символьных значений. В частности, '\л' является целым числом (со значением 10, если используется код ASCII), так что * cout < < " х = " < < х < < '\п' пишет число 10, а не ожидаемый символ новой строки. Этих и похожих проблем можно избежать, определив несколько макрокоманд (с использованием стандартных имен символов ASCII). # define sp < < " " #define ht << "\t" #define nl < < "\n" Пример можно переписать теперь так: ( cout < < "х = " < < х nl; Функции ostream::put(char) и chr(int) (см. $8.2.4.) предназначены для печати символов. Несинтаксические макрокоманды кое-где рассматриваются как плохой стиль, однако некоторым они нравятся. Рассмотрим также примеры: cout << х << " " << у << " " << z << "\п"; cout < < "х = " << х << ", у = " << у << "\п"; Это трудно читать из-за обилия кавычек, а также потому, что визуально оператор вывода слишком Внушителен. Макрокоманды плюс небольшое выравнивание могут помочь: cout < < х sp << у sp << z nl; cout < < ” x = " < < x < < ", у = " < < у nl; 8.2.4. Форматированный вывод В приведенных выше примерах оператор << использовался только для неформатированного вывода. В реальных программах он, как правило, используется именно в этих целях. Однако, в языке C++ предусмотрено несколько форматирующих операторов, создающих строковое представление своих аргументов для использования их при выводе. Второй (дополнитель¬ ный) аргумент этих операторов описывает число используемых символьных 235
позиций. char' oct(long, irA = 0); ' '■ •'* =0); = 0); = 0); = 0); // // // // // восьмеричное представление десятичное представление шестнадцатиричное представление символ строка char" dec(long, pt char" hex(long, int char" срк(1опд, Int char" str(long, i0t только не (описано поле нулевого размера, производится усечение в | - ►ДИЛ ес(" << ") /= oct(" ") = hex(" "Т Если или дополнение; в/ противном случае используется столько, символов, сколько это необходимо. Например: , cout << "deff << < < < < х < < oct(x,6) < < hex(x,4) Если* х = = 15, то н/э выходе dec(15_ = <orf( 17) Можно использовать и общую форматную строку: char" form(tnar“ format ...); Использование /оператора cout< <form() эквивалентно использованию стандартной функции вывода С printfO1. Функция form() возвращает строку, полученную в 'результате преобразования и форматирования аргументов, следующих за ее первым аргументом; преобразование и форматирование управляются форматной строкой format. Форматная строка содержит два типа объектов: { обыкновенные символы, которые просто копируются : в выходной потоке, и спецификаторы преобразования, каждый из которых вызывает преобразование следующего за ним аргумента с последующей его распечаткой. Каждый спецификатор преобразования вводится символом %. Например: получится = hex( f) cout< <1ргт("было %d элементов", no _ of members); В этом случай %d указывает, что по r of _ members должен обрабатываться как целое ’й распечатываться как соответствующая последовательность десятичных цифр. Если no _ of _ members = =127, то вывод выглядит так: было ^27 элементов. Набор/ спецификаторов преобразования достаточно велик и обеспечивав^ весьма высокую степень гибкости. После знака % может стоять: I i — J — Объяснение форматной строки - это слегка отредактированная версия описания функции printf(). I I 236
дополнительный знак минуса, описывающий выравнйвани преобразуемой величины в описанном поле not левей границе, дополнительная строка цифр, описывающая ши® ину поля; если числ символов в преобразуемой величине, меньше, J чем. ширина поля, она дополняется пробелами слева (или с: ' “ z указа;! индикатор выравнивания по левой грат Если значение ширины поля начинается с производится нулями, а не пробелами, дополнительная’^ ширины поля < дополнительная цифровая строка, указывает количество знаков п- преобразования е- и f~, или максимальное символов строки. для представления ширины поля или точности можно использовать знак ". В этом случае описывает ширину поля или точность, дополнительный символ h, указывающий, и соответствуют целому аргументу типа дополнительный символ I, указывающий, и соответствуют целому аргументу типа указывает, что используются. символ, указывающий на то, что следует использовауь преобразован* типа. Символы преобразования и их значение: I < cnpaeaL в гранимо) J к уля, . т f в случае, если был ) до границы пел-? то дополнение л/, точка, предназначенная для \ отделения значения от следующей цифровой строки. I описываюащя точность. Онг ле десятмчной точки кспо распечатываем:-. нужно напечатать вмесрго цифровой строки целом исленный аргумент последующие d, of х или что short. ] что последующие d, о, х или long. С И А'.ВО Л аргументы не целый аргумент преобразуется в десятичную запись, целый аргумент преобразуется в восьмеричную запись, целый аргумент преобразуется в шестнадцгптиричную запись аргумент типа float или double преобразуемся в деся/ичнук г -и.» — j д есятичной точки а ргумента. d о х f запись вида [-Jddd.ddd, где количество d после соответствует спецификации точности для аргумента. Если точность не указана, печатается 6 символов; есл и явно указано что точность равна 0, ни цифры, ни десятинная точка , печатаются. ‘ f е аргумент типа float или double преобразуете^ в десятичную запись вида [-Jd.ddde + dd, где перед десятичной ^точкой ставится одна цифра, а количество цифр после десятичной точк:' соответствует спецификации точности для ap\i "умента. Еслч точность не указана, печатается 6 символов. 1 g аргумент типа float или double печатается в с:тиле d, f е. Выбиодагётся стиль, обеспечивающий кратчайшую^ распечатку с полной точностью. с печатается символьный аргумент. Нулевуые символы игнорируются? ' s аргумент- рассматривается как символьная строк а (указатель¬ на символ). Символы печатаются до тех пор, пой'.а не будет найден нулевой символ, или пока количество р^ спечатанны: символов не окажется равным числу, указанном спей* <.фйкатором точности. Если спецификатор точности опущен ил;и равен 6. печатаются все символы до нулевого символа.
б®?янат! ’ ■ аргумент преобразуется в десятичную ПОЛЯ 1.Ц ирииу. ч-да у . если ши,л ■ случае, . . •?сзня слишком, маленькая ширина ‘ ‘ азана вовсе). Дополнение производится ал шир’чл ноля превышает фактическую г'ЧН-'О 1 г; гкн?:- f : ; .. , jpi е; |--.- <. я. .М cout < < CGllt < < cout -г ' "int а. fonn'lme '■•.4 1 ■ с.... file name); — 1~ ;'г' ,. * ; ; . • -5 ; --i ' a J .. /1 = ’ • ' Г f. ''I.;' ПР, 4 np Q p ер !■■’ /' th. 13 не производится. Ha ifr ::j. а-.и .жирело известный способ получения нд —у^'А. ..г .... .... . .... -л ,ПТ1. ссш!< <form("bad И беспечивает высокую гибкость и •• • rx^HIHRilTk РЬ’КОП.НЬ!^ ГС л.^творнтрпьнь'^ средства дчя • rip. г- ирчочич* пСПЬЗСВЭТепГ' q э е f Н Н СИ О f п б О''! г.' е д е; 1 9.-' •• • Р Ь||. 4<'орг<-'.'г ;^Т.э фуч-:; •: рйб.нч)’г ■’ЯЬ ' ■ ‘ у П?’»' ■ ' .; ( ?• ..■•г.. -'я мп н.це<:;|Ы!ЫЙ с..? ,■ ,.?).уГ. .ЬунКЦИЮ, давать ? отве > с гвующее строковое представление объекта, для которого она вызвана. Например: Однако, функция form д/сдх: .г.‘;: ■• »/• о гЬ . Tf- • •, Ч*; ' ?•■ <»' ’.Г, г е .у, Л ••• Ч * Г - • Л Z - стиле рлг.-’ "о яснее в г?-ед-г • В ВОЗМОЖНО, фузмвода дг^ ' должна определять дополнению, выравнивание соответствии с запросами пользе' подход - создать для типов, опреде> которая, подобно функциям o’ct(), hex<) Я у class complex { float re, im: public: // ... char" string(char’ for*';»a+) { return form(form?.t,. re. } ?3 5
cout << z.$tring("% .3f, % .3f)"); Память для строк, возвращаемых функциями form(), hex() и т.д.; отводится в отдельном статически размещенном цикличном буфере Следовательно, сохранять указатель, возвращаемый одной из этих функций для позднейшего использования, бессмысленно. Символы, на которые указывают эти указатели, все время меняются. 8.1.5. Виртуальные функции вывода Иногда функция вывода должна быть виртуальной. Рассмотрим пример класса shape, представляющий общую концепцию геометрических фигур (1.18): * с class shape { у; //... public: // ... virtual void draw(ostrearn& s);// рисует "this ' на "s" class circle : public shape { int radius; \ public: // ... - void draw(ostream& ); }; To есть, класс circle имеет все атрибуты фигуры и может обрабатываться как фигура, но, кроме того, он имеет и некоторые особенности, которые следует учитывать при работе с этим классом. Для получения образца стандартного выхода для таких классов, можно определить оператор < < так: ostream& operator < <(ostream& s, shape" p) { p-> draw(s); return s; } Полагая, что next - это итератор типа, определенного в 7.3.3, фигуры из списка можно нарисовать так: while ( р = next() ) cout < < р; 8.3. ФАЙЛЫ И ПОТОКИ Потоки обычно связаны с файлами. Стандартный входной поток cin, стандартный выходной поток cout и стандартный поток ошибок сегг созданы 239
библиотекой потока. Программист может открыть и другие файлы и создать для них потоки. 8.3.1. Инициализация выходного потока Класс ostream имеет конструктор: class ostream { ostream(streambuf’ s); ostream(int fd); ostream(int size, char’ p); // связывание с буфером потока // связывание для файла 7/ связывание с вектором Первостепенная задача для этих конструкторов - связывание буфера с потоком. Класс streambuf - это класс, управляющий буфером; он описан в $8.6, как класс filebuf, управляющий классом streambuf для файла. Класс filebuf произведен от класса streambuf. Объявление- стандартных выходных потоков cout и сегг можно найти в исходном коде для библиотеки потоков ввода/вывода. Они выглядят так: // объявляет подходящий размер буферного пространства char cout _ bi’f[BUFSIZE]; // создает "filebuf" для управления этим пространством // связывает его с выходным потоком 1 UNIX (уже открытым) filebuf cout _file( 1 ,cout_ buf,BUFSIZE); // предоставляет ostream для пользовательского интерфейса ostream cout(&cout _ file); char cerr_buf[1]; // имеет длину 0, то есть, это незабуференный // выходной поток 2 UNIX filebuf cerr _ ft!e(2,cerr _ buf,0); ostream cerr(&cerr „ fife); Примеры использования двух других конструкторов ostream приведены в 8.3.3 и 8.5. 8.3.2. Закрытие выходных потоков Деструктор для ostream очищает буфер, используя общую функцию- элемент ostream: :flush(): ostream:: ~ ostream() < flush(); Класс ostream можно очистить явно: Например: c.out.flush(); 240
833. Открытие файлов Подробности процесса открытия и закрытия файлов зависят -- ог.яацноннш систем и здесь детально не описываются Поскольку i-я и с.гг становятся доступными после включения файла <stream.^ > , мио - (если не большинство) программ могут не содержат с код дг.х откры. файлов. Однако, ниже приведена программа, которая открывает два фаь. описанных как аргументы командной строка ь .чопмру?эщие >,з во второй: #include < stream h> void error(charM s/cha»' s?) cerr < < s < < ” " << s2 < < "\n exit(1); } main(int argc, chara argv[]) if (argc ! = 3) еггог(”неправильно указано число apry-мантов”); fiiebuf fl; . if (fl .open(argv[1 ],input) = = 0) еггог("не могу открыть входной файл"),агду[1 ]); istream from(&f1); fiiebuf f2, if (f2.open(argv[2],output) = = 0) еггог("не могу открыть или создать выходной файл",агду[2Г: ostream to(&f2); г char ch; while (from.get(ch)) to.put(ch); if (!from.eof() II to.badQ) еггог("произошло что-то странное”/'"); Последовательность операций при ^создании класса ostream для поименованного файла точно такая же, что и использоаэн.чая для стандартных потоков: (1) во-первых, создается буфер (в данном случае, объявлением fiiebuf); (2) после этого, с ним связывается файл (в данном случае, открытием файла с помощью функции fiiebuf::ореп()); и, наконец, создается сам класс ostream с использованием fiiebuf в качестве аргумента. Входной поток обрабатывается аналогично. Файл можно открыть в одном из двух режимов: enum open _ mode { input, output }; 241
Операция filebuf::open() возвращает 0, если она не может открыть запрошенный файл. Если пользователь пытается открыть несуществующий файл для вывода, то такой файл будет создан. Перед терминацией программа проверяет, приемлемо ли состояние потоков (см. $8.4.2). Открытые файлы по окончании программы неявно закрываются. Файл можно также открыть как для чтения, так и для записи, но, если это необходимо, вид потока редко бывает идеальным. Часто бывает лучше считать такой файл вектором (очень большим). Можно определить тип, который позволит программе обрабатывать файл как вектор (см. упражнение 8-10). 8Л.4. Копирование потоков Поток можно скопировать. Например: cout = сегг; Результатом будут две переменные, обращающиеся к одному потоку. Это исключительно полезно для создания стандартных имен (таких, как cin), обращающихся к чему-то другому (в качестве примера см $3.1.6). 8.4. ВВОД Ввод аналогичен выводу. Существует класс istream, предоставляющий оператор ввода > > ("взять из") для небольшого набора стандартных типов. Для типа, определяемого пользователем, можно определить функцию operator > >. 8.4.1. Ввод встроенных типов ' Класс istream определен так: class istream { // ... public: ... istream& operator>> (char’); // символьная строка istream& operator>> (char&); // символ istream& operator> > (short&); istream& operator>> (int&); istream& operator>> (long&); istream& operator>> (float&); istream& operator> >(double&); // ... Входные функции operator> > определяются таким образом: istream& istream:.operator> >(char& c) { // пропуски не читать int a; 242
// считать каким-то образом символ в "а" с = а; } Пропуски определяются как стандартные пропуски С вызовом функции isspace(), определенной в <ctype.h> (пробел, табуляция, новая строка, пропуск страницы, и возврат каретки). Альтернативный вариант - использование функций get():. class istream { // ... istreamS gef(char& с); // символ istream& get(char" p, int n, int = '\n'); // символьная строка }; Эти функции обрабатывают символы пропусков как и все остальные символы. Функция istream: :get(char) считывает для своего аргумента один символ; вторая функция istream::get считывает максимум п символов в вектор символов, начинающийся по адресу р. Дополнительный третий аргумент используется для указания терминатора, то есть, эта не читаемый символ. Если в потоке обнаруживается терминатор, то он оставляется как первый символ в Потоке. По умолчанию, вторая функция get() считает максимум п символов, но не более, чем одну строку; \п' является терминатором по умолчанию. Дополнительный третий аргумент описывает символ, который не будет считываться. Например: •, cin.get(buf,256,'\t'); считает максимум 256 символов в but; если будет найден символ табуляции ('\t'), функция get терминируется. В данном случае, символ '\t' будет следующим символом, считанным из cin. Л Стандартный заголовочный файл <ctype.h> определяет несколько функций, которые можно использовать при обработке ввода: int isalpha(char) int isupper(char) int islower(char) int isdigit(char) int isxdigit(char) int isspace(char) int iscntrl(char) int ispunct(char) int isalnum(char) int isprint(char) int isgraph(char) int isascii(char c) // z 'k.:t и k.:t // 'a'..'z' Л // O'..'9Z S // O..Z9Z 'a'..'f' A'./F' // ' ' z\tz возврат каретки новая строка // управляющий символ // (ASCII 0..31 и 127) // пунктуация - все, кроме того, что указано // выше // isalpha() I isdigitQ // печатаемый символ: ascii.' // isalpha() I isdigit() I ispunct() { return 0<=c && c< = 127; } Все функции, просмотром таблицы за исключением атрибутов символа, isascii() реализуются простым с использованием символа как 243
индекса. Следовательно, выражения типа: (('а'<=с && c<='z) II С А' < = с && с<= Z)) // алфавитные символы не только утомительны при написании и чреваты ошибками (на машинах, использующих набор символов EBCDIC, в приведенный интервал попадут и неалфавитные символы). Они, помимо того, менее эффективны, чем стандартная функция: isalpha(c) 8.4.2. Состояния потока Каждый поток (istream или ostream) имеет связанное с ним состояние; ошибки и нестандартные условия обрабатываются с помощью соответ¬ ствующей установки, и обработки этого состояния. Поток может находиться в одном из следующих состояний: enum stream_state { _good, — eof, -fail, -bad }; Если состояние потока - _good или _eof, это значит, что предыдущая операция ввода прошла успешно. Если поток находится в состоянии —good, то разрешается следующая операция ввода, при любом другом состоянии потока она запрещена. Иными словами, применение операции ввода к потоку, не находящемуся в состоянии «good - это нулевая операция. Если делается попытка считать что-нибудь в переменную v, и операция не проходит, то значение v должно остаться прежним (оно остается прежним, если v принадлежит к одному из типов, которые обрабатываются функциями -элементами istream и ostream). Различия между состояниями —fail и _bad незначительны и представляют действительный интерес только для реализаторов операций ввода. В состоянии -fail предполагается, что поток не поврежден, и что ни одного символа не утеряно. В состоянии -bad банк сорван. Состояние потока можно проверить таким образом: switch (cin.rdstate()) { case —good: // последняя операция с cin прошла успешно break; case _ eof: // конец файла break; case —fail: // какая-то ошибка форматирования // может быть, дело не очень плохо break; case —bad: // возможно, утеряны символы cin break; Для любой переменной z типа, для которого были определены 244
операторы > > и < <, цикл копирования можно записать так: while (cin> >z) cout << z << "\n"; Например, если z - это символьный вектор, то такой цикл будет принимать стандартный ввод и распечатывать одно слово (то есть, последовательность символов, не являющихся пропусками) на строке в стандартном выводе. Если поток используется как условие, то проверяется состояние потока и тест проходит успешно (то есть, значение условия не равно нулю) только в том случае, если поток находится в состоянии _ good. В частности, в предыдущем цикле проверяется состояние потока istream, возвращаемое cin>>. Чтобы выяснить, почему цикл, или тест не прошли, можно проверить состояние протока. Такая проверка состояния реализуется с помощью оператора преобразования (6.3.2), На самом деле, не очень удобно делать проверку на ошибки после каждой операции ввода или вывода, и обычный источник ошибок заключается в том, что программист не может обнаружить ошибку в том месте, где она произошла. Например, операции вывода, как правило, не проверяются. Форма потока ввода/вывр|да выбрана так, что если/Лкогда для C++ существует механизм исключения-обработки (либо средствами языка, либо через стандартную библиотеку), его легко использовать для упрощения и стандартизации обработки ошибок потока-ваода/вывода. 8.4.3. Ввод типов, определенных пользователем .... ■ Для типов, определенных пользователем, операцию ввода можно определить точно так же, как и операцию вывода, но для операции ввода важно то, что второй аргумент имеет тип обращения по адресу.. Например: istream& operator> >(istream& s, complex& a) Г входные форматы для типа complex; "f" означает тип float: f f ) (f; o ■/ { double re = 0, im = 0; \ char c = 0; / s > > c; if (c = = '(') { s > > re > > c; if (c = = ',') s •>> im >> c; if (c != ')') s.clear( _ bad); // установка состояния } else { s.putback(c); s > > re; } 245
if (s; а - complexes. im). ге+цгг: 3; Нес.м■-? краткость кеда обработки ошибок. с.-. ц?.йствительно обрабатывает большую их часть. Локальная переменная с инициализируется для того, чтобы избежать случайного присваивания ей значения '( после неудачной операции. Зак-т-зчигз/ьная проверка сосго-якия чо;ока гарантирует, что значение аргумента а изменяется только в случае, если все прошло нормально. Операция установки состояния потока называется С1еаг(), поскольку чаще всего эта функция используется для сбросе состояния потока в _gccd, _ good является значением аргумента по умолчанию как для istream::clear(), так и для o$tream::clear(). Над входными операциями ^нужно поработать ечле. В частности-, было- бы неплохо, если - бы можно было описать ввод в терминах образца (как в гзыхах Снобол и Айкон)., а затем просто проверь;ь успех или неудачу полной операции ввода, для подобных операций нужно, естественно, дополнительное буферное пространство для того, чтобы можно было восстановить входной поток до его исходного состояния после того, как операция сравнения с образцом показала неправильный ввод. 8.4=4» Инициализация входных потоков Естественно, что для типа istream, как и для типа crbeam, имеются конструкторы: class istream { // iskeamvstrearnbuf* sf ini sk = Ipostream* t =0); isTT-?,??m(:rP s»ze char" p. inf sk - 1)? istrcam(int id, inf sk =1, ©stream* i - 0); / r Аргумент sk указывает, нужно ли игнорировать пропуски, или нет. Аргумент 4 (дополкительчый) описывает указатель на ostream, с которым связывается istream. Напоиллер, cin связан с cout; это означает, что cin запускает couf.flush(); // запись в буфер вывода перед попыткой считать символы из сво-его файла. Функцию istream: :tie() можно использовать для привязывани,- раззязывания, если вызывается tie(O)) любого osfream к любому is r-. Например: ini у _©r_ n(os’tream& to, isfream& from) /s приглашение от "to", ответ от "from" V { ostream’ old = from.tie (&to); 246
cout << "нажмите Y или N: "; char ch = 0; if (’cin.get(ch)) return 0; if (ch ! = '\n') { // пропустить остаток строки char ch2 = 0; while (cin.get(ch2) && ch2! = '\n'); } switch (ch) { case 'V': case 'y': case '\n': from.tie(old); // восстановить старую связку return 1; case 'N': case 'n': ^y, from.tie(old); // восстановить старую связку return 0; default: cout < < "извините, попробуйте еще раз: "; } } } Если используется буферизованный ввод (как делается пс 'умолчанию), то пользователь не может просто ввести символ "у" и жда ответа. Система ждет терминирующий символ новой строки; y_or_.n0 рассматривает первый символ строки, а остальные игнорирует. Символ можно вернуть обратно в поток с помощью функции istream. putback(char). Это позволяет программе "пересмотреть" входной поток. 8.5. РАБОТА СО СТРОКАМИ Разрешаются операции ввода/вывода с символьными векторами. для этого вектор связывается с istream или ostream. Например, если вектор содержит обычную символьную строку,? оканчивающуюся нулем, для распечатки слов, входящих в состав строки, можно использовать цикл копирования, представленный выше: void word _ per _ line(char v[], int sz) /’ печатаются v размером sz, одно слово на строке 7 istream (ist(sz,v); // создает для v поток istream char Ь2[МАХ]; // больше, чем самое большое слово while (ist> >Ь2) cout << Ь2 << "\п"; } 247
В даннол\ случае, терминирующий нулевой символ интерпретируется как конец файла. Для форматирования сообщений, которые не нужно печатать немедленно, можно использовать ostream: char* р = new charfmessage _ size]; ostream ost(message _ size, p); do _ something(arguments,ost); display(p); Такие операции, как do __ something, можно записать в поток ost, передать ost в соответствующие субоперации и т.д., используя стандартные операции вывода- Проверять переполнение не надо, поскольку ost знает свой размер и при .заполнении перейдет в состояние _fuil. Наконец, функция displayO может записывать сообщение в "реальный" выходной поток. Такой подход наиболее полезен в случаях, когда заключительная операция вывода включает в себя запись в нечто более сложное, чем традиционное устройство вывода, ориентированное на вывод строк. Например, текст из ost можно поместить в некоторую область фиксированного размера на экране. 8.6. БУФЕРИЗАЦИЯ Операции ввода/вывода описываются без какого-либо упоминания о типах файла, но, в соответствии со стратегией буферизации, не со всеми устройствами можно работать одинаковым образом. Например, для потока ostream, связанного с символьной строкой, требуется иной тип буфера, чем для потока ostream, связанного с файлом. Эти проблемы разрешаются предоставлением разных буферов для разных потоков в момент инициализации (обратите внимание на три разных конструктора для класса ostream). Существует только один набор операций для этих типов буферов; так что функции ostream не содержат кода, по которому их можно различить. Однако, функции, обрабатывающие переполнение и потерю значимости, относятся к типу virtual. Этого достаточно для того, чтобы реализовать стратегии буферизации, существующие в настоящее время, и является хорошим примером использования виртуальных функций для единообразной обработки логически эквивалентных средств с различными реализациями. Объявление буфера потока в <stream.h> выглядит так: struct streambuf { // управление буфером потока char* base; // начало буфера char* pptr; // следующий свободный char char* gptr; // следующий занятый char char* eptr; // символ за пределами буфера char alloc; // буфер, размещенный оператором new // Очистка буфера // Возвращает EOF при ошибке, 0 при удачном завершении virtua' int overflow(int с = EOF); 248
// Заполнение буфера // возвращает EOF при ошибке окси- "ни?; ввода ! / В Пг.О/Л СЛ'/’ЗЛ ... .<•; vnlual «пт undeffiu лJ int ^nextcf) // Пил;.г,с.1 i.n;.;.., f I eturn ( I -i b> ». I . i< , -tk. -V z/, } // A- int aiiocote(); / • рэ^чещасг :,c. ..;r< rax -стране тво ■' ,i! - n /ф. р ? streambuf() { ) tti eambuffchar* p, ■ > ~ streambuf() { /' . J } r t ■ Обратите внимание на ч„. .г с здесс- о; ;<аны , • .ггели, необходим л. г для обслуживания буфера, так м с обычнее ■ ч чмб >е операции можно определить как максимально эффективнее inline ни. Только функции oveJivwO л underflow.() доляхны реализовывав * ч-\.:-зи,{для каждой стратегии буферизации. Например: struct filebuf : public streambuf { int fd; // дескриптор файле char opened; // файл открыт int overflow(int c = EOF); int underflow(); // ... // открытие файла: // возвращает 0 при . е. ;а?с. ;.г при нормаль ж //открытии filebuf’ open(char "name, орел int close() { /’ ... ’/ } filebuf() ( opend - 0; } filebuf(int nfd) { /" ... “/ } filebuf(int nfd, char’ p, int I) i '< ~ filebuf() { close(); } }; int filebuf: :underflow() // буфер fd Зополнен if (’opened II allocate() = - E< < j t * : , 249
mt спи.nt -- t -рзd(id,. rtptr-base); if(c:>unt < 1) lreturn £Cr; gptr ~ base; 1 pptr - base count return gptr & 11377: 8J, ЭФФЕКТИВНОСТИ • Можно ожидать, что InocKr nt <у ввод/вывод < stream,h> определен г. использованием общих среАств он должен быть менее эффективен,, чем встроенные средства, фо-ви,■><.•*ому. это не так. Для таких операций, как ''поместить символ в бу|фер f используется замещение вызова функций их елом, так что едино веянные вызовы функций., необходимые на этом уроаке “• это вызов функций для обработки переполнения. и потери значимости. Для каждого Аз простых объектов (целые, строки и т.д.) требуется один вызов функции. По-видимому,. ситуация не отличается от ситуации, складывающейся при применении на этом уровне других средств ввода /вывода к таким объектам. 8.8. УПРАЖНЕНИЯ 1. (ж1.5) Прочитайте' файл,; состоящий из чисел с плавающей точкой, составьте из пар прочитанных чисел комплексные числа и запишите их в файл. 2. ( 1.5) Определите тип name „and „address (имя „ и „адрес). Определите для него операторы << и >>. Скопируйте поток объектов name „ and „ address. 3. (“2) Сконструируйте несколько функций для запроса и считывания информации различных типов. Тривиальный пример ~ функция у„ог„п из $8.4.4. Идеи: целые, числа с плавающей точкой, имя файла, почтовый адрес* дата, персональная информация и т.д. Попытайтесь предусмотреть "защиту от дурака". 4. (’1.5) Напишите программу, которая распечатывает: (1) ■•= все строчные буквы, (2) - все буквы, (3) - все буквы и цифры, (4) все символы, которые могут использоваться в идентификаторах С ; : в Вашей системе, (5) - все символы пунктуации, (6) ~ целочисленные значения всех управляющих символов, (7) - все символы пропуска, (8) ■ целочисленные значения всех символов пропуска, и, наконец, (9) - все печатаемые символы. 5. ("4) Реализуйте стандартную библиотеку ьвода/вывода С (<stdio.h>). используя стандартную библиотеку ввода/вывода C+ + (<stream.п>). 6. ("4) Реализуйте стандартную библиотеку ввода/вывода С + + (< stream.h>), используя стандартную библиотеку ввода/вывода С (<stdio.h>). ; 7. (’4) Реализуйте библиотеки С и С + + так, чтобы их можно был-э бы использовато одновременно i 8. (’2) Реализуйте класс, для кд горою оператор [] переназначается для реализации случайного считывания символов из файла. 9. (’3) То же, что и упражнение ^, но оператор [] должен использоваться 250
как для считывания, так и для записи. Подсказка: пусть [] возвращает объект "типа дескриптор", для. которого операция присваивания означает присваивание файлу через дескриптор, а неявное преобразование к типу char означает нтение из файла через дескриптор. I 10. ("2) То же, что и упражнение 9, но оператор должен индексировать записи какого-то другого вида, не символ^. 11. ("3) Создайте родовую версию класса, определенного в упражнении Ю. I 12. (’3.5) Сконструируйте и реализуйте входную операцию, использующую совпадение с образцом. Для описания образца используйте форматные строки в стиле printf. Предусмотрите возможность использования нес¬ кольких образцов для ввода с тем, чтобы найти истинный формат. Можно попытать^ произвести класс для ввода по совпадению с образцом от класса istream. f 13. (’4) Придумайте (и реализуйте) вид образца, который будет гораздо лучше, чем предыдущие.
"И.В.К." 105023 Москва, Мал. Семеновская, д. 5 Тел.: 936-50-67, 311-52-08 Факс: 203-93-55 СПРАВОЧНОЕ РУКОВОДСТВО 1. ВВЕДЕНИЕ Язык программирования C++ является расширением языка С1. В языке C++ дополнительно введены классы, инлайн-функции, переопреде¬ ление операторов, совмещение названий функций, типы констант, обращения, операторы обработки свободной памяти, проверка аргументов функций, и новый синтаксис определения функции. Различия между C++ и С суммированы в главе 15. Настоящее руководство описывает язык по состоянию на июнь 1985 г. 2. ЛЕКСИЧЕСКИЕ СОГЛАШЕНИЯ Принято шесть типов лексем: идентификаторы, ключевые слова, константы, строки, операторы и прочие разделители. Пропуски, табуляционные остановки, символы новой строки и комментарии (в общем - "пробелы”), игнорируются (как это описано ниже), за исключением тех случаев, когда они разделяют лексемы. Некоторые пробелы необходимы для разделения идентификаторов, ключевых слов и констант, которые, в ином случае, могут сливаться. При разбивке входного потока на лексемы до данного символа, следующая лексема выбирается так, чтобы использовать максимально длинную цепочку символов, составляющую лексему. 2.1 Комментарии Символ /" открывает комментарий, который заканчивается символом "/. Такие комментарии не могут быть вложенными. Символы // открывают комментарий, который заканчивается на кс^це соответствующей строки. 2.2. Идентификаторы (названия) Идентификатор - это последовательность букв и цифр произвольной длины; ’"The С Programming Language" by Brian W. Kernighan and Dennis M. Ritchie. Prentice Hall, 1978. Настоящее руководство составленр на основе системы UNIX V "The С Programming Language Reference Manual". 9 Зак. 1927 252
первый символ должен быть буквой; знак подчеркивания _ считается буквой. Строчные и прописные буквы считаются различными. 2.3. Ключевые слова Следующие идентификаторы зарезервированы для использования в качестве ключевых слов и не могут быть использованы иначе: asm class do float inline overload sizeof typedef while auto cpnst double for , int public stat«iG union break continue else a friend long register struct unsigned case default enum goto new return switch' virtual char delete extern if operator short this void Идентификаторы signed использования. и volatile зарезервированы для дальнейшего . ( 2.4. Константы В языке C++ существует несколько типов констант; они рписаны ниже. Характеристики компьютера, Влияющие на размер объектов, приведены в $2.6. 2.4.1. Целые константы Целая константа, состоящая из последовательности цифр, считается восьмеричной, если она начинается с 0 (цифра 0); в противном случае она считается десятичной. Цифры 8 и 9 не являются восьмеричными. Последовательность цифр, начинающаяся с Ох или ОХ (цифра 0), считается шестнадцатиричным целым. Шестнадцатиричные цифры включают в себя буквы от а или А до f или F, имеющих значений от 10 до 15. Десятичная константа, значение которой превышает максимально возможное целое со знаком, должна иметь тип long; восьмеричная иг|и шестнадцатиричная константа, превышающая максимальное целое ^без знака, также рассматривается как имеющая тип long; в остальных случаях целые константы имеют тип int. 2.4.2. Явийе константы типа long Десятичная, восьмеричная илц шестнадцатиричная константа, за которой следует буква I или L является константой типа long. 2.4.3. Символьные константы Символьная константа - это символ, заключенный в апостроф, например, 'х'. Значением символьной константы является численное значение символа в наборе символов компьютера. Символьные константы имеют тип int. Некоторые неграфические символы, апостроф и обратная черта \ могут быть представлены, согласно следующей таблице, как Е SC- 253
последовательности: > новая строка NL (LF) \n горизонтальная табуляция НТ \t вертикальная табуляция VT ’ \v возврат BS \b возврат каретки . CR \r новая страница FF \f обратная черта \ \\ апостроф * \’ битовое представление Oddd \ddd битовое представление Oxddd \xddd Последовательность \ddd состоит из обратной черты, за которой следует 1, 2 или 3 восьмеричных цифры. Эти цифры описывают значение нужного символа. Особый случай такой последовательности - \0 (за которым не следует цифра); такая последовательность означает символ NUL. Последовательность \xddd состоит из обратной черты, за которой следует буква хг а за ней -1,2 или 3 шестнадцатиричных цифры, описывающие значение нужного символа. Еслич символ, следующий за обратной чертой, не является одним из описанных выше, обратная черта игнорируется. 2.4.4. Константы с плавающей точкой Константа с плавающей точкой состоит из цёлой части, десятичной точки, дробной части, е или Е и, дополнительно, знаковой целочисленной экспоненты. Либо целая, либо дробная части (но не обе сразу) могут быть опущены; либо десятичная точка, либо е (Е) и экспонента (не обе сразу) могут быть опущены). Константа с плавающей точкой имеет тип double. 2.4.5. Перечисляемые константы Имена, объявленные как перечисляемые (см. $8.10) являются константами типа int. 2.4.6. Объявленные константы Объект ($5) любого типа может быть описан как имеющий постоянное значение в области своей видимости ($4.1). Для указателей в этих целях используется декларатор "const ($8.3); для объектов, которые не являются указателями, ^пользуется спецификатор const ($8.2). 2.5. Строки Строка представляет собой цепочку символов, заключенную в кавычки, например/ Строка имеет класс памяти static (статический) (см. ниже, раздел 4), тип “Массив символов" и инициализируется этими символами. Все строки, даже если они записаны одинаково, являются разными.Компилятор помещает в конец < каждой строки нулевой байт \0, так что программы, сканирующие строку, могут определить ее конец. Символу кавычек в строке должна предшествовать обратная черта \; кроме того, можно использовать приведенные выше ESC-последовательности для символьных констант. Наконец, непосредственно после обратной черты \ может начинаться новая 254
строка; в этом случае как символ \, так и символ новой строки игнорируются. 2.6. Аппаратные характеристики В следующей таблице приведены некоторые особенности, которые варьируют от машины к машине. DEC VAX ASCII Motorola 68000 ASCII IBM 370 EBCDIC АТ<&ТЗВ ASCII char 8 бит 8 бит 8 бит 8 бит int 32 16 32 32 short 1.6 16 16 16 long 32 32 32 32 float 3.2 32 32 32 doub1e 64 64 64 64 pointer 32 32 24 32 о б. л а с т ь float Ю±м 10*38 Ю*76 Ю*3» область double 10*38 Ю*зв Ю±76 i О*30® тип поля co знаком без знака без знака без знака порядок поля справа нал слева напр. слева напр. слева напр char co знаком без знака без знака без знака 3. СИНТАКСИЧЕСКАЯ НОТАЦИЯ В синтаксической нотации, принятой в настоящем руководстве, синтаксические категории выделены курсивом, литеральные слова и символы в константах даны латинским шрифтом. Альтернативные варианты приведены на отдельных строках. Дополнительный терминальный или нетерминальный символ отмечается подстрочным символом "opt", так, что {выражение,,?,} представляет собой дополнительное выражение, заключенное в фигурные скобки. Синтаксис резюмирован в разделе 14. 4. ИМЕНА И ТИПЫ Имя обозначает объект, функцию, тип, величину или метку. Имя вводится в программу с помощью декларации * ($8). Имя может использоваться лишь в определенной части текста программы; эта часть называется видимостью имени. Имя имеет тип, который определяет способ его использования. Объект является областью памяти. Объект имеет класс памяти, который определяет время его жизни. Значение величин, находящихся в объекте, определяется типом имени, которое используется для доступа к нему. 4.1. Видимость Существует три типа видимости: локальная, файловая и классовая. 255
Локальная видимость: В общем случае, имя, объявленное в блоке ($9.2) является локальным в этом блоке и может использоваться только в нем после точки объявления и в блоках, внутренних nb отношению к нему. Однако, метки ($9.12) могут использоваться в любом месте функции, в которой они объявлены. Имена формальных аргументов функции обрабатываются так, как если бы они были объявлены в блоке, внешнем по отношению к функции. Файловая видимость: Имя, объявленное вне любого блока ($9.2) или класса ($8.5), может быть использовано в файле, в котором оно объявлено: Классовая видимость: Имя элемента класса является локальным по отношению к своему классу и может использоваться в функции элемента этого класса ($8.5.2) после оператора . , примененного к объекту класса ($7.1) или после оператора ->, примененного к указателю на объект этого класса ($7.1). Можно ссылаться на ; статические элементы класса ($ 8.5.1) и элементы функций через оператор :: ($7.1), если имя соответствующих классов находится в пределах видимости. Класс, объявленной внутри класса ($8.5.15), не рассматривается как элемент и его имя попадает во вложенную область видимости. Имя может быть скрыто явным объявлением того же имени в блоке или классе. Имя в блоке или классе может быть скрыто только именем, объявленным во вложенном блоке или классе. Скрытое нелокальное имя по-прежнему может быть использовано, если его видимость описана оператором :: ($7.1). Имя класса, скрытое именем, не являющимся типом, по-прежнему может быть использовано, если ему предшествует ключевое с^лово class, struct или union ($8.2). Перечисляемое имя, скрытое именем, не являющимся типом, может быть' использовано, если ему предшествует ключевое слово enum ($8.2). 4.2. Определения Объявление является определением ($8), если только оно не объявляет функцию без описания ее тела ($10), не содержит спецификатора extern ($1) И инициализатора или тела функции или не является объявлением имени класса. 4.3. Установление связей Имя, имеющее файловую видимость и необъявленное явно как Статическое (static), является общим для всех файлов в /многофайловой Программе; таковы имена функций. Такие имена называются внешними. Любое объявление внешнего имени в программе ссылается на тот же объект ($5), функцию ($10), тип $(8.7), класс ($8.5), перечисление ($8.10) или реречислямую величину ($8.10). Типы, описываемые во всех объявлениях внешнего имени, должны быть Идентичными. Может быть более одного объявления типа, перечисления, inline-функции ($8.1) или константы (const), не входящей в состав агрегата ($8.2) при условии, что объявления идентичны, располагаются в разных файлах и все инициализаторы являются константными выражениями ($12). 256
Во всех остальных случаях в программе должно быть только одно определение внешнего имени. Для некоторых реализаций может потребоваться, йтобы константа, не входящая в состав агрегата и использованная там, где отсутствует определение константы (const), объявлялась бы явно как внешняя (extern) и определялась бы в программе только один раз. Аналогичное ограничение может налагаться и на inline-функции. 4.4. Классы памяти Существуют два объявляемых класса памяти: автоматический и статический. > О Автоматические объекты локальны для каждого вызова блока и уничтожаются при выходе из него. Статические объекты существуют й сохраняют свои значения b ходе работы всей программы. Некоторые объекты не ассоциированы с именами и их время жизни явно контролируется onepajopaMH new и delete (см. $7.2 й $9.14). Р- 4.5. Фундаментальные типы Объекты, объявленные как символьные (char), достаточно велики для того, чтобы сохранять любой элемент набора символов Данной реализации. Если символ из этого набора сохраняется как символьная переменная, его значение эквивалентно целочисленному коду этого симвова. Существуют три размера данных для целых чисел, объявленных как short int, int и long (короткое целое, целое и длинное целое). Более длинные целые числа требуют не меньше места для хранения, чем более короткие, однако, в зависимости от реализаций, короткие целые, или длинные целые или оба могут быть эквивалентны по длине обыкновенным целым. "Обыкновенные" целые имеют естественный размер, определяемый архитектурой главного компьютера; остальные размеры используются в специальных целях. Каждое перечисление ($8.10) является набором поименованных констант. Свойства данных типа enum аналогичны свойствам данных типа int. Беззнаковые целые, объявленные как unsigned, подчиняются законам арифметики по модулю 2П , где п - число бит в представлении. Числа с плавающей точкой одинарной точности (floafJ^H двойной точности (double) в некоторых реализациях могут быть синонимами. Поскольку объекты описанных типов обычно интерпретируются как числа, мы будем называть их арифметическими типами. Типы char, int всех размеров и enum в совокупности^будут называться целочисленными типами, float и double в совокупности будут называться типами с плавающей точкой. Тип void (пустой) описывает пустое множество значений. Значение (не существующёе) величины типа void не может использоваться никаким образом; нельзя применять ни явные, ни неявные1 преобразования. Поскольку выражения типа void обозначают не существующую величину, такие выражения можно использовать только в операторах выражения ($9.1) или как левые операнды выражения "запятая" ($7.15). Любое выражение можно преобразовать к типу void ($7.2). 257
4.6. Производные типы В принципе существует бесконечное количество производных типов, которые конструируются из фундаментальных типов следующими способами: массивы (array) из объектов данного типа; функции (function), принимающие аргументы таких типов и возвращающие объекты данного типа; указатели (pointer) на объекты данного типа; обращения (reference) к объектам данного типа; константы (constant), являющиеся величинами данного типа; классы (class), содержащие последовательность объектов различного типа, набор функций для работы с этими объектами и набор ограничений для доступа к этим объектам и функциям; структуры (structure), которые являются классами без ограничений для доступа; объединения (union), являющиеся структурами, которые могут содержать объекты различных типов в разное время. В общем случае, такие способы конструирования объектов можно использовать рекурсивно. Объект типа void’ (указатель на void) можно использовать для указания на объект неизвестного типа. 5. ОБЪЕКТЫ И ВЕЛИЧИНЫ LVALUE Объект - это область хранения, величина lvalue - это выражение, ссылающееся на объект. Очевидный пример выражения lvalue - имя объекта. Имеются операторы, которые выдают значения lvalue: например, если Е - выражение типа указатель, то ?Е - является выражением lvalue, которое ссылается на объект, на который указывает Е. Название "lvalue" происходит из выражения присваивания Ё1 =■ Е2, в котором левый операнд Е1 должен быть выражением lvalue. В последующих обсуждениях операторов в каждом случае отмечается, нуждается ли он в lvalue-выражении и выдает ли он значение lvalue. 6. ПРЕОБРАЗОВАНИЯ Действие ряда операторов, в зависимости от их операндов, может приводить к преобразованию значений операндов от одного типа к другому. В этом разделе приведены ожидаемые результаты таких преобразований. В разделе $6.6 суммированы. преобразования, вызываемые наиболее часто применяемыми операторами. В ходе обсуждения каждого оператора этот список будет Пополняться. В разделе $8.5.6 описаны преобразования, определяемые' пользователем. 6.1. Символы и целые Символ или целое типа short могут использоваться везде, где допускается использование целого. Во всех случаях значение преобразуется в целое. Преобразование целого типа short в long всегда включает в себя 258
знаковое расширение; целые являются знаковыми величинами. Будет ли проводиться знаковое расширение для символов или нет, зависит от машины; см. $2.6. Более явный тип unsigned char (беззнаковый символ) дает значения от 0 до машинно-зависимого максимума. На машинах, обрабатывающих символы как знаковые, символы ASCII-кода. всегда рассматриваются как положительные. Однако, символьная константа, описанная как восьмеричная, допускает знаковое расширение и может стать отрицательной; например, '\377' имеет значение -1. Если целое типа long преобразуется в целое типа short или char, оно обрезается слева; лишние биты просто отбрасывается. ‘ >,* .■'Ч' 6.2. Выражения типа float и double Для выражений типа float может использоваться обычная арифметика чисел с плавающей точкой. Преобразование чисел типа float в ч double математически корректны настолько, насколько это позволяют аппаратные средства.; ,6.3. Типы с плавающей точкой и целочисленные типы Преобразование величин с плавающей точкой в целочисленные типы является машинно-зависимым; в частности, направление . "усечения" отрицательных чисел меняется от машины к машине. Ес)ти величина не попадает в пространство, отводимое ей в памяти, результат преобразования не определен. Преобразование целочисленных типов в типы с плавающей точкой проходит нормально. Возможна некоторая потеря точности,, если для величины, являющейся итогом преобразования, не хватает бит. 6.4. Указатели и целочисленные типы Выражение целочисленного типа может прибавлено иди вычтено из указателя; в таком случае, целочисленное выражение преобразуется так, как это описано в обсуждении оператора сложения. Два указателя на объекты одного типа можно вычитать друг из „друга; в этом случае результат преобразуется в тип int или long, в зависимости от машины (см. $7.4). , 6.5. Величины без знака (unsigned) ч При комбинировании/ целого без знака и обычного целргРа обычное целое преобразуется в unsigned и результат является unsigned. Значением является наименьшее целое без знака, конгруэнтное целому со знаком (по модулю 2размер слова). В двоичном дополнительном представлении такое преобразование является концептуальным и изменений ,в битовой комбинации не происходит. Если целое без знака преобразуется к типу long, то результат численно равен значению целого без знака. Следовательно, преобразование сводится к дополнению нулями слева. 259
6.6. Арифметические преобразования Многие операторы вызывают преобразования и приводят к новым типам сходным образом. Такие преобразования будут называться "обычными арифметическими преобразованиями". < Во-первых, любой операнд . типа char, unsigned char или short преобразуются к типу int. Затем,.если какой-либо операнд имеет тип double, то второй операнд преобразуется к типу double; результат также имеет тип double. В ином случае, если один из операндов имеет тип unsigned long, то второй операнд преобразуется к типу unsigned long; результат также имеет тип unsigned long. Или, если один из операндов имеет тип long, то второй операнд ' преобразуется к типу long; результат также имеет тип long. Или, если один из операндов имеет тип unsigned, то второй операнд преобразуется к типу unsigned; результат также имеет тип long. В ином случае оба оператора должны иметь тип int; результат также имеет тип int. 6.7. Преобразования указателей Везде, где над указателями проводятся операции присваивания, инициализации, сравнения и т.д., можно производить следующие преобразования. Константа1 0 может быть преобразована в указатель, причем гарантируется, что эта величина даст в результате указатель, отличный от указателя на любой другой объект. Указатель любого типа может быть преобразован к типу void”. Указатель на класс может быть преобразован к указателю на общий базовый класс этого класса; см. $8.5.3. Имя вектора может быть преобразовано к указателю на его собственный первый элемент. Идентификатор, объявленный как "функция, возвращающая..." при использовании во всех случаях, кроме позиции вызова по функции- имени, преобразуется^ "указателю на функцию, возвращающую...". 6.8. Преобразования обращений При инициализации обращений можно проводить следующее преобразование. Обращение к классу может быть преобразовано к обращению на общий базовый класс этого класса; см. $8.6.3; 7. ВЫРАЖЕНИЯ Старшинство операторов выражения аналогично порядку расположения основных подразделов данного раздела. Так, например, выражения, которые рассматриваются как операнды для + ($7.4) - это выражения, определенные в $$7.i-7.4. В пределах каждого подраздела операторы имеют одинаковое старшинство. Для операторов, обсуждаемых в подразделе, в том же 260
подразделе указано направление ассоциативности (слева направо или справа налево). Старшинство и ассоциативность всех операторов выражений суммированы в разделе, посвященном "грамматике" ($14). Во всех остальных случаях порядок вычислений выражений не определен. В частности, компилятор вычисляет подвыражения в том порядке, который представляется ему наиболее эффективным, даже в том случае, если подвыражения имеют побочные эффекты. Порядок, в котором проявляются побочные эффекты, не определен. Выражения, включающие в себя коммутативные и ассоциативные операторы (', +, &, I, ^), могут перегруппировываться произвольно, даже при наличии скобок; для того, чтобы обеспечить правильный порядок вычислений, нужно использовать промежуточные данные. Обработка случаев переполнения и проверка деления в выражениях является машинно-зависимой. Большинство имеющихся реализаций C++ игнорирует целочисленное переполнение. Обработка случаев деления на 0 и всех исключительных ситуаций при рабрте с числами с плавающей точкой варьируется от машины к машине; согласование обычно достигается с помощью библиотечных функций. В дополнение к стандартным значениям, описанным в $7.2-15, операторы можно переназначить, придав им значения, которые они принимают при применении к типам, определенными пользователем; см. $7.16. Т" 7.1. Первичные выражения Первичные выражения, включающие ., ->, индексацию и вызов функции, группируются слева направо. список выражений: < выражение список выражений, выражение id: идентификатор имя функции оператора , typedef-имя :: идентификатор (typedef-имя - имя, определяющее тип). ■ typedef-имя :: имя функции оператора первичное выражение id < :: идентификатор константа строка this ( выражение ) первичное выражение [ выражение ] первичное выражение ( список выраженийор1 ) первичное выражение . id первичное выражение -> id Идентификатор является первичным выражением при условии, что он 261
был соответственно объявлен ($8). Имя функции оператора - это идентификатор со специальным значением; см. $7.16 и $8.5.11. Оператор за которым -следует идентификатор, имеющий файловую видимость - это то же, что и идентификатор. Он дает возможность ссылаться на объект даже в случае, если его идентификатор скрыт ($4.1)* typedef-имя ($8.8), за которым следует а затем идентификатор, является первичным выражением. Имя typedef должно обозначать класс ($8.5), а идентификатор должен обозначать элемент этого класса. Его тип описывается объявлением идентификатора, typedef-имя может быть скрыто именем, <не являющимся именем типа; но и в этом случае typedef-имя все таки можно найти и использовать. Константа - это первичное выражение. Константа может иметь тип int, long, float или double, в зависимости от вида константы. Строка является первичным выражёнием. Строка имеет тип "массив символов (char)". Обычно она немедленно преобразуется к указателю на свой первый символ ($6.7). Ключевое слово this является локальной переменной в теле функции элемента (см. $8.5; это указатель на объект, с которым работает функция. Выражение^ заключенное в скобки - это первичное выражение, тип и значение которого идентичны типу и значению самого выражения. От наличия скобок не зависит, является данное выражение Lvalue-величиной, или нет. Первичное выражение, за которым следует выражение, заключенное в квадратные скобки, является первичным, выражением.. Интуитивно такая запись представляется как индексация. Как правило, такое первичное выражение имеет тип "указатель на..."; выражение индексации имеет тип int, а тип результата - "...". Выражение Е1[Е2] идентично (по определению) выражению *((Е 1) + Е2)). Все ключи, необходимые для понимания такой нотации, содержатся в этом разделе; здесь же дано обсуждение идентификаторов, операторов ж и -+ ($7.1, $7.2 и $7.4 соответственно). В разделе $8.4.2 (см. ниже) суммирована общая идея такого подхода. Вызов функции является первичным выражением, за которым следует заключенный в скобки список выражений (возможно, пустой), разделенных запятыми. Этот список содержит фактические аргументы функции. Первичное выражение должно иметь тип "функция, возвращающая..." или "указатель на функцию, возвращающую...", а результат вызова функции - тип "...". Каждый формальный аргумент инициализируется ($8.6) своим фактическим аргументом. Проводятся стандартные, ($6.6-8) преобразования и преобразования, определяемые пользователем ($5.6). Функция может изменять значения своих формальных аргументов, но эти изменения не могут влиять на значения фактических аргументов, за исключением тех случаев, когда формальный аргумент имеет тип обращения ($8.4). Функцию можно объявить таким образом, чтобы она принимала больше или меньше аргументов, чем это указано в объявлении функции ($8.4). Любой фактический аргумент типа float, для которого отсутствует формальный аргумент, перед вызовом преобразуется к типу double; аргумент типа char или short преобразуется к типу int и, как обычно, имена массивов преобразуются, в указатели. Порядок вычисления аргументов не определяется в языке; имейте в виду, что компиляторы отличаются друга от друга. Разрешены рекурсивные вызовы любой функции. Первичное выражение, за которым следует вначале точка, а затем 262
идентификатор (или идентификатор, определенный typedef-именем с использованием оператора является выражением. Первое выражение должно быть объектом класса, а идентификатор - именем элемента этого класса. Значением является значение названного элемента объекта и, если первое выражение является Lvalue-выражением, то и значение его является lvalue-значением. Обратите внимание на то, что "объектами класса" могут быть структуры. ($8.5.12) и объединения ($8.5.13). ' Первичное выражение, за которым следует вначале стрелка (->), а затем идентификатор (или идентификатор, определенный typedef-именем с использованием оператора ::), является выражением. Первое выражение должно быть указателем на объект класса, а идентификатор - именем элемента этого класса. Результат представляет собой lvalue-величину, относящуюся к названному элементу класса, на который указывает выражение указателя. Таким образом, выражение El -> MOS эквивалентно выражению ('EI).MOS. Классы обсуждаются в разделе $8.5. Если выражение имеет тип "ссылка на..." (см. $8.4 и $8.6.3), то значение выражения является объектом, на который делается ссылка. Ссылка может рассматриваться как имя. объекта; см. $8.6.3. 7.2. Унарные операторы Выражения, содержащие унарные операторы, группируются справа налево. Унарное выражение: выражение унарного оператора выражение + + выражение — - ) х выражение sizeof sizeof ( имя типа ) (имя типа) выражение имя простого типа ( список выражений ) new имя типа инициализатор^ new ( имя типа ) delete выражение delete [ выражение ] выражение Унарный оператор: один из А + - ! ~ + + - Унарный оператор ’ означает косвенную адресацию: выражение должно быть указателем. Результат является lvalue-величиной, относящейся к объекту, на который указывает выражение. Если тип выражения - "указатель на ..." то результат будет иметь тип Результатом действия унарного оператора А будет указатель на объект, на который ссылается операнд. Операнд должен быть lvalue-величиной. Если тип выражения - "...", то результат будет иметь тип "указатель на ...". Результатом действия оператора + является значение его операнда после того, как проведены обычные арифметические преобразования. Операнд должен иметь арифметический тип. 263
Результатом действия оператора - является величина, обратная по знаку значению его операнда. Операнд должен иметь арифметический тип. Осуществляются обычные арифметические преобразования. Величина, обратная по знаку беззнаковому числу, вычисляется вычитанием его значения 'из 2", где п - количество бит в числе типа int. Результатом действия оператора логического отрицания ! является 1, если значение операнда равно 0, и 0 - если операнд имеет.ненулевое значение. Результат имеет тип int. Оператор 1 применим к любому арифметическому типу или к указателям. Оператор ~ производит дополнение своего операнда до 1. Осуществляются обычные арифметические преобразования. Операнд должен быть целочисленного типа. 7.2.1. Инкремент и декремент Операнд префикса + + увеличивается/ Операнд должен быть lvalue- величиной. Значением является новое значение операнда, но оно не является lvalue-значением. Выражение + +х эквивалентно х+ =1. Информация о преобразованиях дана в разделах, посвященным обсуждению операторов сложения ($7.4) и присваивания ($7.14). Аналогичным образом уменьшается операнд префикса —. Величина, получаемая при применении постфиксного + + - это величина самого операнда. Операнд должен быть lvalue-величиной. После получения результата объект увеличивается так же, как и в случае префиксного оператора + +. Результат имеет тот же тип, что и операнд. Величина, получаемая при применении постфиксного это величина самого операнда. Операнд должен быть .lvalue-величиной. После получения результата объект уменьшается так же, как и в случае префиксного оператора —. Результат имеет тот же тип, что и операнд. 7.2.2. Sizeof Оператор sizeof выдает, размер своего операнда в байтах. (Байт не определяется в яз^же; исключение составляет способ выражения sizeof. Однако, во всех существующих реализациях байт - это объем памяти, не обходимый для хранения объекта типа char). Если оператор применяется к массиву, то результат представляет собой общее число битов в массиве. Размер определяется исходя из объявлений объектов в выражении. Семантически такое выражение является константой без знака (unsigned) и может использоваться везде, где требуется использовать константу. Оператор sizeof может применяться также к имени типа, которое взято в круглые скобки. В таком случае он дает размер (в байтах) объекта указанного типа. 7.2.3. Явное преобразование типа Заключенное в круглые скобки имя простого типа (скобки необязательны) ($8.2), за которым следует заключенное в круглые скобки выражение (или список выражений, если тип является классом с соответственно объявленным конструктором $8.5.5), вызывает преобразование значения выражения к указанному типу. Для преобразования к типу с не простым именем имя типа ($8.7) должно быть взято в круглые скобки. Если имя типа взято в 264
скобки, выражение заключать в скобки необязательно. Такая конструкция называется cast-конструкцией. Указатель можно , явно преобразовать к любому целочисленному типу, размер которого достаточно велик для того, чтобы записать его. Потребуется ли. для этого тип int или long, зависит от .машины. Функция преобразования данных также является машинно¬ зависимой, но для тех, кто знает адресную-структуру машины, в ней нет ничего неожиданного. Для некоторых конкретных машин в $2.6 приведены детали. “ Объект целочисленного типа можно явно преобразовать в указатель. Функция преобразования данных всегда превращает целое, полученное преобразованием указателя обратно в тот же указатель, но в иных случаях она является машинно-зависимой. Указатель на какой-либо тип можно явно преобразовать в указатель на другой тип. При использовании полученного таким образом указателя могут возникнуть аварийные ситуации адресации, связанные с тем, что указатель ссылается на объект, не выравненный в памяти,. Гарантируется, что указатель на объект данного размера можно преобразовать в указатель на объект меньшего размера и обратно без изменений. Разные машины могут отличаться по числу бит в указателях и по требованиям выравнивания для объектов. Агрегаты данных строго выравниваются^ по точным границам, которые необходимы для любого из компонентов агрегата. Объекг можно преобразовать в объект класса, только в том случае^ если был объявлен соответствующий конструктор или оператор преобразования ($8.5.6). Объект можно явно преобразовать в тип обращения по адресу Х& в случае, если указатель на этот объект можно явно преобразовать в X*. 7.2.4. Свободная память Оператор new создает объект имя типа, к которому он применен. Время жизни объекта, созданного с помощью оператора new, не ограничивается областью, в которой он создан. Оператор new возвращает указатель на объект, который он создает. Если этот объект является массивом, возвращается указатель на его первый элемент. Например, и new int, и new int[10] возвращают int". Для определенных объектов класса можно применить инициализатор ($8.6.2). Для того, чтобы зарезервировать память, оператор new ($7.2) вызовет функцию о void" operator new(long); Аргумент определяет количество необходимых битов. Запоминаемая величина не инициализируется. Если operator new не в состоянии выделить нужное количество памяти, он возвращает 0. Оператор delete уничтожает объект, созданный оператором new. Результатом работы является тип void. Операнд delete должен быть указателем, который возвращал оператор new. Эффект применения оператора delete к указателю, полученному не в результате действия оператора new, не определен. Однако, уничтожение указателя, имеющего значение 0, безопасно. Для того, чтобы очистить память, оператор delete вызывает функцию void operator delete(void’); 265
В форме delete [выражение] выражение второе выражение указывает на вектор, а первое выражение представляет собой количество элементов в этом векторе. Описание количества элементов излишне, за исключением случая уничтожения векторов определенных классов; см. $8.5.8. 7 3. О п е ратор ц у м н оже н и я Операторы умножения “, / и % группируются слева ' направо. Осуществляются обычные арифметические преобразования. выражение умножения выражение “ выражение выражение / выражение выражение % выражение Бинарный оператор “ обозначает операцию умножения. Оператор “ ассоциативен и компилятор может перегруппировывать выражения с несколькими операторами умножения, расположенными на одном уровне. Бинарный оператор / обозначает операцию деления. При делении положительных целых усечение производится в направлении к'0, однако если один из операндов Отрицателен, то форма усечения является машинно¬ зависимой. На всех машинах, упоминаемых в настоящем руководстве, остаток имеет тот же знак, что и делимое. Всегда: верно то, что (а/Ь)’Ь + а%Ь равно а (если Ь не равно 0). Результатом действия бинарного оператора % является остаток от деления рервого выражения на второе. Производятся- обычные арифметические преобразования. Операнды не могут принадлежать к типу с плавающей точкой. 7.4. Операторы сложения Операторы сложения + и - группируются слева направо. Производятся обычные арифметические преобразования. Для- каждого оператора есть некоторые дополнительнее возможности, в зависимости от типа. выражение, сложения: выражение + выражение выражение - выражение Результатом действия Оператора + является сумма операндов. . Мджно складывать указатель на объект, содержащийся в массиве и величину любого целочисленного типа. Последняя во всех случаях преобразуется к адресному смещению умножением ее на длину объекта, на который указывает указатель. Результатом является указатель того же типа, что и исходный указатель; новый указатель указывает на другой объект в том же массиве, соответственно смещенный от исходного объекта. Так, если Р является указателем на объект в массиве, выражение Р + 1 является указателем на 266
следующий объект в массиве. Никакие другие комбинации типов для указателей не допускаются. Оператор + ассоциативен и компилятор может* перегруппировывать выражения с несколькими операторами сложения, расположенными на одном уровне. Результатом действия оператора - является, разность операндов. Производятся обычные арифметические преобразования. Кроме того, величину любого целочисленного типа можнд вычесть из указателя, а затем произвести те же преобразования, что и в случае сложения. Если друг из друга вычитаются два указателя на объектыодного типа, результат преобразуется (делением на длину объекта) к целому, представляющему собой количество объектов, разделяющих объекты, на которые указывают, указатели, В зависимости от машины, получаемое целое может иметь тип iftt или long; см. $2.6. В общем случае, такое преобразование дает неожидаемые результаты, если только указатели не указывают на объекту в одном и том же массиве, поскольку указатели, даже на объекты одного , типа, не обязательно различаются на величину, кратную длине объекта. 7.5. Операторы сдвига Операторы •, сдвига >> и << группируются слева направо. Оба производят обычные арифметические преобразования со своими операндами, которые должны быть / целочисленными. Затем правый операнд преобразуется в тип int; результат имеет тип левргр операнда. Белправый операнд отрицателен, больше или равен длине объекта в битах, результат ^действия операторов не определен. выражение сдвига: < выражение < < выражение выражение > > выражение ...... Значением Е1 << ,Е2 является Е1 (интерпретируемое как битовая комбинация), разряды которого сдвинуты влево - нд Е2 бит. Освобождающиеся разряды заполняются нулями. Значением е! > > Е2 является Е1, разряды которого сдвинуты вправо на Е2 би^т. Если Е1 имеет тип unsigned, то гарантируется,/;,что сдвиг вправо будет логическим (заполнение нулями),; в ином случае он можети быть арифметическим (заполнение копиями знакового бита). 7.6. Операторы отношения Операторы отношения группируются слева направо, однако это представляется малополезным; а<Ь<с не означает тфго, на что оно похоже. выражение отношения; выражение < выражение выражение > выражение выражение < = выражение, выражение > = выражение 267
Результатом действия операторов < (меньше, чем), > (больше, чем), < = (меньше или равно) и > = (больше или равно) является 0, если указанное соотношение неверно и 1, если оно верно. Результат имеет тип int. Произ¬ водятся обычные арифметические преобразования. Можно сравнивать два указателя; результат сравнения зависит от относительного расположений в адресном пространстве объектов, на которые они указывают. Сравнение указателей переносимо лишь в случае, если указатели указывает на объекты одного и того же массива. 7.7. Операторы равенства выражение равенства: выражение = = выражение выражение ! = выражение Операторы = = (равно) и ! = (неравно) совершенно аналогичны операторам отношения, за исключением того, что они имеют более низкое старшинство. Следовательно, a<b = = c<d равно 1, если а<Ь и c<d имеют одинаковое значение. Указатель можно сравнивать с 0. 7.8. Оператор "поразрядное И" выражение И: выражение & выражение Оператор & ассоциативен и выражения, включающие в себя этот оператор, могут перегруппировываться. Производятся обычные арифметические преоб¬ разования; результатом является поразрядная функция И, примененная к операндам. Оператор применим только к целочисленным операндам. 7.9. Оператор "поразрядное исключающее ИЛИ" выражений "исключающее ИЛИ": выражение * выражение Оператор л ассоциативен и выражения, включающие в себя этот оператор, могут перегруппировываться. Производятся обычные арифметические преоб¬ разования; результатом является поразрядная функция "исключающее ИЛИ", примененная к операндам. Оператор применим только к целочисленным операндам. 7.10. Оператор "поразрядное включающее ИЛИ" выражение "включающее ИЛИ": выражение' I выражение Оператор I ассоциативен и выражения, включающие в себя этот оператор, могут перегруппировываться. Производятся арифметические преобразования; результатом является поразрядная функция "включающее ИЛИ",примененная к операндам. Оператор применим только к целочисленным операндам. 268
7.11. 'Оператор "логическое И" выражение "логическое И" выражение && выражение Оператор && группируется слева направо. Он возвращает 1, если оба его операнда имеют ненулевое значение; во всех остальных случаях возвращается 0. В отличие от оператора &, && гарантирует вычисление слева направо; более того, второй операнд не вычисляется, если первый операнд равен 0. Операнды могут иметь различные типы, но они должны принадлежать к одному из фундаментальных типов или указателями. Результат всегда имеет тип int. 7.12. Оператор "логическое ИЛИ" выражение "логическое ИЛИ" выражение 11 выражение Оператор II группируется слева направо. Он возвращает 1, если рдин из его операндов имеет ненулевое значение; во всех остальных случаях возвращается 0. В отличие от оператора I, II гарантирует вычисление слева направо; более того, второй^ операнд не вычисляется, если первый операнд не равен 0. ; Операнды могут иметь различные типы, но онй должны принадлежать к одному из фундаментальных типов или указателями. Результат всегда имеет тип int. 7Л 3. Оператор условия .... выражение условия: выражение 1 выражение : выражение Выражения условия группируются справа налево. Вычмсляется значение первого выражения и, если оно ненулевое, результатом являетс^Чзначение второго выражения; в ином случае, результатом является значение третьего выражения. Для приведения второго и третьего выражений к общему типу используются, если это возможно, обычные арифметические преобразования. Для приведения второго и третьего выражений к общему типу используются, если это возможно, преобразования указателя. Результат имеет общий тип; вычисляется либо только второе, либо только третье выражения. 7.14. Операторы присваивания В языке существует несколько операторов присваивания; все они группируются справа налево. В качестве левого операнда для всех этих операторов должна использоваться только lvalue-величина; она может не являться константой (именем массива, именем функции или величиной, 269
объявленной как const). Значением выражения является значение первого операнда после выполнения присваивания. выражение присваивания: выражение, оператор присваивания выражение оператор присваивания: один из = += -= '= /= %= >>= <<= &= = 1 = При простом присваивании с помощью оператора = значение выражения заменяется значением объекта, которому соответствует операнду стоящий в левой части выражения. Если оба операнда имеют арифметический тип, то до присваивания тип правого операнда преобразуется к. типу левого. Если аргумент в левой части имеет тип "указатель", то операнд в правой части должен быть того же типа или типа, который может быть преобразован К типу аргумента из левой части выражения; см. $6.7. Оба операнда могут быть объектами одного и того же класса. К объектам некоторых производных классов оператор присваивания неприменим; см. $8.5.3. Применение оператора присваивания к объекту типа "обращение к ..." присваивает значение объекту, обозначенному обращением. Действие выражения Е1 оператор - Е2 можно представить как эквивалентное El = Е1 оператор (Е2); однако, Е1 вычисляется лишь один раз. В операторах += и -=• левый операнд может быть указателем; в этом случае (целочисленный) правый операнд преобразуется так, как это объяснено в $7.4; все правые операнды и левые операнды, не являющиеся указателями, должны иметь арифметический тип. 7.15. Оператор "запятая" выражение запятая: выражение , выражение Пара выражений, разделенных запятой, группируется слева направо; значение первого выражения отбрасывается. Результат имеет тип и значение правого операнда. Этот оператор группируется слева направо. В контексте, где запятая имеет специальное' значение, например, в списке фактических аргументов функций (7.1) и в списке инициализаторов ($8.6), оператор "запятая" может появляться только в скобках (как это описано в настоящем разделе. Например: f(a, (t = 3,t + 2),с) имеет три аргумента, причем второй из них равен 5. 7.16. Переназначенные операторы Большинство операторов можно переназначить, то есть, объявить таким образом, чтобы оператор мог использовать объекты класса в качестве операндов, (см. $8.5.11). Старшинство операторов, а также значение операторов, применяемых к объектамне классам, изменить нельзя. 270
Предварительно определенное значение операторов = и (унарного) &, применяемых к объектам-классам, изменять можно. Тождественность операторов, применяемых к основным типам (например, а++ ё а+=1) может не сохраняться для операторов, примененных к типам класса. Если некоторые операторы, например, оператор присваивания, применяются к основным типам, то требуется, чтобы операнд был lvalue-величиной; это необязательно в случае, если операторы объявлены для типов класса. 7.16.1. Унарные операторы Унарный оператор, в префиксной или постфиксной форме, может быть определен как функция-элемент (см. $8.5.4), которая не имеет аргументов или как friend-функция (см. $8.5.10), которая принимает один аргумент но не двумя способами одновременно. Следовательно, для любого унарного оператора как x@f так и @х можно интерпретировать либо как х.орега+ог@(), либо как орега+ог@(х). Если операторы + + и — переназначены, то отличить префиксную форму от постфиксной невозможно. 7.16.2. Бинарные операторы Бинарный оператор можно определить либо как функцию-элемент с одним аргументом, либо как friend-функцию с двумя аргументами, но не обоими способами сразу. Следовательно, для любого бинарного оператора @, как х@у можно интерпретировать либо как x.operator@(y), либо как operafor@(x,y). 7.16.3. Специальные операторы Вызов функции первичное выражение ( список выражений^ ) и индексация первичное выражение [ ^ыражение ] рассматриваются как бинарные операторы. Именами определяемых функций являются орега+ог() и operator[], соответственно. Так, вызов х (arg) интерпретируется как x.operator()(arg) для объекта класса х. Индексация х[у] интерпретируется как x.operator[](y). 8. ДЕКЛАРАЦИИ (ОБЪЯВЛЕНИЯ) Декларации (объявления) используются для того, чтобы указать способ интерпретации каждого идентификатора; при этом память, связанная с использованием идентификатора может не резервироваться. Декларации имеют следующую форму: декларация: спецификаторы декларации^ список деклараторовО)>1; 271
декларация имени asm-декларация Деклараторы в списке деклараторов содержат объявляемые идентификаторы. Спецификаторы декларации можно опускать только при определениях внешних функций ($10) или при объявлениях внешних функций. Список деклараторов может быть пустым только при объявлении класса ($8.5) или перечислений ($8.10), то есть, в случаях, если спецификатор является спецификатором класса или спецификатором перечисления (тип enum). Декларации имен описаны в $8.8; asm-декларации описаны в $8.11. спецификатор декларации; спецификатор класса памяти спецификатор типа спецификатор функции friend typedef спецификаторы декларации: спецификатор декларации спецификаторы декларации^, Список должен быть самосогласован так, как это описано ниже. 8.1. Спецификаторы класса памяти Спецификаторы "класса памяти" таковы: спецификатор класса памяти: auto (автоматический) static (статический) extern (внешний) register (регистровый) Декларации, в которых используются спецификаторы auto, static и register, являются также определениями; при этом они резервируют необходимое количество памяти. Если объявление extern не является определением ($4.2), то где-нибудь в другом месте должно быть определение этого идентификатора. Декларация register проще всего рассматривать через декларацию auto; при этом компилятор получает указание на то, что объявленные переменные будут интенсивно использоваться. Компилятор может игнорировать это указание. К таким переменным нельзя применять оператор адресации &. Спецификаторы auto и register можно применять только' для имен объектов, объявленнных в блоке и для формальных аргументов. Если в декларации опущен спецификатор памяти, то по умолчанию переменная считается автоматической внутри функции и статической вне ее. Исключение: функции никогда не бывают автоматическими. Спецификаторы static и extern можно использовать только для имен объектов или функций. Некоторые спецификаторы можно использовать только при декларациях функций: 272
спецификаторы функции: / overload inline virtual Спецификатор overload дает возможность обозначить несколько функций одним . именем; см. $8.9. Спецификатор inline - это лишь подсказка компилятору. Он не влияет на значение программ и может игнорироваться. Он указывает на то, что вместо .обычной реализации вызова функции лучше использовать последовательность реализующих функцию команд. Функция ($8.5.2 и $8.5.10), определенная внутри объявления класса, по умолчанию имеет тип inline. Спецификатор virtual можно использовать только пру объявлениях элементов класса; см. $8.5.4. Спецификатор,, friend используется для обхода правил "скрывания имени" для элементов класса и может применяться только внутри объявления класса; см. $8.5.10. Спецификатор typedef используется для определения имени типа; см. $8.8. 8.2. Спецификаторы типа Спецификаторами типа являются: спецификатор типа: имя простого типа спецификатор класса спецификатор перечисления спецификатор сложного типа const (константа) Слово cons,t можно добавить к .любому разрешенному спецификатору типа. В иных случаях, в объявлении может быть приведен только один спецификатор типа. Объект Tnna^onst не является lvalue-величиной. Если в объявлении спецификатор типа опущен, принимается, что объект имеет тип int. имя простого типа: ) typedef-имя > char short int long unsigned float .■double,. ? void /. Слова long, short и unsigned можно рассматривать как прилагательные. Их можно использовать в сочетании с int; unsigned можно использовать с char, short и long.
Спецификаторы класса и перечисления рассмотрены в $8.5 и $8.10 соответственно. спецификаторы сложного типа: ключ typedef-имя ключ идентификатор ключ: class struct union enum Спецификатор сложного типа можно использовать для обращения к имени класса или перечисления там, где имя могло быть скрыто локальным именем. Например: class х { . . . };. void f(int х) { class х а; // ... } Если имя класса или перечисления ранее не было объявлено; то спецификатор сложного типа действует как объявление имени; см. $8.8. 8.3. Деклараторы Список деклараторов, вставленный в объявление - это последовательность деклараторов, разделенных запятыми; каждый из них может иметь инициализатор. список деклараторов: декларатор инициализатора декларатор инициализатора , список деклараторов декларатор инициализатора: декларатор инициализатора^ Инициализаторы обсуждаются в $8.6. Спецификаторы в декларации обозначают тип и класс памяти объектов, на которые ссылаются деклараторы. Деклараторы имеют синтаксис: декларатор: имя декларатора ( декларатор ) " const декларатор & consQ, декларатор декларатор (список объявлений аргументов) 274
декларатор [ константное выражениеор1 ] простое имя декларатора: простое имя декларатора typedef-имя :: простое имя декларатора простое имя декларатора: идентификатор typedef-имя - typedef-имя имя функции-оператора имя функции-преобразования ■г Группировка проводится так же, как в выражениях. 8.4. Значение деклараторов Каждый декларатор является утверждением; если в выражении появляется конструкция той же формы, что и декларатор, образуется объект указанного типа и класса памяти. Каждый декларатор содержит строго одно имя декларатора; оно описывает объявленный идентификатор. За исключением случаев декларации некоторых специальных функций ($8.5.2), имя декларатора является простым идентификатором. Если в качестве декларатора используется идентификатор без каких-либо добавлений, то он имеет тип, описанный спецификатором, стоящим в заголовке декларации.. Декларатор в скобках идентичен обычному декларатору, нр порядок объединения сложных деклараторов может быть изменен с помощью скобок; см. примеры, приведенные ниже. Теперь предположим, что мы имеем декларацию Т DI Q в котором Т является спецификатором типа (как int и т.д.), a D1 - декларатором. Предположим, что идентификатор, получаемый в результате такой декларации имеет тип " ...Т"; где "А." пусто, если D1 - простой идентификатор (так, что типом х в "int х" является просто int). Тогда, если D1 имеет вид “D то типом содержащегося в декларации идентификатора будет указатель на Т". Если D1 имеет вид ' const D то содержащийся в объявлении идентификатор имеет тип ”... указатель-константа на Т", то есть, тот же тип, что и ’D; однако такой идентификатор не является lvalue-величиной. Если D1 имеет вид 275
&D или & const D то идентификатор имеет тип "... обращение к Т". Поскольку обращение, по определению, не может быть lvalue-величиной, использование слова const является излишним. Обращение к void (void&) невозможно. Если D1 имеет вид D( ^список деклараций аргументов ) то идентификатор имеет тип "... функция, использующая аргументы типа список деклараций аргументов и возвращающая Т". список деклараций аргументов: список деклараций аргументов . ... спи 'к деклгргй аргументов: список деклараций аргументов , декларация аргумента декларация аргумента декларация аргумента спецификаторы декларации спецификаторы декларации спецификаторы декларации спецификаторы декларации декларатор декларатор = выражение абстрактный декларатор абстрактный декларатор = выражение Если список . деклараций аргументов заканчивается многоточием, то количество аргументов функции не определяется; аргументов не должно быть меньше; чем это описано в списке. Если список пуст, то функция не использует, аргументы. Все объявления для функции должны точно согласовываться как по типу возвращаемого значения, так и по количеству и типу аргументов. Список объявлений аргументов используется для проверки и преобра¬ зования фактических аргументов при вызовах функций и для проверки присваиваний значений указателю на функцию- Если вчУбъявлении аргумента описано в: ччие то это выражение является аргументом функции по умолчанию. Аргументы по умолчанию используются при вызовах функции и в том случае, если аргументы, заключающие список, опущены. Аргументы по умолчанию нельзя переопределить последующим объявлением. Однако, с помощью объявления можно пополнить список аргументов по умолчанию, аргументами, которые не были объявлены ранее. f Идентификатор может являться именем аргумента (дополнительно). Если он расположен в объявлении функции, использовать его нельзя, так как рн немедленно выходит за пределы видимости. Если он расположен в определении функции, то он является формальным аргументом. Если D1 имеет вид D[ константное выражение ] или О[] 276
то содержащийся в объявлении идентификатор имеет тип "... массив из Т". В первом случае константное выражение - это выражение, значение которого определено в момент компиляции; выражение должно иметь тип int. (Константные выражения определены в $12). Если несколько описаний "массив из" слиты, то создается многомерный массив; константные выражения, описывающие границы массивов, можно опустить только для первого элемента последовательности. Такая элизия полезна в том случае, когда массив является внешним и действительное определение, которое отводит место в памяти, расположено в другом месте. Первое, константное выражение также можно опустить в случае, если за декларатором следует инициализация. В таком случае размер вычисляется, исходя из количества инициализирующих элементов. Можно создать массив одного из базовых типов, указателей, структур или объединений или другого массива (для получения многомерного массива). Разрешаются не все возможные комбинации описанных выше синтаксических правил. Ограничения таковы: функции не могут возвращать массивы или функции, хотя они могут возвращать указатели на них; нельзя создавать массивы функций, хотя можно создавать массивы указателей на функции. 8.4.1. Примеры. Декларации int i, “pi, f(), ‘fpi(), ('pif)O; объявляют целое i, указатель pi на целое, функцию f, возвращающую целое, функцию fpi, возвращающую указатель на целое и указатель pit на функцию, которая возвращает целое. Особенно полезно сравнить последние две декларации. Mfpi() означает *(fpi()), так что декларация предполагает вызов функции fpi, а затем использование операции косвенной адресации через указатель для получения целого числа; аналогичная конструкция требуется и в выражении. В деклараторе (“pif)() необходимы дополнительные скобки; таким образом определяется, что операция косвенной адресации через указатель на функцию дает на выходе функцию, которая и вызывается в дальнейшем. Функции f и fpi объявлены как функции без аргументов, а pit указывает на функцию без аргументов. Декларации const а = 10, “рс = &а, “const срс = рс; int b, “const ср = &Ь; объявляют а - целую константу, рс - указатель на целую константу, срс - указатель-константу на целую константу, b - целое и срр указатель-константу на целое. Значения а, срс и ср после инициализации изменять нельзя. Значение рс можно изменять, так же, как и объект, на который указывает ср. Примеры запрещенных операций: а = 1; а + +; “рс = 2; ср = &а; срс + + ; 277
Примеры разрешенных операций: Ь .= а; "ср = а; рс + +; рс = срс; Декларация fseek(FILE", long, int); объявляет функцию, использующую три аргумента указанного типа. Поскольку тип возвращаемого значения' не описан, принимается, что оно имеет тип int (8.2). Декларация point (int = 0, int = 0); объявляет функцию, вызываемую с одним или двумя аргументами или вообще без аргументов; аргументы имеют тип int. Ее можно вызвать одним из трех способов: point(1,2); point(l); point(); Декларация printf(char' ...); объявляет функцию, которую можно вызвать с различным числом аргументов разного типа. Например: printf("nPHBeT"); printf("a = %d b = %d", a, b); Однако, в качестве первого аргумента она всегда должна иметь аргумент типа char. Декларация float fa[17], “afp[17]; объявляет массив чисел типа float и массив указателей на числа типа float. Наконец, static int x’3d[3][5][7]; объявляет статический трехмерный массив целых, который имеет ранг 3x5x7. Если разобраться в деталях, то x3d представляет собой массив из трех элементов; каждый из них является массивом из пяти элементов, каждый из которых, в свою очередь, является массивом из семи элементов. Любое из следующих выражений - x3d, x3d[i], x3d[i][j], x3d[i][j][k] - может использоваться в выражениях. 8.4.2. Массивы, указатели и индексация Каждый раз, когда в выражении появляется идентификатор массива, он 278
преобразуется в указатель на первый элемент массива. Такое преобразование приводит к тому, что массивы не могут быть lvalue-величинами. Оператор индексации [] всегда интерпретируется так, что Е1[Е2] идентично “((Е1) + (Е2)); исключение составляет случай, когда этот оператор объявлен для класса ($7.16.3). В соответствии с правилами преобразований, применяемыми к +, если Е1 является массивом, а Е2 - целым, то Е1[Е2] ссылается на Е2-ой элемент Е1. Следовательно, несмотря на кажущуюся асимметричность, индексация является коммутативной операцией. Это правило последовательно действует и в случае многомерных массивов. Если Е - n-мерный массив ранга i х j х .... х к, то Е в составе* выражения преобразуется в указатель на (п-1)-мерный массив ранга j х... х к. Если к этому указателю применяется оператор “ (явно или неявно, как результат индексации), то результат указывает на (п-1)-мерный массив, который, в свою очередь, немедленно преобразуется в указатель. Например, рассмотрим int х[3][5]; В данном примере х представляет собой массив целых размером 3x5. Если х включается в выражение, то он преобразуется в указатель на (первый из трех) 5-элементный массив целых. В выражении x[i], которое эквивалентно "(х + i), х вначале преобразуется в указатель, как это описано выше. Затем х + i преобразуется к типу х; этот процесс включает в себя умножение i на длину объекта, на который указывает указатель, а именно, на 5 целых объектов. Результаты складываются и применяется операция косвенной адресации, в результате чего получаемся массив (из 5 целых), который, в свою очередь, преобразуется в указатель на первое из целых. Если есть другой индекс, вновь применяется тот же аргумент;' в этот раз результат является целым. Из всего этого следует, что массивы в C++ хранятся построчно (последний индекс изменяется быстрее всех) и что первый индекс в объявлении помогает определить количество памяти, необходимое для хранения массива, но не играет никакой роли в индексных вычислениях. 8.5. Объявления классов Класс - это тип. Его имя становится typedef-именем (см.8.8), которое можно использовать даже в пределах собственно спецификатора класса. Объекты-классы состоят из последовательности элементов. спецификатор класса: заголовок класса { список элементовор1 } заголовок класса { список элементов^ public : список элементов^,} заголовок класса: идентификатор агрегата^ идентификатор агрегата : public0AW typedef-имя агрегат: class (класс) struct (структура) 279
union (объединение) Объектам класса можно присваивать значения; они могут использоваться как аргументы функций и возвращаться функциями (за исключением объектов некоторых производных классов; см. 8.5.3). Пользователь может определить и другие операторы (например сравнение на равенство; см. $8.5.11). Структура - это класс, в котором все элементы являются общими; см. $8.5.9. Объединение - это структура, в которой в каждой момент времени содержится только один элемент; см. $8.5.13. В списке элементов можно объявить данные, функцию, класс, перечисление, элементы поля и "дружественные элементы" ($8.5.10). Список элементов может содержать также объявления, согласующие видимость имен элементов; см. $8.5.9). список элементов: декларация элемента список элементовор1 декларация элемента: спецификаторы-деклараторыор1 декларатор элемента; определение функции ;ор, декларатор элемента: декларатор идентификатор^ : константное выражение Элементы, которые являются объектами класса, должны быть объектами ранее объявленнкных классов. В частности, класс с1 не может содержать объект класса cl, но он может содержать указатель на класс cl. Простой пример объявления структуры: struct tnode { char tword[20]; int count; tnode "left; tnode “right; } i Эта структура содержит массив из 20 символов, целое и два указателя на аналогичные структуры. После того, как объявление сделано, объявление tnode s, “sp; означает, что s имеет тип tnode, a sp является указателем на tnode. При использовании таких объявлений sp—> count ссылается на поле count структуры, на которую указывает sp; s.left ссылается на указатель поддерева left в структуре s, а 280
s.right->tword[0] ссылается на первый символ элемента tword поддерева right в структуре s. 8.5.1. Статические элементы Элементы-данные класса могут быть статическими (static), но элементы- функции - нет. Элементы не могут принадлежать к классу auto, register или extern. Только одна копия статического элемента используется совместно всеми объектами класса в программе. К статическому объекту mem класса cl можно обратиться с помощью c1::mem, то есть, не обращаясь к объекту. Он существует даже в/том случае, если не было создано ни одного объекта класса cl. Для статического элемента нельзя описать инициализатор и он не может быть элементом класса с конструктором. 8.5.2. Функции-элементы Функция, объявленная как элемент (без'хпецификатора friend’; см. 8.5.10) называется функцией-элементом и вызывается с использованием синтаксиса для элементов класса. Например: struct tnode { char tword[20]; int count; tnode "left; tiiode ’right; * void set(char’, tnode’ I, tnode" г); О }; tnode hl, n2; nl .set(''asdf”, &n2,0); / n2.set("ghjk",0,0); Определение функции-элемента должно находиться в области видимости ее класса. Это означает, что она может непосредственно использовать имена своего класса. Е^ли определение функции-элёмента лексически расположено вне объявления класса, то имя функции-элемента должно уточняться именем класса с использованием оператора Определения функций обсуждены в $10. Например: void tnode: :set(char" w, tnode’ I, tnode’ r) { count = strlen(w); if (sizeof(tword) < = count) еггог("строка tnode слишком длинна")»’ strcpy(tword,w); left = I; right = r; } . Запись tnode: :set указывает на то, что функция set является элементом 281
класса tnode и находится в его области видимости. Имена элементов tword, count, left и right ссылаются на объекты, для которых вызывается функция. Следовательно, в вызове n1 .set("abc",0,0) tword ссылается на nl.tword,• а в вызове n2.set("def",0,0) tword ссылается на n2.tword. Предполагается, что функции strlen, error и strc^y определены где-то в другом месте; см. $10ч В функции-элементе ключевое слово this является указателем на объект, для которого вызывается функция. Функция-элемент может быть определена в объявлении класса; в таком случае, она имеет тип inline (8.1). Следовательно, struct х { int f() { return b; } int xb; }; эквивалентно struct x { int f() int b; }; inline x::f() { return b; } К функции-элементу можно применять оператор адресации. Однако, тип получаемого при этом указателя на функцию не определен, так что любое использование его зависит от реализации. 8.5.3. Производные классы В конструкции идентификатор агрегата : public^,, typedef-имя typedef-имя должно обозначать ранее объявленный класс, который называется базовым классом для объявляемого. класса. Говорят, что такой класс производится от своего базового класса. О значении public см. $8.5.9. На элементы базового класса можно ссылаться так, как если бы они были элементами производного класса, за исключением тех случаев, когда имя базового элемента переопределяется в производном классе. В этом случае, для ссылки на скрытое имя можно использовать оператор ::. Производный класс сам может использоваться как базовый класс. Образовать производные классы от union нельзя ($8.5.13). Указатель на производный класс может быть явно преобразован в указатель на общий базовый класс ($6.7). Присваивание для объектов класса, произведенного от класса, для которого был определен operator = ($8.5.11), в неявной форме не определено (см. $7.14 и $8.5). Например: class base { public: int a,b: }; 282
class derived : public base ( .* public: int b,c; ' } derived d; ; d.a = 1; d.base::b = 2; d.b = 3; d.c = 4; base", bp = &d; присваивает значения, четырем элементам d и делает bp указателем на d. 8.5.4. Виртуальные функции Если базовый класс base содержит виртуальную (virtual) ($8.1) функцию vf и производный класс derived также содержит функцию vf, то обе функции должны быть одного типа, а функция vf для объекта класса derived производится- через derived::vf. Например: struct base { virtual void vf(); void f(); }; ■?.. class derived : public base { public: void vf(); void f(); " ? }; 3 derived d; base" bp = &d; bp->vf(); bp->f(); Вызов функций производится через derived::vf и base::f, соответственно для объекта d класса derived. То есть, интерпретация вызова виртуальной функции зависит от типа объекта, для которого она вызывается, в то время, как интерпретация вызова невиртуальной функций-элемента зависит только от типа указателя;; обозначающего этот объект. Виртуальная функция не может иметь тип friend ($8.5.10). Функция f в классе, производном от класса, в котором есть виртуальная функция f, сама по себе рассматривается как виртуальная. Виртуальная функция в базовом классе должна быть определена. Виртуальную функцию, определенную в базовом классе, можно не определять в производном классе. В таком случае, во всех вызовах используется функция, определенная для базового класса. 283
8.5.5. Конструкторы Функция-элемент, имеющая то же имя, что и ее класс, называется конструктором; она используется для конструирования величин того же типа, что и ее класс. Если класс имеет какой-либо конструктор, то каждый объект этого класса должен быть инициализирован до того, как этот объект будет использоваться; см. $8.6. < Конструктор не может иметь тип virtual или friend. Если класс содержит базовый класс или объекты-элементы с конструкторами, их конструкторы вызываются перед вызовом конструктора для производного класса. Первым вызывается конструктор базового класса. В разделе $10 дано объяснение того, как можно описать аргументы для таких конструкторов; в разделе $8.5.8 приведено объяснено, как можно использовать конструкторы для обслуживания свободной памяти. Объект класса с конструктором не может быть элементом объединения. Для конструктора нельзя указать, тип возвращаемой величины; в теле конструктора нельзя использовать оператор return. Конструктор можно явно использовать для создания новых объектов, имеющих тот же тип, что и конструктор; для этого используется синтаксис: typedef-имя ( список аргументов^, ) Например, complex zz = complex(1,2.3); cprint( complex(7.8, 1.2) ); Объекты, созданные таким образом, являются безымянными (если только конструктор не используется - как инициализатор, как в случае zz, приведенном выше). Время жизни этих объектов ограничено областью, в которой они созданы. 8.5.6. Преобразования Конструктор, имеющий один аргумент, описывает преобразование от типа его аргумента к типу своего класса. Такие преобразования проводятся неявно в дополнение к стандартным преобразованиям (см. $6.6-7). Присваивание значения объекту класса X разрешено в случаег если тип Т присваиваемой величины есть тип X или если объявлено преобразование от Т к X. Сходным образом конструкторы используются для преобразований инициализаторов ($8.6), аргументов функций ($7.1) и функций, возвращающих значения ($9.10). Например: class X { ... X(int); }; f(X arg) { X a = 1; // a = x = X(1) a = 2; //a = X(2) f(3); // f(X(3)) i 10 Зак. 1927 284
Если конструктор класса X, соответствующий присваиваемому типу, отсутствует, то попытка найти другие конструкторы для преобразования присваиваемой величины в тип, приемлемый для конструктора класса X, не делается. Например: class X { ... X(int); }; class Y { ... Y(X); }; Y a = 1; // неверно: Y(X(1)) не выполняется Функция-элемент класса X, имя которой описано как имя функции преобразования: operator type описывает преобразование от X к type, type не может содержать деклараторы [] "вектор" или () "функция, возвращающая". Функция будет использоваться неявно, как конструктору описанный выше (только в случае, если она уникальна; см. $8.9) или может вызвана явно с использованием cast-нотации (нотации преобразования). Например: class X { // ... operator int(); } i X a; int i = int(a); i = (int)a; ■ =' a; ,■ " Во всех трех случаях присваиваемая величина преобразуется оператором X::operator int(). Преобразования, определяемые пользователем, не ограничиваются только операциями присваивания и инициализации. Например: X а, Ь; // ... int i = (а) ? 1 + а : 0; int j = (a&&b) ? а + b : i; ' 8.5.7. Деструкторы Функция-элемент класса cl, с именем ~с1, называется деструктором; она не имеет аргументов и для нее нельзя описать возвращаемую величину. Она используется для уничтожения величин типа d непосредственно ‘перед тем, как будет уничтожен объект, который содержит эти величины. Деструктор нельзя вызывать явно. Деструктор для базового класса исполняется после исполнения деструктора для производного класса. Деструкторы для объектов элементов исполняются после выполнения деструктора для объекта, элементами которого они являются. В разделе $8.5.8. даны объяснения того, как можно 285
использовать деструкторы для обслуживания свободной памяти. Объект класса с деструктором не может быть элементом объединения. 8,5.8. Свободная память Если объект класса создается с помощью оператора new, то конструктор будет (неявно) использовать оператор operator new для того, чтобы зарезервировать необходимый объем памяти ($7.1). Присваивая значение указателю this перед любым использованием элемента, конструктор может реализовать своё собственное распределение памяти. Присваивая указателю this нулевое значение , деструктор может избежать стандартных операций по очистке памяти, предназначавшейся для объектов своего класса. Например: class cl { int v[10); cl () { this = my _allocator( sizeof(cl)); } ~c1() { my _ deallocator(this); this = 0; } На входе в конструктор указатель this не равен 0, если память уже отведена (как в случае переменных типа auto, static и объектов элементов); во всех остальных случаях this равен 0 Вызовы конструкторов для базовых классов и для объектов элементов проводятся после присваивания значения указателю this. Если присваивание проводит конструктор базового класса, то новое значение используется и конструктором производного класса (если таковой существует). При удалении вектора объектов класса с деструктором должно быть указано количество элементов. Например: class X { ... ~Х(); }; X" р = new Xfsize]; delete[size] р; 8.5.9. Видимость имен элементов Элементы класса, объявленного с ключевым словом class, являются приватными, то есть, их имена можно использовать только через функции- элементы ($8.5.2) и дружественные функции (friend; $8.5.10), если только они не объявлены после метки public: (общий). В последнем случае они являются общими. Общий элемент может использовать любая функция. Структура (struct) - это класс, в котором все элементы являются общими; 6м. $8.5.12. Если производный класс объявлен как struct или если в объявлении производного класса имени базового класса предшествует ключевое слово public, общие элементы базового класса являются общими для производного класса; в ином случае, они являются приватными. Общий элемент mem приватного базового класса base можно объявить так, чтобы он был общим для производного класса. Для этого используется такое объявление: typedef-name :: идентификатор ; 286 10*
где typedef-name обозначает базовый класс, а идентификатор - имя элемента базового класса. Такое объявление должно быть размещено в общей части производного класса. Рассмотрим следующее: class base { int а; public: int b, c; int bf(); }; class derived : base { int d; public: base::c; int e; int df(); } / int ef(derived&); Внешняя функция ef может использовать только имена с, е и df. Будучи элементом класса derived, функция df может использовать имена b, с, bf, d, е и df, но не а. Будучи элементом класса base, функция bf может, использовать элементы а, Ь, с и bf. $.5.10. Дружественные функции (friends) Дружественная классу функция - это функция, не являющаяся элементом, которая может использовать приватные имена Элементов класса. Дружественная функция не находится в области действия класса и не вызывается с помощью синтаксиса выбора элемента (если только он не является элементом другого класса). Следующий пример иллюстрирует различия между элементами\ч дружественными функциями: class private { int а; friend void friend _set(private", int); 1' public: . void member _ set(int); void friend _ set(private“ p, int i) { p->a = 1 } void private::member _set(int i) ( a = i; } private obj; friend _ set(&obj, 10); obj.meme = ber _ set(10); Если декларация friend ссылается на переопределенное имя или оператор, 287
то только функция, специфицированная типами аргумента, становится дружественной. Элемент класса сП может быть другом класса с12. Например: class с12 { friend char" сП ::foo(int); // ... }; Все функции класса сП можно сделать друзьями класса с2 простой декларацией class с12 { friend qlass сП; // ... }; Функция friend, определенная ($10) в декларации класса, является inline- функцией. 8.5.11. Функции-операторы Большинство операторов можно переназначить так, использовать в качестве операндов объекты класса. чтобы они могли имя функции-оператора operator оператор оператор: один иа new delete + * / % & 1 I = < > += - = * = /= %= “ = &= 1 = < < > > > >= < <= »== f = <= >= && 1 1 ++ [] о Последние два оператора - это операторы вызова функции и индексации. Функция-оператор (за исключением operator new и operator delete; см. $7.2) должна быть либо функцией-элементом, либо иметь, по крайней мере, один аргумент класса. См. также $7.16. 8.5.12. Структуры Структура - это класс, все элементы которого являются общими. То есть, struct s { ... }; эквивалентно class s { public: ... }; Структура может содержать функции-элементы (включая конструкторы и деструкторы). База производной структуры является общей. То есть, 288
struct s : b { ... }; эквивалентно class s : public b { public: ... }; 8.5.13. Объединения Объединение можно рассматривать как структуру, все элементы которой начинаются со смещения 0 и размер которой достаточен для сохранения любого из ее объектов-элементов. В любой момент максимум один из объектов-элементов может храниться в объединении. Объединение может содержать функции-элементы (включая конструкторы и деструкторы). Получит класс, производный от объединения, нельзя. Объект класса с конструктором или с деструктором не может быть элементом объединения. Объединение вида union { список элементов }; называется безымянным элементом; оно определяет непоименованный объект. Имена элементов безымянного объединения должны отличаться от остальных имен в области, в которой объявлено объединение; в этой области их можно использовать без применения обычного синтаксиса доступа к элементам ($8.5). Например: union { int a; char" р; }; а = 1; // ... ? р = "asdf"; - 5 В этом примере а и р используются как обычные (не элементы) переменные, но, поскольку они являются элементами объединения, они имеют один и тот же адрес. 8.5.14. Битовые поля Декларатор элемента вида идентификатору : константное выражение описывает поле; длина поля отделяется в описании от названия двоеточием. Поля упаковываются как машинные целые; они не захватывают слова. На некоторых машинах поля запоминаются справа налево, на некоторых - слева направо; см. $2.6. Безымянные поля удобно использовать для заполнения свободных мест в блоке памяти для согласования с внешне налагаемыми форматами. Как особый случай, безымянное поле нулевой ширины определяет выравнивание следующего поля по границе слова. От реализаций не требуется, чтобы они могли поддерживать любые поля помимо целых. Более того, даже поле типа int может рассматриваться как беззнаковое. В связи с этим рекомендуется объявлять поля как 289
беззнаковые (unsigned), К ним нельзя применять оператор адресации так что указателей на поля не существует. Поля не могут быть элементами объединений. 8.5*15. Вложенные классы Класс можно объявить в пределах другого класса. Однако, это лишь принадлежит окружающей облегчает запись, поскольку внутренний класс его области. Например: int х; class enclose { int х; class inner { int у; void f(int); }; int g(inner“); inner а; void inner: :f(int i) { int enclose: :g(inner" х = i; } р) { return р—>у; } // // присваивание :: х ошибка 8.6. Инициализация Декларатор может идентификатора. указывать начальное значение для объявляемого инициализатор: = выражение = { список инициализаторов /opt } ( список выражений ) список' инициализаторов: выражение список инициализаторов , список инициализаторов { список инициализаторов } Все выражения в инициализаторе для статической переменной должны быть константными выражениями (описаны в разделе $12) или выражениями, которые предварительно преобразуются к адресу ранее объявленной переменной, возможно, к смещению на константное выражение. Автоматические или регистровые переменные могут инициализироваться любыми выражениями, включая константы, ранее объявленные переменные и функции. Неинициализированные статические и внешние переменные всегда устанавливаются в 0; неициниализированные автоматические и регистровые переменные всегда получают случайное начальное значение. Если инициализатор применяется к типу scalar (скалярный) (указатель 290
или объект арифметического типа) он состоит из простого выражения (возможно, в фигурных скобках). Начальное значение объекта берется из выражения; производятся те же преобразования, что и при присваивании. Обратите внимание на то, что, поскольку () не является инициализатором, X а();это не объявление класса X, а объявление функции без аргументов, которая возвращает X. 8.6.1. Списки инициализаторов Если объявляемая переменная является агрегатом (классом или массивом), то инициализатор может состоять из списка инициализаторов; список заключается в фигурные скобки, а его элементы разделяются запятыми. Элементы агрегата записываются в порядке возрастания индекса или по порядку элементов.'1' Если массив содержит субагрегаты, это правило рекурсивно применяется к элементам агрегата. Если инициализаторов в списке меньше, чем элементов в агрегате, агрегат дополняется нулями. Фигурные скобки можно обойти следующим образом. Если инициали¬ затор начинается с левой скобки, последующий список инициализаторов, разделенных запятыми, инициализирует элементы агрегата; если инициали заторов больше, чем элементов, то возникает ошибка. Однако, если инициализатор не начинается с фигурной левой скобки, то из списка элементов берется ровно столько элементов, сколько это необходимо для инициализации; все остальные элементы используются для инициализации следующего элемента агрегата, частью которого является текущий агрегат. Например: int х[] = { 1, 3, 5 }; 4 объявляет и инициализирует х как одномерный массив из трех элементов, поскольку размер массива не указан и даны три инициализатора. float у[4][3] = 1, 3, 5 }, 2, 4, 6 }, 3, 5, 7 }, является инициализацией, полностью заключённой в фигурные скобки: Г, 3 и 5 инициализируют первый ряд массива у[0], а именно, у[0][0], у[0][11 и у[0][2]. Аналогично, следующие две строки инициализируют у[1] и у[2]. Инициализатор заканчивается раньше, чем это должно быть, исходя из размера массива, поэтому у[3] инициализируется нулями. Точно такой же эффект может быть достигнут таким образом: float у[4](3] = { 1, 3, 5, 2, 4, 6, 3, 5, 7 }; Инициализатор. для у начинается с левой фигурной скобки, но для у[0] * нет, поэтому используются три элемента из списка. Аналогично следующие три элемента используются для инициализации у[1] и у[2]. Сходным 291
образом, инициализирует первую колонку массива у (который рассматривается как двумерный массив); остальные элементы устанавливаются в 0. 8.6.2. Объекты класса Объект' с приватными элементами, так же, как и объединения, нельзя инициализировать с помощью списка инициализаторов. Объект класса с конструктором должен быть инициализирован. Если класс имеет конструктор без аргументов, то для объектов, которые не инициализируются явно, используется этот конструктор. Список аргументов для конструктора можно присоединить к имени в декларации или к типу в выражении new. Последующие инициализации дают одно и то же значение ($8.4): struct complex { float re, im; complex(float r, float i = 0) { re = r; im = i; } }; complex zz1(1, Oh- complex zz2(1); complex" zpl = new complex(1,0); complex" zp2 = new complex(l); Объекты класса можно также инициализировать явно с помощью оператора = . Например: complex zz3 = complex(1,0); complex zz4 = complex(l); complex zz5 = 1; Complex zz6 = zz3; Если существует конструктор, использующий ссылку на объект собственного класса, он будет вызываться, если объект инициализируется другим объектом этого класса, но не если объект инициализируется конструктором. Объект может быть элементом агрегата только если (1) класс объекта не имеет конструктора; (2) один из его конструкторов не использует аргументы или (3) агрегат является классом с конструктором, который описывает список элементов инициализации (см. $10). В случае 2 конструктор вызывается в момент создания агрегата. Если агрегат является классом (но не вектором), то для вызова конструктора можно использовать аргументы по умолчанию. Если элемент агрегата имеет деструктор, то этот деструктор -вызывается при уничтЪжении агрегата. Конструкторы для нелокальных статических объектов вызываются в том порядке, в котором они находятся в файле; деструкторы вызываются в обратном порядке. Вызов конструктора ‘ й деструктора для локального 292
статического объекта в случае, если функция, в которой объявлен объект, не вызывается, не определен. Если вызывается конструктор для локального статического объекта, то он вызывается после конструкторов для глобальных объектов, которые лексически предшествуют ему. Если вызывается деструктор для локального статического объекта, то он вызывается перед деструкторами для глобальных объектов, которые лексически предшествуют ему. ч 8.6.3. Обращения Если переменная объявлена к£к Т&, то есть, как "обращение к типу Т", она должна инициализироваться объектом типа Т или объектом, который можно преобразовать в "Т. Обращение становится альтернативном именем для объекта. Например: int i; int& г = i; f г = 1; // i получает значение 1 int* р = бсх; Значение обращение нельзя изменять после инициализации. Обратите внимание на то, что инициализация обращения происходит совсем иначе, чем присваивание ему значения. Если инициализатор для обращения к типу Т не является Ivalue- величиНой, то создается объект типа Т, который и инициализируется затем инициализатором. После этого обращение становится именем для этого объекта. Время жизни объекта, созданного таким образом - это область видимости, в которой он был создан. Например: doubled rr = 1; разрешено, и гг будет указывать на double, содержащее значение 1.0. Обратите внимание на то, что обращение к классу В можно инициализировать объектом класса О при условии, что В - это общий базовый класс для D (в таком случае, D - это В). Обращения особенно полезны в качестве формальных аргументов. Например: struct В { ... }; struct D : В { ... }; int f(B&); D а; f(a); 8.6.9. Массивы символов Массив типа char можно инициализировать строкой; следующие друг за другом символы строки инициализируют элементы массива. В примере char msg[] = "Синтаксическая ошибка на строке %s\n"; 293
представлен символьный массив, элементы которого инициализированы строкой. Обратите внимание на то, что sizeof(msg) = = 35. 8.7. Имена типов Иногда (для того, чтобы явно описать преобразования типов или при использовании в качестве аргументов операторов sizeof или new) желательно иметь имя типа данных. Это можно сделать с помощью конструкции имя типа, которая, в принципе, является объявлением для объекта этого типа, в котором опущено имя объекта. имя типа: спецификатор типа абстрактный декларатор абстрактный декларатор: пустой ж абстрактный декларатор абстрактный декларатор ( список объявлений аргументов ) абстрактный декларатор [ константное выражение,,?, ] ( абстрактный декларатор ) Можно точно идентифицировать место в абстрактном деклараторе, где стоял бы инициализатор, если бы конструкция была бы декларатором в объявлении. Тип, которому присваивается имя, - это тип гипотетического инициализатора. Например: int inf int int int int именуют типы "целое", "указатель на целое", "массив из 3 указателей на целые", "указатель на массив из 3 целых", "функция, возвращающая указатель на целое" и "указатель на функцию, возвращающую целое". 8.8. Typedef Декларации, содержащие спецификатор декларатора typedef определяет идентификаторы, которые можно использовать позже так, как если бы они были ключевыми словами типа, именующими базовые или производные типы. имя typedef: идентификатор В пределах видимости декларации, включающей в себя typedef, каждый идентификатор, появляющийся там как часть любого декларатора, становится синтаксически эквивалентным ключевому слову типа, которое именует тип, связанный с идентификатором так, как это описано в 8.4. Спецификатор 294
декларатора typedef нельзя использовать для элемента класса. Имя класса или перечисления также является именем typedef. Например, после typedef int MILE, "KLICKSP; struct complex { double re, im; }; все конструкции MILES distance; extern KLICKSP metricp; complex z, "zp; разрешены; distance имеет тип int, a metricp - "указатель на int". Ключевое слово typedef не вводит новых типов, а только синонимы для типов, которые можно было бы объявить и иным путем. Так, в приведенном выше примере distance рассматривается как перёменная точно такого же типа, что и любой другой объект типа int. Однако, объявление класса все-таки вводит новый тип. Например: struct X { int а; }; struct Y { int a; }; X al; Y a2; int a3; объявляет три переменные трех различных типов. Декларация в форме объявление имени: идентификатор агрегата ; ( идентификатор enum ; указывает на то, что идентификатор является именем какого-то (возможно, еще не определенного) класса или перечисления. Такие объявления позволяют объявлять классы, которые ссылаются друг на друга. Например: class vector; class matrix { // ... friend vector operator"(matrix&, vectorA); class vector { //... friend vector operator"(matrixA, vectorA); } t 8.9. "Совмещенные" имена функций Если под одним именем описаны объявления нескольких (различных) 295
функций, то говорят, что это имя является "совмещенным". При использовании такого имени правильная функция выбирается сопоставлением типов фактических аргументов с типами формальных аргументов. Поиск функции, которую нужно вызвать, проводится в три стадии: Поиск точного совпадения и, если оно обнаружено, использование совпадения. Поиск совпадений с использований стандартных преобразований ($6.6 - 8) и использование любого найденного совпадения. Поиск' совпадений с использованием преобразований, определенных пользователем ($8.5.6). Если обнаружен уникальный набор преобразований, то он используется. Нуль, char или short рассматриваются как точные совпадения для формальных аргументов типа int. Тип float рассматривается как точное совпадение для формального аргумента типа double. Для аргумента "совмещенной" функции применяются только следующие преобразования: int в long, int в double и преобразования указателя и обращения ($6.7-8). Для совмещения имени функции, которая не является элементом или функции operator любому объявлению функции должно предшествовать объявление overload; см. $8.1. Например: overload abs; double abs(double); int dbs(int); abs(1) // вызов abs(int); abs(I.O) // вызов abs(double); Например: class X { ... X(int); }; class Y { ... Y(int); }; class Z { ... Z(’char); }; overload int f(X), f(Y); overload int g(X), g(Z); f(1); // неверно: f(X(1)) или f(Y(1)) g(D; // g(X(D) Оператор адресации & можно применять к совмещенному имени только при присваивании или при инициализации в тех случаях, когда ожидаемый тип определяет, какая функция будет использовать адрес. Например: int operator = (matrix&, matrix&); int operator = (vector&, vector&); int(’pfm)(matrix&,matrix&) = &operator = ; int(’pfv)(vector&,vector&) = &operator=; 296
int("pfx)(...) = &operator=; // ошибка 8.10. Декларации перечислений Перечисления имеют тип inf с поименованными константами. спецификатор перечисления: enum идентификатор^ { список перечислений } список перечислений: перечислитель список перечислений , перечислитель перечислитель: . идентификатор идентификатор = константное выражение Идентификаторы в списке перечислений объявляются как константы и могут появляться везде, где необходимо использовать константы. Если перечислители со знаком = отсутствуют, то значения соответствующих констант начинаются с 0 и возрастают на 1 по мере того, как декларация читается слева направо. Перечислитель со знаком = присваивает соответствующему идентификатору указанное значение; последующие идентификаторы продолжают прогрессию, начиная с присвоенного значения. Имена перечислителей должны отличаться от имен обычных переменных. Имена перечислителей с разными константами также должны быть различными. Значения перечислителей могут совпадать. Роль идентификатора в спецификаторе перечисления совершенно аналогична роли имени класса; он называет отдельное перечисление. Например: enum color { red, yellow, green = 20, blue }; color col = red; color" cp = &col; if("cp = = blue) // ... делает color (цвет) именем типа, который описывает разные цвета (в примере - красный, желтый, зеленый, синий), а затем объявляет col объектом этого типа и ср - указателем на объект этого типа. Возможные значения выбираются из набора {0, 1, 20, 21}. 8.11. Декларации asm Декларация asm имеет форму asm ( строка ) ; Значение декларации asm не определено. Как правило, оно используется для передачи информации через компилятор ассемблеру. 297
9. ОПЕРАТОРЫ За исключением специально оговоренных случаев, операторы вызываются по порядку. 9.L Оператор выражения Большинство операторов являются операторами выражения; они имеют форму выражение ; Обычно операторы выражения - это операторы присваивания или вызова функции. 9.?. Составной Оператор или блок Для того, чтобр»! использовать несколько операторов там, где предполагается один, применяется составной оператор (называемый также "блоком"; составной оператор и блок - эквивалентные понятия). составной оператор; { список операторов^ } список операторов: оператор оператор список операторов Обратите внимание на то, что объявление - это пример оператора ($9.14). 9.3. Условный оператор Условный оператор имеет две формы: if ( выражение ) оператор if ( выражение ) оператор else оператор Выражение должно иметь арифметический тип, тип указателя или тип класса, для которого определено преобразование к арифметическому типу или к указателю (см.$&.5.6). Выражение вычисляется и, если оно ненулевое, то выполняется первый субоператор. Если используется конструкция с else, то, в случае, если значение выражения равно 0, выполняется второй субоператор. Как обычно, неоднозначность трактования оператора else разрешается тем, что очередной оператор else связывается с последним оператором if. 9.4. Оператор while Оператор while имеет форму while (выражение) оператор 298
Субоператор вызывается повторно до тех пор, пока значение выражения остается ненулевым. Проверка производится после каждого выполнения оператора Выражение вычисляется так же, как и в условном операторе ($93). 9.5. Оператор do Оператор do имеет вид do оператор while ( выражение ); Субоператор вызывается повторно до тех лор, пока значение выражения не станет равным нулю. Проверка производится после каждого выполнения оператора. Выражение вычисляется так же, как и в условном операторе ($9.3). 9.6. Оператор for Оператор for имеет вид for ( оператор-1 выражение-1 ; выражение-2ор1 ) оператор-2 Этот оператор эквивалентен следующему оператор-1 while (выражение-1 ) { у оператор-2 выражение-2 ; за исключением того, что оператор continue в операторе-2 будет выполнять выражение-2 до повторного вычисления выражения-1. Следовательно, первый оператор описывает инициализацию для цикла; первое Выражение описывает проверку, которая делается перед каждой итерацией так, чтобы цикл заканчивался после того, как выражение станет равным 0; второе выражение часто описывает инкрементацию, которая производится после каждой итерации. Любое или оба выражения можно опустить. Пропуск выражения-1 полагает подразумеваемый оператор while эквивалентным оператору while(l). Обратите внимание на то, что если оператор-1 является объявлением, область видимости объявленного имени расширяется до конца блока, включающего в себя оператор for. 9.7. Оператор switch Оператор switch передает управление одному из нескольких операторов, в зависимости от значения выражения. Он имеет вид. switch ( выражение) оператор 299
Тип выражения может арифметическим типом или типом указателя. Любой оператор в пределах оператора можно пометить одной или несколькими метками case следующим образом: case константное выражение : Здесь константное выражение должно быть того же типа, что и выражение в switch; проводятся обычные арифметические преобразования. Ни Одна пара констант в операторе case в одном операторе switch не может иметь одинаковое значение. Константные выражения определены в $12. Допускается максимум одна метка вида default : При выполнении оператора switch .вычисляется его выражение, значение которого сопоставляется с каждой константой case. Если одна из констант равна значению выражения, управление передается оператору, следующему за соответствующей меткой case. Если ни одна из констант не совпадает со значением выражения, и если метка default отсутствует, то не выполняется ни оДин из операторов в switch. Сами по себе метки case и default не меняют управление потоком, который просто пропускает эти метки. Для выхода из оператора switch используется оператор break, $9.8. Обычно операторы, являющиеся субъектом switch - это составные операторы. В заголовке каждого оператора могут появляться объявления, однако инициализации автоматических и регистровых переменных не допускаются. 9.8. Оператор break Оператор break ; прерывает выполнение ближайшего вложенного оператора while, do, for или switch. Управление передается оператору, следующему за прерванным оператором. 9.9. Оператор continue В результате применения оператора continue ; стоящие после него операторы ближайшего вложенного цикла while, do или for игнорируются и управление передается в часть цикла, которая завершает цикл, то есть, в конец цикла. Точнее говоря, в каждом из операторов while (...) { do { for (...) { contin: ; contin: ; contin: ; } } while (...); } 300
оператор continue эквивалентен оператору goto contin. (После метки contin: следует пустой оператор, $9.13). 9.10. Оператор return Функция возвращается в точку вызова с помощью оператора return, который имеет одну из следующих форм: returh ; return выражение ; Первую форму можно использовать только в функциях, которые не возвращают значения, то есть, в функциях, имеющих тип возврата void. ВторукЭ форму можно иСпбльзовать только в функциях, возвращающих значение; значение выражения возвращается в точку вызова функции. При необходимости, выражение преобразуется (как это делается при йници^Из^ий) к тиНу функции, в которой оно появляется. Выход потока ЗаКрнеЦ функции экйивалентен возврату без возвращаемого значения. 9.11. Оператор goto Управление можно передать безусловно с помощью оператора goto идентификатор ; Идентификатор должен быть меткой ($9.12), расположенной в лекущей ; функции. Нельзя передать управление за пределы объявления с неявным или явным инициализатором, за исключением передачи управления за пределы внутреннего блока без входа в него. 9.12. Помеченный оператор Любому оператору может предшествовать метка вида идентификатор : Эта запись объявляет идентификатор как метку. Единственное назначение метки - использование ее как указателя перехода для оператора goto. Область действия метки - текущая функция, за исключением всех субблоков, в которых был переобъявлен такой же идентификатор. См. $4.1. • 9.13. Пустой оператор Пустой оператор имеет вид Пустой оператор используется для простановки метки непосредственно перед } составного оператора или для получения пустого тела оператора цикла (например, while). 101
9.14. Оператор декларации Оператор декларации используется для введения в блок нового идентификатора; он имеет вид оператор декларации: декларация Если идентификатор, вводимый декларацией, был ранее объявлен во внешнем блоке, внешнее объявление становится скрытым на протяжении блока. При выходе из блока внешняя декларация восстанавливает свое действие. Все переменные auto или register инициализируются каждый раз, когда выполняется оператор декларации. В блок можно войти, но так, чтобы переменные при этом инициализировались; см. $9.11. Переменные класса памяти .static ($4.4) инициализируются только однажды, в начале выполнения программы. 10. ОПРЕДЕЛЕНИЯ ФУНКЦИЙ Программа состоит из последовательности деклараций. Код функций может располагаться только вне всех блоков и в пределах деклараций классов. Определения функций имеют вид определение функции: спецификаторы объявления^, декларатор функции базовый деклараторО1„ тело функции Спецификаторы объявления register, auto, typedef нельзя использовать в пределах объявления класса, в то время, как friend и virtual можно использовать только в этих • пределах ($8.5). Декларатор функции - это декларатор для "функции, возвращающей..." ($8.4). Формальные аргументы располагаются в области самого внешнего блока по отношению к телу функции. Деклараторы функции имеют вид декларатор функции декларатор ( список объявлений аргументов ) Если аргумент описан как register, то соответствующий фактический аргумент будет в самом начале функции скопирован в регистр (если это возможно). Если в качестве инициализатора для аргумента указано константное выражение, это значение используется Как значение аргумента по умолчанию. Тело функции имеет вид тело функции: * составной оператор Простой пример полного определения функции: 302
int max(int a, int b, int c) { int m = (a > b) ? a : b; return (m > c) ? m :c; } В этом примере int - это спецификатор типа; max(int a, int b, int с) - это декларатор функции; это тело функции. Поскольку в контексте выражения имя массива (в частности, в качестве фактического аргумента) имеет значение указателя на первый элемент массива, объявления формальных аргументов, объявленных "массив из ..." читаются как "указатель на ...". Инициализатору, для базовых классов и для элементов можно описать при определении конструктора. Это наиболее удобный способ; Для объектов классов, констант и обращений, где семантика инициализации и присваивания различается. Базовый инициализатор имеет вид: базовый инициализатор: : список инициализаторов элементов список инициализаторов элементов: инициализатор элемента инициализатор элемента , список инициализаторов элементов инициализатор элемента: идентификатор^ ( список аргументов^ ) Если в инициализаторе элемента присутствует идентификатор, то список аргументов используется для инициализации этого поименованного элемента; если идентификатора нет, то список аргументов используется для базового класса. Например: struct base { base(int); ... }; struct derived : base { derived(int); base b; const c; derived: :derived(int a) : (a + 1), b(a + 2), c(a + 3) { Г 7 } derived d(10); Прежде всего, для объекта d вызывается конструктор базового класса base::base() с аргументом 11; затем вызывается конструктор для элемента b с аргументом 12 и конструктор для элемента с с аргументом 13. После этого выполняется тело функции derived::derived() (см. $8.5.5). Порядок вызова конструкторов элементов не определен. Если базовый класс имеет конструктор, который может вызываться без аргументов, то список 303
аргументов не нужен. Если класс элемента имеет крнструктор, который можно вызвать без аргументов, то для этого элемента список аргументов не нужен, 11. СТРОКИ УПРАВЛЕНИЯ КОМПИЛЯТОРОМ Компилятор имеет препроцессор, который может производить макрозамещения, условную компиляцию и включение поименованных файлов. Для обращения к этому препроцессору используются строки, начинающиеся с #. Эти строки имеют синтаксис, не зависящий от остальной части языка; они могут появляться в любой точке исходного кода и имеют действие до конца исходного программного файла (независимо от области видимости). Обратите внимание на то, что определения const и inline являются альтернативами во многих случаях использования директивы #define. 11.1 Лексемное замещение Строка управления компилятором вида #define идентификатор цепочка лексем ♦ • дает указание препроцессору заменить все вхождения идентификатора на данную цепочку лексем. Точка с запятой внутри или на конце цепочки лексем рассматривается как часть этой цепочки. Строка вида #define идентификатор (идентификатор , ...идентификатор) цепочка лексем без пробела между первым идентификатор и ( является макроопределением с аргументами. Соответствующие вхождения первого идентификатора с последующей (, последовательностью лексем, разделенных запятыми и ) заменяются лексемной цепочкой из определения. Каждое вхождение идентификатора, описанного в списке формальных аргументов определения, заменяется соответствующей лексемной цепочкой из вызова. Фактические аргументы в вызове - это цепочки .лексем, разделенные запятыми; однако запятые в цепочках, взятые в кавычки или заключенные в круглые скобки не являются разделителями аргументов. Количества формальных и фактических аргументов должны совпадать. Строки и символьные константы в лексемных цепочках просматриваются для формальных аргументов, но строки и символьные константы в остальной части программы для определенных идентификаторов не просматриваются. В обеих формах замещающие цепочки для последующих определяемых идентификаторов просматриваются вновь. В обеих формах длинное определение можно перенести на новую строку, .поставив в конце продолжаемой строки символ \. Управляющая строка вида #undef идентификатор отменяет определение идентификатора для препроцессора. 304
11.2. Включение файла Строка управления компилятором вида #include "название файла'1 замещается полным содержанием файла "название файла". Поиск этого файла вначале производится в текущей директории исходного файла, а затем в специфицированных и в стандартных директориях. Напротив, управляющая строка вида #include <название файла> направляет поиск только в специфицированные или стандартные директории, но не в директорию Стандартного файла. Описание направления поиска не является частью языка. Директивы #include могут быть вложенными. 11.3. Условная компиляция Строка тения компилятором вида #if выражение проверяет, не равно ли 0 выражение. Выражение должно быть константным выражением ($12). В добавление к обычным операциям C++, можно использовать унарный оператор defined. Оператор defined, примененный к идентификатору, имеет ненулевое значение в случае, если этот идентификатор был определен с помощью директивы #define и позже не отменялся директивой #undef; в остальных случаях значение его равно 0. Управляющая строка вида #ifdef идентификатор проверяет, определен ли в препроцессоре идентификатор в настоящий момент, то есть, был ли он субъектом действия управляющей строки #define. Управляющая строка вида #ifndef идентификатор проверяет, не отменен ли в настоящий момент идентификатор в препроцессоре. За всеми тремя формами следует произвольное число строк, возможна, содержащие управляющую строку #else а затем управляющую строку #endif 305
Если проверяемое условие верно, то все строим между #else и #endif игнорируются. Если проверяемое условие неверно, то игнорируются все строки между строкой, на которой производится проверка и #else или, при отсутствии #else, #endif. 11.4. Управление строками Для успешной работы других препроцессоров, генерирующих программы С + + , строка #line константа "название файла" инструктирует компилятор (для диагностики ошибок), что номер следующей строки в источнике кода задается константой, а название текущего входного файла - идентификатором. Если идентификатор опущен, то предполагается, что имя файла осталось текущим. . 12.КОНСТАНТНЫЕ ВЫРАЖЕНИЯ Иногда в языке C+ + требуются выражения, результатом вычисления которых являются константы. Таковы выражения, определяющие границы массивов ($8.4), выражения case ($9.7), аргументы функций, принимаемые по умолчанию и инициализаторы ($8.6). В первом случае, выражение может включать в себя только целые константь!, символьные константы, перечисляемые константы, величины const, не являющиеся агрегатами и инициализированные константными выражениями, а также выражения sizeof, возможно, соединенные бинарными операторами . + - ’ / >>==!= < % > & 1 Л <,< <=>=&& II или унарными операторами + - ~ ! или тернарным оператором ?• Для группирования, но не скобки. "" Для вызова функций, используются круглые В других случаях константные выражения могут также содержать унарный оператор &, примененный к внешним или статическим объектам или к внешним и статическим объектам, индексированным константным выражением. Унарный оператор & может применяться неявно появлением неиндеКсированных массивов и функций. Основное* правило таково: результатом вычисления инициализатора должна быть либо константа либо адрес ранее объявленного внешнего или статического объекта плюс или минус константа. 306
13. СООБРАЖЕНИЯ ПЕРЕНОСИМОСТИ Некоторые части языка C++ по определению машинно-зависимы. Последующий список потенциальных "горячих точек" не претендует на полноту, но призван указать основные точки. Чисто аппаратные моменты, такие, как размер слова и особенности арифметики чисел с плавающей точкой и целочисленное деление, как это выяснилось из практики, не представляют особой проблемы. Другие аспекты аппаратного обеспечения отражаются в различных реализациях. Некоторые из них, в частности, знаковое расширение (преобразование отрицательного символа в отрицательное целое) и порядок, в котором байты размещаются в слове, являются нюансом, за которым нужно тщательно следить. Большая часть остальных проблем - это мелочи. Количество переменных типа register, которое можно поместить в регистры, варьируется от машины к машине, так же, как и набор разрешенных типов. Тем не менее, для "своих" машин компиляторы все делают правильно: избыточные или запрещенные объявления register игнорируются. Порядок вычисления аргументов функций не описывается языком. На некоторых машинах они вычисляются справа налево, на некоторых - слева направо. Порядок, в котором проявляются побочные эффекты, также не указан. Поскольку символьные Константы на самом деле являются объектами типа int, разрешены многосимвольные символьные константы. Однако, конкретная реализация в большой степени зависит от ; машины, поскольку на некоторых машинах символы составляются в слово слева направо, а на некоторых - справа налево; х 14. КРАТКОЕ ИЗЛОЖЕНИЕ СИНТАКСИСА Для облегчения понимания ниже приведено резюме синтаксиса С + + . Оно не является строгой формулировкой языка. 14.1. Выражения выражение: элемент выражение бинарный оператор выражение выражение ? выражение : выражение список выражений Список выражений: выражение список выражений , выражение элемент: первичное выражение элемент унарного оператора элемент + + элемент — выражение sizeof 307
sizeof ( имя типа ) имя простого типа ( список выражений ) new имя типа инициализатор^, new ( имя типа ) delete выражение delete [ выражение ] выражение первичное выражение: id :: идентификатор константа цепочка S this ( выражение ) первичное выражение [ выражение ]' первичное выражение ( список выражений ) первичное выражение, . id первичное выражение -> id id: идентификатор имя функции оператора typedef-имя :: идентификатор typedef-имя :: имя функции оператора оператор: унарный оператор бинарный оператор специальный, оператор оператор для работы со свободной памятью Старшинство бинарных операторов убывает в следующей последовательности: бинарный оператор: один из + < < > > < > & ~ I && II оператор присваивания оператор присваивания: один из = += -= -= /= %= -= &= |= >>= << = унарный оператор: рдин из ’ & + - ’ + + — 308
специальный оператор: один из О [] оператор для работы со свободной памятью: один из new delete имя типа: спецификаторы декларации абстрактный декларатор абстрактный декларатор: пустой " абстрактный декларатор абстрактный декларатор ( список объявлений аргументов ) абстрактный декларатор [ константное выражение^ ] имя простого типа: typecjef-имя char short int long unsigned float double void typedef-имя: идентификатор 14.2. Декларации декларация: спецификаторы декларации^ список деклараторов^ ; декларация имени декларация asm декларация имени: идентификатор агрегата идентификатор перечисления (enum) агрегат: class struct union декларация asm asm ( цепочка ) ; спецификаторы декларации: спецификатор декларации спецификаторы декларации^ 309
спецификатор декларации: спецификатор класса памяти спецификатор типа спецификатор функции friend typedef спецификатор типа имя простого спецификатор спецификатор , спецификатор const типа класса перечисления сложного типа спецификатор класса памяти: auto static extern register спецификаторы функции: overload inline virtual спецификаторы сложного типа: ключ typedef-имя ключ идентификатор ключ class struct union enum список деклараторов: декларатор инициализатора декларатор инициализатора , список деклараторов декларатор инициализатора: декларатор инициализатору, декларатор: имя декларатЪра ( декларатор ) “ const декларатор & consTopi декларатор декларатор ( список объявлений аргументов ) декларатор [ константное выражение^ ] 310
имя декларатора: простое им# декларатора typedef-имя :: простое имя декларатора простое имя декларатора: идентификатор typedef-имя - typedef-имя имя функции-оператора имя функции-преобразования имя функции оператора: operator оператор имя функции преобразования: operator тип список деклараций аргументов: список объявлений аргументов^ ...< список деклараций аргументов: список деклараций аргументов , декларация аргумента декларация аргумента декларация аргумента спецификаторы спецификаторы спецификаторы спецификаторы декларации декларации декларации декларации декларатор декларатор = выражение абстрактный декларатор абстрактный декларатор = выражение спецификатор класса: заголовок класса (список элементов^,,} заголовок класса (список элементов^,, риЬНсхлисок элементов^} заголовок класса: идентификатор агрегатаор, идентификатор агрегата : public^ typedef-имя список элементов: декларация элемента список элементовор, декларация элемента: спецификаторы-деклараторыор1 декларатор элемента инициализатор^,; определение функции ; ор, декларатор элемента: декларатор идентификатор^, : константное выражение инициализатор: = выражение 311
= ( список инициализаторов ) ъ = { список инициализаторов , } • ( список выражений ) список инициализаторов: выражение список инициализаторов , список инициализаторов { список инициализаторов } спецификатор перечисления: enum-идентрфикатор^, { список перечислений ) список перечислений: перечислитель список перечислений , перечислитель перечислитель: идентификатор идентификатор = константное выражение 14.3. Операторы составной оператор: { список операторов ор1 } список операторов: оператор оператор список операторов оператор: декларация ’ составной оператор выражение^ ; if ( выражение ) оператор if { выражение ) оператор else оператор while (выражение) оператор do оператор while ( выражение ); for ( оператор выражение^ ; выражение^ ) оператор switch ( выражение) оператор case константное выражение : оператор default : оператор break ; continue; return выражение^ ; goto идентификатор ; идентификатор ': оператор 14.4. Внешние определения программа: внешнее определение 312
внешнее определение программа внешнее определение: определение функции декларация определение функции: спецификаторы декларации^ декларатор функции базовый инициализатор^ тело функции декларатор функции: декларатор ( список деклараций аргументов ) тело функции: составной оператор базовый инициализатор: : список инициализаторов элементов список инициализаторов элементов: инициализатор элемента инициализатор элемента , список инициализаторов элементов инициализатор элемента: идентификатору ( список аргументовор1 ) 14.5. Препроцессор #define идентификатор цепочка лексем # define идентификатор (идентификатор,..^идентификатор) цепочка лексем #else #endif #if выражение #ifdef идентификатор #ifndef идентификатор #include "название файла" #include <название файла> #line константа "название файла" #undef идентификатор 15. ОТЛИЧИЯ ОТ С 15.1. Расширения Можно описывать типы аргументов функций (8); при этом соответствие типов будет проверяться (7.1). Производятся преобразования типов (7.1). Для выражений типа float можно использовать арифметику чисел с плавающей точкой одинарной точности; 6.2. Имена функций можно совмещать; 8.9. Операторы можно перегружать; 7.16, 8.11. 313
Допускается inline-замена функций (подстановка тела функции вместо ьызова ее). Объекты данных могут иметь тип const (константа); 8.4. Можно объявить объекты типа "обращение" (ссылка) 8.4, 8.6.3. Предоставляется возможность работы со свободной памятью с помощью операторов new и delete; 7.2. Классы предоставляют возможность "скрывать" данные (8.5.9), обеспечивают инициализацию (8.6.2), преобразования, определяемые пользователем (8.5.6) и динамическое типиррвание с помощью виртуальных функций (8.5.4). Имя класса или перечисления является именем типа (8.5). Любому указателю можно присвоить тип void" без использования преобразования; 7.14. Объявление в пределах блока является оператором; 9.14. Можно объявлять объединения без имени; 8.5.13. 15.2. Краткая обзор несовместимостей Большинство конструкций языка С разрешены в С + +, причем их значение не меняется. Исключениями являются следующие конструкции: не разрешены программы, использующие одно из новых ключевых слов class new this const delete operator overload victual volatile friend inline public signed в качестве идентификаторов. Объявление функции f(); означает, что f не использует аргументы; в С это означает, что f может использовать аргументы любого типа. В С внешнее имя можно определять несколько раз; в C++ оно должно быть определено только один раз. Имена классов в С + + занимают то же адресное пространство, что и другие имена, так что конструкции типа int s; struct s { /“ ... "/ }; К) { S = 1; } использовать нельзя. Однако, для разрешения большей части конфликтных ситуаций можно явно применять class struct, union, enum (8.2) или :: (7.1). Например: int s; struct s { Z“ ... “/ }; void f() { int s; struct s a; } void g() { ::s = 1; } 15.3. Анахронизмы Расширения, описанные здесь, могут облегчить; использование программ, написанных на языке С, в качестве С+ + -программ. Обратите 314
внимание на то, что каждая из этих особенностей имеет нежелательные аспекты. Реализация, в которой используются эти Особенности, должна также дать пользователю возможность быть уверенным в том, что исходный код не содержит побочных эффектов. До сих.пор йе определенное имя можно использовать в качестве имени функций при вызове ее. В таком случае имя неявно объявляется как имя функции/ возвращающей значение типа int с типом аргумента (...). Для ' того, чтобы показать, что функция должна вызываться без аргументов, можно использовать ключевое слово void, следовательно, (void) эквивалентно ()• Можно использовать программы, включающие в себя синтаксис определения функции из языка С: 'старое определение функции: спецификаторы декларации^, старый декларатор функции список деклараций тело функции старый декларатор функции: декларатор ( список парамётров ) список параметров: идентификатор идентификатор , идентификатор Например: max(a,b) { return (a<b) ? b : a; } Если функция, определенная таким образом, не была объявлена ранее, в качестве типа ее аргумента принимается (...), то есть, тип аргумента не проверяется. Если она была объявлена, то тип аргументов должен совпадать с типом аргумента в декларации; Вместо :: для описания имени в декларации функции-элемента можно использовать точку. Например int cl.fct() { Г ... 7 } Для класса или перечисления и для объектов данных или функций можно использовать одно и то же имя в пределах одной области видимости. 2-я типография издательства «Наука», 121099, Москва, Г-99„ Шубинский пер., 6 Заказ 1927. Тираж 50 000 экз.