Текст
                    

В. В. Подбел ьский ЯЗЫК СИ++ 5-е издание Рекомендовано Министерством образования Российской Федерации в качестве учебного пособия для студентов высших учебных заведений, обучающихся по направлениям «Прикладная математика» и «Вычислительные машины, комплексы, системы и сети» Москва “Финансы и статистика” 2003
УДК 004.438Си(075.8) ББК 32.973.26-018 1я73 П44 РЕЦЕНЗЕНТЫ: кафедра управления и моделирования систем Московской государственной академии приборостроения и информатики; Ю. Г. Дадаев, доктор технических наук, профессор Подбельский В.В. П44 Язык Си++: Учеб, пособие. - 5-е изд. - М.: Финансы и стати- стика, 2003. - 560 с.: ил. ISBN 5-279-02204-7 Подробно рассмотрены синтаксис, семантика и техника программирования объектно-ориентированного языка Си+ + . Приведено большое количество программ, иллюстрирующих возможности и особенности языка Си+ + . В отличие от работ, в которых язык Си++ вводится как расширение своего предшественника языка Си, в данной книге он рассматривается как самостоятельный язык программирования. Для изучения материала достаточ- но, если читатель владеет основами информатики и навыками программиро- вания на любом алгоритмическом языке в объеме стандартного курса по программированию для среднего учебного заведения (4-е издание - 1999 г.). Для студентов, преподавателей, лицеистов, школьников, а также для специалистов, желающих самостоятельно изучить язык Си++. jj 2404000000 — 028 293 — 2003 010(01) ~ 2003 УДК 004.438Си(075.8) ББК 32.973.26-018 1я73 ISBN 5-279-02204-7 © В. В. Подбельский, 1995 © В. В. Подбельский, 1999
ПРЕДИСЛОВИЕ К 5-му ИЗДАНИЮ Отзывы читателей и опыт использования предыдущих изданий посо- бия в учебных заведениях разных уровней (школы с математическим уклоном, колледжи, университеты) показали, что замысел автора об ориентации на читателей, не знакомых с программированием нз Си и/или на Си++, реализован в книге достаточно успешно. Пособие предназначено и пригодно для изучения языка Си++ "с нуля". Книга позволяет читателю не только изучить язык Си++, но и проследить его происхождение от языка Си. Так как язык Си является подмножеством языка Си++, то, изучая Си++ по данному пособию, читатель почти без каких-либо дополнительных усилий осваивает средства и механизмы его предшественника - языка Си. Таким образом, книга не замкнута на высокоуровневые механизмы языка Си++, а подробно знакомит читателей и с базовыми аппаратно ориентированными средствами системного программирования, унаследованными от Си (работа с указателями, особенности представления в стиле Си массивов и строк, возможности битовых полей, объединений, средств адресации памяти - ’’адресная арифметика" и т.д.). Теперь несколько слов об эволюции языка Си+ + . В 1998 г. произошло долгожданное событие - Международный комитет по стандартизации принял стандарт языка Си++ (ISO/IEC 14882 "Standard for the C++ Programming Language"). Возможности, которые при подготовке первого издания пособия только обсуждались (исключения, динамическая идентификация типов и др., см. главу 11), стали обязательными элементами Си++. Работы над "Стандартом" велись в течение нескольких лет, и его промежуточные варианты были постоянно доступны для пользователей языка и для авторов компиляторов. Это привело к замечательному результату: не произошло задержки между утверждением "Стандарта" и его практическим внедрением. Современные версии компиляторов реализуют практически все возможности, заложенные в язык Си++. К счастью для программистов, "Стандарт" и компиляторы обеспечили и преемственность средств языка. Программы, написанные до официального утверждения "Стандарта", сохраняют работоспособность и при тех требованиях, которые предъявил к текстам программ на Си++ утвержденный "Стандарт".
4 Язык Си++ Итак, "Стандарт" добавил к языку новые возможности, но не вычеркнул из него хорошо зарекомендовавшие себя базовые механизмы, подробно описанные в данном пособии. Более того, "Стандарт" не ввел в язык неожиданных средств, которые ранее не обсуждались в публикациях и не были опробованы в промышленных компиляторах. К сожалению, введение "Стандарта" не уменьшило описания самого языка. Объем официального издания "Стандарта" - 748 страниц. Объем нового 3-го издания классической работы Bjarne Stroustrup "The C++ Programming Language", подробно рассматривающей.рредства Си++, утвержденные "Стандартом", вырос почти до 1000 страниц. Поэтому автор настоящего пособия не решился следовать атому примеру, а оставил книгу в прежнем объеме. ПРЕДИСЛОВИЕ К 1-му ИЗДАНИЮ Язык программирования Си++ был разработан на основе языка Си Бьярном Страуструпом (Bjarne Stroustrup) и вышел за пределы его иссле- довательской группы в начале 80-х годов [1]. На первых этапах разработ- ки (1980 г.) язык носил условное название "Си с классами", а в 1983 г. Рик Маскитти придумал название "Си++", что образно отразило происхождение этого нового языка от языка Си. Язык Си++ является расширением (надмножеством) языка Си, поэтому программы, написанные на Си, могут обрабатываться компилятором языка Си++. В программах на языке Си++ можно использовать тексты на языке Си и обращаться к библиотечным функциям языка Си. Таким образом, одно из достоинств Си + + состоит в возможности использовать уже существующие программы на Си. Однако это не единственное достоинство языка. Как пишет его автор [2], язык Си++ был создан, чтобы улучшить язык Си, поддержать абстракцию данных и обеспечить объектно-ориентированное программирование. Настоящая работа посвящена синтаксису, семантике, технике и стилю построения программ языка Си++. На русском языке существует несколько хороших книг по языку Си++ для тех, кто уже программирует на языке Си. В отличие от этих книг, в которых язык Си++ вводится как расширение своего предшественника Си, настоящая работа не требует от читателя знакомства с языком Си. Другими словами, материал книги по возможности "замкнут" и содержит всю необходимую информацию для фундаментального освоения языка Си++ как нового для читателя языка программирования. Например, книгу может изучать читатель, знакомый с языком Паскаль, или Бейсик, или Фортран, или ПЛ/1 и т.п. Знакомство с языком Си никак не повредит читателю, но изложение материала этого не предполагает. Итак, язык Си + + излагается как новый язык, свойства которого раскрываются перед
Предисловие к 1-му изданию 5 читателем постепенно. Здесь мы полностью следуем совету: "...программируя на Си++, нужно... не применять те средства языка, которые представляются вам неясными, как и те, с которыми вы еще не успели ознакомиться" [2]. Приведем несколько пояснений. Изложение в начальных главах до некоторой степени конспективное, но все разбираемые вопросы сопровождаются примерами. При выборе примеров оказалось весьма непросто ограничиваться только уже рассмотренными средствами языка и "не забегать вперед". В тех случаях, когда новые конструкции появляются в примерах до их определения, они тщательно объясняются и комментируются. Тем самым читатель получает возможность неформального предварительного знакомства с некоторыми важными и широко используемыми конструкциями языка Си++, синтаксис которых описывается позже. Приводимые программы в основном отлаживались и выполнялись в интегрированной среде Borland C++ и Turbo C++. Несмотря на то что в книге изложены практически все основные принципы, средства и механизмы языка Си++, необходимые для объект- но-ориентированного программирования, мы не стали только ради моды наспех говорить о технологии объектно-ориентированного программирования на языке Си+ + . Объектно-ориентированное программирование может эффективно использоваться, если ему предшествуют объектно-ориентированный анализ задачи и объектно- ориентированное проектирование программного комплекса. Без объектно-ориентированного проекта попытки применения объектно- ориентированного программирования будут по меньшей мере малоэффективны и весьма трудоемки. В настоящее время существуют фундаментальные работы, посвященные объектно-ориентированному анализу и проектированию систем [15]. Однако требовать от читателя предварительного знакомства с этой областью системотехники и системного анализа было бы слишком опрометчиво. Да этого и не нужно. Средства языка Си++ настолько интересны и богаты, что их следует использовать и не только в русле объектно-ориентированного программирования. Достаточно обратить внимание на поддержку принципов абстракции данных, предоставляемую языком Си++ с помощью механизма классов, чтобы убедиться в целесообразности применения Си++ и вне реализации того или иного объектно- ориентированного проекта. Не повторяя как заклинания термины объектно-ориентированной методологии ("инкапсуляция", "полиморфизм", "иерархичность", "типизация" и т.д.), можно в создаваемых программах использовать промышленные библиотеки классов (одна из них описана в главе 11), вводить собственные типы данных, соответствующие решаемым задачам, и тем самым постепенно и совершенно естественным образом
6 Язык Си++ осваивать стиль и средства объектно-ориентированного програм- мирования на Си++. Дальнейшее продвижение в этом направлении будет определять та ситуация, в которой находится читатель, и тот интерес, который он проявляет собственно к объектно-ориентированной методологии и языку Си+ + . Отметим только, что этапа изучения конструкций языка миновать нельзя, ибо невозможно правильно написать объектно-ориентированную программу на Си+ + , если испытываешь трудности при применении базовых конструкций (описаний, операторов, выражений и т.д.). Тут уж не поможет даже хорошая проработка исходной задачи и качественно выполненное объектно-ориентированное проектирование программы. Проводя аналогию с другой областью человеческого творчества, скажем так: можно написать хорошую повесть, делая ошибки в правописании и надеясь на помощь корректора, однако синтаксических ошибок в тексте программы не прощает ни один компилятор, а их исправление требует знания грамматики языка. Пособие написано для специальностей "Прикладная математика", "Вычислительные машины, комплексы, системы и сети", а также для сту- дентов других специальностей, профессионально изучающих программирование. Им можно пользоваться и при самостоятельном изучении языка Си++, Для успешного усвоения материала пособия от читателя требуются знания основ информатики и некоторые навыки программирования на каком-либо алгоритмическом языке. Материал для главы 12, посвященной исключительным ситуациям, автору помог подобрать С.Г. Чернацкий. Им же написаны и отлажены некоторые программы, помещенные в главе 12. В заключение предисловия несколько слов о той помощи, которую получил автор и за которую он глубоко признателен. Студенты факультета прикладной математики (ФПМ) Московского государственного института электроники и математики (МИЭМ), слушая лекции автора, своими во- просами и замечаниями помогли исправить некоторые шероховатости изложения материала. При печати и редактировании рукописи, а также при анализе результатов выполнения программ автору помогали коллеги по работе. Среди них особую благодарность считаю необходимым выра- зить В.Г. Воросколевской, Н.Н.Ионцеву, А.В. Утолину, С. Г.Чернацкому, О. В. Шехановой.
Глава 1. НЕФОРМАЛЬНОЕ ВВЕДЕНИЕ В СИ++ 1.1. Простые программы на языке Си++ Начнем с объяснения структуры программ на языке Си++, мало обращая внимания на существование языка Си, точнее, не всегда явно отмечая степень преемственности. Пример 1. Следуя классикам [1,3], приведем программу, выводя- щую на экран дисплея фразу Hello, World! (Здравствуй, Вселенная!): //HELLO.СРР - имя файла с программой. #include <iostream.h> void main() { cout « "\nHello, World!\n"; ) Результат выполнения программы: Hello, world! В первой строке текста программы - однострочный комментарий, начинающийся парой символов Ч!\ заканчивающийся неизоб- ражаемым символом “конец строки”. Между этими разделителями может быть помещен произвольный текст. В данном примере в ком- ментарии указано имя файла hello.срр, в котором хранится исход- ный текст программы. Во второй строке помещена команда (директива) препроцессора, обеспечивающая включение в программу средств связи со стан- дартными потоками ввода и вывода данных. Указанные средства на- ходятся в файле с именем iostream.h (мнемоника: "i" (input) - ввод; "о" (output) - вывод; stream - поток; "h" (head) - заголовок). Стан- дартным потоком вывода по умолчанию считается вывод на экран дисплея. (Стандартный поток ввода обеспечивает чтение данных от клавиатуры.) Третья строка является заголовком функции с именем main. Любая программа на языке Си++ должна включать одну и только одну функцию с этим именем. Именно с нее начинается вы- полнение программы. Перед именем main помещено служебное слово
8 Язык Си++ void - спецификатор типа, указывающий, что функция main в данной программе не возвращает никакого значения. Круглые скобки после main требуются в соответствии с форматом (синтаксисом) заголовка любой функции. В них помещается список параметров. В нашем при- мере параметры не нужны и список пуст. Тело любой функции - это заключенная в фигурные скобки после- довательность описаний, определений и операторов. Каждое описа- ние, определение или оператор заканчивается символом "точка с запятой". В теле функции main явных описаний и определений нет и есть только один оператор cout « "\nHello, World!\пи; Имя cout в соответствии с информацией, содержащейся в файле iostream.h, является именем объекта, который обеспечивает вывод информации на экран дисплея (в стандартный поток вывода). Ин- формация для вывода передается объекту cout с помощью операции « ("поместить в"). То, что нужно вывести, помещается справа от зна- ка операции «. В данном случае это строка (строковая константа) "\nHello, World!\п". Строка в языке Си++ определена как заклю- ченная в кавычки почти любая последовательность символов. Среди них могут встречаться обозначения не изображаемых на экране дис- плея управляющих символов.* Например, '\п' - обозначение управ- ляющего символа перехода к началу следующей строки экрана. Таким образом, программа выведет на новой строке экрана фразу Helio, world! и переведет курсор в начало следующей строки экрана. Уже сейчас следует отметить одну из принципиальных осо- бенностей языка Си++, называемую перегрузкой или расширением действия стандартных операций. Лексема « означает операцию вставки ("поместить в") только в том случае, если слева от нее нахо- дится имя объекта cout. В противном случае пара символов « озна- чает бинарную операцию сдвига влево. Итак, в единственном опера- торе этой программы использована операция вставки в выходной по- ток cout значения, помещенного справа от лексемы «. До выполнения программы необходимо подготовить ее текст в файле с расширением .срр; передать этот файл на компиляцию и устранить синтаксические ошибки, выявленные компилятором; без- ошибочно откомпилировать (получится объектный файл с расшире- нием .obj); дополнить объектный файл нужными библиотечными функциями (компоновка) и получить исполняемый модуль про- граммы в файле с расширением .ехе. Схема подготовки исполняемой программы приведена на рис. 1.1, где перед шагом компиляции пока-
Глава 1. Неформальное введение в Си++ 9 name.срр *.СРР ★ .СРР ★ .OBJ * .EXE *.Н Рис. 1. L Схема подготовки исполняемой программы зан шаг препроцессорной обработки текста программы. В нашем примере препроцессор обрабатывает директиву #include <iostream.h> и подключает к исходному тексту программы средства для обмена с дисплеем (для поддержки операции «). Если исходный текст программы подготовлен в файле hello . срр , то препроцессор, выполнив директивы препроцессора, сформирует полный текст программы, компилятор создаст объектный файл hello.OBJ, выбрав (по умолчанию) для него указанное имя, а компоновщик (редактор связей, Linker) дополнит программу библиотечными функциями, например для работы с объектом cout, и построит мо-
10 Язык Си++ дуль hello.exe. Запустив на выполнение файл HELLO.EXE, получим на экране желаемую фразу Helio, world! Особенности выполнения перечисленных действий зависят от конкретного компилятора языка Си++ и той операционной системы, в которой он работает. Технические подробности следует изучить по документации для конкретного программного продукта. Например, при работе с интегрированными средами фирмы Borland необходимая информация может быть получена из руководств пользователя [4, 10]. Независимо от использованного компилятора при работе в MS-DOS исполняемый модуль программы записывается в некоторый каталог (директорию). Если исполняемый модуль создан в каталоге book на диске С:, то для запуска нашей программы нужна команда MS-DOS: >С:\ВООК\HELLO.EXE <Enter> Здесь и далее <Enter> обозначает нажатие клавиши Enter. Пример 2. Для иллюстрации некоторых особенностей определения и инициализации переменных и массивов, а также ввода и вывода данных напишем программу вычисления должностного оклада в со- ответствии с заданным разрядом единой тарифной сетки (ЕТС) опла- ты труда для работников бюджетных отраслей в Российской Фе- дерации. (Установлена постановлением Правительства Российской Федерации №785 от 14 октября 1992 г. Отменена 27 февраля 1995 г.) Исходные данные для расчета: минимальная ставка 1-го разряда (smxn), массив тарифных коэффициентов, т.е. коэффициентов пере- расчета а[], и номер категории (разряда) г. Массив а[] инициализи- руется в тексте программы, и его нельзя изменить без изменения и трансляции программы. Минимальная ставка определена в про- грамме, но может заменяться другим значением с помощью аргумента командной строки при запуске программы на исполнение. Номер раз- ряда всегда должен вводиться пользователем явно с клавиатуры при выполнении программы. Таким образом, программа иллюстрирует три способа задания исходных данных для расчета. //Р1-02.СРР * имя файла с программой //1 ♦include <iostream.h> //2 ♦include <strstrea.h> //3 int main(int narg, char **arg) //4 { float smin « 2250; // Ставка 1-го разряда (1992 г.) //5 // а[] - массив значении тарифных коэффициентов: //6 float а[] = { 1.0, 1.3, 1.69, 1.91, 2.16, 2.44, //7 2.76, 3.12, 3.53, 3.99, 4.51, 5.10, 5.76, //8 6.61, 7.36, 8.17, 9.07, 10.07 //9
Глава 1. Неформальное введение в Си++ 11 int г; //г - разряд тарифной сетки оплаты труда //10 cout « "\пи; //11 cout « "Программа вычисляет оклад в соответствии"; //12 cout « "\пс единой тарифной сеткой оплаты труда"; //13 cout « "\пдля работников бюджетных отраслей"; //14 cout « "в России."; //15 if (narg » 1) //16 ( cout « "\пПо умолчанию минимальный оклад "; //17 cout « smin « " руб."; //18 cout « "\пПри необходимости изменить значение"; //19 cout « " минимального"; //20 cout « "Хпоклада его нужно указать в”; //21 cout « " командной строке.\п"; 1/2.2. } //23 else //24 ( // Чтение из безымянного строкового потока: //25 istrstream{arg[l]) » smin; //26 cout « "\nOnpeделен минимальный оклад в "; //27 cout « smin « " руб.Хп"; //28 } //29 cout « "\пВведите номер разряда тарифной сетки: "; //30 cin » г; // Вводится с клавиатуры номер разряда //31 if (г < 1 || г > 18) //32 { cout « "Ошибка в выборе разряда*"; //33 return 1; // Аварийный выход ив программы //34 } //35 cout « "Введенному разряду соответствует ставка "; //36 cout « (long)(a[r-1]*smin) « " руб."; //37 return 0; // Безошибочное завершение программы //38 } //39 Строки программы справа пронумерованы. Номера входят в комментарии и нужны только для ссылок на строки программы при пояснениях. В строке 2 - директива (команда) препроцессора, подключающая к программе из файла iostream.h средства связи с библиотечными функциями потокового ввода-вывода. Именно там определены стан- дартные потоки ввода данных от клавиатуры cin и вывода данных на экран дисплея cout, соответствующие операции чтения из потока » и записи в поток «. Препроцессорная директива из строки 3 включает в текст про- граммы средства для обмена со строковыми потоками. Мнемоника обозначения файла: str - string (строка), strea - stream (поток), h -
12 Язык Си++ head (заголовок). Именно эти средства позволили в строке 26 создать безымянный строковый ПОТОК И связать его СО строкой агд[1]. Обратите внимание, что в отличие от первой программы здесь функция main() возвращает целочисленное значение int, что явно ука- зано в строке 4. Это целочисленное значение после завершения про- граммы передается в операционную систему и может быть про- анализировано. Принято соглашение, что любая программа при ава- рийном завершении должна возвращать ненулевое значение. При правильном выполнении программы в операционную систему пере- дается нулевой результат. Вопрос об анализе результата в операцион- ной системе в этой книге, посвященной языку Си++, не рассматрива- ется. В списке аргументов функции main (строка 4) первый аргумент int narg служит для передачи в программу количества параметров, использованных в командной строке при запуске программы на вы- полнение. Значение narg всегда больше или равно 1. Если narg равно 1 (это условие проверяется в строке 16), то никакого параметра в ко- мандной строке явно не указано. В этом случае единственный па- раметр, передаваемый как значение символьного массива, адресуе- мого указателем arg[O] - это полное имя файла с исполняемым мо- дулем программы. Например, если программа, находящаяся в ката- логе book, начинает выполняться после ввода командной строки >С:\BOOK\P1-Q2.EXE <Enter> то значением, связанным с arg[O], будет строка "C:\book\pi- 02.exe". После имени программы в команде операционной системы могут быть указаны через пробелы параметры. В этом случае значе- нием narg будет (к + I), где к - количество явно указанных пара- метров. В программе параметры доступны с помощью указателей arg[i], где i= 1,..., к. Например, используя команду >С:\BOOK\P1-Q2.EXE 1800 <Enter> мы передадим в программу параметр "1800" как значение символь- ного массива, адресуемого агд[1]. При этом значением narg будет 2, а массив argto], как и раньше, будет указывать на полное имя файла с программой. В строке 5 определена и инициализирована значением 2250 пере- менная smin. Спецификатор типа float указывает, что это ве- щественная арифметическая переменная с внутренним представлением в формате с плавающей точкой. Далее (строки 7-9) определен и ини- циализирован массив из 18 элементов, каждый из которых является
Глава 1. Неформальное введение в Си++ 13 переменной вещественного типа со внутренним представлением в формате с плавающей точкой. Количество элементов массива и их значения определяются списком начальных значений, помещенным в фигурных скобках справа от знака операции присваивания 1 ='. В строке 10 определена, но явно не инициализирована целая (int) переменная г. Ее значение с помощью операции » ("взять из”) вво- дится при выполнении программы из стандартного входного потока cin (строка 31). В соответствии с принципом перегрузки (расширения действия) стандартных операций в языке Си++ лексема » означает операцию извлечения данных из входного потока только в том слу- чае, когда слева от » находится имя потока. (В противном случае па- ра символов » означает бинарную операцию сдвига вправо, о которой речь пойдет позже.) Итак, в строке 31 используется операция извлечения данных из стандартного входного потока cin. Оператор cin » г; преобразует набираемую пользователем на клавиатуре последовательность символов в целочисленное значение и присваи- вает это значение переменной г. При этом недопустимо появление среди набранных символов чего-либо отличного от цифр, знаков ' + ’, *-• и пробелов. Символы, набираемые на клавиатуре и отображаемые на экране дисплея, становятся доступными программе после нажатия клавиши Enter, что одновременно переводит курсор к началу следую- щей строки. Таким образом, после каждого считывания данных из cin курсор на экране дисплея размещается в начале следующей стро- ки. Обратите внимание на отсутствие символа перевода строки 1 \п' в строках 33 и 36. В строке 16 начинается условный оператор, проверяющий значе- ние narg. Если значение narg равно I, то по умолчанию используется начальное значение smin, о чем выдается сообщение (строки 17 - 22) на экран дисплея, и начинает выполняться оператор из строки 30. В противном случае (строки 25 - 28) значение минимального оклада smin выбирается из параметра командной строки операционной си- стемы с помощью указателя агд[1]. Для преобразования этого пара- метра (например, строки ”1800") в числовое значение используется безымянный строковый поток istrstream(arg[l]). Чтение из этого строкового потока выполняет операция извлечения данных из потока ». Так как справа от этой операции помещена переменная smin типа float, то в параметре можно использовать только символы для изоб- ражения числовых значений. Правильность введенного значения для простоты программы не проверяется, и это может быть источником ошибок. Полученное значение smin вместе с пояснительным текстом
14 Язык Си++ выводится на экран дисплея (строки 27 - 28), и выполнение условного оператора (строки 16 - 29) завершается. В строке 30 формируется на экране подсказка пользователю. За- тем (строка 31) вводится значение переменной г. В строках 32 -г- 35 - условный оператор. Вслед за служебным словом if в круглых скобках записано проверяемое логическое выражение - дизъюнкция двух от- ношений г < 1 и г > 18. Выражение истинно, если значение г мень- ше 1 или больше 18. В этом случае выполняются заключенные в фигурные скобки { } операторы из строк 33, 34, т.е. печатается со- общение об ошибке, оператор return 1; завершает выполнение про- граммы и передает управление операционной системе, возвращая ей ненулевое значение. Если номер разряда введен правильно, то выполняются операторы вывода из строк 36, 37, 38, и программа в строке 38 завершает работу, возвращая операционной системе нулевое значение из оператора return. Отметим, что этот оператор не является обязательным. При его отсутствии возвращаемое программой значение всегда будет нуле- вым. Если пользователь, находясь в MS-DOS, введет команду (расширение .ехе по правилам MS-DOS можно опускать) >С:\ВООК\Р1-02 <Enter> программа выведет на экран: Программа вычисляет оклад в соответствии с единой тарифной сеткой оплаты труда для работников бюджетных отраслей в России. По умолчанию минимальный оклад 2250 руб. При необходимости изменить значение минимального оклада его нужно указать в командной строке. Введите номер разряда тарифной сетки: 11 <Enter> Введенному разряду соответствует ставка 10147 руб. Здесь в ответ на приглашение (подсказку) программы, поль- зователь ввел в качестве номера разряда значение 11 и нажал клавишу Enter. Запуск программы из MS-DOS директивой >С:\ВООК\Р1-02 20400 <Enter> приведет к такому результату: Программа вычисляет оклад в соответствии с единой тарифной сеткой оплаты труда
Глава 1. Неформальное введение в Си++ 15 для работников бюджетных отраслей в России. Определен минимальный оклад в 20400 руб. Введите номер разряда тарифной сетки: 11 <Enter> Введенному разряду соответствует ставка 92004 руб. Следует остановиться на некоторых особенностях вывода на эк- ран числовых значений. В операторах из строк 18, 28 в стандартный выходной поток cout пересылается преобразованное в символьный вид вещественное значение переменной smin. В операторе из строки 37 выводится значение выражения (long) (а[г-1]*smin); Символом ’*’ здесь обозначается операция умножения, а [г-1] - индексированная переменная, обеспечивающая доступ к r-му по порядку элементу массива а[]. Унаследованная от своего предшественника - языка Си особенность языка Си++ - нуме- рация элементов массива, начиная с 0. Таким образом, в нашем при- мере а[0] имеет значение 1.о; а[1]«~1.3; ...; а[1€]«~9.07; а [17] «10.07. (Лексемой '«и»' вне текста программ будем обозначать равенство значений справа и слева от знака Такое соглашение соответствует синтаксису языка Си++ и позволяет отличать присваи- вание от равенства 1 «='.) Вводя номер разряда как значение целочисленной переменной г, мы обращаемся в массив за соответствующим ему коэффициентом с помощью индексированного элемента а [г-1]. Полученное значение тарифного коэффициента умножается на значение минимальной став- ки smin. Соответствующее выражение заключено в скобки. Перед скобками помещена операция (long) явного приведения типа, пре- образующая вычисляемое вещественное значение к целому "длинному" типу. Это сделано для получения значения ставки в руб- лях без дробной части (дробная часть отбрасывается при приведении типа). В операторах из строк 18, 28, 37 операция « применяется по несколько раз. Любой из этих операторов можно заменить несколь- кими. Например, вместо строки 37 можно записать два оператора: cout « (long)(a[r-l]*smin); cout « " руб."; Результат при выполнении программы будет тем же. Интересно отметить, что в начале 1993 г. оператор из строки 37 имел такой вид: cout « (int) (a[r-l] *smin) « " руб.";
16 Язык Си++ Однако положительные целые числа типа int в используемом компи- ляторе Си++ не могут превышать значения 32767. Поэтому при повыше- нии минимальной ставки пришлось перейти к целым числам с большим количеством значащих цифр, т.е. использовать преобразование (long). Не потребуются ли нам значения еще большие, например unsigned long? В качестве упражнения читатель может осовременить программу, учтя постановление Правительства Российской Федерации № 309 от 18 марта 1999 г. В соответствии с этим постановлением: 1. Устанавливаются следующие тарифные коэффициенты по разря- дам: 1,00 (разряд 1); 1,36 (разряд 2); 1,59 (разряд 3); 1,73 (разряд 4); 1,82 (разряд 5); 2,00 (разряд 6); 2,27 (разряд 7); 2,54 (разряд 8); 2,91 (разряд 9); 3,27 (разряд 10); 3,68 (разряд 11); 4,18 (разряд 12); 4,73 (разряд 13); 5,32 (разряд 14); 6,00 (разряд 15); 6,68 (разряд 16); 7,41 (разряд 17); 8,23 (раз- ряд 18). 2. С 1 апреля 1999 г. устанавливается оклад 1-го разряда в размере 110 руб. При расчете тарифных ставок (окладов) выполняется округление полученного значения до целых рублей. Например, оклад 4-го разряда устанавливается 190 руб. (ПО * 1,73 = 190,3), оклад 16-го разряда уста- навливается 735 руб. (110 * 6,68 = 734,8).
Глава 2. ЛЕКСИЧЕСКИЕ ОСНОВЫ ЯЗЫКА СИ++ 2.1. Общие сведения о программах, лексемах и алфавите Общая схема обработки программы и пробельные разделители. Основная программная единица на языке Си++ - это текстовый файл с названием <имя>.срр, где срр - принятое расширение для программ на Си++, а имя выбирается достаточно произвольно. Для удобства ссылок и сопоставления программ с их внешними именами целесооб- разно помещать в начале текста каждой программы строку коммен- тария с именем файла, в котором она находится. Это уже сделано в программах предыдущего параграфа. Текстовый файл с программой на Си++ вначале обрабатывает препроцессор (см. рис. 1.1), который распознает команды (директивы) препроцессора (каждая такая ко- манда начинается с символа *#') и выполняет их. В приведенных вы- ше программах использованы препроцессорные команды linclude <имя_включаемого__файла> Выполняя препроцессорные директивы, препроцессор изменяет исходный текст программы. Команда #inciude вставляет в програм- му заранее подготовленные тексты из включаемых файлов. Сформи- рованный таким образом измененный текст программы поступает на компиляцию. Компилятор, во-первых, выделяет из поступившего к нему текста программы лексические элементы, т.е. лексемы, а затем на основе грамматики языка распознает смысловые конструкции языка (выражения, определения, описания, операторы и т.д.), построенные из этих лексем. Фазы работы компилятора здесь рассматривать нет необходимости. Важно только отметить, что в результате работы компилятора формируется объектный модуль программы. Компилятор, выполняя лексический анализ текста программы на языке Си++, для распознавания начала и (или) конца отдельных лек- сем использует пробельные разделители. К пробельным разделителям относятся собственно символы пробелов, символы табуляции, симво- 2 382)
18 Язык Си++ лы перехода на новую строку. Кроме того, к пробельным разделите- лям относятся комментарии. В языке Си++ есть два способа задания комментариев. Традици- онный способ (ведущий свое происхождение от многих предше- ствующих языков, например, ПЛ/1, Си и т.д.) определяет коммента- рий как последовательность символов, ограниченную слева парой символов /*, а справа - парой символов */. Между этими граничны- ми парами может размещаться почти любой текст, в котором разре- шено использовать не только символы из алфавита языка Си++, но и другие символы (например, русские буквы): /* Это комментарий, допустимый и в Си, ив Си++ */ ANSI-стандартом запрещено вкладывать комментарии друг в друга, однако многие компиляторы предусматривают режим, допус- кающий вложение комментариев. Второй способ (введенный в Си++) определяет комментарий как последовательность символов, началом которой служат символы //, а концом - код перехода на новую строку. Таким образом, одностроч- ный комментарий имеет вид: / / Это однострочный комментарий, специфичный для языка Си++ Алфавит и лексемы языка СИ++. В алфавит языка Си++ входят: • прописные и строчные буквы латинского алфавита; • цифры 0, 1, 2, 3, 4, 5, 6, 7, 8, 9; • специальные знаки: " О , I [1 < ) + - / % \ ; Из символов алфавита формируются лексемы языка: • идентификаторы; • ключевые (служебные, иначе зарезервированные) слова; • константы; • знаки операций; • разделители (знаки пунктуации). Рассмотрим эти лексические элементы языка подробнее.
Глава 2. Лексические основы языка Си** 19 2.2. Идентификаторы и служебные слова Идентификатор - последовательность из букв латинского алфа- вита, десятичных цифр и символов подчеркивания, начинающаяся не с цифры: RUN run hard_RAM__disk сору_54 Прописные и строчные буквы различаются. Таким образом, в >том примере два первых идентификатора различны. На длину разли- чаемой части идентификатора конкретные реализации накладывают ограничение. Компиляторы фирмы Borland различают не более 32-х первых символов любого идентификатора. Некоторые реализации Си++ на ЭВМ типа VAX допускают идентификаторы длиной до 8 символов. Ключевые (служебные) слова- это идентификаторы, зарезервиро- ванные в языке для специального использования. Ключевые слова Си++: asm double new switch auto else operator template break enum private this case extern protected throw catch float public try char for register typedef class friend return typeid const goto short union continue if signed unsigned default inline sizeof virtual delete int static void do long struct volatile while Ранее [1] в языке Си++ был зарезервирован в качестве ключевого слова идентификатор overload. Для компиляторов фирмы Borland (ВС++ и ТС++) дополнительно введены ключевые слова: cdecl ^export _loadds __saveregs cs far near seg _ds huge pascal __ss _es interrupt _regparam Там же введены как служебные слова регистровые переменные: AH BH CH DH SI SP _SS AL BL CL DL DI “cs _ES AX BX ex DX BP DS FLAGS
20 Язык Си++ Отметим, что ранние версии ВС++ и ТС++ не включали в ка- честве ключевых слов идентификаторы throw, try, typeid, catch. He все из перечисленных служебных слов сразу же необходимы программисту, однако их запрещено использовать в качестве произ- вольно выбираемых имен, и список служебных слов нужно иметь уже на начальном этапе знакомства с языком Си++. Кроме того, иденти- фикаторы, включающие два подряд символа подчеркивания (_), ре- зервируются для реализаций Си++ и стандартных библиотек. Идентификаторы, начинающиеся с символа подчеркивания (_), ис- пользуются в реализациях языка Си. В связи с этим начинать выби- раемые пользователем идентификаторы с символа подчеркивания и использовать в них два подряд символа подчеркивания не рекоменду- ется. 2.3. Константы Константа (литерал) - это лексема, представляющая изображе- ние фиксированного числового, строкового или символьного (литерного) значения. Константы делятся на пять групп: целые, вещественные (с пла- вающей точкой), перечислимые, символьные (литерные) и строковые (строки или литерные строки). Перечислимые константы проект стан- дарта языка Си++ [2] относит к одному из целочисленных типов. Компилятор, выделив константу в качестве лексемы, относит её к той или другой группе, а внутри группы - к тому или иному типу дан- ных по ее "внешнему виду" (по форме записи) в исходном тексте и по числовому значению. Целые константы могут быть десятичными, восьмеричными и шестнадцатеричными. Десятичная целая константа определена как последовательность десятичных цифр, начинающаяся не с нуля, если это не число нуль: 16, 484216, 0, 4. Для реализаций ТС++ и ВС++ диапазон допустимых це- лых положительных значений от 0 до 4294967295. Константы, пре- вышающие указанное максимальное значение, вызывают ошибку на этапе компиляции. Отрицательные константы - это константы без знака, к которым применена операция изменения знака. Абсолютные значения отрицательных десятичных констант для ТС++ и ВС++ не ДОЛЖНЫ Превышать 2147483648.
Глава 2. Лексические основы языка Си++ 21 Таблица 2.1 Целые константы и выбираемые для них типы Диапазоны значений констант Тип данных десятичные восьмеричные шестнадцатеричные от 0 до 32767 от 00 ДО 077777 от 0x0000 до 0x7FFF int ОТ 0100000 ДО 177777 от 0x8000 ДО OxFFFF insigned int ОТ 32768 ДО 2147483647 ОТ 0200000 ДО 017777777777 ОТ 0x10000 ДО 0x7FFFFFFF long ОТ 2147483648 ДО 4294967295 ОТ 20000000000 ДО 037777777777 ОТ 0x80000000 ДО OxFFFFFFFF insigned long >4294967295 > 037777777777 > Oxffffffff ошибка Восьмеричные целые константы начинаются всегда с нуля: 016 имеет десятичное значение 14. если в записи восьмеричной константы встретится недопустимая цифра 8 или 9, то это воспринимается как ошибка. В реализациях ТС++ и ВС++ диапазон допустимых значений ДЛЯ положительных восьмеричных констант ОТ 00 ДО 037777777777 . Для отрицательной восьмеричной константы абсолютное значение не должно превышать 020000000000. Последовательность шестнадцатеричных цифр, которой предшествует 0х считается шестнадцатеричной константой. В шестнадцатеричные цифры кроме десятичных входят латинские буквы ота(илиА)до1 (или F). Таким образом, 0x16 имеет десятичное значение 22, a OxF - десятичное значение 15. Диапазон допустимых значений для положительных шестнадцатеричных констант в реализациях ТС++ и ВС++ от 0x0 до Oxffffffff. Для отрицательных шестнадцатеричных констант абсолютные значения не должны превышать 0x80000000. В зависимости от значения целой константы компилятор по-разному представляет ее в памяти ЭВМ. О форме представления данных в памяти ЭВМ говорят, используя термин тип данных. Соответствие между значениями целых констант и автоматически выбираемыми для них компилятором типами данных отображает табл. 2.1, удовлетворяющая требованиям ANSI языка Си, отнесенным ко внутреннему представлению данных для компиляторов семейства IBM РС/ХТ/АТ. Если программиста по каким-либо причинам не устраивает тот тип, который компилятор приписывает константе, то он может явным образом повлиять на его выбор. Для этого служат суффиксы L, 1 (long) и и, u (unsigned). Например, константа 64 L будет иметь тип long, хотя
22 Язык Си++ значению 64 должен быть приписан тип int, как это видно из табл. 2.1. Для одной константы можно использовать два суффикса и (и) и L (1) , причем в произвольном порядке. Например, константы 0x2201, OxllLu, 0x330000UL, 0x551u будут иметь тип unsigned long. При использовании одного суффикса выбирается тип данных, который ближе всего соответствует типу, выбираемому для константы по умолчанию (т.е. без суффикса в соответствии с табл. 2.1. Например, 04L есть константа типа long, 04U имеет тип unsigned int и т.д. Чтобы проиллюстрировать влияние абсолютного значения константы и использованных в ее изображении суффиксов lr,U на тип данных, который ей присваивается на этапе компиляции, приведем следующую программу: //Р2-01.СРР - имя файла с текстом программы //1 # include <iostream.h> //2 void main () //3 { cout « "\n sizeof 111 = "« sizeof 111; //4 cout « "\n sizeof lllu ® "« sizeof lllu; //5 cout « "\n sizeof 111L = "« sizeof 111L; //6 cout « "\n sizeof lllul = "« sizeof lllul; //7 cout « "\n\t sizeof 40000 ® "« sizeof 40000; //8 cout « "\n\t sizeof 40000u » "«sizeof 40000u; //9 cout « "\n\t sizeof 40000L = "«sizeof 40000L; //10 cout « "\n\t sizeof 40000LU="«sizeof 40000 LU; //11 //12 Здесь использована унарная операция языка Си++ sizeof, позволяющая определить размер в байтах области памяти, выделенной для стоящего права операнда. Результат выполнения программы: sizeof 111 = 2 sizeof lllu = 2 sizeof 111L = 4 sizeof llluL= 4 sizeof 40000 = 4 sizeof 40000u = 2 sizeof 40000L = 4 sizeof 40000uL = 4 Заслуживает внимания длина десятичной константы 40000u, соответствующая типу unsigned int. По умолчанию (см. табл. 2.1) такой тип не приписывается никакой десятичной константе. Вещественные константы^ т.е. константы с плавающей точкой, даже не отличаясь от целых констант по значению, имеют другую форму
Глава 2. Лексические основы языка Си++ 23 внутреннего представления в ЭВМ. Эта форма требует использования арифметики с плавающей точкой при операциях с такими константами. Поэтому компиляр должен уметь распознавать вещественные константы. Распознает он их по внешним признакам. Константа с плавающей точкой может включать следующие шесть частей: часть (десятичная целая константа); десятичная точка; дробная часть (десятичная целая константа); признак (символ) экспоненты е или Е; показатель десятичной степени (десятичная целая константа, возможно со знаком); суффикс F (или f) либо L (или 1). В записях вещественных констант могут опускаться целая либо дробная часть (но не одновременно); десятичная точка или признак экспоненты с показателем степени (но не одновременно); суффикс. Примеры: 66. .0 .12 3.14159F 1.12е-2 2E+6L2.71 При отсутствии суффиксов F(f) или L(l) вещественные константы имеют форму внутреннего представления, которой в языке Си++ соответствует тип данных double. Добавив суффикс f или F, константе придают тип float. Константа имеет тип long double, если в ее представлении используется суффикс L или 1. Диапазоны возможных значений и длины внутреннего представления (размер в битах) данных вещественного типа показаны в табл. 2.2 [4,9,21,29]. Таблица 2.2 Данные вещественного типа Тип данных Размер, бит Диапазон значений float 32 ОТ 3.4Е-38 ДО 3.4Е+38 double 64 ОТ 1.7Е-308 ДО 1.7Е+308 long double 80 ОТ 3.4Е-4932 до 1.1Е+4932 Следующая программа показывает, какие участки памяти выделяются вещественным константам разного типа в реализациях ТС++ и ВС++. //Р2-02.СРР - размеры памяти для вещественных констант # include <iostream.h> void main () { cout <<«\n sizeof 3.141592653589793 = «; cout « sizeof 3.141592653589793; cout <<«\n sizeof 3.14159 = « sizeof 3.14159; cout <<«\n sizeof 3.14159f = « sizeof 3.14159f; cout <<«\n sizeof 3.14159L = « sizeof 3.14159L; 1
24 Язык Си++ Результаты выполнения программы - размеры в байтах областей памяти, выделенных для вещественных констант: sizeof 3.141592653589793 = 8 sizeof 3.14159 а 8 sizeof 3.14159f = 4 sizeof 3.14159L » 10 Перечислимые константы (или константы перечисления [3], иначе константы перечислимого типа) вводятся с помощью служебного сло- ва епшп. По существу это обычные целочисленные константы (типа int), которым приписаны уникальные и удобные для использования обозначения. В качестве обозначений выбираются произвольные идентификаторы, не совпадающие со служебными словами и именами других объектов программы. Обозначения присваиваются константам с помощью определения, например, такого вида: enum { one = 1, two = 2, three = 3 }; Здесь enutn - служебное слово, определяющее тип данных "перечисление", one, two, three - условные имена, введенные про- граммистом для обозначения констант 1, 2, 3. После такого определе- ния в программе вместо константы 2 (и наряду с ней) можно использовать ее обозначение two и т.д. Если в определении перечислимых констант опускать знаки и не указывать числовых значений, то они будут приписываться иден- тификаторам (именам) по умолчанию. При этом самый левый в фи- гурных скобках идентификатор получит значение 0, а каждый последующий увеличивается на 1. Например, в соответствии с опреде- лением enum { zero, one, two, three }; перечислимые константы примут значения: zero=0, one=l, two===2, three=»3 Правило о последовательном увеличении на 1 значений перечис- лимых констант действует и в том случае, когда первым из них (слева в списке) явно присвоены значения. Например, определение enum { ten = 10, three = 3, four, five, six }; вводит следующие константы: ten==slO, three==3, four==4, five=»5, six=6
Глава 2. Лексические основы языка Си++ 25 Имена перечислимых констант должны быть уникальными, одна- ко к значениям констант это не относится. Одно значение могут иметь разные константы. Например, определение enum { zero, nought = 0, one, two, paiz = 2, three }; вводит следующие константы: zero—0, nought=0, one==l, two==2, paiz—2, three==3 Значения, принимаемые перечислимыми константами, могут быть заданы не только в виде целочисленных констант, но и в виде выра- жений. Например, enum { two = 2, four = two * 2 }; определит константы two—2 и four==4 Так как отрицательная целая константа - это константа без знака, к которой применена унарная операция (минус), то перечислимые константы могут иметь и отрицательные значения. Для перечислимых констант может быть введено имя типа, соот- ветствующего приведенному списку констант. Имя типа - это произ- вольно выбираемый уникальный идентификатор, помещаемый между служебным словом enum и открывающейся фигурной скобкой ' {'. Например, определение enum week { Sunday, monday, tuesday, Wednesday, thursday, friday, Saturday }; не только определяет константы sunday=o, monday===l, но и вводит перечислимый тип с именем week, который может в дальней- шем использоваться в определениях и описаниях других объектов. Символьные (литерные) константы ~ это один или два символа, заключенные в апострофы. Односимвольные константы имеют стан- дартный тип char. Для представления их значений могут вводится пе- ременные символьного типа, т.е. типа char. Примеры констант: ' Z’, '\012*, 1 \0', »\п' - односимвольные константы, ' db*, ’\x07\x07’, ’\n\t' - двухсимвольные константы. В этих примерах заслуживают внимания последовательности, начинающиеся со знака ' \1. Символ обратной косой черты ' \1 используется, во-первых, при записи кодов, не имеющих графического изображения, и, во-вторых, символов апостроф ('), обратная косая черта (\), знак вопроса (?) и кавычки (")• Кроме того, обратная косая черта позволяет вводить
26 Язык Си++ символьные константы, явно задавая их коды в восьмеричном или шестнадцатеричном виде. Последовательности литер, начинающиеся со знака ' \1, называют эскейп-последовательностями. В табл. 2.3 приведены их допустимые значения. В табл. 2.3 000 - строка от 1 до 3 восьмеричных цифр; hh - строка из 1 или 2 шестнадцатеричных цифр. Строка восьмеричных цифр мо- жет содержать любое целое восьмеричное число в диапазоне от 0 до 377. Превышение этого верхнего значения приводит к ошибке. Наи- более часто в программах используется последовательность 1 \0', обозначающая пустую (null) литеру. В эскейп-последовательности вслед за \х может быть записано любое количество шестнадцатерич- ных цифр. Таким образом, допустимы, например, константа \x0004F и ее аналог \x4F. Однако числовое значение не должно выходить за диапазон от 0x0 до OxFF. Большее значение вызывает ошибку при компиляции. Если непосредственно за символом • \1 поместить сим- вол, не предусмотренный таблицей 2.3, то результат будет неопреде- ленным. Если среди восьмеричных цифр последовательности \000 или шестнадцатеричных в \xhh встретится неподходящий символ, то это считается концом восьмеричного или соответственно шестнадцате- ричного кода. Таблица 2.3 Допустимые ESC-последовательности в языке Сн++ Изобра- жение Внутрен- ний код Обозначаемый символ (название) Реакция или смысл \а 0x07 bel (audible bell) Звуковой сигнал \ь 0x06 bs (backspace) Возврат на шаг (забой) \f ОхОС f f (form feed) Перевод страницы (формата) \п ОхОА If (line feed) Перевод строки (новая строка) \г OxOD cr (carriage return) Возврат каретки \t 0x09 ht (horizontal tab) Табуляция горизонтальная \v 0x0В vt (vertical tab) Табуляция вертикальная \\ 0х5С \ (backslash) Обратная косая черта V 0x27 1 (single quote) Апостроф (одинарная кавычка) \" 0x22 " (double quote) Двойная кавычка \? 0x3F ? (question mark) Вопросительный знак \000 ООО Любой (octal number) Восьмеричный код символа \xhh Oxhh Любой (hex number) Шестнадцатеричный код символа Для использования внутренних кодов символов при работе, на- пример, с экраном дисплея нужна таблица, в которой каждому изоб-
Глава 2. Лексические основы языка Си++ 27 ражаемому на экране символу соответствует числовое значение его кода в десятичном, восьмеричном, шестнадцатеричном представле- нии. На IBM-совместимых ПЭВМ применяется таблица кодов ASCII (см. Приложение 1). Выбирая из кодовой таблицы подходящее значе- ние, можно (если это полезно по каким-либо причинам) использовать их в программе вместо явных изображений символов. Например: //Р2-03.СРР - использование кодов символьных констант ^include Ciostream.h> void main(} ( cout « 1 \xOA* « 1\x48' « '\x65' « 1 \x6C* « ' \x6C'; cout « ' \x6F' « •\x2C'; cout « '\40' « '\127' « 1\157'; cout « '\162' « *\154' « '\144* « '\41'; I Программа с новой строки выведет на экран: Hello, World! В первом и втором из операторов вывода использованы шестнад- цатеричные, а в третьем и четвертом - восьмеричные коды символов: 1 \хОА* - '\nf; '\х48' - • н•; . . .; ,\40’ - "пробел”; ,\127» - »w; . . •\41» - 1! Значением символьной константы является числовое значение ее внутреннего кода. Как упоминалось, в Си++ односимвольная кон- станта имеет тип char, т.е. занимает в памяти 1 байт. Двухсимвольные константы вида '\t\n’ или '\r\07' представляются двухбайтовыми значениями типа int, причем первый символ ('\t• или • \г' в приме- рах) размещается в младшем байте (с меньшим адресом), а второй (в примерах '\п* или '\07!) - в старшем байте. Перечисленные соглашения иллюстрирует следующая программа: //Р2-04.СРР - длины внутренних представлении символьных / / констант ttinclude <iostream.h> void main() ( cout « "\n Длины символьных (литерных) констант "; cout « "(в байтах):"; cout « "\nsizeof\1z\* « " « sizeof 'z'; cout « "\nsizeof\'\\n\' « " « sizeof '\n’; cout « "\nsizeof\'\\n\\t\' = " « sizeof f\n\t’; cout « "\nsizeof\1\\xO7\\xO7\' « " « sizeof '\x07\x07'; cout « "\nsizeof\1\\x0004F\1 = " « sizeof '\x0004F’; cout « "\nsizeof\’\\x4F\' = " « sizeof '\x4F’;
28 ЯзыкСи++ cout « "\nsizeof\1\\111\1 « " « sizeof ' cout « "ХпДесятичное значение и; cout « "кода символа \l\\x0004F\' = " « (int)1\x0004F'; cout « "ХпДесятичное значение cout « "кода символа \ '\\x4F\’ = " « (int)'\x4F'; cout « "ХпДесятичное значение кода пробела « cout « (int)* ’; В результате выполнения программы на экран будет выведено: Длины символьных (литерных) констант (в байтах): sizeof z’ = 1 sizeof \n' = 1 sizeof \n\t' = 2 sizeof\x07\x07' « 2 sizeof \x0004F' = 1 sizeof Xx4F* « 1 sizeof'\111' « 1 Десятичное значение кода символа '\x0004F' = 79 Десятичное значение кода символа 1\x4F' = 79 Десятичное значение кода пробела = 32 Строка, или строковая константа, иногда называемая литерной строкой, определяется как последовательность символов, заключен- ная в кавычки (не в апострофы): "Это строка, называемая также строковой константой" Говоря о строках, иногда используют термины "строковый лите- рал" [5], "стринговый литерал", "стринговая константа", "стринг" [3], однако мы будем придерживаться традиционной терминологии, так как опыт показывает, что возможное неоднозначное толкование тер- мина "строка" легко устраняется контекстом. (В английском языке этой проблемы не возникает, так как используются два различных слова: line - линия, строка текста н string - серия, ряд, последователь- ность, гирлянда...) Среди символов строки могут быть эскейп- последовательности, т.е. сочетания, соответствующие неизобра- жаемым символьным константам или символам, задаваемым значе- ниями их внутренних кодов. В этом случае, как и в представлениях отдельных символов, они начинаются с обратной косой черты ' \ ': //Р2-05.СРР - строки с эскейп-последовательностями #include <iostream.h> void main() { cout « "ХпЭто строка,Хпиначе - \"стринг\",Хпиначе - "; cout « "X"строковый литералХ"."; )
Глава 2. Лексические основы языка Си++ 29 При выводе на экран дисплея этих строк эскейп- последовательности '\п' и *\н’ обеспечат такое размещение инфор- мации: Это строка, иначе - "стринг", иначе - "строковый литерал". Обратите внимание на наличие символа ' \' перед двойной ка- вычкой внутри строки. Именно по наличию этого символа компиля- тор отличает внутреннюю кавычку от кавычки, ограничивающей строку. Строки, записанные в программе подряд или через пробельные разделители, при компиляции конкатенируются (склеиваются). Таким образом, в тексте программы последовательность из двух строк: "Строка - это массив символов. " "Строка имеет тип chart]." эквивалентна одной строке: "Строка - это массив символов. Строка имеет тип char[]." Длинную строковую константу можно еще одним способом раз- местить в нескольких строках текста программы, используя специаль- ное обозначение переноса - ' \'. Пример одной строковой константы, размещенной на трех строках в тексте программы: "Обычно транслятор отводит \ каждом строковой константе \ отдельное место в памяти ЭВМ. ’’ Следующая программа иллюстрирует разные формы "склеивания" строк в одну: //Р2-06.СРР - конкатенация строк tfinclude <iostream.h> void main() { cout « "\nl" ”9" "93" " год"; // При выводе пробелы будут удалены cout « " начался с\ пятницы."; // Пробелы из этой строки сохранятся } Результат выполнения программы: 1993 год начался с пятницы.
30 Язык Си++ Обратите внимание на количество пробелов в результате перед словом ’’пятницы’*. Продолжением перенесенной с помощью символа ' \ ’ строки считается любая информация на следующей строке, в том числе и пробелы. Размещая строку в памяти, транслятор автоматически добавляет в ее конец символ • \0’, т.е. нулевой байт. Таким образом, количество символов во внутреннем представлении строки на 1 больше числа символов в ее записи. Пустая строка хранится как один символ *'\0". Кроме непосредственного использования строк в выражениях, строку можно поместить в символьный (типа char) массив с вы- бранным именем и в дальнейшем обращаться к ней по имени массива. Чаще всего для размещения строковой константы в массиве использу- ется его инициализация. Следующая программа выполняет указанные действия: //Р2-07.СРР -* инициализация массива строковой константой #include <iostream.h> void main{) { char stroka[] = "REPETITIO EST MATER STUDIORUM"; cout « "\nsizeof stroka = " « sizeof stroka; cout « "\nstroka = " « stroka; } Результат выполнения программы: sizeof stroka - 30 Stroka = REPETITIO EST MATER STUDIORUM Обратите внимание, что при определении массива char после его имени stroka в скобках [] не указано количество элементов. Размер массива подсчитывается автоматически во время инициализации и равен количеству символов в строковой инициализирующей констан- те (в нашем случае 29) плюс один элемент для завершающего символа • \о •. Кавычки не входят в строку, а служат ее ограничителями при за- писи в программе. В строке может быть один символ, например, "А" - строка из одного символа. Однако в отличие от символьной констан- ты 9А' длина внутреннего представления строки "А" равна 2. Строка может быть пустой при этом ее длина равна 1, Однако символьная константа не может быть пустой, т.е. запись 1 * в большинстве реали- заций недопустима. //Р2-08.СРР - длины строк и символьных констант (литер) #include <iostream.h>
Глава 2. Лексические основы языка Си++ 31 void main() ( cout « "\nsizeof\"\" = " « sizeof cout « "\tsizeof\1A\* = " « sizeof 'A'; cout « "\tsizeof\"A\" = " « sizeof "A"; cout « "\nsizeof\'\\n\' = " « sizeof '\n'; cout « "\tsizeof\"\\n\" = " « sizeof "\n"; cout « ”\nsizeof\1\\xFF\1 = " « sizeof '\xFF'; cout « "\tsizeof\"\\xFF\" = " « sizeof "\xFF"; Результат выполнения программы: sizeof"" = 1 sizeof'\n' = 1 sizeof"\xFF" = 2 sizeof'A' = 1 sizeof"\n" » 2 sizeof'A" « 2 sizeof'\xFF' = 1 2.4. Знаки операций Знаки операций обеспечивают формирование и последующее вы- числение выражений. Выражение есть правило для получения значе- ния. Один и тот же знак операции может употребляться в различных выражениях и по-разному интерпретироваться в зависимости от кон- текста. Для изображения операций в большинстве случаев использу- ется несколько символов. В ANSI-стандарте языка Си определены следующие знаки операций [3, 5]: [] О ~> + + — & * + — ** 1 sizeof / % « » < > <= >= =яг 1 = А 1 && 1 1 ? : = ★ = /=“ %= + = —s «= »= &= А_ 1 = г # ## Дополнительно к перечисленным в Си++ введены: ->* new delete typeid За исключением операций [], () и ?: все знаки операций рас- познаются компилятором как отдельные лексемы. В зависимости от контекста одна и та же лексема может обозначать разные операции. Например, бинарная операция & - это поразрядная конъюнкция, а унарная операция & - это операция получения адреса.
32 Язык Си++ Одним из принципиальных отличий языка Си++ от предше- ствующего ему языка Си является возможность расширения действия, иначе перегрузки (overload) стандартных операций, т.е. распростране- ния их действия на нестандартные для них операнды. Материал, отно- сящийся к расширению действия (перегрузке) операций, будет рассматриваться в следующих главах. Сейчас опишем кратко стан- дартные возможности отдельных операций. Унарные операции & операция получения адреса операнда; * операция обращения по адресу, т.е. раскрытия ссылки, ина- че операция разыменования (доступа по адресу к значению того объекта, на который указывает операнд). Операндом должен быть адрес; унарный минус - изменяет знак арифметического операнда; + унарный плюс (введен для симметрии с унарным минусом); * поразрядное инвертирование внутреннего двоичного кода целочисленного аргумента (побитовое отрицание); ’ логическое отрицание (НЕ) значения операнда; применяется к скалярным операндам; целочисленный результат 0 (если операнд ненулевой, т.е. истинный) или 1 (если операнд ну- левой, т.е. ложный). В качестве логических значений в языке Си++ используют целые числа: о - ложь и не нуль (’ 0) - ис- тина. Отрицанием любого ненулевого числа будет 0, а от- рицанием нуля будет 1. Таким образом: !1 равно 0; !2 равно 0; ! (-5) равно 0; ! О равно 1; ++ увеличение на единицу (инкремент или автоувеличение): префиксная операция ~ увеличение значения операнда на 1 до его использования; постфиксная операция - увеличение значения операнда на 1 после его использования. Операнд не может быть константой либо другим праводо- пустимым выражением. Записи ++5 или 84++ будут невер- ными. Операндом не может быть и произвольное выражение. Например, ++ (j+k) также неверная запись. Операндом унарных операций ++ и — должны быть всегда леводопустимые выражения, например, переменные (разных типов);
Глава 2. Лексические основы языка Си++ 33 — уменьшение на единицу (декремент или автоуменьшение) - операция, операндом которой не может быть константа и праводопустимое выражение: префиксная операция - уменьшение значения операнда на 1 до его использования; постфиксная операция - уменьшение значения операнда на 1 после его использования; sizeof операция вычисления размера (в байтах) для объекта того типа, который имеет операнд. Разрешены два формата опе- рации: sizeof унарное__выражение И sizeof (тип). Примеры использования операций с простейшими унарными вы- ражениями, к которым относятся константы, приводились в связи с изложением материала о константах (см. Р2-04.срр). Проиллюстри- руем применение этой операции со стандартными типами: //Р2-09.СРР “ размеры разных типов данных linclude <iostream.h> void main() ( cout « "\nsizeof(int) =* ” « sizeof(int); cout « "\tsizeof(short) » ” « sizeof(short); cout « "\tsizeof(long) » ” « sizeof(long); cout « "\nsizeof (float) » '• « sizeof (float) ; cout « "\tsizeof(double) » M « sizeof(double); cout « H\tsizeof (char) ® '• « sizeof(char); ) Результат выполнения программы: sizeof (int) a 2 sizeof (short) a 2 sizeof (long) =* 4 sizeof (float) == 4 sizeof (double) =* 8 sizeof (char) = 1 Бинарные операции. Эти операции делятся на следующие группы: • аддитивные; • мультипликативные; • сдвигов; • поразрядные; • операции отношений; • логические; • присваивания; • выбора компонента структурированного объекта; • операции с компонентами классов; • операция "запятая"; • скобки в качестве операций.
34 Язык Си++ Аддитивные операции: + бинарный плюс (сложение арифметических операндов или сложение указателя с целочисленным операндом); бинарный минус (вычитание арифметических операндов или указателей). Мультипликативные операции: * умножение операндов арифметического типа; / деление операндов арифметического типа. Операция стан- дартна. При целочисленных операндах абсолютное значе- ние результата округляется до целого. Например, 20/3 равно 6, -20/3 равняется -6, (-20)/з равно -6, 20/(-3) равно-6; % получение остатка от деления целочисленных операндов (деление по модулю). При неотрицательных операндах остаток положительный. В противном случае остаток опре- деляется реализацией. В компиляторах ТС++ и ВС++: 13%4 равняется 1, (-13) %4 равняется -1, 13% (-4) равно+1, а (-13) % (-4) равняется-1. При ненулевом делителе для целочисленных операндов всегда выполняется соотношение: (а/Ь)*ь + а%Ьравноа. Операции сдвига (определены только для целочисленных операндов). Формат выражения с операцией сдвига: операнд__левый операция_сдвига операнд_правый « сдвиг влево битового представления значения левого цело- численного операнда на количество разрядов, равное зна- чению правого целочисленного операнда; » сдвиг вправо битового представления значения левого це- лочисленного операнда на количество разрядов, равное значению правого целочисленного операнда. Поразрядные операции: & поразрядная конъюнкция (И) битовых представлений зна- чений целочисленных операндов; I поразрядная дизъюнкция (ИЛИ) битовых представлений значений целочисленных операндов; А поразрядное исключающее ИЛИ битовых представлений значений целочисленных операндов.
Глава 2. Лексические основы языка Си++ 35 Следующая программа иллюстрирует особенности операций сдвига и поразрядных операций. //Р2-10.СРР - операции сдвига и поразрядные операции #include <iostream.h> void main() { cout « "\n4«2 равняется " « (4«2) ; cout « "\t5»l равняется " « (5«1) ; cout « "\п6&5 равняется " « (6&5) ; cout « "\t6|5 равняется ” « (6|5) ; cout « "\t6A5 равняется " « (6А5) ; Результат выполнения программы: 4«2 равняется 16 5»1 равняется 2 6&5 равняется 4 6 | 5 равняется 7 6А5 равняется 3 Тем, кто давно не обращал внимания на битовое представление целых чисел, напоминаем, что двоичный код для 4 равен 100, для 5 - это 101, для 6 - 110 и т.д. При сдвиге влево на 2 позиции код 100 становится равным 10000 (десятичное значение равно 16). Остальные результаты операций сдвига и поразрядных операций могут быть прослежены аналогично. Обратите внимание, что сдвиг влево на п позиций эквивалентен умножению значения на 2", а сдвиг вправо кода уменьшает соответствую- щее значение в 2" раз с отбрасыванием дробной части результата. (Поэтому 5»1 равно 2.) Операции отношения (сравнения): < меньше, чем; > больше, чем; <= меньше или равно; >= больше или равно; = = равно; != не равно.
36 Язык Си++ Операнды в операциях отношения арифметического типа или ука- затели. Результат целочисленный: 0 (ложь) или 1 (истина). Последние две операции (операции сравнения на равенство) имеют более низкий приоритет по сравнению с остальными операциями отношения. Та- ким образом, выражение (х < в == а < х) есть 1 тогда и только тогда, когда значение х находится в интервале от а до в. (Вначале вы- числяются х < в и а < х, ак результатам применяется операция сравнения на равенство =.) Логические бинарные операции: && конъюнкция (И) арифметических операндов или отноше- ний. Целочисленный результат 0 (ложь) или 1 (истина); I | дизъюнкция (ИЛИ) арифметических операндов или отно- шений. Целочисленный результат 0 (ложь) или 1 (истина). (Вспомните о существовании унарной операции отрицания ’’'.) Следующая программа иллюстрирует некоторые особенности операций отношения и логических операций: //Р2-11.СРР - операции отношения и логические операции #include <iostream.h> void main() { cout « п\пЗ<5 равняется " « (3<5); cout « "\t3>5 равняется " « (3>5); cout « "\n3=5 равняется " « (3==5) ; cout « "\t3!“5 равняется " « (3!“5); cout « "\n3!=5 || 3“5 равняется ” « (3! =51 13==5) ; cout « "\n3+4>5 && 3+5>4 && 4+5>3 равняется ” « (3+4>5 && 3+5>4 && 4+5>3); } Результат выполнения программы: 3<5 равняется 1 3>5 равняется О 3==5 равняется О 3’=5 равняется 1 3’=5 || 3==5 равняется 1 3+4>5 && 3+5>4 && 4+5>3 равняется 1 Операции присваивания. В качестве левого операнда в операциях присваивания может ис- пользоваться только модифицируемое /-значение - ссылка на некото- рую именованную область памяти, значение которой доступно
Глава 2. Лексические основы языка Си++ 37 изменениям. Термин /-значение (left value), иначе - леводопустимое выражение, происходит от объяснения действия операции присваива- ния е = D, в которой операнд е слева от знака операции присваива- ния может быть только модифицируемым /-значением. Примером модифицируемого /-значения служит имя переменной, которой выде- лена память и соответствует некоторый класс памяти. Итак, перечис- лим операции присваивания: » присвоить значение выражения-операнда из правой части операнду левой части: р = 10.3 - 2*х; *= присвоить операнду левой части произведение значений обоих операндов: ₽ *« 2 эквивалентно р = р * 2; /= присвоить операнду левой части частное от деления значе- ния левого операнда на значение правого: р /= 2.2 - dэквивалентно р = р / (2.2 - d); %= присвоить операнду левой части остаток от деления цело- численного значения левого операнда на целочисленное значение правого операнда: N %= 3 эквивалентно n = N % 3; += присвоить операнду левой части сумму значений обоих опе- рандов^ += в эквивалентно А « а + в; -= присвоить операнду левой части разность значений левого и правого операндов: х -= 4.3 - z эквивалентнох = х - (4.3 - z); «= присвоить целочисленному операнду левой части значение, полученное сдвигом влево его битового представления на количество разрядов, равное значению правого целочис- ленного операнда: а «= 4 эквивалентно а « а « 4; »= присвоить целочисленному операнду левой части значение, полученное сдвигом вправо его битового представления на количество разрядов, равное значению правого целочис- ленного операнда: а »= 4 эквивалентно а = а » 4; &= присвоить целочисленному операнду левой части значение, полученное поразрядной конъюнкцией (И) его битового представления с битовым представлением целочисленного операнда правой части: е &= 44 эквивалентно е = е & 44;
38 Язык Си++ присвоить целочисленному операнду левой части значение, полученное поразрядной дизъюнкцией (ИЛИ) его битового представления с битовым представлением целочисленного операнда правой части: а | = Ьэквивалентно а = а | Ь; присвоить целочисленному операнду левой части значение, полученное применением поразрядной операции исклю- чающего ИЛИ к битовым представлениям значений обоих операндов: z А= х + у эквивалентно z = z А (х + у). Обратите внимание, что для всех операций сокращенная форма присваивания El ор= E2 эквивалентна El = Е1 ор (е2), где ор - обозначение операции. Для иллюстрации некоторых особенностей выполнения операций присваивания рассмотрим следующую программу: //Р2-12.СРР - операции присваивания tfinclude <iostream.h> void main() { int k; cout « "\n\n k = 35/4 равняется " « (k=35/4); cout « "\tk/=l+l+2 равняется " « (k/=l+l+2); cout « "\n k *= 5 - 2 равняется " « (k*=5-2); cout « "\t k %= 3 + 2 равняется " « (k%=3+2); cout « "\n k += 21/3 равняется " « (k+=21/3); cout « "\t k -= 6 - 6/2 равняется " « (k-=6-6/2); cout « "\n k «= 2 равняется " « (k«=2) ; cout « "\t k »= 6-5 равняется 11 « (k»=6-5) ; cout « "\n k &= 9 + 4 равняется " « (k&=9+4) ; cout « "\t k |=8-2 равняется ” « (k|=8-2) ; cout « ”\n k A= 10 равняется " « (kA=10); В первом присваивании обратите внимание на выполнение деле- ния целочисленных операндов, при котором выполняется округление за счет отбрасывания дробной части результата. Результаты выполнения: к = 35/4 равняется 8 к *= 5 - 2 равняется 6 к += 21/3 равняется 8 к <<= 2 равняется 20 к &= 9 + 4 равняется 8 к А= 10 равняется 4 к /= 1 + 1 + 2 равняется 2 к %== 3 + 2 равняется 1 к -= б - 6/2 равняется 5 к »= 6 - 5 равняется 10 к |= 8 - 2 равняется 14
Глава 2. Лексические основы языка Си++ 39 Полученные числовые значения, во-первых, подтверждают экви- валентность записей ei ор=» Е2 и Е1 « Е1 ор (Е2). Кроме того, ана- лизируя результаты, можно еще раз рассмотреть особенности поразрядных операций. Двоичный код для к, равного 5, будет 101. Сдвиг влево на 2 дает 10100 (десятичное 20). Затем сдвиг на 1 вправо формирует код 1010 (десятичное 10). Поразрядная конъюнкция 1010&1Ю1 дает 1000 (десятичное 8). Затем 1000|110 дает значение 1110 (десятичное 14). Результатом 1110А1010 будет 0100 (десятичное 4). Операции выбора компонентов структурированного объекта: , (точка) прямой выбор (выделение) компонента структурированного объекта, например объединения. Формат применения опе- рации: имя структурированного объекта.имя компонента -> косвенный выбор (выделение) компонента структурирован- ного объекта, адресуемого указателем. При использовании операции требуется, чтобы с объектом был связан указа- тель. В этом случае формат применения операции имеет вид: указатель_на_структурированный_объвкт -> имя_компонента Так как операции выбора компонентов структурированных объ- ектов используются со структурами, объединениями, классами, то не- обходимые пояснения и примеры мы приведем позже, введя Перечисленные понятия и, кроме того, определив указатели. Операции с компонентами классов: . * прямое обращение к компоненту класса по имени объекта и указателю на компонент; *>♦ косвенное обращение к компоненту класса через указатель на объект и указатель на компонент. Комментировать эти операции затруднительно, не введя понятие класса, что будет сделано позже. 1: операция указания области видимости имеет две формы: бинарную и унарную. Бинарная форма применяется для до- ступа к компоненту класса. Унарная операция 1 : :' позво- ляет получить доступ к внешней для некоторой функции именованной области памяти. Следующая программа пояс- няет возможности унарной операции:
40 ЯзыкСи++ //Р2-13.СРР - изменение видимости внешней переменной ttinclude <iostream.h> int k » 15; // Глобальная переменная с начальным значением void main() { int k = 10; // Локальная переменная с начальным значением cout « "\пВнешняя переменная к = " « ::к; cout « "\пВнутренняя переменная к « ” « к; ::к = 0; cout « "\пВнешняя переменная к = ” « : :к; cout « "ХпВнутренняя переменная к « ” « к; } Результат выполнения программы: Внешняя переменная к = 15 Внутренняя переменная к = 10 Внешняя переменная к — 0 Внутренняя переменная к = 10 Как видно из примера, с помощью унарной операции можно организовать доступ из тела функции к внешней переменной, если переменная с тем же именем определена внутри функции. Несколько другие возможности обеспечивает та же операция при работе с клас- сами. Об этих ее особенностях речь пойдет позже. Запятая в качестве операции: несколько выражений, разделенных запятыми, вычисляются последовательно слева направо. В качестве результата со- храняются тип и значение самого правого выражения. Та- ким образом, операция ’'запятая'1 группирует вычисления слева направо. Тип и значение результата определяются са- мым правым из разделенных запятыми операндов (выраже- ний). Значения всех левых операндов игнорируются. На- пример: //Р2-14.СРР - запятая в качестве знака операции #include <iostream.h> void main() ( int d; cout « "\пВыражение d = 4, d*2 равно " « (d=4,d*2) ; cout « ”, d равно ” « d; } Программа выведет на экран:
Г Ляма 2. Лексические основы языке Си++ 41 Выражение d » 4, d*2 равно 8, d равно 4 Круглые 1 () 1 и квадратные ' []1 скобки играют роль бинарных нитраций при вызове функций и индексировании элементов массивов. Дли программиста, мало знакомого с техникой использования указа- tfuteH, мысль о том, что скобки в ряде случаев являются бинарными вибрациями, часто даже не приходит в голову. И это тогда, когда он практически в каждой программе обращается к функциям или приме- няет индексированные переменные. Итак, отметим, что скобки могут служить бинарными операциями, особенности и возможности кото- рых достойны внимания. Круглые скобки обязательны в обращении к функции: имя_функции (список_аргументов) Здесь операндами служат имя__функции и список_аргументов. Ре- зультат вызова вычисляется в теле функции, структуру которого зада- er ее определение. В выражении имя_массива [индекс] операндами для операции ' II' служатимя_массива и индекс. В языках Си и Си++ принято, что индексы массивов начинаются с нуля, т.е. массив int z [3] из трех щементов включает индексированные элементы z [0], z [1], z [2]. Это соглашение языка становится очевидным, если учесть, что индекс определяет не номер элемента, а его смещение относительно начала массива. Таким образом, z [0] - обращение к первому элементу, Z [1] обращение ко второму элементу и т.д. В следующей программе по- кйэано, как обычно используют квадратные скобки при работе с эле- ментами массива: //Р2-15. СРР - работа с элементами массива *include <iostream.h> void main() ( char x[] » "DIXI"; // "Я СКАЗАЛ" (высказался) int i = 0; while (x[i] !» '\0') cout « "\n" « x[i++]; I Результат - слово "DIXI", написанное в столбик (сверху вниз): о X X I
42 Язык Си++ Оператор цикла с заголовком while выполняется, пока верно вы- ражение в скобках, т.е. пока очередной символ массива не равен 1 \о'. При каждом вычислении выражения x[i++] используется текущее значение i, которое затем увеличивается на 1. В данном случае квад- ратные скобки играют роль бинарной операции, а операндами служат имя массива х и индекс i++. В Си++ действие операций * []1 и ' () ' расширено (они перегру- жены - overloaded), но об этом нужно говорить в связи с классами, что будет сделано позже. Именно из-за появления в языке Си++ механизма расширения действия (перегрузки - overload) стандартных операций и в связи с не- обходимостью расширять действие скобок-операций на необычные для них операнды скобки отнесены к стандартным опера- циям языка Си++. Условная операция. В отличие от унарных и бинарных операций условная операция используется с тремя операндами. В изображении условной операции два размещенных не подряд символа •? •, и »:» и три операнда-выражения: выражение__1 ? выражение^ 2 : выражение__3 Первым вычисляется значение выраж©ния_1. Если оно истинно, т.е. не равно нулю, то вычисляется значение выражекия_2, которое становится результатом. Если при вычислении выражония_1 получит- ся 0, то в качестве результата берется значение выражения_з. Класси- ческий пример: х < 0 ? -х : х; Выражение возвращает абсолютное значение переменной х. Операция явного преобразования (приведения) типа в языке Си++ имеет две различные формы. Каноническая, унаследованная от языка Си, имеет следующий формат: (имя_типа) операнд и позволяет преобразовывать значение операнда к нужному типу. В качестве операнда используется унарное выражение, которое в про- стейшем случае может быть переменной, константой или любым вы- ражением, заключенным в круглые скобки. Например, следующие преобразования изменяют длину внутреннего представления целых констант, не меняя их значений:
Глава 2. Лексические основы языка Си++ 43 (long) 1 - внутреннее представление имеет длину 4 байта; (char) 1 - внутреннее представление имеет длину 1 байт. В этих преобразованиях константа не меняла значения и остава- лась целочисленной. Однако возможны более глубокие преобразова- ния, например, (long double)! или (float) 1 не только изменяют длину константы, но и структуру ее внутреннего представления. В ней будут выделены порядок и мантисса. При преобразовании длинных целочисленных констант к вещественному типу (например, к типу float) возможна потеря значащих цифр (потеря точности). Если веще- ственное значение преобразуется к целому, то возможна ошибка при выходе полученного значения за диапазон допустимых значений для целых. В этом случае результат преобразования непредсказуем и це- ликом зависит от реализации. Кроме рассмотренной канонической операции явного приведения типа, которая унаследована языком Си++ от языка Си, в языке Си++ ввсдена еще одна возможность приведения типов, которую обеспечи- вает функциональная форма преобразования типа: (операнд) Она может использоваться только в тех случаях, когда тип имеет про- стое (несоставное) наименование (обозначение): long (2) - внутреннее представление имеет длину 4 байта; double (2) - внутреннее представление имеет длину 8 байтов. Однако будет недопустимым выражение: unsigned long(2) 11 Ошибка! / Операции new и delete для динамического распределения памяти. Это еще две особые унарные операции, появившиеся в языке Си++. Они связаны с одной из задач управления памятью, а именно с ее ди- намическим распределением. Операция new имя_типа Либо new имя_типа инициализатор позволяет выделить и сделать доступным свободный участок в основ- ной памяти, размеры которого соответствуют типу данных, опреде- ляемому именем типа. В выделенный участок заносится значение, определяемое инициализатором, который не является обязательным
44 Язык Си++ элементом. В случае успешного выполнения операция new возвращает адрес начала выделенного участка памяти. Если участок нужных раз- меров не может быть выделен (нет памяти), то операция new возвра- щает нулевое значение адреса (null). Синтаксис применения операции: указатель » new имя^типа инициализатор Здесь необязательный инициализатор - это выражение в круглых скобках. Указатель, которому присваивается получаемое значение адреса, должен относиться к тому же типу данных, что и имя_типа в операции new. О размерах участков памяти уже упоминалось в связи с константами разных типов. Поэтому, не вводя пока других понятий, относящихся к типам данных, приведем несложные примеры. Операция new float выделяет участок памяти размером 4 байта. Операция new int (15) выделяет участок памяти в 2 байта и инициа- лизирует этот участок целым значением 15. Синтаксис использования операций new и delete предполагает применение указателей. Предва- рительно каждый указатель должен быть определен. Определение указателя имеет вид: тип *имя__указателя ; имя^указателя - это идентификатор. В качестве типа можно ис- пользовать, например, уже упомянутые стандартные типы int, long, float, double, char. Таким образом, int *h; - определение указателя h, который может быть связан с участком памяти, выделенным для ве- личины целого типа. Введя с помощью определения указатель, можно присвоить ему возвращаемое операцией new значение: h = new int(15); В дальнейшем доступ к выделенному участку памяти обеспечивает выражение *п. В случае отсутствия в операции new инициализатора значение, ко- торое заносится в выделенный участок памяти, не определено и не следует рассчитывать, что там будет, например, нуль. Если в качестве имени типа в операции new используется массив, то для массива должны быть полностью определены все размерности. Но и при этом инициализация участка памяти, выделяемого для мас- сива, запрещена. Подробнее о выделении памяти для массивов речь пойдет в главе 5.
Г пава 2. Лексические основы языка Си++ 45 Продолжительность существования выделенного с помощью опе- рации new участка памяти - от точки создания до конца программы или до явного его освобождения. Для явного освобождения выделенного операцией new участка памяти используется оператор delete указатель; где указатель адресует освобождаемый участок памяти, ранее выделенный с по- мощью операции new. Например, delete h; - освободит участок па- мяти, связанный с указателем h. Повторное применение операции delete к тому же указателю дает неопределенный результат. Также не- предсказуем результат применения этой операции к указателю, полу- чившему значение без использования операции new. Однако применение delete к указателю с нулевым значением не запрещено, хотя и не имеет особого смысла. Иллюстрацией к сказанному служит следующая программа, в ко- торой с одним указателем целого типа последовательно связываются четыре динамически выделяемых участка памяти: //Р2-16.СРР - динамическое распределение памяти * include <iostream.h> void stain () ( int *i; i = new int(l); cout « "\n*i—" « *i « "\t i=" « i; i ss new int (5) ; cout « "\t*i==" « *i « "\t\t i=" « i; i = new int (2**i); cout « "\n*i="«*i«"\t i=" « i; i = new int (2**i); cout « "\t*i=" « *i « "\t\t i«" « i; delete i; cout « "\n После освобождения памяти cout « ”\n*i»w « *i « ”\t i=" <<i; delete i; // Некорректное применение операции cout « "\t*i=" « *i « "\t i=” « i; ) Результат выполнения программы: ♦ i=l i=0x91790004 *i=5 i=0x917a0004 * i=10 i=0x917b0004 *i=20 i®0x917c0004 После освобождения памяти: ♦ i»20 i»0x917c0004 *i=-28292 i=0x917c0004 Обратите внимание, что после выполнения первого оператора delete i; значение указателя i и содержимое связанного с ним
46 ЯзыкСи++ участка памяти *i еще сохранились. После вторичного применения операции delete значение указателя не изменилось, а содержимое свя- занного с ним участка памяти "испортилось". Указанные изменения и (или) сохранения значения *i не обязательны и полностью зависят от реализации и от конкретного исполнения программы. В ряде случаев при работе с интегрированной средой ВС++ версии 3.1 выдается со- общение об ошибке: Null pointer assignment Для освобождения памяти, выделенной для массива, используется следующая модификация того же оператора: delete [] указатель; где указатель связан с выделенным для массива участком памяти. Подробнее об этой форме оператора освобождения памяти будем го- ворить в связи с массивами и указателями в главе 5. Ранги операций. Завершая краткий обзор операций языка Си++, приведем таблицу приоритетов, или рангов операций [4, 9]. Таблица 2.4 Приоритеты операций Ранг Операции Ассоци- атив- ность 1 ()[]->::. 2 ! - + - ++ — & * (тип) sizeof new delete тип () (функциональное преобразование типа) <— 3 .* ->* 4 * / % (мультипликативные бинарные операции) 5 + - (аддитивные бинарные операции) 6 « » 7 <<«=>«> —> 8 жж 1 ж 9 & 10 А 11 1 12 £6 13 1 1 14 ?: (условная операция) 15 ж *ж /ж %ж -в £в ЛЖ | Ж «Ж »в <— 16 , (операция "запятая") —>
Глнва 2. Лексические основы языка Си++ 47 Грамматика языка Си++ определяет 16 категорий приоритетов операций. В табл. 2.4 категории приоритетов названы рангами. (Эпсрации ранга 1 имеют наивысший приоритет. Операции одного ранга имеют одинаковый приоритет, и если их в выражениях несколько, то они выполняются в соответствии с правилом ассоциативности либо слева направо (-»), либо справа налево (^-). Если один и тот же знак операции приведен в таблице дважды, то первое появление (с меньшим по номеру, т.е. старшим по приоритету, рангом) соответствует унарной операции, а второе - бинарной. Отметим, что кроме стандартных режимов использования операций язык Си++ допускает расширение их действий на объекты классов, вводимых пользователем или уже определенных в конкретной реализации языка. Примером такого расширения (перегрузки) является операция извлечения данных из потока « и операция передачи данных в выходной поток >>, применяемые к потокам ввода cin и вывода cout. 2.5. Разделители Разделители, или знаки пунктуации, входят в число лексем языка: [](){), ; # & Квадратные скобки ’[]' ограничивают индексы одно- и многомерных массивов и индексированных элементов. // Одномерный массив мз пяти элементов: int А[] = {0, 2, 4, б, 8}] ; // е -двумерный массив - матрица размерности 3x2: int к г е [3] [2] ; // Начальному элементу массива е м переменной х // присваивается значение 4 (третий элемент массива А): е [0] [0] = х = А[2]; Круглые скобки'()': 1) выделяют условные выражения (в операторе «если».): if ([<0) х = -х; // Модуль значения // арифметической переменной
48 Язык Си++ 2) входят как обязательные элементы в определение и описание (в прототип) любой функции, где выделяют соответственно список формальных параметров и список спецификаций параметров: float F(float х, int к) //Определение функции { тело функции ) float F(float, int) //Определение функции - ее прототип F(Z,n) ; // Вызов функции 3) обязательны в определении указателя на функцию: int (*func) (void); //Определение указателя на функцию 4) группируют выражения, изменяя естественную последователь- ность выполнения операций: Y «= (а+Ь) с; // Изменение приоритета операций 5) входят как обязательные элементы в операторы циклов: for (i = 0, j = 1; i < J; i + = 2, j++) тело_цикла; while (i<j) тело_цикла; do тело_цикла while (k > 0); 6) необходимы при явном преобразовании типа. Как показано выше при описании операций, в Си++ существуют две формы задания явного преобразования типов: приведение типа (имя__типа) операнд и функциональное приведение-имя_типа(операнд) , где операнд -это любое скалярное выражение. В обеих формах явного приведения типа круглые скобки обязательны. Примеры: long i = 12L; int j;// Определение переменных j = int(i) ; // Функциональная запись преобразования float В; // Определение переменной В = (float)j; // Явное приведение типа j получает значение 12L, преобразованное к типу int. Затем в получает значение 12, преобразованное к типу float. 7) применение круглых скобок настоятельно рекомендуется в макроопределениях, обрабатываемых препроцессором: #define R(x,y) sqrt((х)*(х)+(у)*(у)) Это позволяет использовать в качестве параметров макровызовов арифметические выражения любой сложности и не сталкиваться с нарушениями приоритетов операций (см. гл. 8). Фигурные скобки '{}' обозначают соответственно начало и конец составного оператора или блока. Пример использования составного оператора в условном операторе:
Глава 2. Лексические основы языка Си++ 49 if (d > х) { d—; х++; } Пример блока, являющегося телом функции: float exponent(float х, int n) { float d « 1.0; int i = 0; if (x=0) return 0.0; for(; i < abs (n) ; i++, d*=x); return n > 0 ? d : 1.0/d; } Обратите внимание на отсутствие точки с запятой после закры- вающейся скобки •}1, обозначающей конец составного оператора или блока. Фигурные скобки используются для выделения списка компонен- тов в определениях типов структур, объединений, классов: struct cell ( char *b; // Определение типа int ее; // структуры double U[6]; } ; union smes { unsigned int ii; // Определение типа char cc[2]; // объединения } ; class sir { int В; // Определение класса public: int X, D; sir(int); } ; Обратите внимание на необходимость точки с запятой после опи- сания (определения) каждого типа. Фигурные скобки используются при инициализации массивов и структур при их определении: // Инициализация массива: int month[] = ( 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }; // Инициализация структуры stock типа mixture: struct mixture ( int ii; double dd; char cc; } stock ® { 66€, 3.67, '\f }; В примере mixture - имя типа структуры с тремя компонентами разных типов, stock - имя конкретной структуры типа mixture. Компоненты ii, dd, сс структуры stock получают значения при ини- циализации. и-3821
50 Язык Си++ В следующей программе описана структура с именем constant и выполнена ее инициализация, т.е. ее компонентам типа long double присвоены значения знаменитых констант: //Р2-17.СРР - фигурные скобки в структуре и при ее // инициацияации finclude <iostream.h> void main () { struct { long double pi; long double e; ) constant « { 3.1415926535897932385, 2.7182818284590452354 }; cout « "\n" « constant.pi « ”\t” « constant.e; } На печать (на экран дисплея) выводятся: 3.141593 2.718282 Обратите внимание на точность (только 7 значащих цифр), выби- раемую по умолчанию при работе со стандартным потоком вывода cout. Запятая ', ' разделяет элементы списков. Во-первых, это списки начальных значений, присваиваемых индексированным элементам массивов и компонентам структур при их инициализации: char name[] = {,С','и,,'р',,а,,,н,,'о'}; // Это не строка! struct A {int х; float у; char z;} F e {3, 18.4, 'с*}; Другой пример списков - списки формальных и фактических па- раметров и их спецификаций в функциях. Третье использование запятой как разделителя - в заголовке опе- ратора цикла for: for ( х » pl, у » р2, i = 2; i < n; z=x+y, x ® у, у = z, i++ ); (В данном примере вычисляется л-й член ряда Фибоначчи z по значениям первых двух pl и р2.) Запятая как разделитель используется также в описаниях и опре- делениях объектов (переменных) одного типа: int i, n; float x, у, z, pl, p2; Запятая в качестве операции уже рассматривалась. Следует обра- тить внимание на необходимость с помощью круглых скобок отде-
friaea 2. Лексические основы языка Си++ 51 Лять запятую-операцию от запятой-разделителя. Например, в следующей Программе для элементов массива m используется список с тремя начальными значениями: // Р2-18.СРР - запятая как разделитель и как знак операции | include < iostream.h> Void main () (int i = 1, m[]={i, (i=2,i*i), i}; Cout « "\ni = " <<i«"\tm[0] = " « m[0] ; Cout « "\tm[l] = " « m[l] « ”\tm[2] = ’’ « m[2] ; Результат на экране: 1 - 2m[0] = lm[l] = 4m[2] = 2 В данном примере запятая в круглых скобках выступает в роли знака операции. Операция присваивания ”=" имеет более высокий приоритет, чем операция ’’запятая”. Поэтому вначале i получает значение 2, затем вычисляется произведение i*i, и этот результат служит значением выражения в скобках. Однако значением переменной i остается 2. В качестве еще одной области применения запятой как разделителя нужно отметить описание производного класса, где используются список базовых классов и список вызываемых конструкторов. В каждом из них могут понадобиться запятые. Кроме того, в списке однотипных компонентов класса они отделяются друг от друга запятыми. Перечисленные применения запятой будут понятны после рассмотрения классов. Точка с запятой';' завершает каждый оператор, каждое определение (кроме определения функции) и каждое описание. Любое допустимое выражение, за которым следует воспринимается как оператор. Это справедливо и для пустого выражения, т.е. отдельный символ ’’точка с запятой" считается пустым оператором. Пустой оператор часто используется как тело цикла. Примером может служить цикл for, приведенный выше для иллюстрации особенностей использования запятой в качестве разделителя. (Вычисляется и-й член ряда Фибоначчи.) Примеры операторов-выражений: i++; //Результат выполнения - только изменение значения i F(z,4) // Результат определяется телом функции с именем F Двоеточие':' служит для отделения (соединения) метки и помечаемого ею оператора: метка: оператор; 4’
52 Язык Си++ Метка - это идентификатор. Таким образом, допустимы, например, такие помеченные операторы: XYZ: = (Ь-с) * (d-c); сс: z * = 1; Второе применение двоеточия - описание производного класса, где имя класса отделяется от списка базовых классов двоеточием: ключ__класса имя_._класса: базовый_список {список_компонентов} Ключ^класса - это одно из трех служебных слов: struct, union, class. Имя_класса - произвольно выбираемый идентификатор. Базовыи_список - это список имен порождающих (базовых) классов. Не определяя списка компонентов (к чему вернемся, рассматривая классы), приведем пример определения производного класса: class х: А, В {список_компонентов}; Многоточие - это три точки без пробелов между ними. Оно используется для обозначения переменного числа параметров у функции при ее определении и описании (при задании ее прототипа). При работе на языке Си программист очень часто использует библиотечные функции со списком параметров переменной длины для форматных ввода и вывода. Их прототипы выглядят следующим образом: int printf (char *format,; int scanf (char *format,; Здесь с помощью многоточия указана возможность при обращении к функциям использовать разное количество параметров (не меньше одного, так как параметр format должен быть указан всегда и не может опускаться). Подготовка своих функций с переменным количеством параметров на языке Си++ требует применения средств адресной арифметики, например, предоставляемых головным файлом stdarg.h. Описание макросов va„ arg , va„end, vas tart для организации доступа из тела такой функции к спискам ее параметров приведено в главе 6. Звездочка как уже упоминалось, используется в качестве знака операции умножения и знака операции разыменования (получения значения через указатель). В описаниях и определениях звездочка означает, что описывается указатель на значение использованного в объявлении гида:
Глава 2. Лексические основы языка Си++ 53 int *point; // Указатель на величину типа int char **refer; // Указатель на указатель // на величину типа char Знак как уже упоминалось, является обозначением операции присваивания. Кроме того', в определении он отделяет описание объекта от списка его инициализации: struct {char х, int у} А = {'z ' , 1918}; int F - 66; В списке формальных параметров функции знак ’=’ указывает на выбираемое по умолчанию значение аргумента (фактического параметра): char СС (int Z = 12, char L = '\0 ’) {...} По умолчанию параметр z равен 12, параметр L равен ' \0’. Символ (знак номера или диеза в музыке) используется для обозначения директив (команд) препроцессора. Если тот символ является первым отличным от пробела символом в строке программы, то строка воспринимается как директива препроцессора. Символ играет роль разделителя при определении переменных типа ссылки: int В: // Описание переменной int &А = В; //А - ссылка на В Отметив использование символа в качестве разделителя при описании ссылок, отложим подробное рассмотрение ссылок.
Глава 3. СКАЛЯРНЫЕ ТИПЫ И ВЫРАЖЕНИЯ 3.1. Основные и производные типы Разговор о типах начнем с переменных. В пособиях по языкам программирования переменную чаще всего определяют как пару "имя" - "значение". Имени соответствует адрес (ссылка) на участок памяти, выделенный переменной, а значением является содержимое этого участка. Именем служит идентификатор, а значение соответ- ствует типу переменной, определяющему множество допустимых зна- чений и набор операций, для которых переменная может служить операндом. Множество допустимых значений переменной обычно совпадает со множеством допустимых констант того же типа (см. табл. 2.1 - 2.2). Таким образом, вводятся вещественные, целые и сим- вольные переменные, причем символьные (char) иногда относят к це- лым. Целочисленные и вещественные считаются арифметическими типами. Арифметический (включая символьный) тип является част- ным случаем скалярных типов. К скалярным типам кроме арифмети- ческих относятся указатели, ссылки и перечисления. Перечисления (enum) уже введены при рассмотрении целых констант. К указателям и ссылкам вернемся немного позже. Переменные типизируются с по- мощью определений и описаний. Сразу же введем терминологические соглашения. В отличие от описания определение не только вводит объект (например, переменную), но и предполагает, что на основании этого определения компилятор выделит память для объекта (перемен- ной). Для определения и описания переменных основных типов ис- пользуются следующие ключевые слова, каждое из которых в отдель- ности может выступать в качестве имени типа: • char (символьный); • short (короткий целый); • int (целый); • long (длинный целый); • float (вещественный); • double (вещественный с удвоенной точностью); • void (отсутствие значения).
Глава 3. Скалярные типы и выражения 55 При определении переменных им можно приписывать начальные значения, которые заносятся в выделяемую для них память в процессе инициализации. Примеры определений (описания с инициализацией): char newsimbol = '\n'; long filebegin = OL; double pi = 3.1415926535897932385; В обозначении типа может использоваться одновременно несколько служебных слов. Например, определение long double zebra, stop; Вводит переменные с именами zebra и stop вещественного типа по- вышенной точности, но явно не присваивает этим переменным ника- ких начальных значений. Употребляемые как отдельно, так и вместе с другими именами ти- пов служебные слова unsigned (беззнаковый) и signed (знаковый) по- зволяют для арифметического или символьного типа выбирать способ учета знакового разряда: unsigned int i, j, k; // Значения от 0 до 65535 unsigned long L, M ,N; // Значения от 0 до 4294967295 unsigned char с, s; // Значения от 0 до 255 При таком определении переменные i, j, к могут принимать толь- ко целые положительные значения в диапазоне от 0 до 65535 и т.д. Применение в определениях типов отдельных служебных слов int, char, short, long эквивалентно signed int, signed char, signed short, signed long. Именно поэтому служебное слова signed обычно опускается в определениях и описаниях. Использование при задании типа только одного unsigned эквивалентно unsigned int. При операциях с беззнаковыми (unsigned) целыми не возникает переполнений, так как используется арифметика по модулю 2 в степе- ни и, где п - количество битовых разрядов, выделяемых для представ- ления соответствующих значений. Переменные одного типа занимают в памяти одно и то же количе- ство единиц (байтов), и это количество единиц может быть всегда вы- числено с помощью операции sizeof, как мы это делали в описании ее возможностей. Вот еще несколько примеров: //РЗ-01.СРР - размеры разных типов данных ^include <iostream.h> void main() ( int i;
56 Язык Си++ Таблица 3.1 Основные типы данных Tun данных Размер, бит Диапазон значений Примечание - назначение типа unsigned char 8 0...255 Небольшие целые чис- ла и коды символов char 8 -128...127 Очень малые целые числа и ASCII-коды enum 16 -32768...32767 Упорядоченные набо- ры целых значений unsigned int 16 0...65535 Большие целые н счетчики циклов short int 16 -32768...32767 Небольшие целые, управление циклами int 16 -32768. . .32767 Небольшие целые, управление циклами unsigned long 32 0. . .4294967295 Астрономические расстояния long 32 -2147483648.. . . . .2147483647 Большие числа, популяции float 32 3.4Е-38...3.4Е+38 Научные расчеты (7 значащих цифр) double 64 1.7Е-308...1.7Е+308 Научные расчеты (15 значащих цифр) long double 80 3.4Е-4932... ...1.1Е+4932 Финансовые расчеты (19 значащих цифр) unsigned int ui; long 1; unsigned long ul; double d; long double Id; cout « "\n sizeof cout « "\t sizeof cout « "\n sizeof cout « "\t sizeof cout « "\n sizeof cout « "\t sizeof (int) « " « sizeof(i); (unsigned int) = " « sizeof(ui); (long) = ” « sizeof(1); (unsigned long) * 11 « sizeof(ul); (double) » " « sizeof(d); (long double) = ” « sizeof(Id); } Результаты выполнения: sizeof (int) = 2 sizeof (long) = 4 sizeof (double) = 8 sizeof (unsigned int) = 2 sizeof (unsigned long) = 4 sizeof (long double) = 10
Глава 3. Скалярные типы и выражения 57 В табл. 3.1 приведены типы данных, их размеры в памяти и диапа- зоны допустимых значений для компиляторов, ориентированных на ПЭВМ семейства IBM РС/ХТ/АТ (см., например, [9], с. 19). В таблицу не включены указатели, так как они будут подробно рассмотрены позже. Используя спецификатор typedef, можно в своей программе вво- дить удобные обозначения для сложных описаний типов. В следую- щем примере typedef unsigned char COD; COD simbol; введен новый тип COD - сокращенное обозначение для unsigned char и переменная этого типа simbol, значениями которой являются беззна- ковые числа в диапазоне от 0 до 255. Рассматривая переменные, мы пока использовали базовые (предопределенные целиком или фундаментальные) типы, для обозна- чения которых употребляются по отдельности и в допустимых соче- таниях служебные слова char, int, signed, double, long, unsigned, float, short, void. Из этих базовых типов с помощью операций '*1, ', 1 ' О * и механизмов определения типов структурированных данных (клас- сов, структур, объединений) можно конструировать множество про- изводных типов. Обозначив именем type допустимый тип, приведем форматы некоторых производных типов: type имя[ ] массив объектов заданного типа type. Например: long int м[5]; - пять объектов типа long int, доступ к кото- рым обеспечивают индексированные переменные м[0], М[1], М[2], M[3J, М[4]. typel KMH(type2) ; функция, принимающая аргумент типа type2 и возвращаю- щая значение типа typel. Например: int fl (void); - функция, не требующая аргументов и воз- вращающая значение типа int; void f 2 (double) ; - функция, принимающая аргумент типа double и не возвращающая значений. type *имя; указатель на объекты типа type. Например, char *ptr; определяет указатель ptr на объекты типа char.
58 Язык Си++ type *имя []; массив указателей на объекты типа type. type (*имя) []; указатель на массив объектов типа type. type *имя (type2); функция, принимающая аргумент типа type2 и возвращающая указатель на объект типа typel. typel (*имя) (type2); указатель на функцию, принимающую параметр типа type2 и возвращающую типа typel. type &имя = имя__объекта_типа„Луре; инициализированная ссылка на объект типа type. Например, unsigned char &СС = simbol; определяет ссылку сс на объект типа unsigned char. Предполагается, что ранее в программе присутствует определение unsigned char simbol; typel (&имя)(type2); ссылка на функцию, возвращающую значение заданного типа typel и принимающую параметр типа type2. struct имя {typel имя1; type2 имя2;}; тип структура в данном случае с двумя компонентами, которые имеют typel и type2. Например, struct ST {int х; char у; float zопределяет струк- турный тип ST структуры с тремя компонентами разных типов: целая х, символьная у, вещественная z. (Количество компонентов в определении структуры может быть произвольным.) union имя {typel имя1; type2 имя2;}; тип объединение (в данном случае двух компонентов с типами typel, type2). Например, union UN {int ш; char с [2];}; - объединяющий ТИП UN - объединение целой переменной m и двух элементов символьного массива с [0] ис[1] (Количество компонентов объединения может быть любым.) class имя {typel имя 1; type2 имя2 (type3);};
Глава 3. Скалярные типы и выражения 59 класс, включающий в данном случае два компонента - объект типа typel и функцию типа type2 с аргументом типа type3. Например: class A (int N; float F(char) тип А ~ класс, компо- нентами которого служат целая переменная N и вещественная функция F с символьным аргументом. (Количество компонен- тов класса может быть произвольным.) Еще один производный тип языка Си++ - это указатели на ком- поненты классов. Так как это понятие нужно вводить одновременно с определениями механизмов классов, то отложим рассмотрение этих указателей. Все возможные производные типы принято разделять на скаляр- ные (scalar), агрегатные (agregate) и функции (function). К скалярным типам относят арифметические типы, перечислимые типы, указатели и ссылки (ссылки введены только в Си++, но не в языке Си). Агрегат- ные типы, которые также называют структурированными, включают массивы, структуры, объединения и классы (последние только в Си++). 3.2. Объекты и их атрибуты Одним из основных понятий языка Си++ является унаследованное из языка Си и предшествующих языков понятие объекта как некото- рой области памяти. Переменная - это частный случай объекта как именованной области памяти. Отличительной чертой переменной яв- ляется возможность связывать с ее именем различные значения, сово- купность которых определяется типом переменной. При определении значения переменной в соответствующую ей область памяти поме- щается некоторый код. Это может происходить либо во время компи- ляции, либо во время исполнения программы. В первом случае говорят об инициализации, во втором случае - о присваивании. Опе- рация присваивания е = в содержит имя переменной (е) и некоторое выражение (в). Имя переменной есть частный случай более общего понятия - "леводопустимое выражение" (left value expression), или /- значение. Название "леводопустимое выражение" произошло как раз от изображения операции присваивания, так как только /-значение может быть использовано в качестве ее левого операнда. Леводопу- стимое выражение определяет в общем случае ссылку на некоторый объект. Частным случаем такой ссылки является имя переменной. Итак, объект определяется как некоторая область памяти. Это по- нятие вводится как понятие времени исполнения программы, а не по-
60 Язык Си++ нятие языка Си++. В языке Си++ термин объект зарезервирован как термин объектно-ориентированного подхода к программированию. FIpi 1 этом объект всегда принадлежит некоторому классу, и такие объекты мы будем рассматривать в главах, посвященных классам. Вернемся к объектам как к участникам памяти. Так как с объектом связано значение, то кроме /-выражения для объекта задается тип, который: • определяет требуемое для объекта количество памяти при ее на чальном распределении; • задает совокупность операций, допустимых для объектов; • интерпретирует двоичные коды значений при последующих об ращениях к объекту; • используется для контроля типов с целью обнаружения возможных случаев недопустимого присваивания. Имя объекта как частный случай леводопустимого выражения обеспечивает как получение значения объекта, так и изменение этого значения. Не любые выражения являются леводопустимыми. К лево- допустимым относятся: • имена скалярных арифметических и символьных переменных; • имена переменных, принадлежащих массивам (индексированные переменные); • имена указателей; • уточненные имена компонентов структурированных данных (структур, объединений, классов); имя__структуры. имя^компонента ; • выражения, обеспечивающие косвенный выбор компонентов структурированных данных (структур, объединений, классов): укаэатель_на_объект -> имя__компонента; • леводопустимые выражения, заключенные в круглые скобки; • ссылки на объекты; • выражения с операцией разыменования • вызовы функций, возвращающих ссылки на объекты. Кроме леводопустимых выражений, определены праводопустимые выражения, которые невозможно использовать в левой части оператора присваивания. Например: • имя функции; • имя массива; • имя константы; • вызов функции, не возвращающей ссылки. Кроме типов, для объектов явно либо по умолчанию определяются: • класс памяти (задает размещение объекта):
Глава 3. Скалярные типы и выражения 61 • область (сфера) действия, связанного с объектом идентификато- ра (имени); • видимость объекта; • продолжительность существования объектов и их имен; • тип компоновки (связывания). Все перечисленные атрибуты взаимосвязаны и должны быть явно указаны, в противном случае они выбираются по контексту неявно При определении и (или) описании конкретного объекта. Рассмотрим Подробнее их возможности и особенности. Тип определяет размер памяти, выделенной для значения объекта, Правила интерпретации двоичных кодов значений объекта и набор Допустимых операций. Типы рассмотрены в связи с переменными. Табл. 3.1 содержит основные типы и их свойства. Класс памяти определяет размещение объекта в памяти и продол- жительность его существования. Для явного задания класса памяти При определении (описании) объекта используются или подразумева- ются по умолчанию следующие спецификаторы: ftuto (автоматически выделяемая, локальная память) Спецификатор auto может быть задан только при определении объектов блока, например, в теле функции. Этим объектам память выделяется при входе в блок и освобождается при вы- ходе из него. Вне блока объекты класса auto не существуют. register (автоматически выделяемая, по возможности регистровая па- мять) Спецификатор register аналогичен auto, но для размещения значений объектов используются регистры, а не участки основной памяти. Такая возможность имеется не всегда, и в случае отсутствия регистровой памяти (если регистры заняты другими данными) объекты класса register компилятор обра- батывает как объекты автоматической памяти. static (внутренний тип компоновки и статическая продолжительность существования) Объект, описанный со спецификатором static, будет существо- вать только в пределах того файла с исходным текстом про- граммы (модуля), где он определен. Класс памяти static может приписываться переменным и функциям. extern (внешний тип компоновки и статическая продолжительность существования)
62 Язык Си++ Объект касса extern глобален, т.е. доступен во всех модуле (файлах) программы. Класс extern может быть приписан переменной или функции. Кроме явных спецификаторов, на выбор класса памяти существенн*и влияние оказывают размещение определения описаний объекта в текс i < программы. Такими определяющими частями программы являются блок, функция, файл с текстом кода программы (модуль) и т.д. Таким образом, класс памяти, т.е. размещение объекта (в регистре, стеке, и динамически распределяемой памяти, в сегменте) зависит как oi синтаксиса определения, так и от размещения определения в программе Область (сфера) действия идентификатора (имени) - это часть программы, в которой идентификатор может быть использован для доступа к связанному с ним объекту [5,12]. Область действия зависит о i того, где и как определены объекты и описаны идентификаторы. Здесь имеются следующие возможности: блок, функция, прототип функции файл (модуль) и класс. Если идентификатор описан (определен) в блоке, то область его действия - от точки описания до конца блока. Когда блок является телом функции, то в нем определены не только описанные в нем объекты, но и указанные в заголовке функции формальные параметры. Таким образом, сфера действия формальных параметров в определении функции есть тело функции. Например, следующая функция вычисляет факториал значения своего аргумента. (Сфера действия для параметра z и перемен- ных, описанных в теле функции, есть блок - тело функции): long fact (int z) //1 ( long m = 1; //2 if (z<0) return 0; //3 for (int i = 1; i<z; m=++i*m); //4 return m; //5 } //6 Область действия: для переменной m- строки 2 +• 6; для переменной i - строки 4-5-6; для параметра z ~ блок в целом. Метки операторов в тексте определения функции имеют в качестве сферы действия функцию. В пределах тела функции они должны быть уникальны, а вне функции они недоступны. Никакие другие иденти- фикаторы, кроме меток, не могут иметь в качестве сферы действия функции.
| Гл»а 3. Скалярные типы и выражения 63 ----------------------------------------------------------- | Прототип функции является сферой действия идентификаторов, | умазанных в списке формальных параметров. Конец этой сферы дей- ствия совпадает с концом прототипа функции. Например, в прототипе float expon(float d, int m); Щределены идентификаторы d, а, не существующие в других частях Программы, где помещен данный прототип. Для примера рассмотрим Программу, в которой используется только что определенная функция facto для вычисления факториала: //РЗ-О2.СРР - сфера действия формальных параметров // прототипа llnclude <iostream.h> Ion? fact (int z) ( long a « 1; if (z < 0) return 0; for (int i*l;i<z;aa= ++i * a) ; return a; » •ain() ( int j • 1, К • 3; long fact(int К « О); // Прототип функции for ( ; j <* К; j++) cout « "\n arg « •• « j « ” arg " « fact(j); » Результат выполнения программы: arg * 1 arg ’= 1 arg - 2 arg !* 2 arg * 3 arg !* 6 Программа иллюстрирует независимость идентификаторов списка параметров прототипа функции от других идентификаторов про- граммы. Имя к в прототипе функции и имя переменной к, определен- ной в тексте основной программы (используется в цикле), имеют разные сферы действия и полностью независимы. Файл с текстом программы (модуль) является сферой действия для всех глобальных имен, т.е. для имен объектов, описанных вне любых функций и классов. Каждое глобальное имя определено от точки опи- сания до конца файла. С помощью глобальных имен удобно связы- вать функции по данным, т.е. создавать общее "поле данных" для всех функций файла с текстом программы. Простейший пример:
64 Язык Си++ //РЗ-ОЗ.СРР - область действия глобальных имен #lnclude <iostream.h> int LC; char C[] » "Фраза"; void WW(void) { LC « sizeof(C); } void Prin(void) ( cout « "\n Длина строки С = " « LC; } void main (void) ( WW() ; Print) ; } Результат выполнения программы: Длина строки С « 6 В программе три функции и два глобальных объекта - массив с и целая LC, через которые реализуется связь функций по данным. Определение класса задает множество его компонентов, вклю- чающее данные и функции их обработки. Имеются специальные пра- вила доступа и определения сферы действия, когда речь идет о классах. Подробным рассмотрением этих вопросов мы займемся поз- же в связи с классами и принадлежащими им объектами. С понятием области (сферы) действия связано пространство имен - область, в пределах которой идентификатор должен быть "уникальным" [5,12]. С учетом пространства имен используемые в программе идентификаторы делятся на четыре группы [5,9]: • Имена меток, используемых в операторе goto. Эти имена долж- ны быть уникальными в той функции, где они введены. • Имена структур, классов, объединений и перечислимых данных должны быть уникальными в пределах того блока, где они определены. Если эти имена описаны вне функций и классов, то они должны быть уникальными относительно всех таких же глобальных имен. • Имена компонентов структур, объединений, классов должны быть уникальными в пределах соответствующего определения. В разных структурах, объединениях, классах допустимы компо- ненты с одинаковыми именами. • Имена переменных и функций, названия типов, которые введе- ны пользователем (с помощью служебного слова typedef), и име- на элементов перечислений должны быть уникальными в сфере определения. Например, внешние идентификаторы должны быть уникальными среди внешних и т.д.
Глава 3. Скалярные типы и выражения 65 Понятие видимость объекта понадобилось в связи с возмож- ностью повторных определений идентификатора внутри вложенных влоков (или функций). В этом случае разрывается исходная связь име- ни с объектом, который становится "невидимым" из блока, хотя сфера Действия имени сохраняется. Следующая программа иллюстрирует |ту ситуацию: //РЗ-04.СРР - переопределение внешнего имени внутри блока llnclude <iostream.h> Void main () ( char cc[] « "Число ”; // Массив автоматической памяти float pi = 3.1415926; // Переменная типа float cout « ”\n Обращение к внешнему* имени: pi = ” « pi; { // Переменная типа double переопределяет pi: double pi ~ 3.1415926535897932385; // Видимы double pi и массив cc[]: ' cout « '\n' « cc " double pi = *' « pi; } // Видимы float pi и массив cc[] : cout « ’ \n' « cc " float pi » « « pi; ) Результат выполнения программы: Обращение к внешнему имени: pi = 3.1415926 Число double pi = 3.1415926535897932385 Число float pi = 3.1415926 Достаточно часто сфера (область) действия идентификатора и ви- димость связанного с ним объекта совпадают. Область действия мо- жет превышать видимость, но обратное невозможно, что иллюстрирует данный пример. За описанием переменной double pi внутри блока внешнее имя переменной float pi становится невиди- мым. Массив char сс[] определен и видим во всей программе, а пе- ременная float pi видима только вне вложенного блока, внутри которого действует описание double pi. Таким образом, float pi Невидима во внутреннем блоке, хотя сферой действия для имени pi является вся программа. Для переменной double pi и сферой дей- ствия, и сферой видимости служит внутренний блок, вне которого она недоступна и не существует. Второй пример изменения видимости объектов при входе в блок: //РЗ-05.СРР - переопределение внешнего имени внутри блока llnclude <iostream.h> void main () 3X21
66 Язык Си++ { int k « 0, j = 15; { cout « "\пВнешняя для блока переменная к » " «к; char к = 'Д’ ; // Определена внутренняя переменная cout « "\пВнутренняя переменная к » " «к; cout « ”\пВидимая в блоке переменная j » ” « j; j e 30; // Изменили Значение внешней переменной } // Конец блока cout « "\пВне блока: к » " « к « ", j » ” « j; } Результат выполнения программы: Внешняя для блока переменная к » 0 Внутренняя переменная к = А Видимая в блоке переменная j » 15 Вне блока: к » 0, j » 30 Как видно из примера, внутри блока сохраняется сфера действия внешних для блока имен до их повторного описания (переменная к). Определение объекта внутри блока действует от точки размещения определения до конца блока (до выхода из блока). Внутреннее опре- деление изменяет видимость внешнего объекта с тем же именем (объект невидим). Внутри блока видимы и доступны определенные во внешних блоках объекты (переменная j). После выхода из блока вос- станавливается видимость внешних объектов, переопределенных внутри блока. (Вывод значения переменной к после выхода из блока.) Язык Си++ позволяет изменить видимость объектов с помощью операции Программа р2-13.срр, иллюстрирующая возмож- ность такого изменения видимости, приведена при описании опера- ции указания области действия •::1. Рассмотрим еще один пример обращения к "невидимой" внутри функции внешней строке с по- мощью операции указания области действия: //РЗ-Об.СРР - доступ из функции к внешнему объекту, / / имя которого переопределено в теле функции #include <iostream.h> char сс[] = ’’Внешний массив'1; void func(void) // Определение функции ( char сс[] = "Внутренний массив"; // Обращение к локальному объекту сс: cout « ' \п' « сс; / / Обращение к глобальному объекту сс: cout « '\п’ « ::сс; } void main(void)
Глава 3. Скалярные типы и выражения 67 ( void func(void); // Прототип функции func(); // Вызов функции I Результат выполнения программы: Внутренний массив Внешний массив Следующая программа и соответствующая ей схема (рис. 3.1) обобщают соглашения о сферах действия идентификаторов и о види- мости объектов. Программа готовится в виде одного текстового фай- ла. В программе три функции, из которых одна главная (main): //P3-07.CFP - файл с текстом программы (модуль) #include <iostream.h> char dc[] = "Объект 1"; // Глобальный для модуля объект 1 void fund (void) ( cout « "\nfl.dc = " « de; // Виден глобальный объект 1 char de С] = "Объект 2"; // Локальный для funcl() объект 2 cout « "\nfl.dc = " « de; // Виден локальный объект 2 { // Внутренний блок для funcl() // Виден локальный для funcl() объект 2: cout « "\nfl.block.de = " « de; // Локализованный в блоке объект 3: char dc[] = "Объект 3"; // Виден локальный объект 3: cout « "\nfl.block.dc = " « de; // Виден глобальный объект 1: cout « "\nfl.block.::de = " « ::dc; } // Конец блока // Виден локальный для funcl() объект 2: cout « "\nfl.dc - " « de; // Виден глобальный объект 1: cout « "\nfl.::dc = " « ::dc; ) // Конец функции funcl() void func2(char *dc) // de - параметр функции { cout « "\nf2.параметр.dc=" « de; // Виден параметр // Виден глобальный объект 1: cout « "\nf2.::dc = " « ::dc; { // Внутренний блок для func2() // Локализованный в блоке объект 4: char dc[] = "Объект 4"; // Виден локальный для func2() объект 4: cout « "\nf2.dc == " « de; } // Конец блока
68 Язык Си++ } // Конец функции f unc2() void main (void) { 11 Виден глобальный объект 1: cout « "\nfmain.dc » " « de; char dc[] » "Объект 5"; // Локальный для main() объект 5 fund () ; func2(de); ------------- Текстовый файл программы (модуль) type 1 имя; <-------------------------- г- • функция 1----------------------------1 видимость ------------------------1— имя — О Б Ъ Е . функция2 (type имя) имя - видимость : : имя — видимость - блок ............ type 4 имя;<---т--- ; имя — видимость — К Т « ОБЪЕКТ О О - функция main - имя — видимость Рис. 3.1. Видимость объектов, связанных с одним идентификатором (именем), в однофайловой программе
Глава 3. Скалярные типы и выражения 69 // Виден локальный для main() объект 5: cout « "\nfmain.de = " « de; I Результат выполнения программы: fmain.de = Объект 1 fl.de » Объект 1 fl.de = Объект 2 fl.block.dc = Объект 2 fl.block.dc ж Объект 3 fl.block.::de = Объект 1 fl.de = Объект 2 fl.::dc = Объект 1 f2.параметр.de = Объект 5 f2.::de = Объект 1 f2.de = Объект 4 fmain.de » Объект 5 На рис.3.1 для имени объекта с типом typel областью действия служит файл в целом, однако видимость этого объекта различна внутри блоков и функций. Если определение typel имя; (char dc[] • "объект!";) поместить в конце файла, то ничего хорошего не по- лучится - действие определения не распространяется вверх по тексту программы. Все попытки обратиться к глобальному "ОбъектуI" в /том случае приведут к синтаксическим ошибкам, выявленным ком- пилятором. Объект, определенный в тексте программы ниже (позже) своего первого использования, должен быть описан в той функции, где он используется, с атрибутом extern: void fl(void) ( extern int ex; // Описание внешней переменной cout « "\nfl:ex = " « ex; I void main(void) ( fl() ; ) int ex = 33; // Определение с инициализацией Результат выполнения программы: fl rex = 33 Продолжительность существования объектов определяет период, в течение которого идентификаторам в программе соответствуют
70 Язык Си++ конкретные объекты в памяти. Определены три вида продолжитель- ности: статическая, локальная и динамическая. Объектам со статической продолжительностью существования память выделяется в начале выполнения программы и сохраняется до окончания ее обработки. Статическую продолжительность имеют все функции и все файлы. Остальным объектам статическая продол- жительность существования может быть задана с помощью явных спецификаторов класса памяти static и extern. При статической продолжительности существования объект не обязан быть глобальным. При отсутствии явной инициализации объекты со статической продолжительностью существования по умолчанию инициализируются нулевыми, или пустыми значениями. Следующий пример содержит переменную к со статической продолжительностью существования, сферой действия для которой является только тело функции (блок), т.е. переменная к локализована в теле функции counter: //РЗ-08.СРР - инициализация и существование локальных // статических объектов # include <iostream.h> int counter (void) // Определение функции { static int К; // Статическая переменная, return ++К; // локализованная в теле функции } void main (void) ( int counter (void); // Прототип функции int K=3; // Локальная переменная функции main for (; К » =0; К--) так как cout « "\п Автоматическая К = "«К; cout « "\tC4eT4HK=" « counter ();} Результат выполнения программы: Автоматическая К = 3 Автоматическая К = 2 Автоматическая К = 1 Счетчик = 1 Счетчик = 2 Счетчик = 3 В данном примере статическая переменная к, локализованная в теле функции counter, по умолчанию инициализируется нулевым значением, а затем сохраняет значение после каждого выхода из функции, так как продолжительность существования переменной к статическая, и выделенная ей память будет уничтожена только при выходе из про- граммы.
Глина 3. Скалярные типы и выражения 71 В основной функции примера определена целая переменная к с локальной продолжительностью существования. Такие переменные Называются автоматическими. Они создаются при каждом входе в блок Ими функцию, где они описаны, и уничтожаются при выходе. Как образно выразились авторы руководств [5, 12], объекты с локальной Продолжительностью жизни "ведут более случайное существование", чем объекты со статической продолжительностью. Объекты с локальной Продолжительностью должны быть инициализированы только явным Образом, иначе их начальное значение непредсказуемо. Память для них При входе в блок или функцию выделяется в регистрах или в стеке. Область действия для объекта с локальной продолжительностью существования всегда локальна - это блок или функция. Для задания локальной продолжительности при описании объекта можно использовать спецификатор класса памяти auto, однако он всегда избыточен, так как этот класс памяти по умолчанию приписывается всем объектам, определенным в блоке или функции. Следует обратить внимание, что не для всех объектов с локальной областью действия определена локальная продолжительность существования. Например, в функции counter () переменная к имеет локальную область действия (тело функции), однако для нее спецификатором static определена статическая продолжительность существования. Объекты с динамической продолжительностью существования создаются (получают память) и уничтожаются с помощью явных Операторов в процессе выполнения программы. Для создания исполь- зуется операция new или функция malloc (), а для уничтожения - операция delete или функция free() . Пример программы с динами- ческим распределением памяти для объектов приведен при Описании Возможностей операций new и delete. Приведем программу, в которой для тех же целей используются библиотечные функции malloc () и traa (). Указанные функции находятся в стандартной библиотеке языка Си и его наследника - языка Си + + , а их прототипы включены в заголовочный файл alloc. h. Прототип функции для выделения памяти: void *malloc(int size); Исходной информацией для функции malloc() служит целое шачение size, определяющее в байтах требуемый размер выделяемой памяти. Функция возвращает адрес выделенного участка памяти. Возвращаемый адрес равен 0, если выделить память не удается. В • цедующей программе при выделении памяти проверка для упрощения 'С выполняется:
72 Язык Си++ //РЗ-09.СРР - динамическое выделение памяти для объектов linclude <alloc.h> // Прототипы malloc() и free() tfinclude <ioatream.h> // Для cout void main (void) { int *t; // Память выделена только для t, но не для *t // Память выделена для m и *ш: int *m « (int *)malloc(sizeof(int)); *m = 10; t = m; // Запомнили значение указателя // Память выделена для *т: т “ (int *) malloc(sizeof(int)); *m “ 20; cout « "\n Второе значение *m = " « *m; free(m); // Освободить память, выделенную для *m cout « "\n Первое значение *m » ” « *t; free(t); // Освободить память, выделенную для *m } При определении указатель t получил память, но память для це- лого *t нс выделена. При определении указателя m функцией malloc () выделена память для целого значения *т. В этот участок памяти заносится значение 10. При втором использовании malloc() указатель m устанавливается на новый участок памяти. В *т заносится значение 20 и следует печать. Остальное поясняют комментарии. Результат выполнения программы: Второе значение *т » 20 Первое значение *т = 10 Тип компоновки, или тип связывания, определяет соответствие иден- тификатора конкретному объекту или функции в программе, исход- ный текст которой размещен в нескольких файлах (модулях). В этом случае каждому имени, используемому в нескольких файлах, может соответствовать либо один объект (функция), общий для всех файлов, либо по одному и более объектов в каждом файле. Файлы программы (модули) могут транслироваться отдельно, и в этом случае возникает проблема установления связи между одним и тем же идентификатором и единственным объектом, которому он соответствует. Такие объекты (функции) и их имена нуждаются во внешнем связывании, которое вы- полняет компоновщик (редактор связей) при объединении отдельных объектных модулей программы. Для остальных объектов (функций), которые локализованы в файлах, используется внутреннее связывание. Тип компоновки (связывания) никак специальным образом не обозначается при определении объектов и описании имен, а устана- вливается компилятором по контексту, местоположению этих объяв- лений и использованию спецификаторов класса памяти static и extern.
Глава 3. Скалярные типы и выражения 73 Например, имена статических объектов (static) локализованы в своем файле и могут быть использованы как имена других объектов и функ- ций в других независимо транслируемых файлах. Такие имена имеют Внутренний тип компоновки (связывания). Если имена объектов или функций определены со спецификатором extern, то имя будет иметь Внешний тип компоновки (связывания). Рассмотрим следующую про- грамму, функции которой и определения переменных рассредоточены НО трем текстовым файлам: -------------------------Модуль 1------------------------- //РЗ-10-1.СРР - первый файл многомодульной программы int К = О; // Для К - внешнее связывание Mold counter(void) // Для counter - внешнее связывание ( static int K__IN = 0; // Для K_IN - внутреннее связывание К += ++K_IN; I Ц -----------------------Модуль 2 ------------------------- //РЗ-10-2.СРР - второй (основной) файл программы |include <iostream.h> Void main(void) ( void counter(void); // Прототип - внешнее связывание void display(void); // Прототип - внешнее связывание //К - локальный объект - внутреннее связывание: for (int К = 0; К < 3; К++) { cout « "\пПараметр цикла К = " «К; counter () ; // Изменяет свою K^IN и внешнюю К display () ; } ff-----------------------Модуль 3 — //P3-10-3.СРР - третий файл программы ^include <iostream.h> Void display (void) ( extern int К; // Для display - внешнее связывание // Для К - внешнее связывание Static int K_IN » 0; // Для K_IN - внутреннее связывание cout « "\пВнешнее К " Внутреннее = " « К++ « K~IN из функции display = " « K_IN++ ; I Результат выполнения: параметр цикла К = 0 Смешнее К » 1 Внутреннее K_IN из функции display = 0 чраметр цикла К = 1
74 Язык Си++ Внешнее К = 4 Внутреннее K__IN из функции display = 1 Параметр цикла К = 2 Внешнее К = 8 Внутреннее K_IN из функции display = 2 Анализируя текст и результаты программы, обратите внимание на следующее: • внешняя переменная с именем к является общей для файлов 1 и 3 (внешнее связывание); • внутренняя переменная с именем к существует только в основ ном модуле; • статические переменные с именем k_in различны в модулях 1 и 3, т.е. это различные объекты с одинаковым именем. Для каждо- го из них реализуется внутреннее связывание. Для некоторых имен тип компоновки не существует. К ним отно- сятся параметры функций, имена объектов, локализованных в блоке (без спецификаторов extern), идентификаторы, нс являющиеся имена- ми объектов и функций (например, имя введенного пользователем типа). 3.3. Определения и описания Различия между определениями и описаниями. Все взаимосвязан- ные атрибуты объектов (тип, класс памяти, область (сфера) действия имени, видимость, продолжительность существования, тип компонов- ки) приписываются объекту с помощью определений и описаний, а также контекстом, в котором эти определения и описания появляются. Определения устанавливают атрибуты объектов, резервируют для них память и связывают эти объекты с именами (идентификаторами). Кроме определений, в тексте программы могут присутствовать описа- ния, каждое из которых делает указанные в них идентификаторы из- вестными компилятору. В переводной литературе и особенно в документации наряду с терминами "описание” и "определение" ис- пользуют "объявление" и "декларация". Не втягиваясь в терминологи- ческую дискуссию, остановимся на варианте, примененном в переводах работ [1] и [25]. Итак, появление двух терминов "определение" и "описание" объ- ясняется тем фактом, что каждый конкретный объект может быть многократно описан, однако в программе должно быть только един- ственное определение этого объекта. Определение обычно выделяет объекту память и связывает с этим объектом идентификатор - имя
Глава 3. Скалярные типы и выражения 75 объекта, а описания только сообщают свойства того объекта, к которому относится имя. Говорят, что описание ассоциирует тип с именем, а определение, кроме того, задает все другие свойства (объем памяти, Внутреннее представление и т.д.) объекта и выполняет его инициа- лизацию. В большинстве случаев описание одновременно является и определением. Однако это не всегда возможно и не всегда требуется. Определять и описывать можно следующие объекты: • переменные; • функции; • классы, их компоненты и указатели на компоненты; • типы, вводимые пользователем с помощью typedef; • типы и имена структур, объединений, перечислений; • компоненты (элементы) структур и объединений; • массивы объектов заданного типа; • перечислимые константы; • метки операторов; • макросы препроцессора; • указатели на объекты или функции заданного типа; • ссылки на объекты или функции заданного типа; • константы (значения) заданного типа. Определения и описания переменных. Из всего перечисленного разнообразия объектов только переменным соответствуют основные типы. Определение переменных заданного типа имеет следующий формат: з ш тип имя! иниц. 1, имя2 иниц. 2 t ...; 1ДС s - спецификатор класса памяти (auto, static, extern, register, typedef) - подробно описан в п. 3.2; m - модификатор const или volatile; тип - один из основных типов (табл, 3.1); имя - идентификатор; иииц. - необязательный инициализатор, определяющий начальное значение соответствующего объекта. Синтаксис инициализатора (иниц.) переменной: = инициализирующее_выражение либо (инициализирукпцее„выражение)
76 Язык Си++ Наиболее часто в качестве инициализирующего выражения используется константа. Вторая "скобочная" форма инициализации разрешена только внутри функций, т.е. для локальных объектов Напомним, что описание является определением, если: • описывает переменную; • содержит инициализатор; • полностью описывает функцию (включает тело функции); • описывает объединение или структуру (включая компоненты); • описывает класс (включая его компоненты); Описание не может быть определением, если: • описывает прототип функции; • содержит спецификатор extern; • описывает статический компонент класса; • описывает имя класса; • описывает имя типа, вводимого пользователем (typedef). Приведем примеры описаний: extern int g; float fn(int, double); extern const float pi; struct st; typedef unsigned char simbol; //Внешняя переменная //Прототип функции //Внешняя константа //Имя структуры (класса) //Новый тип simbol. Примеры определений: char sm; //Переменная автоматической или внешней памяти float dim = 10.0; //Инициализированная переменная double Euler 2.718282); //Инициализированная переменная //автоматической памяти const float pi==3.14159; //Константа float х2 (float х) {return х*х;} //Функция struct {char a; int b;} st; //Структура enum {zero, one, two); //Перечисление Некоторая неоднозначность есть в определении переменной char sm; . Если такое описание находится в блоке, то это определение неинициализированной переменной автоматической памяти. Если такое описание появляется вне блоков и классов, то это определение статической внешней переменной, которой по умолчанию присваивается нулевое значение, а для символьной переменной char - пробел. В файлах, где нужен доступ к этой глобальной переменной, должно быть помещено ее описание вида extern char sm;. Подобное описание нужно для доступа из функций к внешним объектам, определения которых
Глава 3. Скалярные типы и выражения 77 Втмещены в том же файле, но ниже текста определения функций. Пример: // РЗ-11.СРР - определения и описания переменных (Include <iostream.h> float pi » 3.141593; // Определение с явной инициализацией |nt sO; // Определение sO (инициализация по умолчанию) Int s2 = 5; // Определение s2 с явной инициализацией ¥Oid main () ( extern int sO; // Описание sO extern char si; // Описание si int s2(4); // Описание s2 с явной инициализацией cout « "\n Инициализация no умолчанию: sO » " « sO; cout « "\n Явная инициализация: si » " « si; cout « ”\n Внутренняя переменная: s2 == " « s2; cout « "\n Внешняя переменная: pi = " « pi; I fhar sl='@'; // Определение si с явной инициализацией //Конец файла с текстом программы Результат выполнения программы: Инициализация по умолчанию: sO = О Явная инициализация: si = @ Внутренняя переменная: s2 = 4 Внешняя переменная: pi в 3.141593 Для инициализации переменной s2, относящейся к автомати- ческой памяти, использована "скобочная" форма задания начального значения. Внешние переменные таким образом инициализировать нельзя - компилятор воспринимает их как неверные определения функций. В программе обратите внимание на переменную pi, которая определена (и инициализирована) вне функции main(), а внутри нее ие описана. Так как программа оформлена в виде одного файла, то все внешние переменные, определенные до текста функции, доступны в ней без дополнительных описаний. Таким образом, описание extern int sO; в данной однофайловой программе излишнее, а описание extern char si; необходимо. Следующая программа еще раз иллюстрирует доступ к внешним переменным из разных функций однофайловой (одномодульной) про- граммы: //РЗ-12.СРР - обмен между функциями через внешние / / переменные (include <iostream.h>
78 Язык Си++ int х; // Определение глобальной переменной void main() ( void func(void); // Необходимый прототип функции extern int х; // Излишнее описание х = 4; func(); cout « ”\п х = ” « х; } void func(void) ( extern int x; // Излишнее описание x = 2; } Результат выполнения: х = 2 Отметим некоторые особенности спецификаторов класса памяти. Спецификатор auto редко появляется в программах. Действительно, его запрещено использовать во внешних описаниях, а применение внутри блока или функции излишне - локальные объекты блока по умолчанию (без спецификаторов static или extern) являются объектами автоматической памяти. Спецификатор register также запрещен во внешних описаниях, однако его применение внутри блоков или функ- ций может быть вполне обосновано. Появление typedef среди спецификаторов класса памяти (auto, static, ...) объясняется не семантикой, а синтаксическими аналогиями. Служебное слово typedef специфицирует новый тип данных, который в дальнейшем можно использовать в описаниях и определениях. Од- нако не следует считать, что typedef вводит новый тип. Вводится толь- ко новое название (имя, синоним) типа, который программист хочет иметь возможность называть по-другому. Сравним два описания: static unsigned int ui; typedef unsigned int NAME; В первом определена статическая целая беззнаковая переменная ui, а во втором никакой объект не определен, а описано новое имя типа name для еще не существующих беззнаковых целых объектов. В даль- нейшем name можно использовать в описаниях и определениях. На- пример, запись register NAME rn = 44; // Допустим спецификатор // класса памяти эквивалентна определению
Глава 3. Скалярные типы и выражения 79 register unsigned int rn = 44; Заметим, что register не имя типа, а спецификатор класса памяти. Имена типов, введенные с помощью typedef, нельзя употреблять в од- ном описании (определении) с другими спецификаторами типов. На- пример, будет ошибочной запись long NAME start; // Ошибочное сочетание // спецификаторов типов Однако определение const name сп » 0; вполне допустимо, fonst - не имя типа, а модификатор. Спецификатор typedef нельзя употреблять в определениях функ- ций, однако можно в их описаниях (прототипах). Имя типа, введенное с помощью typedef, входит в то же простран- ство имен, что и прочие идентификаторы программ (за исключением меток), и подчиняется правилам области (сферы) действия имен. Например: typedef long NL; unsigned int NL » 0; // Ошибка - повторное определение NL void func() ( int NL = 1; // Верно - новый объект определен I Модификаторы const и volatile. Эти модификаторы позволяют со- общить компилятору об изменчивости или постоянстве определяе- мого объекта. Если переменная описана как const, то она недоступна в других модулях программы, ее нельзя изменять во время выполнения программы. Единственная возможность присвоить ей значение - это инициализация при определении. Объекту с модификатором const не только нельзя присваивать значения, но для него запрещены операции инкремента (++) и декремента (—). Указатель, определенный с моди- фикатором const, нельзя изменять, однако может быть изменен объ- ект, который им адресуется. Примеры с константами: const zero = 0; // По умолчанию добавляется тип int const char *ptrconst = "variable"; // Указатель const // на строку nhar *point = "строка"; // Обычный указатель на строку char const *ptr = "константа"; // Указатель на // строку-константу *<har *varptr = ptr; // Запрещено 'его +а 4; // Ошибка - нельзя изменить константу
80 Язык Си++ ptreonst = point; // Ошибка - указатель , постоянным цолжен быт> strepy(ptreonst, point); // Допустимо - меняется строка адресуемая strepy(ptreonst, ptr); // H Допустимо, значения и ptr не изменяются ptreonst Отметим ошибочную попытку присвоить указателю (не констан i ному) varptr значение указателя на константу. Это запрещено, так каi в противном случае можно было бы через указатель varptr измени и константу. Модификатор volatile отмечает, что в процессе выполнения про граммы значение объекта может изменяться в промежутке между явными обращениями к нему. Например, на объект может повлиять внешнее событие. Поэтому компилятор не должен помещать его в регистровую память и не должен делать никаких предположений о постоянстве объекта в те моменты, когда в программе нет явных операций, измс няющих значение объекта. Модификаторы const и volatile имеют особое значение при работе с классами, и мы к ним еще обратимся. Кроме спецификаторов класса памяти и модификаторов const, volatile диалекты языка Си++, реализованные в компиляторах для ПЭВМ типа IBM PC, включают модификаторы [9]: • cdecl (для функций и переменных); • pascal (для функций и переменных); • interrupt (для функций и обработки прерываний); • near (для указателей, функций, переменных); • far (для указателей, функций, переменных); • huge (для указателей и функций). Эти модификаторы предназначены для влияния на распределение памяти при размещении объектов и учета особенностей сегментной организации и адресации памяти в процессорах семейства 80x86. 3.4. Выражения и преобразования типов Выражение ~ это последовательность операндов, разделителей и знаков операций, задающая вычисление. Порядок применения операций к операндам определяется рангами (приоритетами) операций (см. табл.2.4) и правилами группирования операций (их ассоциативностью). Для изменения порядка выполнения операций и их группирования используют разделители (круглые скобки). В общем случае
IM" 3. Скалярные типы и выражения 81 ..' ' '' ' .. ..........- П унарные операции (ранг 2), условная операция (ранг 14) и операции Присваивания (ранг 15) правоассоциативны, а остальные операции Щтоассоциативны (см. табл. 2.4). Таким образом, х « у » z означает I « (у = z),ax + у - z означает (х + у) - z. Кроме формирования результирующего значения, вычисление вы- ражения может вызвать побочные эффекты. Например, значением вы- ражения z » 3, z + 2 будет 5, а в качестве побочного эффекта z Примет значение 3. В результате вычисления х > 0 ? х— : х будет Получено значение х, а в качестве побочного эффекта положительное ВИачениех будет уменьшено на 1. В языке Си++ программист может расширить действие стандарт- ных операций (overload - перегрузка), т.е. придавать им новый смысл При работе с нестандартными доя них операндами. Отметим, что опе- I рации могут быть распространены на вводимые пользователем типы, | вдиако у программиста нет возможности изменить действие операций I Hi операнды стандартных типов. Эту связанную с классами возмож- ность языка Си++ рассмотрим позже, а сейчас остановимся на неко- ! Торых свойствах операций, стандартно определенных для тех типов, s для которых эти операции введены. * Формальный синтаксис языка Си++ [2,5] предусматривает рекур- ? Оивиое определение выражений. Рекурсивность синтаксических опре- i делений (не только для выражений) и широкие возможности j конструирования новых типов делают попытки "однопроходного" ? Изложения семантики сразу всего языка Си++ практически безнадеж- 1 иыми. Поясним это, рассмотрев выражения. Основным исходным элементом любого выражения является пер- вичное выражение. К ним относятся: • константа • имя • (выражение) • ::идентификатор • ::имя~фуккции-операции • : : квалифицированное_имя • this • псевдопеременная (реализация Си++ для ПЭВМ). константа К константам относятся: • целая константа • символьная константа • перечислимая константа 6 3821
82 Язык Си++ • вещественная константа • строковая константа (строка) Все типы констант - это лексемы, и они уже в предыдущих главах рассмотрены. Приведены форматы констант, предельные значения для реализаций Си++ и даны примеры. имя К именам относятся: идентификатор может использоваться в качестве имени только в том случае, если он введен с помощью подходящего определения. Самый распространенный представитель - идентификатор как имя переменной. имя_функции-операции вводится только в связи с расширением действия (с перегруз кой) операций. Механизм перегрузки возможно объяснить только после определения понятия класс. имя_функции__приведения функции приведения, или преобразующие функции являются компонентами классов, и для объяснения их семантики тре- буется ввести соответствующие понятия, относящиеся к клас- сам. ~имя__класса обозначает обращение к специальному компоненту класса - к деструктору. квалифицированнов__имя (уточненное__имя) имеет рекурсивное определение следующего формата: квалифицированнов__имя__класса: : имя Таким образом, чтобы определить понятие квалифицирован- ное_имя, требуется ввести понятие квалифицирован- иов_имя_класса и уже иметь определение имени. Следова- тельно, ие вводя понятие "класс", можно.в качестве имен из всего перечисленного использовать только идентификаторы. (выражение) Третий вариант первичного выражения содержит рекурсию, так как это произвольное выражение, заключенное в круглые скобки.
tea 3. Скалярные типы и выражения 83 «Ментификятор В Четвертый вариант первичного выражения ::идентификатор Июпочает операцию изменения области действия, смысл которой объ- ИЬнялся. В Все остальные представители первичных выражений (за исключе- Щйем псевдопеременных) невозможно объяснять и иллюстрировать Номерами, не вводя понятие класса. Таким образом, следуя логике ^Нашего изложения (алфавит - лексемы - базовые типы - скалярные |К|пы - выражения) и не вводя структурированных типов, к которым Износятся классы, придется рассматривать не все варианты первичных ^Выражений и тем более не все варианты выражений. Учитывая это Шраничение, продолжим "конструирование” выражений. На основе арвичных выражений вводится постфиксное выражение, которым может быть: Ш • первичнов^выражение Ш • постфиксное_выражениа [выражение] •Ж • постфиксное_выражение (список~выражений) Я • имя^простого^типа (списки_выражекии) й • постфиксное выражение.имя ' i • постфиксное_выражение->имя • постфиксное_выражение++ Jk • постфихсное_выражение— |rf Индексация. Интуитивный смысл постфиксного выражения Щ (Обозначим его ре), за которым в квадратных скобках следует выра- 1 Жение (IE), есть индексация: ) int j = 5, d[100] ; // Определение целой переменной j // и массива d » • • ।..d[j]... // Постфиксное выражение В записи ре [IE] постфиксное выражение (ре) должно быть, на- пример, именем массива нужного типа, выражение ie в квадратных скобках должно быть целочисленного типа. Таким образом, если ре - указатель на массив, то ре [IE] - индексированный элемент этого мас- сива. Примеры работы с индексированными переменными (с элемен- тами массивов) yate несколько раз приводились. Например, индексирование анализировалось в связи с рассмотрением квадрат- ных скобок в качестве бинарной операции в п.2.4.
84 Язык Си++ Обращение к функции. Постфиксное выражение РЕ(список_выражений) представляет обращение к функции. В этом случае ре - имя функции или указатель на функцию, или ссылка на функцию. Спи сок_выражекий - это список фактических параметров. Значения фак тических параметров вычисляются до выполнения функции, поэтом\ и побочные эффекты проявляются до входа в нее и могут сказываться во время выполнения операторов тела функции. Порядок вычислени । значений фактических параметров синтаксисом языка Си++ не опрс делен. Например [2], функция f (int, int) при таком обращении int m = 2; f (m—, m—) ; может быть вызвана в зависимости от реализации со следующими значениями параметров: f(2,2), или f(i,2), или f(2,i) (Компилятор ВС++ вызывает эту функцию в виде f (1,2).) В то же время за счет побочного эффекта значением переменной m станет О Для проверки конкретных правил вычисления значений фактически* параметров можете выполнить с помощью доступного компилятора такую программу: //РЗ-14.СРР - порядок вычисления фактических параметров tinclude <iostream.h> int m = 5; // Глобальная переменная void p(int i,int j,int k) // Определение функции { cout « "\ni » "« i « « j - « « j « " k ж ” « k; cout « "\пВнутри функции p(. . .) m«" « m; ) void main() { void p(int, int, int); // Прототип функции p(m++, (m « m * 5, m * 3), m—) ; cout « "\пВ главной программе после вызова р(...)"; cout « " m « " « m; ) Результат выполнения программы для компилятора ВС++: i = 20 j = 60 k = 5 Внутри функции р(...) m = 21 В главной программе после вызова р(..,) m » 21
hnua 3. Скалярные типы и выражения 85 Как видно из результатов, параметры вычисляются справа налево. После вычисления очередного параметра выполняются постфиксные операции для операндов, входящих в выражение фактического пара- метра. Еще раз отметим, что при обращении к функции перед скобками в Ввчестве постфиксного выражения может использоваться не только Имя функции, но и указатель на функцию и ссылка на функцию. Под- робнее возможности этих обращений будут рассмотрены позже. Явное преобразование типа. Постфиксное выражение type(списоК—Выражений) служит для формирования значений типа type на основе спис- ив^выражений, помещенного в круглых скобках. Если выражений больше одного, то тип должен быть классом, и данное постфиксное Выражение вызывает конструктор класса. Если в списке выражений Всего одно выражение, a type - имя простого типа, то имеет место уже рассмотренное в разделе 2.4 непосредственное функциональное при- ведение типа. Общая форма такой функциональной записи явного Преобразования типа имеет вид: имя_простого_типа (выражение). Примеры: int (3.14), float (2/5), int (* А') • Функциональная запись не может применяться для типов, не имеющих простого имени. Например, попытка трактовать конструк- ции unsigned long (х/3+2) ИЛИ char *(0777) Вйк функциональные преобразования вызовет ошибку компиляции. Напомним (см. п. 2.4), что кроме функциональной записи для явного Преобразования типа можно использовать каноническую операцию Приведения (cast) к требуемому типу. Для ее изображения использует- ся обозначение типа в круглых скобках. Те же примеры можно запи- сать с помощью операции приведения типа так: (int) 3.14, (float)2/5, (int)*А'. Каноническая операция приведения к типу может применяться и для типов, имеющих сложные обозначения. На- пример, можно записать (unsigned long)(х/3+2) пли (char *)0777 и гем самым выполнить нужные преобразования.
86 Язык Си++ Другую возможность явного преобразования для типов со слоя ным наименованием обеспечивает введение собственных обозначении типов с помощью typedef. Например: typedef unsigned long int ULI; typedef char *PCH; После введения пользователем таких простых имен типов можно применять функциональную запись преобразования типа: ш (х/з+2) или рсн(0777). При преобразовании типов существуют некоторые ограничения. Но прежде чем остановиться на них, рассмотрим стаи дартные преобразования типов, выполняемые при необходимости по умолчанию. Стандартные преобразования типов. При вычислении выражений некоторые операции требуют, чтобы операнды имели соответствую щий тип, а если требования к типу не выполнены, принудительно вы зывают выполнение нужных преобразований. Та же ситуация возникает при инициализации, когда тип инициализирующего выра- жения приводится к типу определяемого объекта. Напомним, что в языках Си и Си++ присваивание является бинарной операцией, по- этому сказанное относительно преобразования типов относится и ко всем формам присваивания. Правила преобразования в языке Си++ для основных типов полностью совпадают с правилами преобразова- ний, стандартизованными в языке Си. Эти стандартные преобразова- ния включают перевод ’’низших" типов в "высшие" в интересах точности представления и непротиворечивости данных [5, 12]. Среди преобразования типов в языке Си++ выделяют: • преобразования операндов в арифметических выражениях', • преобразования указателей', • преобразования ссылок', • преобразования указателей на компоненты классов. При преобразовании типов нужно различать преобразования, из- меняющие внутреннее представление данных, и преобразования, из- меняющие только интерпретацию внутреннего представления. Напри- мер, когда данные типа unsigned int переводятся в тип int, менять их внутреннее представление не требуется - изменяется только интерпре- тация. При преобразовании типа float в тип int недостаточно изме- нить только интерпретацию, необходимо изменить длину участка памяти для внутреннего представления и кодировку. При таком пре- образовании из float в int возможен выход за диапазон допустимых
Ьшва 3. Скалярные типы и выражения 87 (Качений типа int, и реакции на эту ситуацию существенно зависит от Конкретной реализации. Именно поэтому для сохранения мобиль- ности программ в них рекомендуется с осторожностью применять Преобразование типов. Рассмотрим этапы (последовательность выполнения) преобразо- |виия операндов в арифметических выражениях. 1. Все короткие целые типы преобразуются в типы неменьшей длины в соответствии с табл. 3.2 (см. [5, 12]). Затем оба значения, уча- ствующие в операции, принимают тип int или float либо double в соот- ветствии со следующими правилами [2, 5, 12]. 2. Если один из операндов имеет тип long double, то второй тоже |удет преобразован в long double. Таблица 3.2 Правила стандартных арифметических преобразований Исходный тип Преобразу- ется в Правила преобразований char int Расширение нулем или знаком в зави- симости от умолчания для char unsigned char int Старший байт заполняется нулем signed char int Расширение знаком short int Сохраняется то же значение unsigned short unsigned int Сохраняется то же значение enum int Сохраняется то же значение битовое поле int Сохраняется то же значение 3. Если п.2 не выполняется и один из операндов есть double, дру- гой приводится к типу double. 4. Если п. 2 4- 3 не выполняются и один из операндов имеет тип float, то второй приводится к типу float. 5. Если п.2 т 4 не выполняются (оба операнда целые) и один операнд long int, а другой unsigned int, то, если long int может предста- вить все значения unsigned int, последний преобразуется к long int; ина- че оба операнда преобразуются к unsigned long int. 6. Если п.2 4- 5 не выполняются и один операнд есть long, другой преобразуется к long. 7. Если п.2 4- 6 не выполнены и один операнд unsigned, то другой преобразуется к unsigned. 8, Если п.2 4- 7 не выполнены, то оба операнда принадлежат типу Int.
88 Язык Си++ Используя арифметические выражения, следует учитывать приве- денные правила и не попадать в "ловушки" преобразования типов, так как некоторые из них приводят к потерям информации, а другие изме- няют интерпретацию битового (внутреннего) представления данных. На рис. 3.2, взятом с некоторыми сокращениями из проекта стан- дарта языка Си++ [2], стрелками отмечены арифметические преобра- зования, гарантирующие сохранение точности и неизменность численного значения. unsigned char unsigned short unsigned int unsigned long Рис.3.2. Последовательности арифметических преобразований типов, гарантирующие сохранение значимости При преобразованиях, которые не отнесены схемой (рис.3.2) к безопасным, возможны существенные информационные потери. Для оценки значимости таких потерь рекомендуется проверить обрати- мость преобразования типов. При арифметических преобразованиях необратимость вполне объяснима и естественна. Преобразование це- лочисленных значений к вещественному типу осуществляется на- столько точно, насколько это предусмотрено аппаратурой. Если цело- численное значение не может быть точно представлено как веще- ственное, то младшие значащие цифры теряются. Преобразование вещественного значения к целому типу выпол- няется за счет отбрасывания дробной части. Обратное преобразова- ние целой величины к вещественному значению может привести к потере точности. Следующая программа иллюстрирует сказанное: //РЗ-16.СРР - потери информации при преобразованиях типов #include <iostream.h>
Глава 3. Скалярные типы и выражения 89 void main () { long k = 123456789; float g = (float) k; cout « "\n к = "« k; // Печатает: к = 123456789 cout « "\n g = к = (long)g; "<<g; // Печатает: g = 1.234567е+"08 cout «”\п к = "« к; g = (float) 2.222222е+; int m = (int) g; // Печатает: к = 123456792 cout « "\n g = "«g; // Печатает: g = 222.222198 cout « "\n m = g = (float)m; ”«m; // Печатает: m = 222 cout « ”\n g = "« g; // Печатает: g = 222 } К менее предсказуемым результатам может привести необратимость преобразования типов для указателей, ссылок и указателей на компоненты классов.
Глава 4. ОПЕРАТОРЫ ЯЗЫКА СИ++ Материал, относящийся к операторам, по-видимому, наиболее традиционный. Здесь язык Си++ почти полностью соответствует язы ку Си, который, в свою очередь, наследует конструкции классических алгоритмических языков лишь с небольшими усовершенствованиями Операторы, как обычно, определяют действия и логику (порядок) выполнения этих действий в программе. Среди операторов выделяю! операторы, выполняемые последовательно, и управляющие one раторы. 4.1. Последовательно выполняемые операторы Каждый оператор языка Си++ заканчивается и идентифицируется разделителем "точка с запятой". Любое выражение, после которого поставлен символ "точка с запятой", воспринимается компилятором как отдельный оператор. (Исключения составляют выражения, вхо дящие в заголовок цикла for.) Часто оператор-выражение служит для вызова функции, не воз вращающей никакого значения. Например: //Р4-01.СРР - обращение к функции как оператор-выражение tfinclude <iostream.h> void cod^char(char c) ( cout « ”\n " « c « " = " « (unsigned int)c; } void main(> { void cod_char(char); // Прототип функции cod__char (' A') ; // Оператор-выражение cod_char (' x') ; // Оператор-выражение } Результат выполнения программы: А = 65 X = 120
Глава 4, Операторы языка Си++ 91 Еще чаще оператор-выражение - это не вызов функции, а выраже- ние присваивания. Именно в связи с тем, что присваивание относится К операциям и используется для формирования бинарных выражений, В языке Си++ (и в Си) отсутствует отдельный оператор присваивания. Оператор присваивания всего-навсего является частным случаем опе- ратора-выражения. Специальным случаем оператора служит пустой оператор. Он Представляется символом "точка с запятой", перед которым нет ника- кого выражения или не завершенного разделителем оператора. Пус- той оператор не предусматривает выполнения никаких действий. Он Используется там, где синтаксис языка требует присутствия операто- ра, а по смыслу программы никакие действия не должны выполняться. Пустой оператор чаще всего используется в качестве тела цикла, ког- да все циклически выполняемые действия определены в его заголовке: // Вычисляется факториал: 5! for (int i » 0, р = 1; i < 5; i++, р *» i); Перед каждым оператором может быть помещена метка, отделяе- мая от оператора двоеточием. В качестве метки используется произ- вольно выбранный программистом уникальный идентификатор: АВС: х = 4 + х * 3; Метки локализуются в сфере действия функции. Описания и опре- деления, после которых помещен символ "точка с запятой", считаются Операторами. Поэтому перед ними также могут помещаться метки: metka: int z = 0, d = 4; // Метка перед определением С помощью пустого оператора, перед которым имеет право стоять Метка, метки можно размещать во всех точках программы, где син- таксис разрешает использовать операторы. Прежде чем привести Пример, определим составной оператор как заключенную в фигурные екобки последовательность операторов. Если среди операторов, на- водящихся в фигурных скобках, имеются определения и описания, то составной оператор превращается в блок, где локализованы все опре- деленные в нем объекты. Синтаксически и блок, и составной оператор валяются отдельными операторами. Однако ни блок, ни составной оператор не должны заканчиваться точкой с запятой. Для них огра- ничителем служит закрывающая фигурная скобка. Внутри блока (и составного оператора) любой оператор должен оканчиваться точкой с запятой:
92 Язык Си++ { int a; char Ь = ’О’; а = (int)b; } // Это блок { func(z + 1.0, 22); е*4*х-1; ) // Составной оператор // Составной оператор с условным переходом к его окончанию: { i--; if (i > k) goto MET; k++; MET:; ) // Помечен пустой // оператор Говоря о блоках, нужно помнить правила определения сферы дей ствия имен и видимости объектов. Так как и блок, и составной опера тор пользуются правами операторов, то разрешено их вложение причем на глубину вложения синтаксис не накладывает ограничений О вложении составных операторов и блоков удобнее говорить в связи с циклами, функциями и операторами выбора, к которым мы и перей дем. О входе в блок и выходе из блока речь пойдет в связи с операто рами передачи управления (п. 4.4). 4.2. Операторы выбора К операторам выбора, называемым операторами управления по- током выполнения программы, относят: условный оператор (if...else) и переключатель (switch). Каждый из них служит для выбора пути вы- полнения программы. Синтаксис условного оператора: if (выражение) оператору!; else оператор__2; Выражение должно быть скалярным и может иметь арифметиче- ский тип или тип указателя. Если оно не равно нулю (или не есть пус- той указатель), то условие считается истинным и выполняется оператор_1. В противном случае выполняется оператору. В качестве операторов нельзя использовать описания и определения. Однако здесь могут быть составные операторы и блоки: if (х > 0) ( х » -х; f(х * 2); ) else { int i = 2; х *= i; f(х); ) При использовании блоков (т.е. составных операторов с опреде- лениями и описаниями) нельзя забывать о локализации определяемых в блоке объектов. Например, ошибочна будет последовательность: if (j > 0) ( int i; i = 2 * j; ) else i = -j; так как переменная i локализована в блоке и не существует вне его.
Драиа 4, Операторы языка Си++93 Допустима сокращенная форма условного оператора, в которой Отсутствует else и оператор_2. В этом случае при ложности (равенстве Нулю) проверяемого условия никакие действия не выполняются: if (а < 0) а = -а; В свою очередь, оператор_1 и оператор_2 могут быть условными, что позволяет организовывать цепочку проверок условий любой Глубины вложенности. В этих цепочках каждый из условных операторов (После проверяемого условия и после else) может быть как полным условным, так и иметь сокращенную форму записи. При этом могут возникать ошибки неоднозначного сопоставления И и else. Синтаксис языка предполагает, что при вложениях условных операторов каждое else соответствует ближайшему к нему Предшествующему if. В качестве примера неверного толкования этого правила в документации [5] приводится такой пример: if (х == 1) if (у « l)cout « "х равно 1 и у равно 1"; else cout «”х не равно 1"; При х, равном 1, и у, равном 1, совершенно справедливо печатается фриза "х равно 1 и у равно Г'. Однако фраза "х не равно 1” может оыть напечатана только при х, равном 1, и при у, не равном 1, так как flic относится к ближайшему if. Внешний условный оператор, где Проверяется х==1, является сокращенным и в качестве оператора_1 включает полный условный оператор, в котором проверяется условие Таким образом, проверка этого условия выполняется только при й, равном 1! Простейшее правильное решение этой микрозадачи можно Получить, применив фигурные скобки, т.е. построив составной оператор. Нужно фигурными скобками ограничить область действия внутреннего условного оператора, сделав его неполным. Тем самым внешний Оператор превратится в полный условный: if (х==1) {if (у===1) cout « "х равно 1 и у равно 1";} •Ise cout « ”х не равно 1"; Теперь else относится к первому if, и выбор выполняется верно. В качестве второго примера вложения условных операторов рассмотрим функцию, возвращающую максимальное из значений трех аргументов: int тахЗ (int х, int у, int z) { if (х < у}
94 Язык Си++ if (у < z) return z; else return y; else if (x < z) return z; else return x; } В тексте соответствие if и else показано с помощью отступов. Переключатель является наиболее удобным средством для орган и зации мультиветвления. Синтаксис переключателя таков: s wi tch (переключажяцее^выражение) { case констаитное__выражеиие__1: операторы__1; case константное__выражение__2 : операторы__2; са se константное_выражеиие__п: операторы__п ; default: операторы; } Управляющая конструкция switch передает управление к тому ш помеченных с помощью case операторов, для которого значение кон стантного выражения совпадает со значением переключающего вы ражения. Переключающее выражение должно быть целочисленным или его значение приводится к целому. Значения константных выра жений, помещаемых за служебными словами case, приводятся к тип\ переключающего выражения. В одном переключателе все констанз ные выражения должны иметь различные значения, но быть одного типа Любой из операторов, помещенных в фигурных скобках после конструкции switch(.. .), может быть помечен одной или несколь кими метками вида case константное__выражеиие: Если значение переключающего выражения не совпадает ни с од ним из константных выражений, то выполняется переход к оператору отмеченному меткой default:. В каждом переключателе должно быть не больше одной метки default, однако эта метка может и отсутство вать. В случае отсутствия метки default при несовпадении переклю чающего выражения ни с одним из константных выражений, помещаемых вслед за case, в переключателе не выполняется ни один из операторов. Сами по себе метки case константное_выражение^ : и default: не изменяют последовательности выполнения операторов. Если не предусмотрены переходы или выход из переключателя, то в нем по-
Г лева 4. Операторы языка Си++ 95 вдедовательно выполняются все операторы, начиная с той метки, на Которую передано управление. Пример программы с переключателем: Z/P4 -02.СРР - названия нечетных целых цифр, не меньших // заданной finclude <iostream.h> Void main() ( int ic; cout « ”\пВведите любую десятичную цифру: cin » ic; cout « 1 \n' ; switch (ic) { case 0: сазе 2: сазе 4: сазе 6: : сазе 1: : сазе 3: : сазе 5: : сазе 7: cout <• cout < cout <- cout < С "один, " С "три, С "пять, " С " семь, ’’ г сазе 8: : сазе 9: cout <’ С "девять. И • / break; // Выход из переключателя default: cout <’ < "Ошибка! Это не цифра!"; ) // Конец переключателя 1 // Конец программы Результаты двух выполнений программы: Введите любую десятичную цифру: 4 <Enter> Пять, семь, девять Вводите любую десятичную цифру: z <Enter> Ошибка! Это не цифра! Кроме сказанного о возможностях переключателя, приведенная Программа иллюстрирует действие оператора break. С его помощью Выполняется выход из переключателя. Если поместить операторы Break после вывода каждой цифры, то программа будет печатать на- звание только одной нечетной цифры. Несмотря на то, что в формате переключателя после конструкции twitch () приведен составной оператор, это не обязательно. После twitch () может находиться любой оператор, помеченный с исполь- зованием служебного слова case. Однако без фигурных скобок такой Оператор может быть только один, и смысл переключателя теряется: он превращается в разновидность сокращенного условного операто- ра. В переключателе могут находиться описания и определения объ- ектов, т.е. составной оператор, входящий в переключатель, может
96 Язык Си++ быть блоком. В этом случае нужно избегать ошибок "перескакивания через определения [2]: switch (п) // Переключатель с ошибками { char d = ’D'; // Никогда не обрабатывается case 1: float f = 3.14; // Обрабатывается только // для п = 1 case 2 : ... if (int (d) != int (f)) . . . // Ошибка: // d и (или) f не определены ) 4.3. Операторы цикла Операторы цикла задают многократное исполнение операторов тела цикла. Определены три разных оператора цикла: • цикл с предусловием: while (выражение-условие) тело цикла • цикл с постусловием: do тело__цикла while (выражение-условие); • итерационный цикл: for (инициализация__цикла; выражение-условие; список__выражении) тело__цикла Тело_цикла не может быть описанием или определением. Это ли бо отдельный (в том числе пустой) оператор, который всегда завер шается точкой с запятой, либо составной оператор, либо бло» (заключаются в фигурные скобки). Выражение-условие - это во всех операторах скалярное выражение (чаще всего отношение или арифме тическое выражение), определяющее условие продолжения выполнс ния итераций (если его значение не равно нулю). Инициализация_цик ла в цикле for всегда завершается точкой с запятой, т.е. отделяется этим разделителем от последующего выражения-условия, которо< также завершается точкой с запятой. Даже при отсутствии инициали
Лава 4. Операторы языка Си++ 97 фВции__цикла и выражения-условия в цикле for символы "точка с за- й" всегда присутствуют. Список_выражекий (в цикле for) - после- «вательность скалярных выражений, разделенных запятыми. Прекращение выполнения цикла возможно в следующих случаях: • нулевое значение проверяемого выражения-условия; • выполнение в теле цикла оператора передачи управления (break, goto, return) за пределы цикла. Последнюю из указанных возможностей проиллюстрируем позже, осматривая особенности операторов передачи управления. Оператор while (оператор "повторять пока (истинно условие)") на- пвается оператором цикла с предусловием. При входе в цикл вычис- ется выражение-условие. Если его значение отлично от нуля, то Выполняется тело_цикла. Затем вычисление выражения-условия и Шолнение операторов тела_цикла повторяются последовательно, »ка значение выражения-условия не станет равным 0. Оператором while удобно пользоваться для просмотра всевозмож- IX последовательностей, если в конце каждой из них находится за- днее известный признак. Например, по определению, строка есть Последовательность символов типа char, в конце которой находится Нулевой символ. Следующая функция подсчитывает длину строки. Веданной в качестве параметра: int length (char *stroke) 1 { int len = 0; while (*stroka++) len++; return len; } Здесь выход из цикла - равенство нулю того элемента строки, ко- торый адресуется указателем stroka. (Обратите внимание на порядок Вычисления проверяемого выражения. Вначале будет выбрано значе- ние указателя stroka, затем оно будет использовано для доступа по Вдресу, выбранное значение будет значением выражения в скобках и Цтем значение указателя будет увеличено на 1.) В качестве проверяемого выражения-условия часто используются Отношения. Например, следующая последовательность операторов Вычисляет сумму квадратов первых к натуральных чисел (членов на- турального ряда): int i = 0; // Счетчик int s = 0; // Будущая сумма while (i < К) 1
98 Язык Си++ s += ++1 * 1; // Цикл вычисления суммы Если в выражении-условии нужно сравнивать указатель с нул» вым значением (с пустым указателем), то следующие три проверки эквивалентны: while (point !» NULL) ... while (point) ... while (point != 0) ... Используя оператор цикла с предусловием, необходимо следить з.1 тем, чтобы операторы тела_цикла воздействовали на выражение условие, либо оно еще каким-то образом должно изменяться во время вычислений. (Например, за счет побочных эффектов могут изменяться операнды выражения-условия. Часто для этих целей использую! унарные операции ++ и —.) Только при изменении выражения условия можно избежать зацикливания. Например, следующий опе- ратор обеспечивает бесконечное выполнение пустого оператора в теле цикла: while (1); // Бесконечный цикл с пустым // оператором в качестве тела Такой цикл может быть прекращен только за счет событий, про исходящих вне потока операций, явно предусмотренных в программе Самый жесткий вариант такого события - отключение питания ЭВМ Обычно в конкретных реализациях языка возможности выхода из бесконечного цикла обеспечивают средства доступа к механизму пре рываний. Оператор do (оператор "повторять") называется оператором цик- ла с постусловием. Он имеет следующий вид: do тело__цикла while (выражение-условие) ; При входе в цикл do обязательно выполняется тело_цикла. Затем вычисляется выражение-условие и, если его выражение не равно 0. вновь выполняется тело_цикла. При обработке некоторых последо- вательностей применение цикла с постусловием оказывается удобнее, чем цикла с предусловием. Это бывает в тех случаях, когда обработку нужно заканчивать не до, а после появления концевого признака. На- пример, следующая функция переписывает заданную строку (указатель star) в другую, заранее подготовленную строку (nov):
Глава 4, Операторы языка Си++ 99 void copy.str(char *star, char *nov) { do *nov = *star++; while (*nov++); ) Еще один вариант того же цикла с пустым телом.цикла: do г while (*nov ++ = *star++); Даже если строка пустая, в ней (по определению строки) в конце Присутствует знак "\о". Именно его наличие проверяется после записи По адресу nov каждого очередного символа. К выражению-условию требования те же, что и для цикла while с предусловием - оно должно изменяться при итерациях либо за счет опе- раторов тела цикла, либо при вычислениях. Бесконечный цикл: do ; while(1); Оператор итерационного цикла for имеет формат: for (инициализация.цикла; выражение.условие; список.выражений) тело.циклая Здесь иннциализация__цикла - последовательность определений (описаний) и выражений, разделяемых запятыми. Все выражения, входящие в инициализацию цикла, вычисляются только один раз при входе в цикл. Чаще всего здесь устанавливаются начальные значения счетчиков и параметров цикла, выражение-условие такое же, как и в циклах while и do: если оно равно 0, то выполнение цикла прекращается. В случае отсутствия выражения_условия следующий за ним разделитель "точка с запятой" сохраняется. При отсутствии выражения-условия предполагается, что его значение всегда истинно. При отсутствии инициализации цикла соответствующая ему точка с запятой сохраняется. Выражения из списка_ныражении вычисляются на каждой итерации после выполнения операторов тела цикла и до следующей проверки выражения-условия. Тело_цикла может быть блоком, отдельным опе- ратором, составным оператором и пустым оператором. Следующие операторы for иллюстрируют разные решения задачи суммирования квадратов первых к членов натурального ряда:
100 Язык Си++ for (int i = 1, s = 0; i <= K; i++) s += i * i; for (int i = 0, s = 0; i <= K; s += ++i * i); for (int i ~ 0, s = 0; i <= K; ) s += ++i * i; for (int i « 0, s « 0; i <=® K; ) ( int j; j = ++i; s += j * j; } Во втором операторе тело__цикла - пустой оператор. В третьем отсутствует список__выражений. Во всех операторах в инициализа ции_циклов определены (и инициализированы) целые переменные. Итак, еще раз проследим последовательность выполнения итера ционного цикла for. Определяются объекты и вычисляются выраже ния, включенные в инициализацию__цикла. Вычисляется выражение условие. Если оно отлично от нуля, выполняются операторы те ла_цикла. Затем вычисляются выражения из списка выражений, вновь вычисляется выражение-условие и проверяется его значение. Далее цепочка действий повторяется. При выполнении итерационного цикла for выражение-условие может изменяться либо при вычислении его значений, либо под дей ствием операторов тела цикла, либо под действием выражений из списка заголовка. Если выражение-условие не изменяется либо отсут ствует, то цикл бесконечен. Следующие операторы обеспечивают бес конечное выполнение пустых операторов: for( ; ;) ; // Бесконечный цикл for( ; 1; ); // Бесконечный цикл В проекте стандарта языка Си++ нет специальных соглашении относительно области действия имен, определенных в инициализи рующей части оператора цикла. В конкретных реализациях принято, что область действия таких имен - от места размещения цикла for до конца блока, в котором этот цикл используется. Например, следую- щая последовательность операторов for (int i = 0; i < 3; i++) cout « "\t" « i; for (; i > 0; i--) cout « "\t" « i; выводит на печать такие значения переменной i: 0 1 2 3 2 1 Если во второй цикл поместить еще одно определение той же пе- ременной i, т.е.
Глава 4. Операторы языка Си++ 101 for (int i = 0; i < 3; i++) cout « "\t" « i; ! for (int i = 3; i > 0; i—) // Ошибка!’ § cout « "Xt" « i; (ТО получим сообщение компилятора об ошибке: "многократное опре- деление переменной i". Разрешено и широко используется вложение любых циклов в лю- бые циклы. В этом случае в инициализации внутреннего цикла for Может быть определена (описана с инициализацией) переменная с та- ким же именем, что и переменная, определенная во внешнем цикле. //Р4-03.СРР - вложение циклов flnclude <iostream.h> void main (void) ( for (int i = 0; i < 3; i++) ( cout « ”\пДо цикла: i = " « i; cout « " t пложаюапл цикл: " ; for (int i == 6; i > 3; i--) cout « " i = " « i; cout « M.\n После: i ® " « i « ) > Результат выполнения этой программы несколько неожиданный: До цикла: i = 0, вложенный цикл: i = 6 i = 5 i = 4. После: i == 3 До цикла: i » 1, вложенный цикл: i = 6 i = 5 i = 4. После: i = 3 До цикла: i == 2, вложенный цикл: 1 = 6 i = 5 i = 4. После: i = 3 До внутреннего цикла действует определение переменной i в ини- циализации внешнего цикла for. Инициализация внутреннего цикла определяет другую переменную с тем же именем, и это определение остается действительным до конца тела внешнего цикла. | 4.4. Операторы передачи управления | К операторам передачи управления относят оператор безусловно- 10 перехода, иначе - оператор безусловной передачи управления (goto), оператор возврата из функции (return), оператор выхода из цикла или переключателя (break) и оператор перехода к следующей итерации цикла (continue).
102 Язык Си++ Оператор безусловного перехода имеет вид: goto идентификатор; где идентификатор - имя метки оператора, расположенного в той я < функции, где используется оператор безусловного перехода. Передача управления разрешена на любой помеченный оператор » теле функции. Однако существует одно важное ограничение: запрете но '’перескакивать" через описания, содержащие инициализацию объ ектов. Это ограничение не распространяется на вложенные блоки которые можно обходить целиком. Следующий фрагмент иллюстри рует сказанное: goto В; // Ошибочный переход, минуя описание float х = 0.0; // Инициализация не будет выполнена goto В; // Допустимый переход, минуя блок { int п = 10; // Внутри блока определена переменная х«п*х + х; } В: cout « "\tx = " « х; Все операторы блока достижимы для перехода к ним из внешних блоков. Однако при таких переходах необходимо соблюдать то самое правило: нельзя передавать управление в блок, обходя иннциа лизацию. Следовательно, будет ошибочным переход к операторам блока, перед которыми помещены описания с явной или неявной ини цианизацией. Это же требование обязательного выполнения инициа лизации справедливо и прн внутренних переходах в блока Следующий фрагмент содержит обе указанные ошибки: { ... // Внешний блок goto АВС; //Во внутренний блок, минуя описание ii { int ii я» 15; // Внутренний блок ABC: goto XYZ; // Обход описания СС char СС ж ’ 1; XYZ: } }
i Глава 4. Операторы языка Си++ 103 : Принятая в настоящее время дисциплина программирования ре- | МОМендует либо вовсе отказаться от оператора goto, либо свести его | Применение к минимуму и строго придерживаться следующих реко- > Цендаций [16]: • не входить внутрь блока извне; । «не входить внутрь условного оператора, т.е. не передавать управление операторам, размещенным после служебных слов if * или else; • не входить извне внутрь переключателя (switch); I • не передавать управление внутрь цикла. !' Следование перечисленным рекомендациям позволяет исключить (возможные нежелательные последствия бессистемного использования Оператора безусловного перехода. Полностью отказываться от опера- тора goto вряд ли стоит. Есть случаи, когда этот оператор обеспечи- вает наиболее простые и понятные решения. Один из них - это Ситуация, когда в рамках текста одной функции необходимо из раз- ; ПЫХ мест переходить к одному участку программы. Если по каким- । Либо причинам эту часть программы нельзя оформить в виде функции, То наиболее простое решение - применение безусловного перехода с Помощью оператора goto. Второй случай возникает, когда нужно выйти из нескольких вло- женных друг в друга циклов или переключателей. Оператор break прерывания цикла и выхода из переключателя здесь не поможет, так , Ык он обеспечивает выход только из самого внутреннего вложенного ; Цикла или переключателя. Например, в задаче поиска в матрице хотя одного элемента с заданным значением для перебора элементов Матрицы обычно используют два вложенных цикла. Как только эле- мент с заданным значением будет найден, нужно выйти сразу из двух ; циклов, что удобно сделать с помощью goto. j Оператор возврата из функции имеет вид: г г return выражение; или просто return; Выражение, если оно присутствует, может быть только скаляр- ным. Например, следующая функция вычисляет и возвращает куб значения своего аргумента: float cube(float z) ( return z * z * z; }
104 Язык Си++ Выражение в операторе return не может присутствовать в том сл\ чае, если возвращаемое функцией значение имеет тип void. Например следующая функция выводит на экран дисплея, связанный с потоком cout, значение третьей степени своего аргумента и не возвращает в точку вызова никакого значения: void cube_print(float z) { cout « r,\t cube я " « z * z * z; return; } В данном примере оператор возврата из функции не содержит вы ражения. Оператор break служит для принудительного выхода из цикла или переключателя. Определение "принудительный" подчеркивает бел условность перехода. Например, в случае цикла не проверяются и нс учитываются условия дальнейшего продолжения итераций. Оператор break прекращает выполнение оператора цикла или переключателя и осуществляет передачу управления (переход) к следующему за циклом или переключателем оператору. При этом в отличие от перехода < помощью goto оператор, к которому выполняется передача управлс ния, не должен быть помечен. Оператор break нельзя использовать нигде, кроме циклов и переключателей. Необходимость в использовании оператора break в теле цикли возникает, когда условия продолжения итераций нужно проверять не в начале итерации (циклы for, while), не в конце итерации (цикл do), а в середине тела цикла. В этом случае тело цикла может иметь такую структуру: { операторы if (условие) break; операторы 1 Например, если начальные значения целых переменных i, j тако- вы, что i < j, то следующий цикл определяет наименьшее целое, не меньшее их среднего арифметического: while (i < j) { i++; if (i «= j) break;
105 Глава 4. Операторы языка Си++ -------------------------- Для i « 0, j — 3 результат i = j == 2 достигается при вы- воде из цикла с помощью оператора break. (Запись i =» j « 2 не в Пакете программы означает равенство значений переменных i, j и (онстанты 2.) Для i = о, j = 2 результат i = j «= 1 будет полу- 1вн при естественном завершении цикла. Оператор break практически незаменим в переключателях, когда с Их помощью надо организовать разветвление. Например, следующая Программа печатает название любой, но только одной, восьмеричной Цифры: //Р4-04.СРР - оператор break в переключателе (include <iostream.h> «Old main () ( int ic; Cout « "\n Введите восьмеричную цифру: cin » ic; cout « "\n" « ic; witch (ic) { case 0: cout « " - нуль"; break; case 1: cout « " * один"; break; case 2: cout « " - два"; break; case 3: cout « " * три"; break; case 4: cout « ” - четыре"; break; case 5: cout « " - пять"; break; case 6: cout « " - лесть"; break; case 7: cout « " - семь"; break; default: cout « " - это не восьмеричная цифра' "; ) cout « "\пКонец выполнения программы."; I Программа напечатает название только одной введенной цифры и Прекратит работу. Если в ней удалить операторы break, то в переклю- чателе будут последовательно выполнены все операторы, начиная с Помеченного нужным (введенным) значением. Циклы и переключатели могут быть многократно вложенными. Однако следует помнить, что оператор break позволяет выйти только ИЗ самого внутреннего цикла или переключателя. Например, в сле- дующей программе, которая в символьном массиве подсчитывает ко- личество нулей (ко) и единиц (к1), в цикл вложен переключатель: //Р4-05.СРР - break при вложении переключателя в цикл •include <iostream.h> void main(void)
106 Язык Си++ { char с[] = "АВС1ОО111"; int kO = 0, kl = 0; for (int i »s 0; c[i] ! = '\0'; i++) switch (c[i]) { case 'O’: k0++; break; case '1': kl++; break; default: break; } cout « "\nB строке " « kO « " нуля, " « kl « " единицы"; } Результат выполнения программы: В строке 2 нуля, 4 единицы Оператор break в данном примере передает управление из пере- ключателя, но не за пределы цикла. Цикл продолжается до естествен- ного завершения. При многократном вложении циклов и переключателей оператор break не может вызвать передачу управления из самого внутреннего уровня непосредственно на самый внешний. Например, при решении задачи поиска в матрице хотя бы одного элемента с заданным значе- нием удобнее всего пользоваться не оператором break, а оператором безусловной передачи управления (goto): for (int i = 0; i < n; i++) for (int j = 0; j < m; j++) ( if (A[i][j] =» x) goto success; // Действия при отсутствии элемента в матрице } // Конец цикла success: cout « "\пЭлемент х найден. Строка i = '• « i; cout « ", столбец j = ” « j; В качестве примера, когда при вложении циклов целесообразно применение оператора break, рассмотрим задачу вычисления произве- дения элементов строки матрицы. В данном случае вычисление произ- ведения элементов можно прервать, если один из сомножителей окажется равным 0. Возможный вариант реализации может быть та- ким: for (i = 0; i < n; i++) // Перебор строк матрицы
Глава 4. Операторы языка Си++ 107 // Перебор элементов строки: for (j = 0, p[i] «в 1; j < m; 3++) if (A[i][jJ SSBS 0.0) // Обнаружен нулевой элемент ( P[i] “ 0.0; break; } else p[i] *= A[i][j]; При появлении в строке нулевого элемента оператор break преры- вает выполнение только внутреннего цикла, однако внешний цикл перебора строк всегда выполнится для всех значений i от 0 до п - 1. Оператор continue употребляется только в операторах цикла. С его помощью завершается текущая итерация и начинается проверка усло- вия дальнейшего продолжения цикла, т.е. условий начала следующей итерации. Для объяснений действия оператора continue рекомендуется [2] рассматривать следующие три формы основных операторов цикла: while (foo) { . . . contin: } do < . . . contin: } while (foo); for (;foo;) < . . . contin: } В каждой из форм многоточием обозначены операторы тела цик- ла. Вслед за ними размещен пустой оператор с меткой contin. Если среди операторов тела цикла есть оператор continue и он выполняется, то его действие эквивалентно оператору безусловного перехода на метку contin. Типичный пример использования оператора continue: подсчитать среднее значение только положительных элементов одномерного мас- сива: for (s в 0.0, k = 0, i = 0; i<n; i++) { if (x[i] <= 0.0) continue; k++; // Количество положительных s += x[i]; // Сумма положительных ) if (k > 0) s = s/k; // Среднее значение
Глава 5. АДРЕСА, УКАЗАТЕЛИ, МАССИВЫ, ПАМЯТЬ 5.1. Указатели и адреса объектов Специальными объектами в программах на языках Си и Си++ яв- ляются указатели. О них уже кратко говорилось, например, в связи с операциями new и delete доя динамического управления памятью. Различают указатели-переменные (именно их мы будем называть указателями) и указатели-константы. Значениями указателей служат адреса участков памяти, выделенных для объектов конкретных типов. Именно поэтому в определении и описании указателя всегда присут- ствует обозначение соответствующего ему типа. Эта информация по- зволяет в последующем с помощью указателя получить доступ ко всему сохраняемому объекту в целом. Указатели делятся на две категории - указатели на объекты и ука- затели на функции. Выделение этих двух категорий связано с от- личиями в свойствах и правилах использования. Например, указатели функций не допускают применения к ним арифметических операций, а указатели объектов разрешено использовать в некоторых арифме- тических выражениях. Начнем с указателей объектов. В простейшем случае определение и описание указателя- переменной на некоторый объект имеют вид: type *имя_укаЗатвля; где type - обозначение типа; имя_указатвля - это идентификатор; * - унарная операция раскрытия ссылки (операция разыменования; опе- рация обращения по адресу; операция доступа по адресу), операндом которой должен быть указатель (именно в соответствии с этим прави- лом вслед за ней следует имя__указателя). Признаком указателя при лексическом разборе определения или описания служит символ '**, помещенный перед именем. Таким обра- зом, при необходимости определить несколько указателей на объекты одного и того же типа этот символ помещают перед каждым име- нем. Например, определение int *ilp, *i2p, *i3p, i;
Глава 5. Адреса, указатели, массивы, память 109 вводит три указателя на объекты целого типа ilp, i2p, i3p и одну переменную i целого типа. Переменной i будет отведено в памяти 2 байта (ТС++ или ВС++), а указатели ilp, i2p, i3p разместятся в участках памяти, размер которых также зависит от реализации, но Которые только иногда имеют длину 2 байта. В совокупности имя типа и символ перед именем воспринимаются как обозначение особого типа данных "указатель на объект данного типа". При определении указателя в большинстве случаев целесообразно выполнить его инициализацию. Формат определения станет таким: type *иия_указателя инициализатор; Как упоминалось, инициализатор имеет две формы записи, поэтому допустимы следующие две формы определения указателей: type *иия_указателя = инициализирующее_выражение; type *иия_указателя (инициализирующее_выражение); В качестве инициализирующего_выражения должно использоваться константное выражение, частными случаями которого являются: • явно заданный адрес участка памяти; • указатель, уже имеющий значение; • выражение, позволяющее получить адрес объекта с помощью операции Если значение инициирующего выражения равно нулю, то это нуле- вое значение преобразуется к пустому (иначе нулевому) указателю. Синтаксис языка "гарантирует, что этот указатель отличен от указателя на любой объект" [2]. Кроме того, внутреннее (битовое) представление пустого указателя может отличаться от битового представления целого значения 0. В компиляторах ТС++ и ВС++ условное нулевое значение адреса, соответствующее значению пустого указателя, имеет специальное обозначение null. Примеры определений указателей: char cc = 'd'; // Символьная переменная (типа char) char *рс = &сс; // Инициализированный указатель на объект // типа char char *ptr(NULL);// Нулевой указатель на объект типа char char *р; // Неинициализированный указатель на // объект типа char
110 Язык Си++ Переменная сс инициализирована значением символьной кон- станты ’d'. После определения (с инициализацией) указателя рс до- ступ к значению переменной сс возможен как с помощью ее имени, так и с помощью адреса, являющегося значением указателя- переменной рс. В последнем случае должна применяться операция разыменования (получение значения через указатель). Таким обра- зом, при выполнении оператора cout « "\п сс равно "« сс «" и *рс = "« *рс; будет выведено: сс равно d и *рс = d Указатели ptr и р, определенные в нашем примере, пользуются различными "правами". Указатель ptr получил нулевое начальное значение (пустой указатель), и попытка его разыменования будет бес- перспективной. Не нужно надеяться, что пустой указатель связан с участком памя- ти, имеющим нулевой адрес или хранящим нулевое значение [2]. Син- таксис языка Си++ этого не гарантирует. Однако, присвоив затем ptr значение адреса уже существующего объекта, можно осмысленно применять операцию разыменования. Например, любой из операторов присваивания ptr » &сс; или ptr = рс; свяжет ptr с участком памяти, выделенным для переменной сс, т.е. после их выполнения значением *ptr будет ’d’. Присвоив указателю адрес конкретного участка памяти, можно с помощью операции разыменования не только получать, но и изме- нять содержимое этого участка памяти. Например, операторы при- сваивания: *рс = '+'; или ptr = рс; *ptr = ’ + ' ; сделают значением переменной сс символ ' +1. Унарное выражение *ужазат«ль обладает в некотором смысле правами имени переменной, т.е. *рс и *ptr служат синонимами (псевдонимами, другими именами) имени сс. Выражение *указатель
л t’ htaea 5. Адреса, указатели, массивы, память 111 "I .... ' ... . МО^кет использоваться практически везде, где допустимо использова- ние имен объектов того типа, к которому относится указатель. Одна- ХО Это утверждение справедливо лишь в том случае, если указатель Инициализирован при определении явным способом. В нашем приме- ра не инициализирован указатель р. Поэтому попытки использовать Выражение *р в левой части оператора присваивания или в операторе Ввода неправомерны. Значение указателя р неизвестно, а результат Ванесения значения в неопределенный участок памяти непредсказуем и Иногда может привести к аварийному событию. ♦р ж ’ %; // Ошибочное применение неинициализированного р Если присвоить указателю адрес конкретного объекта (р « &сс;) ИЛИ значение уже инициализированного указателя (р - рс;), то это Превратит *р в синоним (псевдоним) уже имеющегося имени объекта. Чтобы связать неинициализированный указатель с новым участком памяти, еще не занятым никаким объектом программы, ис- пользуется оператор new или присваивается указателю явный адрес: р • new char; // Выделили память для переменной типа char // и связали указатель р с этим участком // памяти р " (char *) ОхЬвОООООО; // Начальный адрес видеопамяти // ПЭВМ для цветного дисплея // в текстовой режиме Обратите внимание на необходимость преобразования числового значения к типу указателя (char *). После любого из таких операторов можно использовать *р для записи в память нужных символьных значений. Например, станут до- пустимы операторы: *р - или cin » *р; Числовое значение адреса может быть использовано не только во Время присваивания указателю значения в ходе выполнения програм- мы, но и при инициализации указателя при его определении. Нужно только не забывать о необходимости явного преобразования типов. Например, в следующем определении указатель с именем computer при инициализации получает значение адреса того байта, в котором содержатся сведения о типе компьютера, на котором выполняется программа (справедливо только для ЭВМ, совместимых с IBM PC):
112 Язык Си++ char «Computer * (char *)OxFOOOFFFE; Сведения об адресах таких "знаменитых" участков памяти ПЭВМ, как байт идентификации типа ПЭВМ или байты состояния клавиату- ры или адрес страницы видеопамяти, можно получить, например, из книги Р. Джордейна1. Работая с указателями и применяя к ним операцию (разымено- вания), стоит употреблять словесное описание ее действия. Операцию разыменования ’*’ вместе с указателем при их использовании в выра- жении можно объяснить как "получение значения, размещенного по адресу, равному значению указателя". Если та же конструкция нахо- дится слева от знака операции присваивания или в операторе ввода данных, то действие таково: "разместить значение по адресу, равному значению указателя". В соответствии с соглашениями, принятыми в операционной си- стеме MS-DOS, байт основной памяти, имеющий шестнадцатеричный адрес oxfooofffe, может содержать следующие коды: • FF (для IBM PC); • fe (для IBM PC XT); • fd (для IBM PCjr); • fa (для IBM PC AT). С помощью введенного выше указателя computer несложно полу- чить доступ к содержимому этого байта идентификации типа ПЭВМ. Следующая программа решает эту задачу: //Р5-01.СРР - проверка типа компьютера (обращение к байту // памяти) #include <iostream.h> void main(void) { char «Computer «= (char *)OxFOOOFFFE; cout « ” \пПрограмма выполняется на "; switch («Computer) { case (char)OxFF: cout « break; "ПЭВМ типа IBM PC."; case (char)OxFE: cout « break; "ПЭВМ типа IBM PC XT." case (char)OxFD: cout « break; "ПЭВМ типа IBM PCjr. case (char)OxFC: cout « break; "ПЭВМ типа IBM PC AT." 1 Джордейн P. Справочник программиста персональных компьютеров типа IBM PC, XT н AT- М: Финансы и статистика, 1991.— 544 с.
Глава 5. Адреса, указатели, массивы, память 113 default: cout « "ПЭВМ неизвестного типа."; } ) Результат выполнения на ПЭВМ с процессором 80386 при исполь- зовании модели памяти Large: Программа выполняется на ПЭВМ типа IBM PC AT. I В тексте программы обратите внимание на явные преобразования | типов. Во-первых, целочисленный шестнадцатеричный код адреса преобразуется к типу char * определяемого указателя computer. Зна- чением *computer служит величина типа char, поэтому в метках пере- ключателя после case также должны быть значения типа char. Явные преобразования типов (char) помещены перед шестнадцатеричными кодами. При определении указателя как сам он, так и его значение могут быть объявлены константами. Для этого используется модификатор const: type const * const имя^укаэателя инициализатор; Модификаторы const - это необязательные элементы определения. Ближайший к имени указателя модификатор const относится соб- ственно к указателю, a const перед символом определяет '’константность" начального значения, связанного с указателем. Мне- моника очевидна, так как выражение *имя__ука»ателя есть обращение к содержимому соответствующего указателю участка памяти. Таким образом, определение неизменяемого (константного) указателя имеет следующий формат: type * const имя__указателя инициализатор; Для примера определим указатель-константу key_byte и свяжем его с байтом, отображающим текущее состояние клавиатуры ПЭВМ IBM PC: char * const key__byte « (char *)1047; Значение указателя key_byte невозможно изменить, он всегда указывает на байт с адресом 1047 (шестнадцатеричное представление 0x0417). Это так называемый основной байт состояния клавиатуры. Так как значение указателя-константы изменить невозможно, то имя указателя-константы можно считать наименованием конкретного фиксированного адреса участка основной памяти. Содержимое этого участка памяти с помощью разыменования указателя-константы в
114 Язык Си++ общем случае доступно как для чтения, так и для изменений. Следую- щая программа иллюстрирует эти возможности указателя-константы: //Р5-02.СРР - указатель-константа на байт состояния / / клавиатуры tfinclude <iostream.h> void main(void) { char * const key__byte ж ((char *) 0x0417); cout «”\пБайт состояния клавиатуры: " « *key_byte; *key__byte = ’ В' ; cout « "\пБайт состояния клавиатуры: " « *key byte; ) В нормальном состоянии клавиатуры, когда отключены режимы Caps Lock, Num Lock, Scroll Lock и включен режим вставки (Insert), результат выполнения программы, оттранслированной с моделью памяти Large, будет таким: Байт состояния клавиатуры: А Байт состояния клавиатуры: Ё Кроме того, присваивание *key_byte » 1Ё'; проявляется не только в изменении кода в байте с адресом 1047, но и во внешнем из- менении состояния клавиатуры. Контрольные лампочки регистров Caps Lock, Num Lock, Scroll Lock загораются, и для возврата клавиа- туры в нормальное состояние необходимо нажать соответствующие клавиши переключения регистров. Обратите внимание на использо- ванный инициализатор указателя key_byte. Его формат для примера взят другим, нежели в Р5-01.СРР для определения начального значения указателя Computer. Итак, содержимое участка памяти, на который "смотрит" указа- тель-константа, можно явно изменять. Попытку изменить значение самого указателя-константы, т.е. операцию вида key__byte “ NULL; не допустит компилятор и выдаст сообщение об ошибке: Error...: Cannot modify a const object. Формат определения указателя на константу: type const * имя__указателя инициализатор; Например, введем указатель на константу целого типа со значением 0:
•М Глава 5. Адреса, указатели, массивы, память 115 |—». — - — - - 1 - " " " — — const int zero = 0; // Определение константы int const *point_to_const = fizero; // Указатель на i/ • // константу 0 Операторы вида *point_to__const я 1; f cin » *point__to__const ; I недопустимы, так как каждый из них - это попытка изменить J значение константы 0. Однако операторы ; point__to__const = &СС; t point__to__const « NULL; - вполне допустимы. Они разрывают связь указателя point_to_const с константой 0, однако не меняют значения этой константы, т.е. не из- меняют ее изображение в фиксированном участке основной памяти. Можно определить неизменяемый (постоянный) указатель на кон- станту. Например, иногда полезен так определенный указатель- константа на константное значение: const float pi = 3.141593; float const *const pointpi = &pi; Здесь невозможно изменить значение константы, обращаясь к ней с помощью выражения *pointpi. Нельзя изменить и значение указа- теля pointpi, т.е. он всегда ’’смотрит1’ на константу 3.141593. Работая с указателями, постоянно используют операцию & - по- лучение адреса объекта. Для нее существуют естественные огра- ничения: • Нельзя определять адрес неименованной константы, т.е. недо- пустимы выражения &3.141593 или &' ?'. • Нельзя определять адрес значения, получаемого при вычислении скалярных выражений, т.е. недопустимы конструк- ции &(44 * х - z) ИЛИ &(а + Ь) != 12 • Нельзя определить адрес переменной, относящейся к классу па- мяти register. Следовательно, ошибочной будет последователь- ность операторов: int register Numb » 1; int *prt__Numb = &Numb; Цитируя проект стандарта языка и обобщая сказанное, можно сделать вывод, что операция & применима к объектам, имеющим имя и размещенным в памяти. Ее нельзя применять к выражениям, неиме- 8*
116 Язык Си++ нованным константам, битовым полям структур и объединений, к ре гистровым переменным и внешним объектам (файлам). Однако допустимо получать адрес именованной константы, т.е правомерна, например, такая последовательность определений: const float Euler = 2.718282; float *pEuler = (float *)&Euler; (Обратите внимание на необходимость явного приведения типов, так как SEuler имеет тип const float *, а не float *.) 5.2. Адресная арифметика, типы указателей и операции над ними Во многих языках, предшествовавших языкам Си и Си++, например в ПЛ/1, указатель относился к самостоятельному типу указателей, который не зависел от существования в языке других типов. Для расширения возможностей адресной арифметики в языках Си и Си++ каждый указатель связан с некоторым типом. В качестве типа при определении указателя может быть использован как основной тип, так и производный. В языке Си++ производных типов может быть бесконечно много, однако правила их конструирования из более простых (а в конечном итоге из основных) типов точно определены. К производным типам отнесены массивы, функции, указатели, ссылки, константы, классы, структуры, объединения и. наконец, определенные пользователем типы. Начнем с указателей, относящихся к основным типам, а также массивам, указателям, ссылкам и константам. Основные типы, как обычно, определяются ключевыми словами: char, int, float, lond, double, short, unsigned, signed, void. Примеры указателей, относящихся к основным типам char, int и float, уже рассматривались. Вот несколько других определений: long double Id = 0.0; //Id - переменная long double *ldptr = &ld;;// Idptr - указатель void *vptr; // vptr - указатель типа void * unsigned char *cucptr; // cucptr - указатель без fl начального значения unsigned long int *uliptr = NULL // uliptr - указатель... Если операция & получения адреса объекта всегда дает одно- значный результат, который зависит от размещения объекта в памяти,
Глава 5. Адреса, указатели, массивы, память 117 то операция разыменования *ухазатель зависит не только от значения указателя, но и от его типа. Дело в том, что при доступе к памяти с помощью разыменования указателя требуется информация не только о размещении, но и о размерах участка памяти, который будет использоваться. Эту дополнительную информацию компилятор получает из типа указателя. Указатель char *ср; при обращении к памяти "работает" с участком в 1 байт. Указатель long double *idp; будет "доставать" данные из 10 смежных байт памяти и т.д. Иллюст- рирует сказанное следующая программа, где указателям разных типов присваиваются значения адреса одного участка памяти: //Р5-03.СРР - выбор данных из памяти с помощью разных // указателей tinclude <iostream.h> void main() ( unsigned long L = 0xl2345678L; char *cp e (char *)&L; // *cp равно 0x78 int *ip = (int *)&L; // *ip равно 0x5678 long *lp = (long *)£L; // *lp равно 0x12345678 cout « hex; // Шестнадцатеричное представление // выводимых значении cout « "\пАдрес L, т.е. &L = ” « &L; cout « "\ncp = ” « (void *)cp « "\t*cp = Ox" « (int)*cp; cout « "\nip = " « (void *)ip « "\t*ip = Ox" « *ip; cout « "\nlp = " « (void *)lp « "\t*lp = Ox" « *lp; 1 Результат выполнения программы: Адрес L, т.е. &L = 0xlE190FFC ср = 0xlE190FFC *ср = 0x78 ip = 0xlE190FFC *ip = 0x5678 Ip = 0xlE190FFC *lp - 0x12345678 Обратите внимание, что значения указателей совпадают и равны адресу переменной L. В программе потребовалось явное приведение типов. Так как ад- рес &ь имеет тип unsigned long *, то при инициализации указателей его значение явно преобразуется соответственно к типам char *, int *, long *. При выводе значений указателей они преобразуются к типу void *, ибо нас не интересуют длины участков памяти, связанных со значениями указателей. В программе при выводе результатов в поток cout (по умолчанию он связан с экраном дисплея) использован новый для нашего изложе-
118 Язык Си++ ния элемент - манипулятор hex форматирования выводимого значе- ния. Этот манипулятор hex обеспечивает вывод числовых кодов в шестнадцатеричном виде (в шестнадцатеричной системе счисления). Подробнее о форматировании вводимых и выводимых данных будем говорить при описании потоков ввода-вывода, которые не описаны в проекте стандарта [2] и полностью зависят от реализации. Программы этой главы выполнялись с помощью компилятора ВС++ версии 3.1. При выводе значения *ср использовано явное преобразование ти- па (int), так как при его отсутствии будет выведен не код (= 0x78), а соответствующий ему символ *х' ASCII-кода. Еще один неочевидный результат выполнения программы связан с аппаратными особенно- стями ПЭВМ IBM PC - размещение числовых кодов в памяти, начи- ная с младшего адреса. За счет этого пары младших разрядов шестнадцатеричного числового кода размещаются в байтах памяти с меньшими адресами. Именно поэтому *хр равно 0x5678, а не 0x1234, и *ср равно 0x78, а не 0x12. Сказанное иллюстрирует рис. 5.1. Рис. 5.1. Схема размещения в памяти ПЭВМ IBM PC переменной L типа unsigned long для программы Р5-03. СРР (младшие разряды числа в байте с меньшим адресом) Обратите внимание, что значения указателей разных типов в примере совпадают, а количество байтов, "извлекаемых" из памяти при разыменовании указателя, зависит от его типа. Явное преобразование типов при работе с разными указателями в одном выражении необходимо для всех указателей, кроме тех, кото- рые имеют тип void *. При использовании указателя типа void * операция преобразования типов применяется по умолчанию. В отли- чие от других типов, тип void предполагает отсутствие значения. Ука- затель типа void * отличается от других указателей отсутствием сведений о размере соответствующего ему участка памяти. Указатель типа void * как бы создан "на все случаи жизни", но, как всякая аб-
Глава 5. Адреса, указатели, массивы, память 119 ? стракция, ни к чему не может быть применен без конкретизации, ко- / торая в данном случае заключается в приведении типа. Возможности "связывания” указателя void * с объектами разных типов иллюстри- рует следующая программа: //Р5-04.СРР - неявное приведение типа void * к стандартным ' II типам | tinclude <iostream.h> 2 void main() I { void *vp; I int i = 77; I float Euler = 2.718282; I cout « "\пНачальное Значение vp = " « vp; [ vp = &i; // "Настроились" на int f cout « "\nvp = " « vp « j. "\t*(int *)vp « " « *(int *)vp; vp = &Euler; // "Настроились" на float cout « "\nvp « ” « vp « j "\t*(float *)vp = " « * (float *)vp; p Результат выполнения программы: i Начальное значение vp ш 0х2Е030000 vp = 0x8D8F0FFA *(int *)vp « 77 vp == 0x8D8F0FF6 * (float *)vp = 2.718282 P Возможность связывать указатели типа void * с объектами раз- р ных типов эффективно используется в "родовом программировании" на языке Си. Основная идея "родового программирования” состоит в том, что программа или отдельные функции создаются таким обра- зом, чтобы они могли работать с максимальным количеством типов ] данных* . Именно поэтому указатели типа void * называются родо- выми (generic) указателями [9]. Возможности родового программирования в языке Си++ в боль- J шинстве случаев обеспечиваются шаблонами (см. след, главу), однако ; термин "родовой указатель" закрепился за указателями типа void * и продолжает использоваться в литературе по Си++. Как уже демонстриробалось в ряде программ, при инициализации указателей и при использовании указателей, например в присваива- ниях, могут выполняться преобразования типов. В этих случаях нуле- вое арифметическое значение преобразуется к нулевому указателю, 1 Масич Д. и др. Современная технология программирования на Турбо Си. Развитие Турбо Си.- Термоарт, 1990.- 348 с.
120 Язык Си++ который иногда называется пустым указателем (null pointer). В ком- пиляторах ТС++ и ВС++ это значение указателя обозначается именем null . Синтаксис языка гарантирует, что этот указатель не адресует никакой объект. Однако синтаксис не гарантирует, что внутреннее представление значения пустого указателя будет совпадать с кодом целого числа 0. Разрешено неявное (умалчиваемое) преобразование значения любого неконстантного и не имеющего модификатора volatile указателя к указателю типа void *. Никакие другие преобразования типов указателей по умолчанию не выполняются. Например, в предыдущей программе неверной будет такая последовательность операторов: void *vp; int *ip; ip = vp; Транслятор сразу же выводит сообщение об ошибке: Error ... : Cannot convert 'void *' to 'int *' Такой запрет на преобразование типа void * к другим типам объясняется недопустимостью ситуации, когда к одному и тому же объекту будет доступ с помощью указателей разных типов. Например, в той же программе Р5-04 ,срр последовательность операторов vp = &Euler; int *ip; ip = vp; позволила бы обращаться к значению типа float (2.718282) с помощью *ip. Приблизительно по тем же причинам не все будет допустимо в следующих операторах. int i; int *ip = NULL; void *vp; char *cp; vp = i ? vp : cp; vp = ip ? ip : cp; 11Допустимый оператор //Ошибка в выражении: операнды должны //иметь одинаковый тип Во втором операторе присваивания выражение содержит указатели ip, ср разных типов, которые не могут быть неявно преобразованы к одному типу. В первом из операторов присваивания выполняется неявное преобразование значения ср к типу void ♦, и никаких ошибок не возникает. Операции над указателями можно сгруппировать таким образом: • операция разыменования или доступа по адресу (*); • преобразование типов (приведение типов);
Глава 5. Адреса, указатели, массивы, память 121 • присваивание; • получение (взятие) адреса (&); • сложение и вычитание (аддитивные операции); • инкремент или автоувеличение (++); • декремент или автоуменьшение (—); • операции отношений (операции сравнения). Разыменование, приведение типов, присваивание мы уже рассмот- рели и проиллюстрировали примерами. О получении адреса указателя можно сказать очень кратко: указатель есть объект и как объект имеет адрес соответствующего ему участка памяти. Значение этого адреса доступно с помощью операции £, применяемой к указателю: unsigned int *uipl = NULL, *uip2; uip2 = (unsigned int *)£uipl; Здесь описаны два указателя, первый из которых uipl получает нулевое значение при инициализации, а второму uip2 в качестве зна- чения присваивается адрес указателя uipl. Обратите внимание на яв- ное преобразование типа в операторе присваивания. При его отсутствии, т.е. для оператора uip2 « fiuipl; будет выдаваться со- общение об ошибке. Начинать изучение аддитивных операций удобнее с вычитания. Вычитание применимо к указателям на объекты одного типа и к ука- зателю и целой константе. Вычитая два указателя одного типа, можно определять "расстояние” между двумя участками памяти. "Расстояние" определяется в единицах, кратных длине (в байтах) объекта того типа, к которому отнесен указатель. Таким образом, разность указателей, адресующих два смежных объекта любого типа, по абсолютной вели- чине всегда равна 1. Сказанное иллюстрирует следующая программа: //Р5-05.СРР - вычитание указателей *include <iostream.h> void main() ( char ас » 'f’, be « '2‘; char *pac = £ac, *pbc » £bc; long int al = 3, Ы = 4; long int *pal = fial, *pbl » £bl; cout « "\пЗначения и разности указателей:11; cout « "\npac « " « (void *)pac « "\tpbc e " « (void *)pbc; cout « "\t(pac - pbc) « "« рас - pbc; cout « "\npal = ” « pal « "\tpbl = ” « pbl « ”\t(pbl - pal) « ” «(pbl - pal);
122 Язык Си++ cout « "\пРазности числовых значений указателей:'1; cout « "\n(int)pac - (int)pbc ж " « (int) рас - (int)pbc; cout « "\n(int)pbl - (int) pal « " « (int)pbl - (int)pal; } Результаты выполнения программы: Значения и разности указателей: рас « 0xle240fff pbc « 0xle240ffe (рас - pbc) ® 1 pal « 0xle240ff2 pbl * 0xle240fee (pbl - pal) * 1 Разности числовых значений указателей: (int)рас - (int)pbc « 1 (int)pbl - (int)pal ж -4 Анализируя результаты, нужно обратить внимание на два важных факта. Первый относится собственно к языку Си++ (или к Си). Он подтверждает различие между разностью однотипных указателей и разностью числовых значений этих указателей. Хотя (int)рас - (int) pbc равно 1, a (int)pbl - (int)pal равно -4, разности соот- ветствующих указателей в обоих случаях по абсолютной величине равны 1. Второй факт относится не к самому языку Си++, а к реализации. В соответствии с интуитивным представлением о механизме распреде- ления памяти те переменные, определения которых помещены в про- грамме рядом, размещаются в смежных участках памяти. Это (см. PS- 05. срр) видно из значений связанных с ними указателей (адресов). Однако совершенно неочевиден тот факт, что переменная, определен- ная в тексте программы позже, имеет меньший адрес, чем предше- ствующие ей в тексте программы объекты. Именно поэтому разности рас - pbc и (int)pac - (int)pbc равны 1, а разности pbl - pal и (int)pbl - (int)pal отрицательны. "Обратный" порядок размещения объектов в памяти объясняется особенностями работы компилятора. При разборе текста программы компилятор последовательно распознает и помещает в стек имена всех объектов, для которых нужно выделить место в памяти. Затем, после окончания лексического анализа, на этапе распределения памя- ти имена объектов выбираются из стека, и им отводятся смежные по- следовательно размещенные участки памяти. Так как порядок записи в стек обратен порядку чтения (выбора) из стека, то размещение объ- ектов в памяти оказывается обратным по сравнению с их взаимным расположением в определениях текста программы.
Глава 5. Адреса, указатели, массивы, память 123 Еще один пример иллюстрирует правила вычитания указателей и их отличия от вычитания численных значений адресов: //Р5-06.СРР - вычитание адресов и указателей разных типов #include <iostream.h> void main () ( double aa » 0.0, bb e 1.0; double *pda « fiaa, *pdb » £bb; float *pfa « (float *)fiaa, *pfb » (float *)£bb; int *pia » (int *)£aa, *pib » (int *)£bb; char *pca « (char *)£aa, *pcb « (char *)£bb; cout « "\пАдреса объектов: &аа « " « fiaa « "\t£bb « " « fibb; cout « "\пРазность адресов: (fibb - fiaa) « " « cout cout cout (fibb - fiaa); « "\пРазность значении адресов: " « "((int)fibb - (int)fiaa) « " « ((int)fibb - (int)fiaa); « ”\пРазности указателей:”; « ”\n double *: (pdb - pda)«" « (pdb ~ pda) ; cout « "\n float *: (pfb - pfa)»" « (pfb - pfa) ; cout « "\n int *: (pib - pia)»” « (pib - pia); cout « M\n char *: (pcb - pea)»” « (pdb - pea); Результат выполнения программы: Адреса объектов: fiaa » 0x21e90ff8 fibb » 0x21e90ff0 Разность адресов: (fibb - £аа) » -1 Разность значении адресов: <(int)£bb - (int)fiaa) = -8 Разности указателей: double *: (pdb - pda) » -1 float *: (pfb - pfa) » -2 int *: (pib - pia) « -4 char * .* (pcb - pea) « -8 Из результатов видно, что определенные последовательно объек- ты аа и ьь, имея тип double, размещаются в памяти рядом на "расстоянии" 8 байт. Однако разность адресов (fiaa - fibb) равна 1. Это подтверждает тот факт, что значение, получаемое с помощью операции £имк_объекта, имеет права указателя того типа, к которому принадлежит объект. Остальные результаты очевидны - разности указателей вычисляются в единицах, кратных длине участка памяти для соответствующего типа данных. Из указателя можно вычитать целочисленные значения. При этом числовое значение указателя уменьшается на величину k * sizeof(type)
124 Язык Си++ где к - "вычитаемое", type - тип объекта, к которому отнесен указа- тель. Аналогично выполняется и операция сложения указателя с цело- численным значением. (Отметим, что суммировать два указателя за- прещено синтаксисом языка Си++. Таким образом, операция сложения по сравнению с операцией вычитания еще беднее для указа- телей.) Следующая программа иллюстрирует особенности увеличения указателей на целую величину: //Р5-07.СРР - увеличение указателя #include <iostrearn.h> void nain() { float zero ~ 0.0, pi = 3.141593, Euler = 2.710202; float *ptr ~ SEuler; cout « "\nptr = ” « ptr « •' *ptr = " « *ptr; cout « "\n(ptr + 1) « •' « (ptr + 1) « " * (ptr + 1) = " « *(ptr+l); cout « "\n(ptr + 2) = " « (ptr + 2) « " *(ptr + 2) = " « *(ptr + 2); } Результат выполнения программы: ptr = 0x22510ff4 *ptr = 2.710202 (ptr + 1) = Ox2251Off0 *(ptr + 1) = 3.141593 (ptr + 2) = 0x22510ffc *(ptr + 2) « 0 Как видно из результата, изменяя значение указателя, можно пе- ремещаться по участкам памяти и получать доступ к разным объек- там. Однако при этом не следует полагаться на то, что объекты всегда будут размещаться в памяти подряд в соответствии с положением их определений в тексте программы. Синтаксис языка Си++ этого не га- рантирует, и все зависит от реализации. Полную уверенность в после- довательном размещении объектов дает только их агрегирование в массивы, структуры, объединения и классы. Декремент (автоуменьшение) указателей (унарная операция —) и инкремент (автоувеличение) указателей (унарная операция ++) не имеют никаких новых особенностей. Как и вычитание единичной кон- станты, операция — изменяет конкретное численное значение указа- теля типа type на величину sizeof (type), где type * - тип указате- ля. Тем самым указатель "перемещается" к соседнему объекту с мень- шим адресом. Аналогично и действие операции единичного прираще- ния ++. В зависимости от положения (до операнда-указателя или после него) выполнение унарных операций ++ и — осуществляется либо до.
Глава 5. Адреса, указатели, массивы, память 125 либо после использования значений указателя. Но это обычное свойство Инкрементных и декрементных операций, которое не связано с особенностями указателей. В ряде нестандартных случаев при работе с указателями нужно "смещать" их значения на величины, не кратные размеру участка памяти соответствующего их типу. Непосредственное изменение значения ука- зателя на целую величину такой возможности не дает. Вместо этого Можно использовать выражения, операндами которых являются чис- ленные значения адресов. Покажем это на примерах: //R5-08.CPP - изменения указателя на произвольную величину # include < iostream.h> Void main () { long LI = 12345678; int i = 6; double d = 66.6; long L2 = 87654321; cout « "\nHe кратные для long адреса: &L1 = " « &L1 « " &L2 = " « &L2; cout « "\n Разность некратных адресов: &L1 - &L2 = " « &L1 - &L2; cout « ”\n(&L2+3) » ’’ « (&L2 + 3) ; int *pi; long *pl = &L1; cout « "\npl = ” «pl« ” *pl = " « *pl; //Явно "переместим" указатель: pl = (long *) ((long) &L1 - sizeof(int) - sizeof (double) - sizeof(long); cout « "\npl = " «pl « " *pl = " «*pl; // Сформируем значение int * исходя из long * pi = (int* ) ((long) &L2 + sizeof(long)+ sizeof (double)); cout « "\npi = " « pi « " *pi = " « *pi; I Результаты выполнения: He кратные для long адреса: &L1 = 0x8d890ffc &L2 = 0x8d890fee Разность некратных адресов: &L1 - &L2 = 3 (&L2+3) = 0x8d890ffa pl = 0x8d890ffc *pl = 12345678 pl = 0x8d890fee *pl = 87654321 pi = 0x8d890ffa *pi = 6 Переменные LI и L2, имея шестнадцатеричные адреса ... ffс и ..fee, отстоят в памяти друг от друга на 14 (десятичное число) байт. Длина переменной типа long 4 байта. Таким образом, "расстояние" между Ы и L2 не кратно длине переменной типа long. Разность &L1-
126 Язык Си++ £L2 равна 3, т.е. округленному значению выражения ((long)&Ll (long) &L2) / sizeof (long). Добавив эту величину к адресу &L2 получили значение .. .ffa, не совпадающее с адресом &ы. Остальньк результаты иллюстрируют особенности явного "перемещения” п<> байтам памяти. Обратите внимание на необходимость приведения типов (long *), (int *) в операциях присваивания и (long) при получении численных значений адресов, используемых в выражениях Некоторые особенности размещения данных этой программы иллю стрирует рис. 5.2. &Ы = 0x8d890ffc &L2 « 0x8d890fee байты: FF FE FD FC FB FA F9 F8 F7 F6 F5 F4 F3 F2 Fl FO EF EE 12 34|56|78 00| Об 87 65|43 21 long Ы (4) int i (2) double d (8) long L2 (4) » 0x8d890ffa Puc. 5.2. Схема размещения в памяти ПЭВМ IBM PC переменных программы Р5-08. СРР Указатели, связанные с однобайтовыми данными символьного типа, при изменении на 1 меняют свое "внутреннее" числовое значение именно на 1. Поэтому изменить имеющееся значение адреса (текущее значение указателя) на произвольное количество единиц (байтов) можно с помощью вспомогательного указателя типа (char *). В на шем примере "перенести” значение указателя pl от переменной L1 к переменной L2 можно еще и таким способом (см. рис. 5.2): pl = &L1; char *рс = (char *)pl; // рс указывает на начало Ы рс = рс - 14; // рс указывает на начало L2 pl = (long *)рс; // *р! - содержимое переменной L2 (если самый простой способ - явное присваивание pl « &L2; - нас почему-либо не устраивал.) Еще раз обратим внимание на особенность вычитания указателей в тех случаях, когда они адресуют объекты, размещенные в памяти на расстоянии, не кратном длине одного объекта. (Вычитание указателей разных типов попросту запрещено синтаксисом языка, и никаких умалчиваемых преобразований здесь не предусматривается.) Как уже отмечено выше, вычитание двух указателей type *pi, *р2; как бы
Глава 5. Адреса, указатели, массивы, память 127 ни были определены их значения, выполняется в соответствии с соот- ношением: pl - р2 ** ((long)pl - (long)p2)) / sizeof(type) В данном выражении у операции деления операнды целочислен- ные, поэтому результат округляется до целого отбрасыванием дроб- ной части, если значения pl и р2 не кратны sizeof (type). Обладая правами объекта (как именованного участка памяти), указатель имеет адрес, длину и значение. О значениях указателей мы поговорили, следующая программа печатает значения адресов и длин некоторых типов указателей: //Р5-09.СРР - адреса и длины указателей разных типов linclude <iostream.h> void main () ( char *pac, *pbc; long *pal, *pbl; cout « "\пАдреса указателей:"; cout « M\n fipac = ” « fipac « " fipbc = "« fipbc; cout « "\n fipal = " « fipal « " fipbl = "« fipbl; cout « ”\пДлины указателей некоторых типов:"; cout « ”\n sizeof(void *) = " « sizeof(void *); cout « "\n sizeof(char *) = " « sizeof(char *); cout « "\n sizeof(int *) = " « sizeof(int *); cout « "\n sizeof(long *) = " « sizeof(long *); cout « "\n sizeof(float *) « " « sizeof(float *); cout « "\n sizeof(double *) = ” « sizeof(double *); cout « ”\n sizeof(long double *) = ” « sizeof(long double *); I Результат выполнения программы: Адреса указателей: £рас = Ox0d09Offc fipbc ~ Oxd09Off0 fipal = Ox0d09Off4 fipbl « Oxd09OffO Длины указателей некоторых типов: sizeof(void ♦) = 4 sizeof(char *) =4 sizeof(int *) = 4 sizeof(long *) =4 sizeof(float *) « 4 sizeof(double *) = 4 sizeof(long double *) = 4
128 Язык Си++ Раз указатель - это объект в памяти, то можно определять указа- тель на указатель и т.д. сколько нужно раз. Например, в следующей программе определены такие указатели и с их помощью выполнен доступ к значению переменной: //Р5-10.СРР - цепочка указателей на указатели #include <iostream.h> void main() ( int i = 00; int *pi = &i; int **ppi ~ £pi; int ***pppi = &ppi; cout « "\n ***pppi = " « ***pppx; } Результат выполнения программы: ***pppi = as Напомним, что ассоциативность унарной операции разыменова- ния справа налево, поэтому последовательно обеспечивается доступ к участку памяти с адресом pppi, затем к участку с адресом (*pppi) == ppi, затем к (*ppi) = pi, затем к (*pi) == i. С помощью скобок последовательность разыменований можно пояснить таким выраже- нием *(*(*рррх))). Работая с адресами и указателями, нужно внимательно относиться к последовательности выполнения операций *, ++, —, £, так как они в выражениях могут употребляться в самых разнообразных сочетаниях. Пропагандировать такой стиль программирования не стоит, од- нако нужно уметь понимать смысл запутанных выражений с адресами и указателями. Следующая программа иллюстрирует особенности и приоритеты этих операций: //Р5-11.СРР - приоритеты унарных операций #include <iostream.h> void main () { int il = 10, x2 = 20, i3 = 30; int *p = &i2; // Значение i2: cout « »\n*£i2 = " « *£x2; // Значение i2 сначала увеличенное на 1: cout « ,f\n*£++i2 = " « *&++x2; // Значение i2: cout«"\n*p = ” « *p; // Значение i2, p увеличивается на 1:
Глава 5. Адреса, указатели, массивы, память 129 cout « "\п*р++ = " « *р++; // Значение 11 cout « "\п*р — " « *р; // Значение 11 сначала увеличенное на 1: cout « "\п++*р » » « ++*р; // Значение ±2, сначала уменьшается р cout « и\п*—р = ” « *--р; // Значение 13, сначала уменьшается р, затем полученное // значение 13 увеличивается: cout « "\п++*—р = ” « ++*—р; Результат выполнения программы: * £12 = 20 * £++i2 = 21 * р « 21 * р++ = 21 * р = 10 ++*р = 11 ♦ — р = 21 . ++*—р = 31 Результаты иллюстрируют "левонаправленность" выполнения расположенных рядом унарных операций *, ++, —, £. Однако выра- жение *р++ вычисляется в таком порядке: вначале выполняется разыменование (обращение по адресу), и полученное значение (21) служит значением выражения в целом. Затем выполняется операция р++, и значение указателя увеличивается на 1. Тем самым он "устанавливается" на переменную 11. (Особенность реализации - уже упомянутый "обратный" порядок размещения в памяти переменных 11, 12, 13. Последовательно уменьшая на 1 значение р, переходим от 11 к участкам памяти, отведенным для 12 и 13.) Рассматривая различные сочетания в одном выражении перечис- ленных унарных операций, обратите внимание на недопустимость, например, таких записей: ++&12; // Ошибка: требуется 1-выражение —&12++; // Ошибка: требуется 1-выражение С^ысл ошибки очевиден, ведь адрес участка памяти не есть лево- допустимое выражение, адрес - это константа и его нельзя изменять. 0 -3821
130 Язык Си++ 5.3. Массивы и указатели В предыдущих главах уже определены и проиллюстрированы не которые понятия, относящиеся к массивам. В первой главе программ;! для расчета должностных окладов содержит одномерный массив вс щественного типа float для значений тарифных коэффициентов float а [ ] = { 1.0, 1.3, ... }; (Список инициализации здесь приведен не полностью.) Инициализация массива типа chart] значением строковой кон станты продемонстрирована в программах главы 2: char имя_массива [ ] * '’строковая_константа'’; (Напомним, что количество элементов в таком символьном массиве на 1 больше, чем количество символов в строковой константе, исполь зованной для инициализации. Последний элемент массива в этом слу чае всегда равен ' \01.) Несколько раз показано на примерах обращение к элементам од номерного массива с помощью индексирования. Отмечались роль разделителей [ ] (при описании и определении массивов) и существо- вание в языке Си++ операции []. С помощью этой операции обеспе чивается доступ к произвольному элементу массива по имени массива и индексу - целочисленному смещению от начала: имя массива[индекс] Теперь необходимо тщательно разобрать соотношение между мас- сивами и указателями. Самое загадочное в массивах языков Си и Си++ - это их различ- ное "поведение" на этапах определения и использования. При опреде- лении массива ему выделяется память так же, как массивам других алгоритмических языков (например, ПЛ/1 или Паскаль). Но как толь- ко память для массива выделена, имя массива воспринимается как константный указатель того типа, к которому отнесены элементы массива. Существуют исключения, например применение имени мас- сива в операции sizeof. В этой операции массив "вспоминает" о своем отличии от обычного указателя, и результатом является размер в бай- тах участка памяти, выделенного не для указателя, а для массива в целом. Исключением является и применение операции & (получения адреса) к имени массива. Результат - адрес начального (с нулевым индексом) элемента массива. В остальных случаях значением имени
Глава 5. Адреса, указатели, массивы, память 131 массива является адрес первого элемента массива, и это значение не- возможно изменить. Таким образом, для любого массива соблюдается равенство: имя__массива == &имя__массива == &имя__массива [0] Итак, массив - это один из структурированных типов языка Си++. От других структурированных данных массив отличается тем, что все его элементы имеют один и тот же тип и что элементы массива расположены в памяти подряд. Определение одномерного массива типа type: type имя_массива[хонстантное_выражение]; Здесь имя__массива - идентификатор; хонстантное_выражение, если оно присутствует, определяет размер массива, т.е. количество элементов в массиве. В некоторых случаях допустимо описание масси- ва без указания количества его элементов, т.е. без константного вы- ражения в квадратных скобках. Например: extern unsigned long UL[]; суть описание внешнего массива, который определен в другой части программы, где ему выделена память и (возможно) присвоены начальные значения его элементам. При определении массива может выполняться его инициализация, т.е. элементы массива получают конкретные значения. Инициализа- ция выполняется по умолчанию (без вмешательства программиста), если массив статический или внешний. В этих случаях всем элементам массива компилятор автоматически присваивает нулевые значения: void f(void) ( static float F[4J; // Внутренний статический массив long double A [10]; // Массив автоматической памяти I void main() ( extern int D[]; // Описание массива f О ; } int D[8J; // Внешний массив (определение) Массивы D[8] и F[4] инициализированы нулевыми значениями. В основной программе main () массив D описан без указания количества
132 Язык Си++ его элементов. Массив а[10] не получает конкретных значений своих элементов при определении. Явная инициализация элементов массива разрешена только при его определении и возможна двумя способами: либо с указанием раз- мера массива в квадратных скобках, либо без явного указания (без конкретного выражения) в квадратных скобках: char СН[] = { 'А', 'В', ’С’, ' D' } ; // Массив из 4 элементов int IN[6] = { 10, 20, 30, 40 }; // Массив из 6 элементов char STR[] = "ABCD"; // Массив из 5 элементов Количество элементов массива сн компилятор определяет по числу начальных значений в списке инициализации, помещенном в фигурных скобках при определении массива. В массиве IN шесть эле- ментов, но только первые четыре из них явно получают начальные значения. Элементы IN[4], in[5] либо не определены, либо имеют нулевые значения, когда массив внешний или статический. В массиве str элемент str[4] равен ' \0', а всего в этом массиве 5 элементов. При отсутствии константного выражения в квадратных скобках список начальных значений в определении массива обязателен. Если размер массива явно задан, то количество элементов в списке начальных значений не должно превышать размера массива. Оши- бочные определения: float А[]; // Ошибка з определении массива - нет размера double В[4] = { 1, 2, 3, 4, 5, б }; // Ошибка // инициализации В тех случаях, когда массив не определяется, а описывается, спи- сок начальных значений задавать нельзя. В описании массива может отсутствовать и его размер: extern float Е[]; // Правильное описание внешнего массива Предполагается, что в месте определения массива е для него выде- лена память и выполнена инициализация. Описание массива (без указания размера и без списка начальных значений) может использоваться в списке формальных параметров определения функции и в спецификации параметров прототипа функ- ции. Примеры: float MULTY (float G[] , float F[]) // Определение функции // MULTY { . . . тело_функции
Глава 5. Адреса, указатели, массивы, память 133 } void print_array(int ![]); // Прототип функции print_array Доступ к элементам массива с помощью индексированных пере- менных мы уже несколько раз демонстрировали на примерах. Приве- дем еще один, но предварительно обратим внимание на полезный прием, позволяющий контролировать диапазон изменения индекса массива при его "просмотре", например в цикле. С помощью операции sizeof (имя_массива) можно определить размер массива в байтах, т.е. размеры участка памяти, выделенного для массива. Так как все элементы массива имеют одинаковые размеры, то частное sizeof (имя^массива) /sizeof (имя__массива [0]) определяет количе- ство элементов в массиве. Следующий фрагмент программы печатает значения всех элементов массива: int m[] « ( 10, 20, 30, 40 } ; for (int i » 0; i < sizeof(m)/sizeof(m[0]); i++) cout « "i[ « i « ”] « " « m[i] « " Результат на экране дисплея: m[0] = 10 m[l] = 20 m[2] ® 30 m[3] « 40 Еще раз отметим, что для первого элемента массива индекс равен 0. Цикл завершается при достижении i значения 4. По определению, имя массива является указателем-константой, значением которой служит адрес первого элемента массива (с индек- сом 0). Таким образом, в нашем примере &а == т. Раз имя массива есть указатель, то к нему применимы все правила адресной арифмети- ки, связанной с указателями. Более того запись имя_массива [индекс] является выражением с двумя операндами. Первый из них, т.е. имя_массива, - это константный указатель - адрес начала массива в основной памяти, индекс - это выражение целого типа, определяющее смещение от начала массива. Используя операцию обращения по ад- ресу * (раскрытие ссылки, разыменование), действие бинарной опера- ции [ ] можно объяснить так: * (имя_массива + индекс) Таким образом, операндами для операции [] служат имя массива и индекс. В языках Си и Си++ принято, что индексы массивов начи- наются с нуля, т.е. массив int z[3] из трех элементов включает ин-
134 Язык Си++ дексированные элементы z[0],z[l],z[2]. Это соглашение языка ста- новится очевидным, если учесть, что индекс определяет не номер эле- мента, а его смещение относительно начала массива. Таким образом, *z - обращение к первому элементу z[0], *(Z + 1) - обращение ко второму элементу z [1] и т.д. В следующей программе показано, как можно не использовать квадратные скобки при работе с элементами массива: //Р5-12.СРР - работа с элементами массива без скобок [] #include <iostream.h> void main() { char x[] » "DIXI11; // "Я сказал (высказался)" int i » 0; while (*(x + i) != AO1) cout « "\n" « *(x + i++) ; ) Результат выполнения программы: слово "DIXI", написанное в столбик (см. Р2-15 .срр). В данном примере оператор цикла с заголовком while выполняет- ся, пока верно выражение в скобках, т.е. пока очередной символ мас- сива не равен АО*. Это же условие можно проверять и при таком заголовке цикла: while (*(х + i)) В цикле при каждом вычислении выражения х + i++ используется текущее значение i, которое затем увеличивается на 1. Тот же резуль- тат будет получен, если для вывода в ццкле поместить оператор cout « An1 « x[i++];. (Квадратные скобки играют роль бинарной операции, а операндами служат имя массива х и индекс i++.) Индек- сированный элемент можно употребить и в заголовке цикла: while(х[i]). Обращение к элементу массива в языке Си++ относят к постфикс- ному выражению вида ре[хе]. Постфиксное выражение ре должно быть указателем на нужный тип, выражение ie в квадратных скобках должно быть целочисленного типа. Таким образом, если ре - указа- тель на массив, то ре [IE] - индексированный элемент этого массива. * (РЕ + IE) - другой путь доступа к тому же элементу массива. По- скольку сложение коммутативно, то возможна такая эквивалентная запись* (хе + ре) и, следовательно, ie [РЕ] именует тот же элемент массива, что и ре[1Е]. Сказанное иллюстрирует следующая программа:
Глава 5. Адреса, указатели, массивы, память 135 //Р5-13.СРР - коммутативность операции [] finclude <iostream.h> void main () { int m[] = { 10, 20, 30, 40 }; int j — 1; cout « ”\nm[j] = " « m[j]; cout « ’* * (m + j++) = " « *(m + j++); cout « "\n*(++j + m) = " « * (++j + m) ; cout « " j[m] = " « j[m]; cout « "\n*(j— + m) = " « *(j-- + m); cout « " j—[m] = " « j--[m]; cout « "\n*( — j + m) = " « *(--j + m) ; cout « " --j[m] = " « i' cout « "\n3[m] = "« 3[m] « " 2[m]="«2[m] « " l[m] = ” « 1 [m] «" 0[m] = " « 0[m]; >. ) 4 Впечатляющий результат на экране: m[j] =20 *(m + j++) = 20 * (++j + m) =40 j [m] = 40 * (j-- + m) =40 j--[m] = 30 * ( — j + m) =10 —j [m] = 9 3[m] = 40 2[m] = 30 l[m] = 20 0[m] = 9 Обратите внимание на порядок вычислений. В выражении j — [m]: вычисляется j(m], а затем j—. В выражении —j[m]: вычисляется j [m], и результат уменьшается на 1, т.е. — (j [m]). В некоторых не совсем обычных конструкциях можно использо- вать постфиксное выражение ре [те] с отрицательным значением ин- декса. В этом случае ре должен указывать не на начало массива, т.е. не на его нулевой элемент. Например, последовательность операторов: char А[] = "СОН"; char *U = &А[2]; cout « "\n" « U[0] « U[-l] « U[-2]; приведет к выводу на экран слова нос. Тот же результат будет по- лучен при использовании оператора cout « '^\п" « *и « *и-- « *и—; То же самое слово будет выведено на экран при таком использо- вании вспомогательной переменной-индекса: int i = 2; cout « "\n" « i[A] « i[A - 1] « i[A - 2];
136 Язык Си++ Как видно из приведенных примеров, перемещение указателя oi одного элемента к другому выполняется в естественном порядке, т.е при увеличении индекса или указателя на 1 переходим к элементу с большим номером. Внутри массива нет проблемы "обратного" раз мешения в памяти последовательно определенных в программе объек- тов. Так как имя массива есть не просто указатель, а указатель- константа, то значение имени массива невозможно изменить. Попыт ка получить доступ ко второму элементу массива int z[4] с помощью выражения *(++2) будет ошибочной. А выражение *(2+1) вполне допустимо. Следующая программа иллюстрирует естественный порядок раз- мещения в памяти элементов массива и обратный порядок располо- жения массивов, последовательно определенных в программе. //Р5-14.СРР - адреса массивов и использование указателей // для доступа ttinclude <iostream.h> void main(void) { int A[]={1, 2, 3,4, 5, 6}; int B[] = { 1, 2, 3, 4, 5, 6 }; int *pA = A, *pB = &B[5]; cout « "ХпАдреса массивов: &A = "« &A « " &B = " « &B « "\n"; while (*pA < *pB) cout « " *pA++ + *pB— = " « *pA++ + *pB—; cout « "ХпЗначения указателей после цикла:”; cout << "\n pA = " « pA « ” pB = " « pB; } Результат выполнения программы: Адреса массивов: &А = OxSdSeOfec &В = OxSdSeOfeO *рА++ + *рВ— = 7 *рА++ + *рВ-- = 7 *рА++ + *рВ-- = 7 Значения указателей после цикла: pA = 0x8d8e0ff2 pB = 0x8d8e0fe4 Обратите внимание, что тот же результат будет получен, если оп- ределить указатели таким образом: int *рА = &А[0], *рВ = (В + 5); Как видно по значениям адресов &а, &в, массивы айв размещены в памяти в обратном порядке по сравнению с их определением в про-
Глава 5. Адреса, указатели, массивы, память 137 грамме. Внутри массивов элементы размещены в естественном поряд- ке. Инициализация символьных массивов может быть выполнена не Только с помощью строк, но и с помощью списка инициализации, где последовательно указаны значения каждого отдельного элемента: char stroka[] e { 'S', 'X', 'С, *\0' ); При такой инициализации списком в конце символьного массива можно явно записать символ ' \0'. Только при этом одномерный мас- сив (в данном случае stroke) получает свойства строки, которую можно использовать, например, в библиотечных функциях для рабо- ты со строками или при выводе строки на экран дисплея с помощью Оператора cout « stroka; Продолжая изучать массивы и указатели, рассмотрим конструк- j цию: type *имя; I В зависимости от контекста она описывает или определяет раз- I личные объекты типа type *. Если она размещена вне любой функ- f ции, то объект есть внешний указатель, инициированный по умолча- нию нулевым значением. Внутри функции это тоже указатель, но не имеющий определенно- го значения. В обоих случаях его можно связать с массивом элементов I типа type несколькими способами, как во время определения, так и в [ процессе выполнения программы. В определениях существуют сле- । дующие возможности: i typ® *имя = имявуже_определеняого_массива_типа_^уре; ! typ® *имя = new type [размер_массива] ; J type *имя = (type *)malloc(размер * sizeof (type)) ; j Например: ! long arlong[] — ( 100, 200, 300, 400); // Определили // массив i long *arlo = arlong; // Определили указатель, // связали его с массивом int *arint » new int[4]; // Определили указатель // и выделили участок памяти float *arfloat - new float[4]; // Определили указатель // и выделили участок памяти double *ardouble » // Определили указатель и (double *)malloc(4 * sizeof(double)); // выделили участок // памяти
138 Язык Си++ В примерах определены четыре массива из 4-х элементов в каж дом. Массив arlong инициализирован списком начальных значений в фигурных скобках. Массив, связанный с указателем arfloat, с по мощью операции new получил участок памяти нужных размеров (16 байт), однако эта память явно не инициализирована. Без инициализа ции остается и массив, связанный с указателем arint. Память для элементов массива, связанного с указателем ardouble, выделена с по мощью библиотечной функции mallocO языка Си. В ее параметре приходится указывать количество выделяемой памяти (в байтах). Так как эта функция возвращает значение указателя типа void *, то по- требовалось явное преобразование типа (double *). Выделенная память явно не инициализирована. В отличие от имени массива указатель, связанный с массивом, ни- когда не "вспоминает" об этом факте. Операция sizeof, применяемая к такому указателю, вернет количество байтов, занятых именно этим указателем, а вовсе не размер массива, связанного с указателем. Опе- рация &ухазатель возвращает адрес указателя в основной памяти, а никак не адрес начала массива, на который настроен указатель. Та- ким образом, для наших примеров: sizeof arint =«= 4 - длина указателя int * sizeof *arint “2 - длина элемента arint[0] sizeof arlong “= 16 sizeof arlo == 4 Как и при обычном определении массивов типа char, указатели char * могут инициализироваться с помощью символьных строк: char *имя_ухазателя =» "символьная строка"; char *имя_указателя » { "символьная строка" } ; char *имя_указателя("символьная строка"); В этом случае количество элементов в символьном массиве, свя- занном с указателем, как обычно, на I больше, чем количество симво- лов в инициализирующей строке. Примеры определения массивов типа char: char *carl - "строка-1"; char *car2 = { "cipojca-2" } ; char *car3("сярока-З");
Глава 5. Адреса, указатели, массивы, память 139 Длины массивов, связанных с указателями carl, саг2, сагЗ, оди- наковы. В последнем элементе каждого из этих массивов находится символ '\0'. Операция sizeof, примененная к указателю на символьный массив, возвращает длину не массива, а самого указателя, например, sizeof(carl) 4. Как и при обычном определении массивов к элементам массивов, связанных с указателями, существует несколько путей доступа. Прин- ципиально различных путей два: с помощью операции [] и с помо- щью операции разыменования. В качестве иллюстрации приведем пример программы, использующей оба способа доступа: //Р5-15.СРР - Копирование массивов-строк tinclude <iostream.h> tinclude <string.h> // Для функции strlenO void main () { char *arch = "0123456789"; // Массив из 11 элементов int k « strlen(arch) 4-1; // k - размер массива char * newer = new char[k]; for (int i я 0; i < k;) { newar[i++] - *arch++; if (! (i%3)) cout « "\narch ® " << arch; ) cout « "\nk = " « k « " newar «'• « newar; cout « "\nsizeof(arch) = " « sizeof(arch); > Результат выполнения программы: arch = 3456789 arch я 6789 arch « 9 k = 11 newar = 0123456789 sizeof(arch) e 4 Для определения длины массива, не имеющего фиксированного имени, нельзя использовать операцию sizeof. Поэтому в заголовке программы включен файл string.h с прототипами функций для ра- боты со строками (см. табл. П3.4 в приложении 3). Одна из них, а именно функция strlenO» определяющая длину строки-параметра, использована для определения количества элементов в массиве, свя- занном с указателем arch. Функция strlenO возвращает количество "значащих" символов в строке без учета конечного нулевого символа.
140 Язык Си++ Именно поэтому при определении значения к к результату strlen(arch) прибавляется I. В программе определен и инициализирован символьный массив- строка, связанный с указателем arch, и выделена память операцией new для такого же по типу и размерам, но динамического и неинициа- лизированного массива, связанного с указателем newar. Длина каж- дого из массивов с учетом "невидимого" в строке инициализации символа ' \0' равна II. "Перебор" элементов массивов в программе выполняется по-разному. Доступ к компонентам массива, связанного с указателем newar, реализован с помощью операции [], к элементам второго массива - с помощью разыменования *. У массива, связанного с указателем newar, изменяется индекс. Указатель arch изменяется под действием операции ++. Такой возможности не существует для обычных масси- вов. В программе использована еще одна возможность вывода с по- мощью операции « в стандартный поток cout - ему передается имя (указатель) массива, содержащего строку, а на экран выводятся значения всех элементов массива в естественном порядке, за ис- ключением последнего символа ’\0 ’. При этом необязательно, чтобы указатель адресовал начало массива. Указатель arch "перемещается" по элементам массива, поэтому в цикле выводятся в поток cout разные "отрезки" исходной строки. Чтобы сократить количество печати, в цикл добавлен условный оператор, в котором проверяется значение модуля i%3. Обратите внимание, что здесь выполнен вывод массива-строки. Если бы указатель newar был связан не со строкой, а с массивом про- извольного типа, то вывод содержимого на экран дисплея с помощью cout « был бы невозможен. Итак, в случае определения массива с использованием указателя этот указатель является переменной и доступен изменениям. Такими свойствами обладают arch и newar в нашей программе. Вот еще ва- рианты циклов копирования: for (; *newar?s’\0'; *newar++ « *arch++); while (*newar++ « *arch++); Результат будет тем же самым. Однако указатель newar в обоих случаях сместится с начала массива, и его нельзя в дальнейшем ис- пользовать, например, для печати строки.
Глава 5. Адреса, указатели, массивы, память 141 При определении указателя ему может быть присвоено значение другого указателя, уже связанного с массивом того же типа: int pil [) « { 1, 2, 3, 4 } ; int *pi2 « pil; // pi2 - "другое имя" для pil double pdl [] » { 10, 20, 30, 40, 50 }; double *pd2 « pdl; // pd2 - "другое имя" для pdl После таких определений к элементам каждого из массивов воз- можен доступ с помощью двух разных имен. Например: cout « рх2[0]; *рх1 = 0; cout « *рх2; cout « pdl[3] ; *(pd2 + 3) » 77; cout « pdl[3] и Выводится 1 // Изменяется pil[0] // Выводится 0 и Выводится 40 и Изменяется pd2[3] II Выводится 77. Такие же присваивания указателям допустимы и в процессе ис- полнения программы, т.е. последовательность операторов int *pi3; pi3 « pil; свяжет еще один указатель pi3 с тем же самым массивом int из четырех элементов. Возможность доступа к элементам массива с помощью нескольких указателей не следует путать с продемонстрированной в программе Р5-15.СРР схемой присваивания одному массиву значений элементов другого массива. Рассмотрим такой пример: char str[J = "массив"; char *pstr « str; pstr « "строка"; // Определили массив с именем str // Определили указатель pstr и // "настроили" его на массив str // Изменили значение указателя, // но никак не изменили массив str Присваивание указателю pstr не переписывает символьную стро- ку "строка” в массив str, вместо этого изменится значение самого указателя pstr. Если при определении он указывал на начало массива с именем str, то после присваивания его значением станет адрес того участка памяти, в котором размещена строковая константа "строка". Чтобы в процессе выполнения программы изменить значения элемен- тов массива, необходимо, явно или опосредованно (с помощью указа- телей или средств ввода данных), выполнить присваивания. Напри-
142 Язык Си++ мер, заменит содержимое массива-строки str такой дополнительный оператор while (str++ » pstr++); илн его аналог с индексированными переменными: for (int i = 0; str[i] = pstr[i]; i++); При переписывании одного массива в другой длина заполняемого массива должна быть не меньше длины копируемого массива, так как никаких проверок предельных значений индексов язык Сн++ не пред- усматривает, а выход за границу индекса часто приводит к аварийной ситуации. В обоих операторах учтено, что длины строк "массив” и "строка" одинаковы, а в конце строки всегда размещается нулевой символ, по достижении которого цикл завершается. Примечание. Для копирования строк в стандартной библиотеке языков Си и Си++ имеется функция strcpy(), прототип которой находится в заго- ловочном файле string. h (см. табл. П3.4). Возможно "настроить" на массив указатели других типов, однако при этом потребуются явные приведения типов: char *pch » (char *) pil; float *pfl » (float *) pil; Так, определенные указатели позволят по-другому "перебирать" элементы массива. Выражения * (pch + 2) илирсЬ[2] обеспечивают доступ к байту с младшим адресом элемента pil[l]. Индексирован- ный элемент pfl[l] и выражение * (pfl + 1) соответствуют четырем байтам, входящим в элементы pil [2], pil [3]. Например, присваива- ние значения индексированному элементу pfl[l] изменит в общем случае как pil [2] » 3, так и pil [3] » 4. После выполнения опера- торов pfl(l] = 1.0/3.0; cout « "\npil[2] » " « pil [2] « ” pil[3] » ” « pil[3]; на экране появится такой результат: pil[2] « -21845 pil[3] - 16042 что совсем не похоже на исходные значения: pil (2] « 3 pil(3] « 4
Глава 5. Адреса, указатели, массивы, память 143 Итак, допустимо присваивать указателю адрес начала массива. Однако имя массива, являясь указателем, не обладает этим свойством, так как имя массива есть указатель-константа. Рассмотрим пример: long arl[] = { 10, 20, 30, 40 }; long *pl == new long[4]; Определены два массива по 16 байт каждый. Операторы присваивания для имен этих массивов обладают раз- ными правами: arl 88 pl; // Недопустимый оператор pl = arl; // Опасный оператор Первый оператор недопустим, так как имя массива arl соответст- вует указателю-константе. Второй оператор синтаксически верен, од- нако приводит к опасным последствиям - участок памяти, выделенный операцией new iong[4], становится недоступным. Его нельзя теперь не только использовать, но и освободить, так как в опе- рации delete нужен адрес начала освобождаемой памяти, а его значение потеряно. Мы неоднократно отмечали особую роль символьных строковых констант в языках Си и Си++. В языке Си++ нет специального типа данных "строка". Вместо этого каждая символьная строка в памяти ЭВМ представляется в виде одномерного массива типа char, послед- ним элементом которого является символ 1 \0». Изображение строко- вой константы (последовательность символов, заключенная в двойные кавычки) может использоваться по-разному. Если строка используется для инициализации массива типа char, например, так: char array[] = "инициализирующая строка"; то адрес первого элемента строки становится значением указателя- константы (имени массива) array. Если строка используется для инициализации указателя типа char * char * pointer = "инициализирующая строка"; то адрес первого элемента строки становится значением указателя- переменной (pointer). И, наконец, если использовать строку в выражении, где разрешено применять указатель, то используется адрес первого элемента строки: char *string; string = "строковый литерал";
144 Язык Си++ В данном примере значением указателя string будет не вся строка "строковый литерал", а только адрес ее первого элемента. 5.4. Многомерные массивы, массивы указателей, динамические массивы Многомерный массив в соответствии с синтаксисом языка есть массив массивов, т.е. массив, элементами которого служат массивы. Определение многомерного массива в общем случае должно содер- жать сведения о типе, размерности и количествах элементов каждой размерности: type имлммассива[KI] [К2] . . . [KN] ; Здесь type - допустимый тип (основной или производный), имя__иассива - идентификатор, N - размерность массива, К1 - количе- ство в массиве элементов размерности N-1 каждый и т.д. Например: int ARRAY[4][3][6]; Трехмерный массив array состоит из четырех элементов, каждый из которых - двухмерный массив с размерами 3 на 6. В памяти массив array размещается в порядке возрастания самого правого индекса (рис. 5.3), т.е. самый младший адрес имеет элемент array[O] [0] [0], затем идет array [0] [О] [1] и т.д. ARRAY[3] ARRAY[2] ARRAY[1] ARRAY[0] | Массив 3 на 6 | Массив 3 на 6 [ Массив 3 на 6 | Массив 3 на 6 | < .. Возрастание адресов —...... ARRAY [i] [2] ARRAY[i][l] ARRAY[i][0] | Массив из 6 элементов | Массив из 6 элементов | Массив из 6 элементов | < ARRAYfi][j][5] —- Возрастание адресов 1 ARRAYfi][j][4] ... ARRAY[i][j][0] | Скалярный элемент | Скалярный элемент | .. . | Скалярный элемент | Рис. 5.3. Схема размещения в памяти трехмерного массива
Глава 5. Адреса, указатели, массивы, память 145 Следующая программа иллюстрирует перечисленные особенности размещения в памяти многомерных массивов: //Р5-16.СРР ~ адреса элементов многомерных массивов #include <iostream.h> void main() { int ARRAY[4][3][6]; cout « "\n &ARRAY[O] « " « 6ARRAY[0]; cout « ”\n 6ARRAYC1] « " « &ARRAY[1); cout « "\n &ARRAY[2] = " « 6ARRAY[2]; cout « "\n &ARRAY[3] « ’• « &ARRAY[3J; cout « ”\n fiARRAY[2][2][2] = " « fiARRAY[2] [2] [2] ; cout « "\n fiARRAY[2][2][3] « " « &ARRAY[2][2][3]; cout « "\п\"Расстояние\":\n (unsigned long) &ARRAY [1 ]’* ” - (unsigned long)fiARRAY[0] ~ " « (unsigned long)fiARRAY[1] - (unsigned long)fiARRAYfO]; ) Результат выполнения программы: fiARRAY[0] « 0x8d840f70 fiARRAY[1] = 0x8d840f94 fiARRAY[2] = 0x8d840fb8 fiARRAY[3] = 0x8d840fdc fiARRAY[2][2][2] = 0x8d840fd4 fiARRAY[2](2][3] » 0x8d840fd6 ”Расстояние”: (unsigned long)fiARRAY[1]-(unsigned long)intfiARRAY[0] » 36 Обратите внимание на равную двум разность адресов элементов array[2] [2] [3] и array[2] [2] [2]. Массив целочисленный, и на 16- разрядной ПЭВМ "длина” одного элемента равна двум байтам. "Расстояние" в байтах от элемента array[1] до arrayjoj равно 36, что соответствует целочисленному массиву с размерами 3 на 6. С учетом порядка расположения в памяти элементов многомерно- го массива нужно размещать начальные значения его элементов и в списке инициализации. (Поправка на правостороннее написание фраз и слов в европейских языках в отличие от направления возрастания адресов на рис. 5.3. совершенно естественна.) int ARRAY(4][3][6] « ( 0, 1, 2, 3, 4, 5, 6, 7 }; В данном определении начальные значения получили только "первые" 8 элементов трехмерного массива, т.е.: ARRAY[0][0][0] ®= 0 ARRAYC0](0][1] =« 1
J 46 ЯзыкСи++ ARRAYEO][0][2] = 2 ARRAYEO][0][3] = 3 ARRAYEO][О][4] жж 4 ARRAYEO][0][5] ж. 5 ARRAYEO][1]ЕО] “ 6 ARRAYEO] El] СИ ет 7 Остальные элементы массива array остались неиницнализир< ванными и получат начальные значения в соответствии со статусен массива. Если необходимо инициализировать только часть элементов мне гомерного массива, но они размещены не в его начале нли не подряд то можно вводить дополнительные фигурные скобки, каждая пари которых выделяет последовательность значений, относящихся к од ной размерности. (Нельзя использовать скобки без информации внут ри них.) Следующее определение с инициализацией трехмерного массива int АС4]Е5]Еб] » { { {0} }, { (100), {110, 111} }, { (200), {210}, {220, 221, 222} }; так задает только некоторые значения его элементов: АЕО]СО][0] = О, АЕИЕ01Е0] = 100, АЕ1][1] [О] — 110, АЕ1] [1] [1] « 111 АС2]СО]СО] = 200, АС2]С1]СО] =» 210, А[2][2]СО] = 220, АЕ21Е23Е1] — 221, АЕ2](2]С2] — 222 Остальные элементы массива явно не инициализируются. Если многомерный массив при определении инициализируется, то его самая левая размерность может в скобках не указываться. Ко- личество элементов компилятор определяет по числу членов в ини- циализирующем списке. Например, определение float matrix El 15] = { (1}, {2}, {3} }; формирует массив matrix с размерами 3 на 5, но не определяет явно начальных значений всех его элементов. Оператор cout « ”\nsizeof(matrix) = " « sizeof (matrix}; выведет на экран: sizeof (matrix) = 60
Глава 5. Адреса, указатели, массивы, память |4. Начальные значения получают только matrix[O] [0] »*= 1 matrix[1][0] “ 2 matrix[2][О] з Как и в случае одномерных массивов, доступ к элементам много- мерных массивов возможен с помощью индексированных переменных и с помощью указателей. Возможно объединение обоих способов в одном выражении. Чтобы не допускать ошибок при обращении к эле ментам многомерных массивов с помощью указателей, нужно по- мнить, что при добавлении целой величины к указателю его внутреннее значение изменяется на "длину" элемента соответствую- щего типа. Имя массива всегда константа-указатель. Для массива, Определенного как type AR [N] [М] [L], ar — указатель, поставленный в соответствие элементам типа type [М] [L]. Добавление I к указате- лю AR приводит к изменению значения адреса на величину sizeof(type) * М * L . Именно поэтому выражение * (ar + 1) есть адрес элемента ar [1]. Т.е. указатель на массив меньшей размерности, отстоящий от начала массива, т.е. от &ar[0], на размер одного элемента type[M] [L]. Ска занное иллюстрирует следующая программа: //P5-17.CPP - многомерные массивы - доступ no указателям ^include <iostream.h> void main() ( int b[3][2][4] = { 0, 1, 2, 3, 10, Hr 12, 13, 100, 101, 102, 103, 110, 111, H2, 113, 200, 201, 202, 203, 210, 211, 212, 213 ); // Адрес массива Ь[][][] cout « и\пЬ » ° « Ь; // Адрес массива Ь[0][][]: cout « "\n*b - " « *b; // Адрес массива b[0][0][]: cout « ”\n**b «= " « **b; // Элемент b[0][0][0]: cout « "\n***b = " « ***b; // Адрес массива b[l][][]: cout « ”\n*(b +1) « " « *(b + 1); I/ Адрес массива b[2] [][] : cout « "\n*(b +2) « ” « *(b + 2); 10*
148 Язык Си++ /! Адрес массива b[0][1] [] : cout « ”\n‘(»b + 1) x= »» « * (*b + 1) ; cout « "\n*(*(*(b + *(*(*(b + 1) 1) + 1) + 1) + + 1) 1) ; я " « cout « "\n*(b[l][l] // Элемент b[2] [0][0] + 1) « " « *(b[l][l] + 1); cout « "\n*(b[l] + 1)fl] - ” « *(b[l] + 1) [1] ; } Результаты выполнения программы: Ь = 0x8d880fd0 *b « 0x8d880fd0 **b - 0x8d880fd0 ***ь = о *(b + 1) - 0x8d880fe0 * (b + 2) « 0x8d880ff0 *(*b + 1) * 0x8d880fd8 *(*(*(Ь + 1) + 1) + 1) - 111 * <Ь си [1] + 1) * 111 * (b [1] + 1) [1] » 200 В программе доступ к элементам многомерного массива осу- ществляется с помощью операций с указателями. В общем случае для трехмерного массива индексированный элемент b[x] [j] [к] соответ ствует выражению *(*(* (Ь + 1) + j) + к) . В нашем примере: *(*(*(Ь + 1) +1) +1) — Ь[1] [1] [1] == 111 Допустимо в одном выражении комбинировать обе формы досту- па к элементам многомерного массива: *(Ь[1][1] + 1} — 111 Как бы ни был указан путь доступа к элементу многомерного мае- сива, внутренняя адресная арифметика, используемая компилятором, всегда предусматривает действия с конкретными числовыми значе- ниями адресов. Компилятор всегда реализует доступ к элементам мас- сива с помощью указателей и операции разыменования. Если в программе использована, например, такая индексированная перемен- ная: AR[x] [j] [k], принадлежащая массиву type ar[N] [М] [L], где ы, м, L — целые положительные константы, то последовательность дей- ствий компилятора такова: • выбирается адрес начала массива, т.е. целочисленное значение указателя ar, равное (unsigned long) ar;
'лава 5. Адреса, указатели, массивы, память 149 • добавляется смещение i * (М * L) * sizeof (type) для вычисления начального адреса i-ro массива с размерами м на L, входящего в исходный трехмерный массив; • добавляется смещение для вычисления начального адреса j-й строки (одномерный массив), включающей L элементов. Теперь смещение равно (х * (М * L) + j * L) * sizeof (type); ! • добавляется смещение для получения адреса k-ro элемента в строке, т.е. получается адрес (unsigned long) (i * (М * l) + j * L + k) * sizeof(type); • применяется разыменование, т.е. обеспечивается доступ к со- держимому элемента по его адресу: * ((unsigned long) (i * (М * L) + j * L + k)). Массивы указателей. Синтаксис языка Си++ в отношении указа- телей непротиворечив, но весьма далек от ясности. Для понимания, что же определено с помощью набора звездочек, скобок и имен типов, Приходится аккуратно применять синтаксические правила, учитывающие последовательность выполнения операций. Например, Следующее определение int *аггау[б]; (Вводит массив указателей на объекты типа int. Имя массива array, он состоит из шести элементов, тип каждого int *. Определение int (*ptr)[6]; вводит указатель ptr на массив из шести элементов, каждый из кото- рых имеет тип int. Таким образом, выражение (array + 1) соответ- ствует перемещению в памяти на sizeof (int *) байтов от начала j массива (т.е. на длину указателя типа int *). Если прибавить 1 к ptr, То адрес изменится на величину sizeof (int [6]), т.е. на 12 байт при двухбайтовом представлении данных типа int. Эта возможность создавать массивы указателей порождает инте- ресные следствия, которые удобно рассмотреть в контексте много- мерных массивов. По определению массива, его элементы должны быть однотип- ными и одного "размера". Предположим, что мы хотим определить массив для представления списка фамилий (учеников класса, сотруд- ников фирмы и т.п.). Если определять его как двухмерный массив эле- ментов типа char, то в определении для элементов массива необходимо задать предельные размеры каждого из двух индексов.
150 Язык Си++ Таким образом, "прямолинейное" определение массива для хранс ния списка фамилий может быть таким: char spisok[25][20]; Для примера здесь предполагается, что количество фамилий в списке не более 25 и что длина каждой фамилии не превышает 19 сим волов (букв). После такого определения или с помощью инициализа цин в самом определении в элементы spisok[0], spisok[i], .. можно занести конкретные фамилии, представленные в виде строк Размеры так определенного массива всегда фиксированы. При определении массива одни из его предельных размеров (самого левого индекса) можно не указывать. В этом случае количе ство элементов массива определяется, например, инициализацией: char spisok[][20] « ( "Иванов", "Петров", "Сидоров" ); Теперь в массиве spisok только 3 элемента, каждый из них длиной 20 элементов типа char (рис. 5.4). 20 байт . Массивы char [20] spisok ► Иванов 0 0 о о о о 0 0 0 0 0 0 0 0 (имя массива - Петров 0 0 0 0 0 0 0 0 0 0 0 0 0 0 указатель-константа) С и д о р о в 0 0 0 0 0 0 0 0 0 0 0 0 0 pointer ------------► (имя массива - указатель-константа) Указатели- Строковые переменные константы *. ... ► Иваново * ► ПетровО * ► СидоровО (char ♦) Рис. 5.4. Двухмерный массив char spisok[3] [20] и одномерный массив указателей char *pointer [ 3 ], инициализированные одинаковыми строками Нерациональное использование памяти и в этом случае налицо - даже для коротких строк всегда выделяется одно и то же количество
Глава 5. Адреса, указатели, массивы, память 151 байтов, заранее указанное в качестве предельного значения второго индекса массива spisok. ? В противоположность этому при определении и инициализации Теми же символьными строками одномерного массива указателей ти- па char * память распределяется гораздо рациональнее: char *pointer [] я ( "Иванов", J "Петров", "Сидоров" }; Для указателей массива pointer, в котором при таком определе- нии 3 элемента и каждый является указателем-переменной типа char *, выделяется всего 3*sizeof (char *) байтов. Кроме того, компиля- тор размещает в памяти три строковые константы "Иванов" (7 байт), "Петров" (7 байт), "Сидоров" (8 байт), а их адреса становятся значе- ниями элементов pointer[0], pointer[1], pointer[2]. Сказанное i иллюстрирует рис. 5.4. Применение указателей и их массивов позволяет весьма рацио- нально решать задачи сортировки сложных объектов с неодинаковы- J ми размерами. Например, для упорядочения (хотя бы по алфавиту) • списка строк можно менять местами не сами строки, а переставлять ’ значения элементов массива указателей на эти строки. Такой одно- мерный массив pointer [] использован в только что приведенном примере (см. рис. 5.4.). Накладными расходами при этой "косвенной" сортировке списков объектов является требование к памяти, необхо- димой для массива указателей. Выигрыш - существенное ускорение ; сортировки. В качестве конкретной задачи такого рода рассмотрим сортиров- , ку строк матрицы. Матрица с элементами типа double представлена двухмерным массивом double array [n] [ml, где пит- целочислен- ные константы. Предположим, что целью сортировки является упоря- дочение строк матрицы в порядке возрастания сумм их элементов. Чтобы не переставлять сами строки массива array[n][ml, введен вспомогательный одномерный массив указателей double * раг[п]. Инициализируем его элементы адресами одномерных массивов типа double [ml, составляющих двухмерный массив array [nl [ml. После такой инициализации массива указателей к элементам ис- ходного массива появляются два пути доступа: • прямой - с помощью индексации имени массива array [ii [ji и • косвенный - с помощью указателей вспомогательного массива par[ij[jJ.
152 Язык Си++ Чтобы не усложнять программу, применим самый простой "линейный" метод сортировки, а в качестве начальных значений эле ментов массива выберем номера строк, к которым элементы относят ся. В программе три раза напечатаем матрицу - до и после сортировки с помощью вспомогательного массива указателей и (после сортиров ки) с использованием основного имени массива. Комментарии в текс те программы поясняют остальные детали реализации: //Р5-18.СРР - перестановка указателей на одномерные массивы #include <iostream.h> // Для ввода-вывода void main() { const int п = 5; // Количество строк матрицы const int m = 7; // Количество столбцов матрицы double array[п][mJ; // Основной массив (матрица) for (int i » 0; i < п; i++) for (int j = 0; j < m; j++) // Заполнение матрицы array[i][j] = n - i; double *par[n); // Вспомогательный массив указателей for (i =0; i < n; i++) // Цикл перебора строк par[i] = (double *)&array[i); // Печать массива no строкам (через массив указателей) : cout « "\пДо перестановки элементов массива ” « "указателей:"; for (i =0; i < п; i++) // Цикл перебора строк { cout « "\пстрока " « (i+1) « ; for (int j « 0; j < m; j++) // Цикл печати cout«"\t"«par [i] [ j] ; // элементов строки ) // Упорядочение указателей на строки массива double si,sk; for (i = 0; i < n - 1; i++) { for (int j = 0, si = 0.0; j < m; j++) si += par[i][j]; // Сумма элементов i-й строки for (int k«i+l;k<n; k++) { for (j » 0, sk = 0.0; j < m; j++) sk += par[k][j]; // Сумма элементов k-й строки if (si > sk) { double *pa » par[i]; par[i] = par[k]; par[k] = pa; double a = si; si = sk; sk = a; > > ) // Печать массива no строкам (через массив указателей) : cout « "\пПосле перестановки элементов массива ";
Глава 5. Адреса, указатели, массивы, память 153 "указателей:"; for (i =0; i < n; i++) // Цикл перебора строк { cout « "\п строка " « (i + 1) « for (int j = 0; j < m; j++) // Цикл печати cout « "\t" « par[i] [j] ; // элементов строки } // Печать исходного массива по строкам (обращение через // имя массива) : cout « "\пИсходный массив остался без изменений:"; for (i = 0; i < п; i++) // Цикл перебора строк { cout « "\п строка " « (i + 1) « for (int j « 0; j < m; j++) // Цикл печати cout « "\t" « array[i][j]; // элементов // строки ) > Результаты выполнения программы: До перестановки элементов массива указателей: строка 1: 5555555 строка 2: 4444444 строка 3: 3333333 строка 4: 2222222 строка 5: 1111111 После перестановки элементов массива указателей: строка 1: 1111111 строка 2: 2222222 строка 3: 3333333 строка 4: 4444444 строка 5: 5555555 Исходный массив остался без изменений: строка 1: 5555555 строка 2: 4444444 строка 3: 3333333 строка 4: 2222222 строка 5: 1111111 Обратите внимание на неизменность исходного массива аггау[п] [ш] после сортировки элементов вспомогательного массива указателей. Для иллюстрации действия механизма сортировки нари- суйте схему взаимосвязи массивов array[] [1 и раг[]. В качестве об- разца можно воспользоваться рис. 5.5.
154 Язык Си++ Массивы динамической памяти. В соответствии с синтаксисом операция new при использовании с массивами имеет следующий фор мат: new тип__массива Такая операция позволяет выделить в динамической памяти учас ток для размещения массива соответствующего типа, но не позволяс i его инициализировать. В результате выполнения операция new воз вратит указатель, значением которого служит адрес первого элемент массива. При выделении динамической памяти для массива его размеры должны быть полностью определены. long (*1р)[2] [4]; // Определили указатель Ip = new long[3][2][4]; // Выделили память для массива В данном примере использован указатель на объекты в виде двух мерных массивов, каждый из которых имеет фиксированные размеры 2 на 4 и содержит элементы типа long. В определении указателя следу ет обратить внимание на круглые скобки, без которых обойтись нель зя. После выполнения приведенных операторов указатель 1р стано вится средством доступа к участку динамической памяти с размерами 3*2*4* sizeof (long) байтов. В отличие от имени массива (имени у этого массива из примера нет) указатель 1р есть переменная, что позволяет изменять его значение и тем самым, например, переме- щаться по элементам массива. Изменять значение указателя на динамический массив нужно с осторожностью, чтобы не "забыть", где же находится начало массива, так как указатель, значение которого определяется при выделении памяти для динамического массива, используется затем для освобож- дения памяти с помощью операции delete. Например, оператор: delete [] 1р; освободит целиком всю память, выделенную для определенного выше трехмерного массива, если 1р адресует его начало. Следующая про- грамма иллюстрирует сказанное: //Р5-19.СРР - выделение и освобождение памяти для массива #include <iostream.h> void main() { long (*lp)[2][4]; Ip = new long [3] [2][4]; cout « "\n";
'лава 5. Адреса, указатели, массивы, память 155 for (int i == 0; i < 3; i++) { cout « ”\n"; for (int j = 0; j < 2; j++) i for (int k « 0; k < 4; k++) ( lp[iJ[j][k] = i + j + k; cout « ’\t' « lp[i][j][k]; } } delete [] Ip; I Результаты выполнения программы: 01231234 1 2 3 4 2 3 4 5 1 3 4 5 3 4 5 6 В отличие от определения массивов, не относящихся к динами- ческой памяти, инициализация динамических массивов не выполняет- ся. Поэтому при выделении памяти для динамических массивов их размеры должны быть полностью определены явно. Только из ти- Иа^иассива операция new получает информацию о его размерах: new long[] // Ошибка, размер неизаестен new long[][2][4] // Ошибка, размер неизвестен new long[3][][4] // Ошибка, размер неизвестен Существует еще одно ограничение на размерности динамических Массивов. Только первый (самый левый) размер массива может быть Задан с помощью переменной. Остальные размеры многомерного мас- сива могут быть определены только с помощью констант. Это несколько затрудняет работу с многомерными динамическими масси- вами. Например, если пытаться создать матрицы в виде двухмерных Массивов, то затруднения возникнут при попытке написать функцию, формирующую в динамической памяти транспонированную матрицу ПО исходной матрице с заранее не определенными размерами. Обойти указанное ограничение многомерных динамических мас- сивов позволяет применение массивов указателей. Однако при ис- пользовании массивов указателей для имитации многомерных дина- мических массивов усложняется не только их формирование, но и освобождение динамической памяти. В следующей программе форми- руется, заполняется данными, затем печатается и уничтожается мас- сив, представляющий прямоугольную диагональную единичную мат- рицу, порядок которой (размеры массива) вводится пользователем с клавиатуры:
156 Язык Си++ //Р5-20.СРР - единичная диагональная матрица с изменяемым // порядком #include <iostream.h> // Для ввода-вывода void main() { int п; // Порядок матрицы cout « "\пВведите порядок матрицы: "; cin » п; // Определяются размеры массива float **matr; // Указатель для массива указателей matr = new float *[п]; // Массив указателей float * if (matr == NULL) { cout « "He создан динамический массив!"; return; // Завершение программы ) for (int i = 0; i < n; i++) { // Строка-массив значении типа float: matr[i] = new float[n]; if (matr[i] = NULL) { cout « "He создан динамический массив!"; return; // Завершение программы ) for (int j = 0; j < n; j++) // Заполнение матрицы // Формирование нулевых элементов: if (i != j) matr[i][j] « 0; else // Формирование единичной диагонали: matr[i][j] = 1; ) for (i == 0; i < n; i++) // Цикл перебора строк { cout « "\n строка " « (i + 1) « // Цикл печати элементов строки: for (int j = 0; j < n; j++) cout « "\t" « matr[i][j]; ) for (i ® 0; i < n; i++) delete matr[i]; delete[]matr; } Результаты выполнения: Введите размер матрицы: 5 <Enter> Строка 1: 10000 Строка 2: 01000 Строка 3: 00100 Строка 4: 00010 Строка 5: 00001
Глава 5. Адреса, указатели, массивы, память 157 t На рис. 5.5 изображена схема взаимосвязи (л + 1)-одномерных .Массивов, из п элементов каждый. Эти (и + 1) массивов совместно [ Имитируют квадратную матрицу с изменяемыми размерами, форми- |руемую в программе Р5-19.СРР. Рис. 5.5. Схема имитации двухмерного динамического массива с помощью массива указателей и набора одномерных массивов 5.5. Организация памяти в процессорах 80x86 и указатели языка Си++ Сегментная адресация памяти. В стандартизируемом варианте \ языка Си++ предполагается [2], что все указатели одинаковы, т.е. ! внутреннее представление адресов всегда одно и то же. Однако в реа- лизациях компиляторов для конкретных вычислительных машин это не всегда справедливо. Практически все компиляторы языка Си++ обязательно учитывают архитектурные особенности аппаратных средств и включают дополнительные возможности для того, чтобы программист мог их эффективно использовать. Рассмотрим этот во- прос подробнее, ориентируясь на ПЭВМ типа IBM PC. Читатели, ра- ботающие с компиляторами для других платформ, могут безболезнен- но пропустить этот раздел. Процессоры семейства 80x86 (80286, 80386, 80486) используют сегментированную организацию памяти, и это существенно влияет на внутреннее представление адресов. Основная память ПЭВМ - это память с произвольной выборкой, т.е. с непосредственным (прямым) доступом к участку с любым адре- сом независимо от того, к какому участку выполнялось предыдущее обращение. Наименьшим адресуемым участком основной памяти является байт, содержащий 8 бит (двоичных разрядов). 256 возможных значе-
158 Язык Си++ ний байта (28 = 256) могут рассматриваться либо как положительные числа в диапазоне от 0 до 255 (unsigned), либо как целые числа со зна ком в диапазоне от -128 до +127. В последнем случае старший разряд считается знаковым, а остальные 7 бит представляют абсолютное зна чение хранимого числа. Физические адреса байтов памяти начинают ся с 0 и возрастают. Полный сегментированный адрес любого типа формируется и । двух 16-разрядных чисел, которые можно условно записать в вид* двух шестнадцатеричных чисел вида Охнннн: Охнннн, где н - любая шестнадцатеричная цифра (от 0 до F). Первое из них (старшее) назы вают сегментной частью адреса, второе (младшее) именуют смещением или относительной частью адреса. Рассмотрим эти понятия подроб нее. Любые два смежных байта памяти образуют 16-разрядное слово Адресом слова считается младший из адресов байтов, образующих сДово. В отличие от байтов понятие "слово" относительно, так как один байт может входить в два смежных слова с последовательными адресами. Участки памяти длиной 16 байт, начинающиеся с адресов, крат ных 16, называют параграфами. Параграфы пронумерованы последо вательно и в памяти с объемом 1 Мбайт имеют нумерацию от 0 до 65535. Физический адрес каждого параграфа, т.е. адрес байта, с кото рого он начинается в памяти, равен его номеру, умноженному на 16. Начало любого параграфа может быть принято за начало сегмен та длиной не более 4096 параграфов. Таким образом, адрес начала произвольного сегмента всегда кратен 16, а длина сегмента не может превышать 64Кбайт (65536 байт). При задании сегмента необходимо указать только его начало. Размеры сегмента нигде не указываются п ограничены разрядностью относительной части адреса. В полном сегментированном адресе Охнннн:Охнннн старшее шест надцатиразрядное число - сегментная часть адреса - это номер пара графа, с которого начинается сегмент. Номер параграфа однозначно определяет размещение сегмента в памяти. Младшее шестнадцатерич ное число - относительная часть адреса - определяет смещение адре суемого байта от начала сегмента. Обе части полного адреса - это четырехразрядные шестнадцатеричные числа, т.е. они могут прини мать значения от 0 до 65535 (64К). Любой байт пространства памяти может быть отнесен к несколь ким сегментам, имеющим разные начальные адреса и (или) разные длины. Физический адрес байта, отнесенного к конкретному сегменту формируется как сумма относительной части его адреса и увеличен
Глава 5. Адреса, указатели, массивы, память 159 ^Ной в 16 раз сегментной части адреса. Например, по известному пол- ному адресу 0x2222:0x3333 будет сформирован 20-разрядный фи- зический адрес 0x25553. . Для организации работы с полными сегментированными адреса- ми в процессорах семейства 80x86 имеются регистры сегментов: • CS (Code Segment) - регистр кодового (программного) сегмента ; используется для формирования адресов выполняемых команд программы; • DS (Data Segment) - регистр сегмента данных используется для формирования адресов данных, участвующих в операциях про- цессора; • SS (Stack Segment) - регистр сегмента стека используется для вычисления адресов данных из стека; • ES (Extra Segment) - регистр сегмента расширения используется ', для формирования адресов дополнительных данных. Каждый сегментный регистр может содержать при выполнении Программы конкретные значения сегментной части адреса. Таким об- разом, процессор одновременно может адресовать 4 различных сег- мента памяти, каждый из которых может быть размером до 64К. Эти 1 Сегменты могут пересекаться и даже могут быть размещены все в од- ! ном участке памяти размером 64К (рис. 5.6). Рассматривая механизм сегментации, обратите внимание на не- полное использование потенциально возможного адресного про- странства, образуемого двумя четырехразрядными шестнадцатерич- ными числами (сегмент:смещение). Действительно, с помощью этих 'чисел можно было бы адресовать 216 х 216 = 232 (т.е. - 4 млрд.) байт , памяти. Однако адресуются только 220 (~ 1 млн.) байт. Причина тако- го положения - допустимость пересечения сегментов. Напомним, что началом любого сегмента может быть любой байт, адрес которого кратен 16, т.е. оканчивается нулем в шестнадцатеричном представле- нии порядкового номера байта. Таким образом, следующие полные адреса 0x0000:0x0413, 0x0001:0x0403, 0x0021:0x0203 представляют один и тот же физический адрес 0x0413 (0x00000 + 0x0413 == 0x00010 + 0x0403 “ 0x00210 + 0x0203 — 0x413), определяющий слово в памяти, где хранится информация о размерах памяти ПЭВМ типа IBM PC (размеры в Кбайтах). Сегментная адресация и указатели в компиляторах ТС++ и ВС++. С сегментной организацией памяти и вопросами адресации при ее ис- пользовании тесно связаны вопросы внутреннего представления зна- чений указателей. В конкретной реализации компиляторов языка
160 Язык Си++ Основная память Си++ стандартное понятие указателя расширено. Введены дополни тельные модификаторы - ключевые (служебные) слова, позволяющие программисту выбирать внутреннее представление указателей. Указатели ТС++ и ВС++ делятся на 4 группы: • near-’’ближние"; • far-’’дальние"; • huge - "нормализованные" ("сверхдальние" или "огромные"); • сегментные (_seg). Модификатор размещается при определении указателя непосред ственно перед символом • *1 и относится (как символ 1 * ’ и как ини- циализатор) только к одному указателю в определении. Ближние указатели (пеаг-указатели) имеют длину внутреннего представления 2 байта (16 бит). Они позволяют определять адреса объектов в конкретном сегменте, так как каждый пеаг-указатель хра- нит только смещение полного адреса, пеаг-указатели можно исполь зовать только в тех случаях, когда начальный адрес сегмента нс
Глава 5. Адреса, указатели, массивы, память 161 I требуется указывать явно. С помощью пеаг-указателей организуется доступ к функциям или данным внутри одного сегмента. Определение ближнего указателя: тип near *имя_указателя инициализатор ; Ближние указатели достаточно удобны, т.к. при операциях с ближними указателями нет необходимости следить за начальным ад- ресом сегмента. Однако пеаг-указатели не позволяют адресовать более 64К памяти. Дальние указатели (far-указатели) занимают 4 байта (32 бита) и содержат как сегментную часть полного адреса, так и смещение. С помощью far-указателей можно адресовать память до I Мбайта. Определение дальнего указателя: тип far *имя_указателя инициализатора Для формирования значения far-указателя в компиляторах языка Си-Г+ имеется унаследованный от языка Турбо Си макрос, определе- ние которого находится в заголовочном файле dos.h. Макрос имеет вид: void far *MK_FP (unsigned segment, unsigned offset) ; С его помощью формируется дальний указатель по заданным це- лочисленным неотрицательным значениям сегментной части адреса (segment) и смещению (offset). Как уже говорилось, представление одного и того же физического адреса с помощью полного сегментированного адреса неоднозначно. Один и тот же байт памяти можно относить к разным сегментам и тем самым адресовать разными парами сегмент:смещение. Например, как показано в следующей программе, один и тот же основной байт со- стояния клавиатуры с физическим адресом 0x417 можно адресовать несколькими указателями: //Р5-21.СРР - Представление дальних (far) указателей tfinclude <iostream.h> *include <dos.h> // Для макроса MK_FP() f void main(void) ( void far *p0 = (void far *)0x417; void far *pl; pl = MK_FP(0x41,0x007); void far *p2; p2 = MK-FP(0x20,0x217); cout « ”\np0 « " « p0 « 1Г3821
162 Язык Си++ " * (char *)рО = " « * (char *)рО; cout « ”\npl ж " « pl « “ *(char *)pl « " « *(char *)pl; cout « "\np2 = ” « p2 « " *(char *)p2 = " « *(char *)p2; 1 Результат выполнения программы pO = 0x00000417 pl « 0x00410007 pO = 0x00200217 ♦(char *)p0 = A *(char *)pl - A *(char *)p2 = A Указатель pO определен с инициализацией. Указателям pl, p2 ни чальные значения присвоены с помощью макроопределения мк__гг Интересен внешний вид выводимых шестнадцатеричных значении указателей. Они выводятся как восьмиразрядные числа. Первые четы ре разряда - сегментная часть полного адреса, младшие четыре разря да - смещение. Все три указателя “смотрят" на один и тот же бай i состояния клавиатуры. Как мы видели в программе Р5-02.СРР (см. и 5.1), содержимое байта с номером 0x417 при нормальном состоянии клавиатуры именно такое. Если выполнить программу, включив рс гистры Caps Lock, Num Lock и Scroll Lock, то результатом разымено вания будет 'Ё'. Обратите внимание на необходимость явньо приведений типов как при инициализации рО, так и при выводе в стандартный поток cout результатов разыменования указателей ро р!,р2. Итак, значением far-указателя рО будет полный сегментный адры 0x0:0x417, а значением pl будет 0x41:0x7. При выполнении сравнс ния указателей рО и pl значением выражения рО = pl будет нуш (ложь), несмотря на то, что оба указателя адресуют один и тот участок памяти с порядковым номером 0x417. Сравнение far указателей выполняется "поэлементно", т.е. попарно сравниваются адреса сегментов и адреса смещений, а не абсолютные физические ад реса, которые им соответствуют. Точнее, при сравнении far указателей на равенство каждый из них представляется в виде длинно го целого без знака (unsigned long), и попарно сравниваются все 32 би та их внутренних представлений. При сравнении дальних указателей на “старшинство" (т.е. в отно шениях >, >=, <=, <) в сравнениях участвуют только смещения. Такиу образом, отношение рО >~ pl для far-указателей из программы буде: истинным (равным 1), а сравнение pl >= р2 даст значение о (ложь).
Глава 5. Адреса, указатели, массивы, память 163 При увеличении (или уменьшении) far-указателя за счет прибав- ления (или вычитания) константы изменяется только смещение. Это правило справедливо и при выходе смещения за разрядную сетку, в случае которого указатель циклически переходит к "противополож- ной" границе того сегмента, на который указывает сегментная часть его значения. При изменении far-указателя на значение константы соблюдаются обычные правила арифметических действий с указателями. Для указа- теля типа type прибавление (или вычитание) целой величины с изме- няет внутреннее значение смещения на величину с * size (type). Перечисленные особенности far-указателей иллюстрирует следующая программа: //Р5-22.СРР - сравнения и аддитивные операции с // fаг-укаэателями tinclude <iostream.h> linclude <dos.h> // Для макроса MK_F₽() void main(void) ( int far *ul, far *u2; ul = (int far *)MK_FP(0x8000,OxFFFO); j u2 = ul + 0x20; ; cout « ”\nul = ” « ul; cout « "\nu2 « " « u2; ! cout « ”\nu2 - 0x20=" « u2 - 0x20; u2 = (int far *)MK_FP(0x6000,OxFFFF); cout « ”\nu2 = " « u2; cout « "\n(ul >= u2) = " « (ul >= u2); } Результаты выполнения программы: ul = OxBOOOfffO ’ u2 = 0x80000030 u2 - 0x20 = OxBOOOfffO u2 = 0x6000ffff (ul >= u2) - 0 В программе нужно обратить внимание на особенность адди тивных операций с указателями. Напомним, что изменение указателя типа type * на 1 увеличивает либо уменьшает его числовое значение на величину sizeof (type). Именно поэтому, прибавив 0x20 к ul, по- лучили смещение 0x0030, так как смещение ul изменилось на 0x40, т.е. на 0x20 * sizeof (int).
164 Язык Си++ Обобщая полученные результаты, отметим, что, не изменив яви* значения сегментной части в far-указателе, невозможно адресован участки памяти вне сегмента размером 64К. Кроме макроопределения mk_fp(), позволяющего сформирован far-указатель по известным значениям сегмента и смещения, в файл» dos. h находятся еще два макроопределения для работы с удаленными указателями: unsigned FP__OFF(void far *ptr) ; unsigned FP__SEG (void far *ptr) ; Первое - fpjdff - возвращает беззнаковое целое, представляющее значение смещения из значения far-указателя ptr, использованного н качестве параметра. Второе - fp__seg - возвращает беззнаковое целое, представляющее значение адреса сегмента из значения far-указателя ptr, использован ного в качестве параметра. В описании библиотеки ТС++ имеется несколько примеров, иллю стрирующих особенности применения макроопределений. Следующая программа построена на их основе: //Р5-23.СРР - сегментная и относительная части // far"указателей Minclude <iostream.h> #include <dos.h> void main(void) { char far *str ~ "Строка автоматической памяти"; cout « "ХпАдрес строки: " « (void *)str; cout « "\пСегментная часть адреса строки: "; cout « hex « FP_SEG(str) ; cout « "\пСмедение для адреса этой строки: cout « hex « FPjOFF(str) ; } Результат выполнения программы: Адрес строки: 0x8d200094 Сегментная часть адреса строки: 8d20 Смещение для этой строки равно: 94 Нормализованные указатели (huge-указатели) определяются таким образом: тип huge *имя_указателя инициализатор; Инициализатор, как всегда, необязателен. Нормализованные ука затели имеют длину 32 разряда и позволяют однозначно адресован
Глава 5. Адреса, указатели, массивы, память 165 I Память до 1 Мбайта. В отличие от far-указателей huge-указатели со- держат только нормализованные адреса, т.е. адрес любого объекта в Памяти представляется единственным сочетанием значений его сег- ментной части и смещения. Однозначность представления полного Сегментированного адреса выполняется за счет того, что смещение может принимать значение только в пределах от 0 до 15 [шестнадцатеричное F). Например, полный адрес 0х0000:0х0417 бу- дет нормализован так: 0x0041: 0х0007. В качестве адреса сегмента Всегда выбирается максимальное для конкретного адреса значение. Например, адрес 0x8000:0xffff в нормализованном виде будет всег- да представлен однозначно: 0x8FFF:0x000F. Именно такое значение содержит соответствующий huge-указатель. Второе существенное отличие huge-указателя от far-указателя со- стоит в том, что значение huge-указателя (некоторый адрес) рассмат- ривается всегда как одно беззнаковое целое число (32 разряда). г Тем самым при изменении значения нормализованного указателя | может изменяться как смещение, так и сегментная часть. Если смеще- 1цие превышает oxf или становится меньше 0x0, то изменяется сег- 0 Ментная часть адреса. Например, увеличение на 2 huge-указателя со |! Значением 0x8FFF:0xF приведет к формированию 0x9000:0x1. | Уменьшение последнего значения на 3 приведет к значению > OxSFFF*. ОхЕ И Т.Д. Ц При выполнении всех операций сравнения нормализованных Е (huge-) указателей используются все 32 разряда их внутренних пред- В Ставлений. р Так как каждый адрес представляется в виде huge-указателя един- I ственным образом, то сравнение на равенство (== или ’=) выполняет- I ся вполне корректно. Сравнение на старшинство (>, >=, <, <=) также Т Приводит к правильному результату. Удобство применения huge-указателей обеспечивается применени- ем соответствующих операций нормализации, которые компилятор автоматически добавляет в программу. Это приводит в ряде случаев к замедлению выполнения программы, что нужно учитывать при про- граммировании. В качестве примера использования нормализованных указателей приведем программу вычисления суммы всех слов области адресов BIOS, начиная с адреса 0xF000:0x0oq0. Получаемая сумма уникальна для каждого варианта микросхемы BIOS и может использоваться, на- пример, для настройки программы на конкретную ПЭВМ. Програм- ма с некоторыми простейшими исправлениями и изменениями взята из работы [17]. Обратите внимание, что указатель ptr становится рав-
166 Язык Си++ ным 0 после прибавления к его значению Oxffffe величины 2, т.е значения sizeof (unsigned): //Р5-24.СРР ~ Нормализованные указатели ~ обращение к / / памяти linclude <iostream.h> void main(void) { unsigned huge *ptr = (unsigned huge *) OxFOOOOOOOL; long unsigned bios__sum = 0; // Цикл пока указатель отличен от нуля: while (ptr) bios_sum += *ptr++; cout « "\пСумма кодов BIOS: " « bios__sum; } Результат выполнения программы: Сумма кодов BIOS: 837681152 Особый тип близких указателей в ВС++ и ТС++ - это сегментные указатели. Для определения и описания сегментных указателей в ка честве модификаторов используются служебные слова _cs, _ds, _es, js, _seg, которые отсутствуют в стандарте языка Си++. В полном соответствии с обозначениями модификаторы _cs, _ds, _es, _ss по зволяют определить четыре вида близких указателей, каждый из ко торых соответствует сегментному регистру, см. рис. 5.6. Определим для примера сегментный указатель pss: int _ss *pss; Присваивая указателю pss шестнадцатибитовое значение смете ния, можно получать доступ к тому сегменту стека, адрес начала ко торого в текущий момент находится в регистре сегмента стека Другими словами, полный адрес участка памяти при работе с cei ментным указателем формируется из содержимого соответствующего сегментного регистра (сегментная часть адреса) и из значения cei ментного указателя (смещение). Сегментные указатели могут обоснованно использоваться в том случае, если надлежащим образом определены значения сегментных регистров. Для доступа к сегментным и другим регистрам в ТС++ и ВС++ введены в виде служебных слов регистровые переменные _cs j)S, jes, _ss (см. п. 2.2). К сожалению, для более подробного обсуж дения особенностей и достоинств сегментных указателей требуется рассматривать задачи системного программирования, что выходит за
Глава 5. Адреса, указатели, массивы, память 167 Ьамки настоящей работы. Читателей можно отослать к справочному Пособию А.И. Касаткина1- < С помощью модификатора jeg определяются и описываются Шестнадцатиразрядные сегментные указатели, имеющие особые Свойства. Формат определения такого сегментного указателя: тип_данных _seg *имя__указателя; ^Например: long _seg *ptrseg; l( Руководство программиста из документации ВС++ таким обра- зом перечисляет свойства этих сегментных указателей [9]. I Во-первых, разыменование сегментного указателя по умолчанию [Предполагает нулевое смещение. Во-вторых, при арифметических опе- рациях с использованием сегментных указателей справедливы сле- дующие правила: 1. К сегментным указателям разрешено применять операции ++, 2. Нельзя вычитать один сегментный указатель из другого. f 3. В результате добавления ближнего (near-) указателя к сег- ментному указателю формируется дальний (far-) указатель, значением которого служит полный адрес, сегментная часть которого выбирает- ся из сегментного указателя, а смещением является значение ближнего указателя. В связи с этим оба указателя (сегментный и ближний) должны быть одного типа либо один из них должен быть указателем На тип void. Независимо от типа указателей никаких изменений значе- ния смещения не предусматривается. 4. Когда к сегментному указателю применяется разыменование, ‘он предварительно автоматически преобразовывается к дальнему (far-) указателю, имеющему нулевое смещение. 5. При суммировании сегментного указателя с целочисленным операндом формируется удаленный указатель. Сегментной частью Значения этого far-указателя служит значение сегментного указателя, I смещение вычисляется как значение целочисленного операнда, умноженного на длину объекта того типа, к которому относится сег- ментный указатель. При вычитании целого операнда из сегментного указателя фор- мируется far-указатель. Сегментной частью значения этого far- указателя служит значение сегментного указателя, а смещение опреде- 1 Касаткин А.И. Профессиональное программирование на языке Си - Т. 2. Управление ресурсами.- Минск: Вышейшая школа, 1992 - 432 с. ' Там же. Т. 3. Системное программирование.- 301 с.
168 Язык Си++ ляется вычитанием из (Oxffff + 1) значения операнда, умножением < на размер объекта того типа, к которому относится исходный cei ментный указатель. Арифметические операции с сегментными указл телями выполняются так же, как сложение и вычитание для fai указателей. Другими словами, прибавляя (или вычитая) какой угоди, целый операнд к сегментному указателю, мы всегда получаем fat указатель с сегментом, равным значению сегментного указателя. Ич меняться может только смещение. 6. Сегментные указатели могут быть инициализированы, мм можно присваивать значения, их можно передавать в функции и значения можно получать из функций. Сегментные указатели можн<- сравнивать. При сравнениях они воспринимаются как беззнаковые шестнадцл тиразрядные целые. Другими словами, если не считать перечисленные особенностей и ограничений, то сегментные указатели обрабаты ваются точно так же, как и другие указатели. Проиллюстрируем некоторые из сформулированных правил слс дующей программой, где формируется значение удаленного указатс ля, адресующего основной байт состояния клавиатуры: //Р5-25.СРР - Некоторые особенности сегментных указателей #include <iostream.h> void main (void) { int near *pn « (int near *)0x0007; int _aeg *ps = (int __seg *) 0x0041; int far *pf; pf = ps + pn; cout « "\npf “ " « pf; cout « " *pf = " « * (char *)pf; Г Результат выполнения программы: pf = 0x00410007 *pf = A При изменении состояния клавиатуры значение *pf изменяется Это мы уже несколько раз рассматривали выше.
Глава 6. ФУНКЦИИ, УКАЗАТЕЛИ, ССЫЛКИ В.1. Определения, описания и вызовы функций Если в таких языках, как Алгол, Фортран, ПЛ/1, Паскаль и др. де- Дается различие между программами, подпрограммами, процедурами, функциям, то в языке Си++ и в его предшественнике - в языке Си - !ользуются только функции. При программировании на языке Си++ функция - это основное сятие, без которого невозможно обойтись. Во-первых, каждая про- мма обязательно должна включать единственную функцию с именем п (главная функция). Именно функция main обеспечивает создание ки входа в откомпилированную программу. Кроме функции с именем п, в программу может входить произвольное количество неглавных [кций, выполнение которых инициируется прямо или опосредованно ювами из функции main. Всем именам функций программы по шчанию присваивается класс памяти extern, т.е. каждая функция ;ет внешний тип компоновки и статическую продолжительность сествования. Как объект с классом памяти extern, каждая функция бальна, т.е. при определенных условиях доступна в модуле и даже во х модулях программы. Для доступности в модуле функция должна гь в нем определена или описана до первого вызова. Итак, каждая программа на языке Си++ - это совокупность функ- i, каждая их которых должна быть определена или по крайней мере «писана до ее использования в конкретном модуле программы. В определении функции указываются последовательность действий, выполняемых при ее вызове, имя функции, тип функции (тип воз- вращаемого ею значения, т.е. тип результата) и совокупность фор- мальных параметров (аргументов). Каждый формальный параметр не только перечисляется, но и специфицируется, т.е. для него задается тип. Имя функции и совокупность формальных параметров определяют сигнатуру функции. Этот термин активно используется в связи с перегрузкой функций (см. п. 6.89). Сигнатура функции зависит от
170 Язык Си++ количества параметров, от их типов и от порядка их размещения <• спецификации формальных параметров. Определение функции, в котором выделяются две части - заголов< и и тело, имеет следующий формат: ТИП—функции имя—функции (спецификация—формальных—параметров) тело_функции Здесь тип_функции (не входит в сигнатуру) - тип возвращаемо! функцией значения, в том числе void, если функция никакого значенн - не возвращает. Имя__функции - идентификатор. Имена функций к;и имена внешние (тип extern) должны быть уникальными среди други имен из модулей, в которых используются функции. Спецификаци формальных параметров - это либо пусто, либо void, либо списо! спецификаций отдельных параметров, в конце которого может бы i поставлено многоточие. Спецификация каждого параметра в onpt делении функции имеет вид: тип имя—параметра тип имя—параметра = умалчиваемое_значение Как следует из формата, для параметра может быть задано (а можс । отсутствовать) умалчиваемое значение. И, более того, синтаксис языь j разрешает параметры без имен, если последние не используются в тс и функции. В проекте стандарта языка Си + + [2] отмечается, чь использование спецификации параметра без имени "полезно д;г1 резервирования места в списке параметров". В дальнейшем этот па раметр может быть введен в функции без изменения интерфейса, т.е. 6с изменения вызывающей программы. Такая возможность бывает удобно! * при развитии уже существующей программы за счет изменения входящп в нее функций. (См. в качестве примера определенную ниже функции Norma () .) Тело__функции - это всегда блок или составной оператор, т.< последовательность описаний и операторов, заключенная в фигурны* скобки. Очень важным оператором тела функции является оператор возврата в точку вызова: return выражение; ИЛИ return; Выражение в операторе return определяет возвращаемое функцией значение. Именно это значение будет результатом обращения к функции Тип возвращаемого значения определяется типом функции. Еслг
‘лава 6. Функции, указатели, ссылки 171 (функция не возвращает никакого значения, т.е. имеет тип void, то вы- ражение в операторе return опускается. В этом случае необязателен и кам оператор return в теле функции. Необходимые коды команд воз- врата в точку вызова компилятор языка Си++ добавит в объектный модуль функции автоматически. В теле функции может быть и несколько операторов return. Оператор return можно использовать и в ^функции main. Если тип возвращаемого функцией main значения от- ечен от void, то это значение анализируется операционной системой. (Принято, что при благоприятном завершении программы возвра- щается значение 0. В противном случае возвращаемое значение от- шично от 0. [ Даже в том случае, когда функция не должна выполнять никаких [действий и не должна возвращать никаких значений, тело функции [будет состоять из фигурных скобок { ). (Такая функция может потре- боваться при отладке программы в качестве "заглушки”.) [ Примеры определений функций с разными сигнатурами: fvoid print (char *name, int value) // Ничего не возвращает cout « "\n" « name « value; // Нет оператора return > float min (float a, float Ь) // В функции два оператора / / возврата (if (а < b) return a; // Возвращает минимальное return b; I // из значении аргументов float cube(float x) // Возвращает значение типа float ( return x * x * x; // ) Возведение в куб вещественного числа int max(int n, int m) // Вернет значение типа int { return n < m ? m : n; } // Возвращает максимальное // из значении аргументов Void write (void) // Ничего не возвращает, ( // ничего не получает cout « "\П НАЗВАНИЕ: ”; II Всегда печатает одно и то же } Заголовок последней функции может быть записан без типа void в списке формальных параметров: void write () // Отсутствие параметров эквивалентно void Следующая функция вычисляет на плоскости несколько необыч- ное расстояние между двумя точками, координаты которых передают- ся как значение парамеров типа double:
172 Язык Си++ double Norma (double XI, double Yl, double X2, double Y2, double) { return X2 - XI > Y2 - Y1 ? X2 - XI : Y2 - Y1; } Последний параметр, специфицированный типом double, в тезк функции Normal) не используется. В дальнейшем без изменения ин терфейса можно изменить функцию Normal), добавив в нее еще один параметр. Например, функция сможет вычислять расстояние межд\ точками (Xi, yi), (Х2, Y2) на плоскости, используя более сложную мет рику, в которую будет входить показатель степени, передаваемый как значение последнего из параметров функции Normal). Например, так можно ввести Евклидову метрику. При обращении к функции, формальные параметры заменяются фактическими, причем соблюдается строгое соответствие параметров по типам. В отличие от своего предшественника - языка Си Си++ нс предусматривает автоматического преобразования в тех случаях, ког да фактические параметры не совпадают по типам с сответствующимп им формальными параметрами. Говорят, что язык Си++ обеспечиваем "строгий контроль типов". В связи с этой особенностью языка Си++ проверка соответствия типов формальных и фактических параметров выполняется на этапе компиляции. Строгое согласование по типам между формальными и фактиче скими параметрами требует, чтобы в модуле до первого обращения к функции было помещено либо ее определение, либо ее описание (прототип), содержащее сведения о ее типе (о типе результата, т.е. воз- вращаемого значения) и о типах всех параметров. Именно наличие такого прототипа либо полного определения позволяет компилятору выполнять контроль соответствия типов параметров. Прототип (описание) функции может внешне почти полностью совпадать с заго ловком ее определения: тип_функции имя__фунхции (спецификация_формальных_параметров); Основное различие - точка с запятой в конце описания (прототипа). Второе отличие - необязательность имен формальных параметров в прототипе даже тогда, когда они есть в заголовке опре- деления функции. Приведем прототипы определенных выше функций: void print (char *, int); // Опустили имена параметров float min(float a, float b) ; float cube(float x);
6. Функции, указатели, ссылки 173 B.nt max (int,. int m) ; //Опустили одно имя broid write (void); //Список параиетров может быть пустым oouble Norma (double, double, double, double, double,); j Обратите внимание на прототип функции Norma () , в котором Ьпущены имена всех параметров. Компилятор, "глядя" на прототип функции, "никогда не догадается" (да и не станет об этом гадать), есть Пи в определении функции конкретные имена формальных параметров. Именно поэтому не будет предупреждающих сообщений при обращении К функции Norma () с пятью фактическими параметрами вместо первых Четырех, которые реально используются в ее теле. 1 Обращение к функции (иначе - вызов функции) - это выражение с операцией "круглые скобки". Операндами служат имя функции (либо указатель на функцию) и список фактических параметров: имя_функции (список_фактических_параиетров) Значением выражения "вызов функции" является возвращаемое функцией значение, тип которого соответствует типу функции. Фактический параметр (аргумент) функции - это в общем случае выражение. Соответствие между формальными и фактическими пара- метрами устанавливается по их взаимному расположению в списках. Фактические параметры передаются из вызывающей программы в функцию по значению, т.е. вычисляется значение каждого выражения, представляющего аргумент, и именно это значение используется в теле ’функции вместо соответствующего формального параметра. Таким образом, спнсок_фактических_параиетров - это либо пусто, либо void, либо разделенные запятыми фактические параметры. Проиллюстрируем сказанное о функциях программой, в которую включим некоторые из приведенных выше функций. Предположим, что программа создается в виде одного модуля (т.е. в одном файле с расширением .срр); //Рб-01.СРР - определения, прототипы и вызовы функций # include <iostream.h> int max(int n, int m) // Определение до вызова функции { return n<m ? m : n; // Точка с запятой не нужна void main(void) // Главная функция (void print (char *, int);// Прототип до определения float cube(float x ® 0);//«Прототип до определения int sum = 5, k == 2; //Вложенные вызовы функции; sum = max((int)cube(float (k)), sum); print("\nsum в ”, sum);
174 Язык Си++ } void print(char * name, int value) 11 Определение функции { cout « ’’\n’’ « name « value; } float cube (float x) // Определение функции { return x * x * x; } Результат выполнения программы: sum = 8 Отметим необходимость преобразований типов фактических па раметров при вызовах функций max () и cube О. Преобразования тре буются для согласования типов формальных и фактических параметров. В иллюстративных целях формы записи преобразований взяты различными - каноническая и функциональная. Для функции max () прототип не потребовался - ее определение размещено в том же файле до вызова функции. Прототипы функций print () и cube () в программе необходимы, так как определения функций размещены после обращения к ним. Если в качестве эксперимента убрать (например, превратить в комментарий с помощью скобок /* */ или //) прототип любой из функций print() или cube(), то компилятор выдаст сообщение об ошибке. ВС++ делает это так: Error Р6-01.СРР 10: Function ’cube’ should have a prototype Такое же сообщение появится, если в нашей программе перенести определение функции тах() в конец модуля и не ввести прототипа в main. При наличии прототипов вызываемые функции не обязаны раз мещаться в одном файле (модуле) с вызывающей функцией, а могут оформляться в виде отдельных модулей либо могут находиться уже в оттранслированном виде в библиотеке объектных модулей. Сказанное относится не только к функциям, которые готовит программист для включения в свою программу, но и к функциям из стандартных би блиотек используемого компилятора. В последнем случае определения библиотечных функций, уже оттранслированные и оформленные в виде объектных модулей, находятся в библиотеке компилятора, а опи- сания функций в виде прототипов необходимо включать в программу дополнительно. Обычно это делают с помощью препроцессорных ко- манд tinclude <имя файла>
Глава 6. Функции, указатели, ссылки 175 f Здесь имя_фаила определяет текстовый (заголовочный) файл, со- держащий прототипы той или иной группы стандартных для данного компилятора функций. Например, в текстах практически всех напи- санных нами программ присутствует команда j tinclude <iostream.h> Которая из файла с именем iostream.h включает в программу описа- ния библиотечных классов и принадлежащих им функций для ввода и (вывода данных. (Из всех средств, описанных в файле iostream.h, мы (До сих пор использовали только объекты потокового ввода-вывода cout, cin и соответствующие им операции », «.) Попробуйте удалить Из любой работающей программы директиву linclude <iostream.h> (если, конечно, она в ней есть) и посмотрите на возмущенные сообще- ния компилятора - он перестанет "узнавать’* многие конструкции в тексте программы. Например, в нашей программе станет неизвестным символ cout. Подобно тому, как это сделано в библиотеке стандартных функ- ций компилятора, следует поступать и при разработке своих про- грамм, состоящих из достаточно большого количества функций, размещенных в разных модулях. Прототипы функций и описания внешних объектов (переменных, массивов и т.д.) помещают в отдель- ный файл, который препроцессорной командой linclude "имя файла" включают в начало каждого из модулей программы. В отличие от би- блиотечных функций компилятора имя такого заголовочного файла в команде #include записывается не в угловых скобках о, а в кавычках При этом не нужно беспокоиться об увеличении размеров созда- ваемой программы. Прототипы функций нужны только на этапе ком- пиляции и не переносятся в объектный модуль, т.е. не увеличивают машинного кода. А прототипы тех функций, которые не вызываются в модуле, вообще не используются компилятором. Например, для тех функций, которые мы определили выше, можно написать такой заголовочный файл: //EXAMPLE. НРР - прототипы функций иэ примеров: void print (char * = "номер страницы", int k » 1) ; float min(float a, float b); float cube(float x = 1); int max(int, int m ® 0) ;
176 ЯзыкСи++ void write(void); double Norma (double, double, double, double, double); Начальные (умалчиваемые) значения параметров. Как уже показа- но, в определении функции спецификация параметра может содержать его умалчиваемое значение. Это значение используется в том случае, если при обращении к функции соответствующий параметр опущен. При задании начальных (умалчиваемых) значений должно соблю- даться следующее соглашение. Если параметр имеет умалчиваемое значение, то все параметры, специфицированные справа от него, так- же должны иметь начальные значения. Например, можно так опреде лить функцию печати: void print (char* name = "Номер дома: ", int value = 1) ( cout « "\n" « name « value; } В зависимости от количества и значений фактических параметров в вызовах функции на экран будут выводиться такие сообщения: print(); // Выводит: 'Номер дома: 1' print ("Иомер комнаты: ") ; // Выводит: 'Номер комнаты: 1' print(,15); // Ошибка - можно опускать только параметры, // начиная с конца их списка В данном примере мы неудачно разместили параметры в списке Удобнее в использовании будет, наверное, такая функция вывода ни дисплей: void display(int value = 1, char *name = "Номер дома: ") ( cout « ”\n" « name « value; } Обращения к ней могут быть такими: di splay () ; // Выводит: 'Иомер дома: 1' display(15); // Выводит: 'Номер дома: 15' display (6, "Размерность пространства: ") ; // Выводит: // 'Размерность пространства: 6' 6.2. Функции с переменным количеством параметров В языках Си и Си++ допустимы функции, количество параметров у которых при компиляции определения функции не определено Кроме того, могут быть неизвестными и типы параметров. Количе-
Глава 6. Функции, указатели, ссылки 177 ство и типы параметров становятся известными только в момент вы- зова функции, когда явно задан список фактических параметров. При ‘определении и описании таких функций, имеющих списки параметров ^Неопределенной длины, спецификация формальных параметров за- канчивается многоточием. Формат прототипа функции с переменным списком параметров: тип имя (спецификация__явных_параметров, . . .) ; Здесь тип - тип возвращаемого функцией значения; имя - имя функции; спецификация_~явных_параметров - список спецификаций отдельных параметров, количество и типы которых фиксированы и Известны в момент компиляции. Эти параметры можно назвать обяза- тельными. После списка явных (обязательных) параметров ставится йеобязательная запятая, а затем многоточие, извещающее компиля- тор, что дальнейший контроль соответствия количества и типов па- раметров при обработке вызова функции проводить не нужно. Сложность в том, что у переменного списка параметров нет даже име- ни, поэтому непонятно, как найти его начало и где этот список закан- чивается. К сожалению, в программировании волшебство мало распростра- нено, и поэтому каждая функция с переменным списком параметров должна иметь механизм определения их количества и их типов. Прин- ципиально различных подходов к созданию этого механизма всего два. Первый подход предполагает добавление в конец списка реально использованных необязательных фактических параметров специаль- ного параметра-индикатора с уникальным значением, которое будет сигнализировать об окончании списка. В теле функции параметры последовательно перебираются, и их значения сравниваются с заранее известным концевым признаком. Второй подход предусматривает передачу в функцию значения реального количества фактических па- раметров. Значение реального количества используемых фактических параметров можно передавать в функцию с помощью одного из явно задаваемых (обязательных) параметров. В обоих подходах - и при задании концевого признака, и при указании числа реально исполь- зуемых фактических параметров - переход от одного фактического параметра к другому выполняется с помощью указателей, т.е. с ис- пользованием адресной арифметики. Проиллюстрируем сказанное примерами. Следующая программа включает функцию с изменяемым списком параметров, первый из которых (единственный обязательный) опре- 12-382!
178 Язык Си++ деляет число действительно используемых при вызове необязательных фактических параметров. //Р6-02.СРР - заданное количество необязательных параметров #include <iostream.h> // Функция суммирует значения своих // параметров типа int long summa(int k, ...) // k - число суммируемых параметров { int *pik = &k; long total s 0; for(; k; k—) total +» *(++pik); return total; } void main() { cout « ”\n summa(2, 6, 4) = ” « summa(2,6,4); cout « "\n summa(6, 1, 2, 3, 4, 5, 6) » M « summa 16,1,2,3,4,5,6); } Результат выполнения программы: summa(2, 6, 4) ~ 10 summa(6, 1, 2, 3, 4, 5, 6) == 21 Для доступа к списку параметров используется указатель pik типа int *. Вначале ему присваивается адрес явно заданного параметра к, т.е. он устанавливается на начало списка параметров в памяти (в сте- ке). Затем в цикле указатель pik перемещается по адресам следующих фактических параметров, соответствующих неявным формальным параметрам. С помощью разыменования *pik выполняется выборка их значений. Параметром цикла суммирования служит аргумент к, значение которого уменьшается на 1 после каждой итерации и, нако- нец, становится нулевым. Особенность функции - возможность рабо- ты только с целочисленными фактическими параметрами, так как указатель pik после обработки значения очередного параметра ’’перемещается вперед" на величину sizeof (int) и должен быть всег- да установлен на начало следующего параметра. Следующий пример содержит функцию для вычисления произве- дения переменного количества параметров. Признаком окончания списка фактических параметров служит параметр с нулевым значени- ем. //Р6-03.СРР - индексация конца переменного списка // параметров #include <iostream.h> / / Функция вычисляет произведение параметров:
‘лава 6. Функции, указатели, ссылки 179 Rouble prod (double arg, ...) ( double aa = 1.О; // Формируемое произведение double *prt = &arg; // Настроили указатель 11 на первый параметр if (*pr t »» 0.0) return 0.0; for (; *prt; prt++) aa ♦« *prt; 'j- return aa; ) Void main() ( double prod (double, . ..); // Прототип функции cout « ”\n prod(2e0, 4e0, 3e0, OeO) * ” « prod(2e0,4e0,3e0,OeO); cout « "\n prod(1.5, 2.0, 3.0, 0.0) * " « prod(1.5,2.0,3.0,0.0) ; cout « "\n prod(1.4, 3.0, 0.0, 16.0, 84.3, 0.0) = cout « prod(1.4,3.0,0.0,16.0,84.3,0.0); cout « "\n prod(OeO) = ” « prod(0e0); > Результат выполнения программы: prod(2e0, 4e0, 3e0, OeO) = 24 prod(1.5, 2.0, 3.0, 0.0) = 9 prod(1.4, 3.0, 0.0, 16.0, 84.3, 0.0) « 4.2 prod(OeO) » 0 В функции prod перемещение указателя prt по списку фактиче- ских параметров выполняется всегда за счет изменения prt на величи- ну sizeof(double). Поэтому все фактические параметры при обращении к функции prod О должны иметь тип double. В вызовах функции проиллюстрированы некоторые варианты задания пара- метров. Обратите внимание на вариант с нулевым значением пара- метра в середине списка. Параметры вслед за этим значением игнорируются. Чтобы функция с переменным количеством параметров могла воспринимать параметры различных типов, необходимо в качестве исходных данных каким-то образом передавать ей информацию о ти- пах параметров. Для однотипных параметров возможно, например, такое решение - передавать с помощью дополнительного обязатель - ного параметра признак типа параметров. Запишем функцию, выби- рающую минимальное из значений параметров, которые могут быть двух типов: или только long, или только int. Признак типа параметра будем передавать как значение первого обязательного параметра. Второй обязательный параметр определяет количество параметров, из 17*
180 Язык Си++ значений которых выбирается минимальное. В следующей программе предложен один из вариантов решения сформулированной задачи: //Р6-04.СРР - меняются тип и количество параметров функции #include <iostream.h> void main () ( long minimum(char z , int k, . . .) ; // Прототип функции cout « "\n\tminimum('1Tt 3, 10L, 20L, 30L) = ” « minimum(’1',3,10L,20L,30L); cout « "\n\tminimum(1i*, 4, 11, 2, 3, 4) = " « minimum (’i1,4,11,2,3,4); cout « "\n\tminimum ('k' , 2, 0, 64) » *' « minimum(' k •,2,0,64); } I / Функция с переменным списком параметров long minimum(char z, int k, ...) ( if (z «= * i') { int *pi » &k + 1; // Настроились на первый // необязательный параметр int min = *pi; // Значение первого /I необязательного параметра for(; k; k--, pi++) min = min > *pi ? *pi : min; return (long)min; } if (z « 11•) { long *pl ж (long*)(&k+l); long min « *pl; // Значение первого параметра for(; k; k™~, pl++) min « min > *pl ? *pl : min; return (long)min; } cout « "\пОшибка! Неверно задан 1-й параметр:"; return 2222L; } Результат выполнения программы: mini туп* ( 1 1 1 , 3, 10L, 20L, 30L) = 10 minimum ('i', 4, 11, 2, 3, 4) » 2 Ошибка! Неверно задан 1-й параметр: minimum('к’,2,0,64)=2222 В приведенных примерах функций с изменяемыми списками пара метров перебор параметров выполнялся с использованием адресной арифметики и явным применением указателей нужных типов. К про
Глава 6. Функции, указатели, ссылки 181 иллюстрированному способу перехода от одного параметра к другому нужно относиться с осторожностью. Дело в том, что при обращении к функции ее параметры помещаются в стек, причем порядок их размещения в стеке зависит от реализации компилятора. В компиляторах имеются опции, позволяющие изменять последовательность помещения рначений параметров в стек. Стандартная для языка Си++ шоследовательность размещения параметров в стеке предполагает, что (первым обрабатывается и помещается в стек последний из параметров (функции. При этом у него оказывается максимальный адрес (так стек (устроен в реализациях на IBM PC). Противоположный порядок Обработки и помещения в стек будет у функций, определенных и опи- ИСанных с модификатором pascal. Этот модификатор и его антипод - (модификатор cdecl являются дополнительными ключевыми словами, [определенными для компиляторов ТС++ и ВС++. Не останавливаясь [Подробно на возможностях, предоставляемых модификатором pascal, (отметим три факта. Во-первых, применение модификатора pascal [Необходимо в тех случаях, когда функция, написанная на языке Си или |Си++, будет вызываться из программы, подготовленной на Паскале. [Во-вторых, функция с модификатором pascal не может иметь [Переменного списка параметров, т.е. в ее определении и в ее прототипе [нельзя использовать многоточие. Третий факт имеет отношение к [разработке программ в среде Windows. Дело в том, что большинство из [функций библиотеки API (Application Programming Interface - интерфейс [прикладного программирования) для разработки приложений для млад- [щих версий системы Windows являются функциями, разработанными с [Использованием модификатора pascal. I Но вернемся к особенностям конструирования функций со списками [параметров переменной длины и переменных типов. Предложенный [выше способ передвижения по списку параметров имеет один [ существенный недостаток - он ориентирован на конкретный тип машин [и привязан к реализации компилятора. Поэтому функции могут ’ оказаться непереносимыми. / Для обеспечения мобильности программ с функциями, имеющими изменяемые списки параметров, в каждый компилятор языка Си (и языка Си++) стандарт предлагает включать специальный набор макроопределений, которые становятся доступными при включении в текст программы заголовочного файла stdarg.h. Макрокоманды, обеспечивающие простой и стандартный (не зависящий от реализации) способ доступа к конкретным спискам фактических параметров переменной длины, имеют следующий формат:
182 Язык Си++ void va__start (va_list param, последний_явныи_параметр) ; type va_arg(va_list param, type); void va_end(va_list param) ; Кроме перечисленных макросов, в файле stdarg.h определен спе- циальный тип данных va_iist, соответствующий потребностям об- работки переменных списков параметров. Именно такого типа должны быть первые фактические параметры, используемые при об- ращении к макрокомандам va_start() , va_arg() , va_end() . ОБЪЯС- НИМ порядок использования перечисленных макроопределений в теле функции с переменным списком параметров. Напомним, что каждая из функций с переменным списком параметров должна иметь хотя бы один явно специфицированный формальный параметр, за которым после запятой стоит многоточие. В теле функции обязательно опреде- ляется объект типа va_iist. Например, так: va__list factor; Определенный таким образом объект factor обладает свойствами указателя. С помощью макроса va_start() объект factor связывает- ся с первым необязательным параметром, т.е. с началом списка неиз- вестной длины. Для этого в качестве второго аргумента при обращении к макросу va_start() используется последний из явно специфицированных параметров функции (предшествующий много- точию): va_start (factor, послвднии__явныи__параметр) ; Рассмотрев выше способы перемещения по списку параметров с помощью адресной арифметики, мы уже знаем, что указатель factor сначала "нацеливается" на адрес последнего явно специфицированно- го параметра, а затем перемещается на его длину и тем самым устана- вливается на начало переменного списка параметров. Именно поэтому функция с переменным списком параметров должна иметь хотя бы один явно специфицированный параметр. Теперь с помощью разыменования указателя factor мы можем получить значение первого фактического параметра из переменного списка. Однако нам неизвестен тип этого фактического параметра. Как и без использования макросов, тип параметра нужно каким-то образом передать в функцию. Если это сделано, т.е. определен тип type очередного параметра, то обращение к макросу va__arg (factor, type)
Глава 6. Функции, указатели, ссылки 183 позволяет, во-первых, получить значение очередного (вначале перво- '< го) фактического параметра типа type. Вторая задача макрокоманды va_arg() - заменить значение указателя factor на адрес следующего фактического параметра в списке. Теперь, узнав каким-то образом тип, например typel, этого следующего параметра, можно вновь об- ратиться к макросу: > va_arg (factor, typel) Это обращение позволяет получить значение следующего факти- ческого параметра и переадресовать указатель factor на фактический параметр, стоящий за ним в списке, и т.д. Примечание. Реализация ТС++ и ВС++ запрещает [6] использовать с мак- рокомандой va__arg () типы char, unsigned char, float. Макрокоманда va_end() предназначена для организации кор- ректного возврата из функции с переменным списком параметров. Ее единственным параметром должен быть указатель типа va_iist, ко- торый использовался в функции для перебора параметров. Таким об- разом, для наших рассуждений вызов макрокоманды должен иметь вид va__end (factor) ; Макрокоманда va_end() должна быть вызвана после того, как функция обработает весь список фактических параметров. Макроко- манда va_end() обычно модифицирует свой аргумент (указатель типа va_list), и поэтому его нельзя будет повторно использовать без предварительного вызова макроса va_start (). Для иллюстрации особенностей использования описанных макро- сов рассмотрим следующую программу, в которой определена и ис- пользуется функция для конкатенации любого количества символьных строк. Строки, предназначенные для соединения в одну строку, передаются в функцию с помощью списка указателей- параметров. В конце списка неопределенной длины всегда помещается нулевой указатель null. //Р6-05.СРР - макросредства для переменного списка // параметров ftinclude <iostream.h> ftinclude <string.h> // Для работы со строками ttinclude <stdarg.h> // Для макросредств ftinclude <stdlib.h> // Для функции nalloc() char *concat(char *sl, ...)
184 Язык Си++ { va_list par; // Указатель на параметры списка char *ср « si; int len « strlen(sl) ; // Длина 1-го параметра va_start(par, al); // Начало переменного списка // Цикл для определения общей длины параметров-строк: while (ср » va__arg(par, char *)) len +“ strlen(cp); // Выделение памяти для результата: char *stroka « (char *)malloc(len + 1); strepy(stroke, si); va_start(par, si); // Начало переменного списка // Цикл конкатенации параметров строк: while (ср “ va_arg(par, char *)) street(stroke, ср); // Конкатенация двух строк va_end(par) ; return stroke; } void main() { char* concat(char* si, . ..); // Прототип функции char* s; // Указатель для результата s « concat("\nNulla ", "Dies ", "Sine ", "Lines’", NULL); s « concat(s, " - Ни одного дня без черточки!", "\n\t", " (Плинии Старший о художнике Апеллесе) ", NULL) ; cout « s; ) Результат выполнения программы: Nulla Dies Sine Lines! - Ни одного дня без черточки! (Плиний Старший о художнике Апеллесе) В приведенной функции concatO тип параметров заранее из- вестен и фиксирован. В ряде случаев полезно иметь функцию, пара- метры которой изменяются как по числу, так и по типам. В этом случае, как уже говорилось, нужно сообщать функции о типе очеред- ного фактического параметра. Поучительным примером таких функ- ций служат библиотечные функции форматного ввода/вывода языка Си: printf(char* format, ...); scanf(char* format, ...); В обоих функциях форматная строка, связанная с указателем format, содержит спецификации преобразования (%d - для десятичных чисел, %е - для вещественных данных в форме с плавающей точкой, %f - для вещественных значений в форме с фиксированной точкой и т.д.). Кроме того, эта форматная строка в функции printf() может содер-
Глава 6. Функции, указатели, ссылки 185 жать произвольные символы, которые выводятся на дисплей без како- го-либо преобразования. Чтобы продемонстрировать особенности построения функций с переменным числом параметров, классики язы- ка Си [3] рекомендуют самостоятельно написать функцию, подобную функции print?(). Последуем их совету, применяя простейшие сред- ства вывода языка Си++. Разрешим использовать только специфика- ции преобразования "%d" и "%f". //Р6-06.СРР - упрощенный аналог print?О //По мотивам K&R, (3] , стр. 152 #include <iostream.h> tinclude <stdarg.h> // Для макросредств переменного списка / / параметров void miniprint(char *format, . ..) { va_liat ар; // Указатель на необязательный параметр char *р; // Для просмотра строки ?ormat int ii; // Целые параметры double dd; // Параметры типа double va start (ар, format); // Настроились на первый параметр ?ог (р ж format; *р; р++) ( if (*р ’= '%') ( cout « *р; continue; switch (*++р) { case 'd*: ii = va_arg(ap,int); cout « ii; break; case '?': dd = va_arg(ap,double); cout « dd; break; default: cout « *p; } // Конец переключателя } // Конец цикла просмотра строки-формата va_end(ap); // Подготовка к завершению функции ) void main() ( void miniprint(char *, ...); // Прототип int k « 154; double e = 2.718282; miniprint("\пЦелое k « %d,\t4Mcno e s %f", k, e); ) Результат выполнения программы: Целое k - 154, число е “ 2.718282
186 Язык Си++ Интересной особенностью предложенной функции miniprint () п ее серьезных прародителей - библиотечных функций языка Си printf () и scanf () - является использование одного явного параметра и дли задания типов последующих параметров, и для определения их коли чества. Для этого в строке, определяющей формат вывода, записывается последовательность спецификаций, каждая из которых начинается символомКоличество спецификаций должно быть в точности равн<» количеству параметров в следующем за форматом списке. Конец обмени и перебора параметров определяется по достижению конца строки формата. 6.3. Рекурсивные функции В классической работе Д. Баррона1, анализируя соотношение междх рекурсией и итеративными методами, автор в шутливой форме утверждает, что самой страшной "ересью" в программировании счи талась вера (или неверие) в рекурсию. Именно в связи с неоднозначным отношением к рекурсии средства для ее реализации либо вовсе нс включались в создаваемые языки программирования, либо языки программирования не реализовывали самые очевидные итерационны! методы. В настоящее время дискуссии о целесообразности рекурсии можно считать законченными. В публикациях Н. Вирта1 2 и в работах других авторов достаточно четко очерчены границы эффективности применения рекурсивного подхода. Его рекомендуют избегать в тех случаях, когда есть очевидное итерационное решение. Например классический метод рекурсивного определения факториала удобен для объяснения понятия рекурсии, однако не дает никакого практическое < выигрыша в программной реализации. Рекурсивные алгоритмы эффективны в тех задачах, где рекурсия использована в определении обрабатываемых данных. Поэтому серьезное изучение рекурсивных методов нужно проводить, вводя такие динамические структуры данных как стеки, деревья, списки, очереди и другие данные, данные < рекурсивной структурой. Здесь же рассмотрим только принципиальны! возможности, которые предоставляет язык Си++ для организации рекурсивных алгоритмов. 1 Баррон Д. Рекурсивные методы в программировании. - М.: Мир, 1974 MACDONALD: LONDON, 1969. 2 Вирт Н. Алгориты и структуры данных. - М.: Мир, 1989.
Глава 6. Функции, указатели, ссылки 187 I Предварительно отметим, что различают прямую и косвенную ре- курсии. Функция называется косвенно рекурсивной в том случае, если она содержит обращение к другой функции, содержащей прямой или । косвенный вызов определяемой (первой) функции. В этом случае по тексту определения функции ее рекурсивность (косвенная) может быть не видна. Если в теле функции явно используется вызов этой функции, то имеет место прямая рекурсия, т.е. функция, по определению, рекур- сивная (иначе - самовызываемая или самовызывающая: self-calling). Классический пример - функция для вычисления факториала неотри- цательного целого числа. long fact (int k) ( if (k < 0) return 0; if (k = 0) return 1; return k * fact(k-l); ) Для отрицательного аргумента результат по определению факто- риала не существует. В этом случае функция возвратит нулевое значе- ние. Для нулевого параметра функция возвращает значение 1, так как, по определению, О» равен 1. В противном случае вызывается та же функция с уменьшенным на 1 значением параметра и результат умно- жается на текущее значение параметра. Тем самым для положительно- го значения параметра к организуется вычисление произведения к * (к-1) * (к-2) * ... *3*2*1*1 Обратите внимание, что последовательность рекурсивных обращений к функции fact прерывается только при вызове fact(0). Именно этот вызов приводит к последнему значению 1 в произведении, так как последнее выражение, из которого вызывается функция, имеет вид: 1 * fact(1-1) Так как в языке Си++ отсутствует операция возведения в степень, то следующая рекурсивная функция вычисления целой степени веще- ственного ненулевого числа может оказаться полезной: double expo(double a, int n) { if (n 0) return 1; if (a «« 0) return 0; if (n > 0) return a * expo(a, n-1); if (n < 0) return expo(a, n+1) I a; }
188 Язык Си++ При обращении вида expo (2.0, 3) рекурсивно выполняются вы зовы функции expo () с изменяющимся вторым аргументом expo (2.0,3), expo (2.0,2), expo (2.0,1), expo (2.0,0). При ЭТИХ вы зовах последовательно вычисляется произведение 2.0 * 2.0 * 2.0 * 1 и формируется нужный результат. Вызов функции для отрицательного значения степени, например expo (5.0, -2) эквивалентен вычислению выражения ехро(5.0,0) / 5.0 / 5.0 Отметим некоторую математическую неточность. В функции expo () для любого показателя при нулевом основании результат ра вен нулю, хотя возведение в нулевую степень нулевого основания должно приводить к ошибочной ситуации. Рекурсивный алгоритм можно применить для определения раз биений целых чисел1. Разбиениями целого числа называют способы его представления в виде суммы целых чисел. Например, разбиениями числа 4 являются 4, 3 + 1, 2 + 2, 2 + 1 + 1, 1 + 1 + 1 + 1. Для подсчет числа различных разбиений произвольного целого n удобно восполь зоваться вспомогательной функцией q(m,n), которая подсчитываем количество способов представления целого m в виде суммы при уело вии, что каждое слагаемое не превосходит значения п. Определив та кую функцию, можно вычислить число различных разбиений произвольного к как значение q(N,N). Функция q(m,n) при n = i или m = 1 должна возвращать значение 1. Если m <- п, то результат определяется выражением 1 + q(m,т-1). В противном случае,т.е. при m > n, q(m,n) равно сумме: q(m,n-1) + q(m-n,n). В соответствии < этими соотношениями определим "прямолинейную” и малоэффек тивную рекурсивную функцию; int q(int m, int n) ( if (m 1 || n “ 1) return 1; if (m <= n) return 1 + q(m, m-1); return (q(m,n-l) + q(m-n,n)); ) Неэффективность функции связана с тем, что некоторые значения q будут вычисляться многократно. Более рациональное решение зада 1 Баррон Д. Рекурсивные методы в программировании - М.: Мир, 1974: MACDONALD: LONDON, 1969.
Глава 6. Функции, указатели, ссылки 189 чи можно получить, если ввести таблицу значений q и вычислять только те значения, которые действительно нужны. Таблицу рекомен- дуется реализовать в виде динамического списка. Средства для созда- ния динамических списков мы еще не рассмотрели... В качестве еще одного примера рекурсии рассмотрим функцию определения с заданной точностью eps корня уравнения fix) = 0 на отрезке [a, 6]. Предположим для простоты, что исходные данные за- даются без ошибок, т.е. еря > 0, b > at f(a) * f(b) < 0, и вопрос о воз- можности нескольких корней на отрезке [а, Ь] нас не интересует. Не очень эффективная рекурсивная функция для решения поставленной задачи содержится в следующей программе: //Р6-07.СРР - рекурсия при определении корня математической // функции tinclude <iostream.h> tinclude <math.h> // Для математических функций tinclude <stdlib.h> // Для функции exit() // Рекурсивная функция для поиска корня методом // деления пополам: double recRoot(double f(double), double a, double b, double eps) ( double fa = f(a), fb = f(b), c, fc; if (fa * fb > 0) ( cout « "ХпНеверен интервал локализации корня’’’; exit(l); ) с = (а + b)/2.0; fc » f(с) ; if (fc “0.0 || b - а < eps) return c; return (fa * fc < 0.0) ? recRoot(f, a, c, eps) recRoot(f, c, b, eps); ) static int counter « 0; // Счетчик обращений к тестовой // функции void main() ( double root, A = 0.1, // Левая граница интервала В = 3.5, // Правая граница интервала EPS ш 5е-5; // Точность локализации корня double giper(double); // Прототип тестовой функции root = recRoot(giper, А, В, BPS); cout « ”\пЧисло обращений к тестовой функции = 11 « counter; cout « "\пКорень = ” « root; ) // Определение тестовом функции:
190 Язык Си++ double gipsr(double x) { extern int counter; counter++; // Счетчик обращении return (2.0/x * cos(x/2.0)); Результат выполнения программы: Число обращений к тестовой функции = 54 Корень = 3.141601 В рассматриваемой программе пришлось использовать библио течную функцию exito, специфицированную в заголовочном файле process.h Функция exito позволяет завершить выполнение про граммы и возвращает операционной системе значение своего пари метра. Неэффективность предложенной программы связана, например, < излишним количеством обращений к программной реализации функ ции, для которой определяется корень. При каждом рекурсивном вы зове recRooto повторно вычисляется значение f (a), f (b), хотя они уже известны после предыдущего вызова. Предложите свой вариан। исключения лишних обращений к f о при сохранении рекурсивности 6.4. Подставляемые (инлайн-) функции Некоторые функции в языке Си++ можно определить с использо ванием специального служебного слова inline. Спецификатор inline позволяет определить функцию как встраиваемую, иначе говоря под ставляемую или "открыто подставляемую" [2], или "инлайн-функцию [19]. Например, следующая функция определена как подставляемая: inline float module(float x = 0, float у = 0) ( return sqrt(x * x + у * y) ; } Функция module О возвращает значение типа float, равное "расстоянию" от начала координат на плоскости до точки с коорди натами (х,у), определяемыми значениями фактических параметров В геле функции вызывается библиотечная функция sqrt() для вычис ления вещественного значения квадратного корня положительного аргумента. Так как подкоренное выражение в функции всегда неотри цательно, то специальных проверок не требуется. Обрабатывая каж дый вызов встраиваемой функции, компилятор "пытается" подставить в текст программы код операторов ее тела. Спецификатор inline для
Глава 6. Функции, указатели, ссылки 191 функций, не принадлежащих классам (о последних будем говорить в Связи с классами), определяет для функций внутреннее связывание. Во Всех других отношениях подставляемая функция является обычной i функцией, т.е. спецификатор inline в общем случае не влияет на ре- зультаты вызова функции, она имеет обычный синтаксис определения 1 и описания, подчиняется всем правилам контроля типов и области действия. Однако вместо команд передачи управления единственному Е экземпляру тела функции компилятор в каждое место вызова функции ! помещает соответствующим образом настроенные команды кода опе- । раторов тела функции. Тем самым при многократных вызовах под- ставляемой функции размеры программы могут увеличиться, однако исключаются затраты на передачи управления к вызываемой функции и возвраты из нее. Как отмечает проект стандарта Си++, кроме эко- ; номии времени при выполнении программы, подстановка функции позволяет проводить оптимизацию ее кода в контексте, окружающем вызов, что в ином случае невозможно [2]. ' Наиболее эффективно использовать подставляемые функции в тех случаях, когда тело функции состоит всего из нескольких операторов. ; Идеальными претендентами на определение со спецификатором inline являются несложные короткие функции, подобные тем, которые в ка- ’ честве примеров использовались в п. 6.1. Удобны для подстановки ; функции, основное назначение которых - вызов других функций либо выполнение преобразований типов. Так как компилятор встраивает код подставляемой функции вмес- то ее вызова, то определение функции со спецификатором inline долж- но находиться в том же модуле, что и обращение к ней, и размещается до первого вызова. Синтаксис языка не гарантирует обязательной подстановки кода функции для каждого вызова функции со специфи- катором inline. Более того, "определение допустимости открытой под- становки функции в общем случае невозможно" [2]. Например, J. следующая функция, по-видимому, не может быть реализована как подставляемая даже в том случае, если она определена со специфика- , тором inline [2]: inline void f() { char ch « 0; if (cin » ch ££ ch ! 'q') f(); } ' Функция f () в зависимости от вводимого извне значения пере- I менной ch либо просто возвращает управление, либо рекурсивно вы- зывает себя. Допустима ли для такой функции подстановка вместо стандартного механизма вызова? Это определяется реализацией...
192 Язык Си++ Следующий случай, когда подстановка для функции со специфи кагором inline проблематична, - вызов этой функции с помощью ука зателя на нее (т.е. с помощью ее адреса, см., например, п.6.6) Реализация такого вызова, как правило, будет выполняться с по мощью стандартного механизма обращения к функции. Проект стандарта [2] перечисляет следующие причины, по кого рым функция со спецификатором inline будет трактоваться как обыч ная функция (не подставляемая): • встраиваемая функция слишком велика, чтобы выполнить ее подстановку; • встраиваемая функция рекурсивна; • обращение к встраиваемой функции в программе размещено до ее определения; • встраиваемая функция вызывается более одного раза в выраже нии; • встраиваемая функция содержит цикл, переключатель или one ратор перехода. Ограничения на выполнение подстановки в основном зависят о t реализации. В компиляторе ВС++ принято, что функция со специфи кагором inline не должна быть рекурсивной и не может содержать операторов for, while, do, switch, goto. При наличии таких служебных слов в теле подставляемой функции компилятор выдает сообщение об ошибке. Если же для функции со спецификатором inline компилятор не мо жет выполнить подстановку из-за контекста, в который помещено обращение к ней, то функция считается статической (static) и выдается предупреждающее сообщение. Хотя теоретически подставляемая функция ничем не отличается по результатам от обычной функции, существует несколько особенно стей, которые следует учитывать, "подсказывая" компилятору с по мощью спецификатора inline в определении функции целесообраз ность подстановок. Так как порядок вычисления фактических параметров функций нс определен синтаксисом языка Си++, то возможна различная реализа ция вычисления фактических параметров при обычном вызове функ ции и при ее подстановке. Проект стандарта предупреждает [2], что возможна ситуация, koi да функция со спецификатором inline в одной и той же программе имеет две формы реализации вызова - подстановкой и стандартным механизмом обращения к функции. При этом разные формы вызова могут реализовывать разный Порядок вычисления фактических пара
Глава 6. Функции, указатели, ссылки 193 Метров, и два обращения к одной и той же функции с одинаковыми ^фактическими параметрами могут привести к различным результа- там. Еще одна особенность подставляемых функций - невозможность Их изменения без перекомпиляции всех частей программы, в которых >Эти функции вызываются. 6.5. Функции и массивы Массивы могут быть параметрами функций, и функции могут возвращать указатель на массив в качестве результата. Рассмотрим эти возможности. При передаче массивов через механизм параметров возникает за- дача определения в теле функции количества элементов массива, ис- пользованного в качестве фактического параметра. При работе со строками, т.е. с массивами типа chart], последний элемент каждого из которых имеет значение '\0», затруднений практически нет. Ана- лизируется каждый элемент, пока не встретится символ АО’, и это считается концом строки-массива. В следующей программе введена функция len () для определения длины строки, передаваемой в функ- цию с помощью параметра: //Р6-08.СРР - массивы-строки в качестве параметров tinclude <iostream.h> // Для ввода-вывода int len(char е[]) ( int m « 0; while (e[m++J) ; return m - 1; } void main() { char E[] = "Pro Tempore!"; // "Своевременно" (лат.) cout « "\пДлина строки \"Pro Tempore!равна " « len(E) ; } ! Результат выполнения программы: Длина строки "Pro Tempore!" равна 12 В функции len () строка-параметр представлена как массив, и об- ращение к его элементам выполняется с помощью явного индексиро- вания. 13-3X21
194 Язык Си++ Если массив-параметр функции не есть символьная строка, п нужно либо использовать только массивы фиксированного, заране определенного размера, либо передавать значение размера массива и функцию явным образом. Часто это делается с помощью дополни тельного параметра. Следующая программа иллюстрирует эту во* можность на примере функции для вычисления косинуса угла межд двумя многомерными векторами, каждый из которых представлен одномерным массивом-параметром: //Р6-09.СРР - одномерные массивы в качестве параметров finclude <iostream.h> // Для ввода-вывода #include <math.h> // Для математических функций float cosinus(int n, float x[], float y[]) { float a 0, b » 0, с 0; for (int i » 0; i < n; i++) { a +- x[i] * y[i]; b += x[i] * x[i]; с +« у[i] * у [i] ; } return a/sqrt(double(b * c)); } void mainO { float E[] « { 1, 1, 1, 1, 1, 1, 1 }; float G[] = { -1, -1, -1, -1, -1, -1, -1 }; cout « "\пКосинус = " « cosinus (7, E, G) ; } Результат выполнения программы: Косинус ж -1 Так как имя массива есть указатель, связанный с началом массива то любой массив, используемый в качестве параметра, может быв изменен за счет выполнения операторов тела функции. Например, । следующей программе функция max__yect() формирует массив z каждый элемент которого равен максимальному из соответствующи значений двух других массивов-параметров (х и у): //Р6-10.СРР - указатели на одномерные массивы в качестве // параметров finclude <iostream.h> void max__vect (int n, int *x, int *y, int *z) { for (int i » 0; i < n; i++) z[i] = x[i] > y[i] ? x[i] : y[i]; } void main()
Глава 6. Функции, указатели, ссылки 195 { int а[] а { 1, 2, 3, 4, 5, 6, 7}; int Ь[] = { 7, 6, 5, 4, 3, 2, 1}; int с[7]; max_vect(7, а, b, с) ; for (int i » 0; i < 7; i++) cout « "\t" « c[i]; } Результат выполнения программы: 7 6 5 4 5 6 7 Как и в функции cosinusO, параметр int п служит для опреде- ления размеров массивов-параметров. В качестве функции, возвращающей указатель на массив, рас- смотрим функцию, формирующую новый массив на основании двух целочисленных массивов, элементы в каждом из которых упорядоче- ны по неубыванию. Новый массив должен включать все элементы двух исходных массивов таким образом, чтобы они оказались упоря- доченными по неубыванию. //Р6-11.СРР - функция, возвращающая указатель на массив tinclude <iostream.h> // Функция ’’слияния” двух упорядоченных массивов int *fusion(int n, int* a, int m, int* b) ( int *x » new int[n + m]; // Массив с результатом int ia 0, ib « 0, ix = 0; while (ia < n fifi ib < m) // Цикл до конца одного из // массивов if (a[ia] > b[ib]) x[ix++] - b[ib++]; else x[ix++] ® a[ia++J; if (ia >« n) // Переписан массив a[] while (ib < m) x[ix++] = b[ib++); else // Переписан массив b[] while (ia < n) x[ix++] « a[ia++J; return x; } void main(void) { int c[] = ( 1, 3, 5, 7, 9 }; int d[] - { 0, 2, 4, 5 }; int *h; // Указатель для массива с результатом int kc » sizeof(c)/sizeof(с[0]); // Количество элементов // в с[] int kd = sizeof(d)/sizeof(d[0)); // Количество элементов // в d[] h — fusion(kc, c, kd, d); 13
196 Язык Си++ cout « ”\пРвзультат объединения массивов:\п"; for (int i » 0; i < kc + kd; i++) cout « " ” « h[i]; delete[] h; } Результат выполнения программы: Результат объединения массивов: 012345579 Особенность и в некотором смысле недостаток языка Си++ (и его предшественника языка Си) - несамоопредсленность массивов, под которой понимается невозможность по имени массива (по указателю на массив) определять его размерность и размеры по каждому измере- нию. Несамоопределенность массивов затрудняет их использование в качестве параметров функций. Действительно, простейшая функция - транспонирование квадратной матрицы - требует, чтобы ей были из- вестны не только имя массива, содержащего элементы матрицы, но и размеры этой матрицы. Если такая функция транспонирования мат- рицы для связи по данным использует аппарат параметров, то в число параметров должны войти указатель массива с элементами матрицы и целочисленный параметр, определяющий размеры матрицы. Однако здесь возникают затруднения, связанные с одним из принципов язы- ков Си и Си++. По определению, многомерные массивы как таковые не существуют. Если мы описываем массив с несколькими индексами, например, так: double prim[6][4][2]; то мы описываем не трехмерный массив, а одномерный массив с име- нем prim, включающий шесть элементов, каждый из которых имеет тип double[4] [2]. В свою очередь, каждый из этих элементов есть одномерный массив из четырех элементов типа double[2]. И, нако- нец, каждый из этих элементов является массивом из двух элементов типа double. Мы не случайно так подробно еще раз остановились на особенно- стях синтаксиса и представления многомерных массивов. Дело в том, что эти тонкости не бросаются в глаза при обычном определении мас- сива, когда его размеры (и размерность) фиксированы и явно заданы в определении. Однако при необходимости передать с помощью пара- метра в функцию многомерный массив начинаются неудобства и не- приятности. Вернемся к функции транспонирования матрицы.
"лава 6. Функции, указатели, ссылки 197 Наивное, неверное и очевидное решение - определить заголовок функции таким образом: void transponir (double х[][], int n) ... Здесь n - предполагаемый порядок квадратной матрицы; double *[![]; - попытка определить двухмерный массив с заранее неиз- вестными размерами. На такую попытку транслятор отвечает гнев- ным сообщением: Error ...: Size of type is unknown or zero И он прав - при описании массива (и при спецификации массива- параметра) неопределенным может быть только первый (самый ле- вый) размер. Вспомним - массив всегда одномерный, а его элементы должны иметь известную и фиксированную длину. В массиве х [ ] [ ] не только неизвестно количество элементов одномерного массива, но и ничего не сказано о размерах этих элементов. Примитивнейшее разрешение проблемы иллюстрирует следующая программа: //Р6-12.СРР - многомерный массив в качестве параметра finclude <iostream.h> // Очень неудачная функция для транспонирования матриц void transp(int n, float d[][3]) { float r; • for (int i=0; i < n - 1; i++) for (int j » i + 1; j < n; j++) ( r « d[i][j]; d[i] [j] =» d[j] [i] ; d[jl [i] =» r; } } Void main() { float x[3] [3] « { 0, 1, 1, 2, 0, 1, 2, 2, 0 }; int n = 3; transp(3 ,x); for (int i = 0; i < n; i++) // Цикл перебора строк { cout « "\n строка " « (i+1) « for (int j ~ 0; j < n; j++) // Цикл печати элементов // строки cout « ”\t" « x[i][j]; } }
198 Язык Си++ Результат выполнения программы: строка 1: 0 2 2 строка 2: 1 0 2 строка 3: 1 1 О Примитивность и нежизненность продемонстрированного реше ния состоят в том, что в функции transp() массив-параметр специ- фицирован с фиксированным вторым размером, т.е. транспонируемая квадратная матрица может быть только с размерами 3 на 3. Указанные ограничения на возможность применения многомер ных массивов в качестве параметров можно обойти несколькими пу тями. Первый путь - подмена многомерного массива одномерным и имитация внутри функции доступа к многомерному массиву. (Здесь будут полезны макроопределения с индексами в качестве параметров.) Второй путь - использование вспомогательных массивов указателей на массивы. Третий путь предусматривает применение классов для представления многомерных массивов. Об этом будет сказано позже. Подробно остановимся только на представлении и передаче мат риц (двухмерных массивов) с использованием вспомогательных мае сивов указателей на одномерные массивы. Одномерные массивы служат в этом случае для представления строк матриц. Так как и вспомогательный массив указателей, и массивы-строки матрицы яв ляются одномерными, то их размеры могут быть опущены в соответ ствующих спецификациях формальных параметров. Тем самым появляется возможность обработки в теле функции двухмерных, а и более общем случае и многомерных массивов с изменяющимися раз мерами. Конкретные значения размеров должны передаваться в тело функции либо с помощью дополнительных параметров, либо с ис пользованием глобальных (внешних) переменных. Следующая программа иллюстрирует один из способов передачи в функцию информации о двухмерном массиве, размеры которого заранее неизвестны. Функция trans о выполняет транспонирование квадратной матрицы, определенной вне тела функции в виде двухмер кого массива. Параметры функции: int п - порядок матрицы; double *p[i] - массив указателей на одномерные массивы элементов типа double. В теле функции обращение к элементам обрабатываемого мас- сива осуществляется с помощью двойного индексирования. Здесь р [i] - указатель на одномерный массив (на строку матрицы с элементами типа double), p[i] [j] - обращение к конкретному элементу двухмер ного массива. Текст программы:
Глава 6. Функции, указатели, ссылки 199 //Рб-13.СРР - вспомогательный массив указателей на массивы tinclude <iostream.h> // Функция транспонирования квадратных матриц void trans(int n, double *p[J) ( double x; for (int i = O;i<n-l; i++) for (int j = i + 1; j < n; j++) ( x » p[i][j]; p[i][j] = p[j][il; p[j] [i] = x; } void main() ( // Заданный массив - матрица, подлежащая // транспонированию: double А[4] [4] - { 11, 12, 13, 14, 21, 22, 23, 24, 31, 32, зз, 34, 41, 42, 43, 44 } ; // Вспомогательный одномерный массив указателей: double *ptr[] а { (double *)£А[0], (double *)&A[1], (double *)fcA[2], (double *)fcA[3] }; int n « 4; trans(n, ptr); // Печать результатов обработки матрицы: for (int i a 0; i < n; i++) // Цикл перебора строк { cout « "\n строка ” « (i+1) « // Цикл печати элементов строки: for (int j « 0; j < n; j++) cout « "\tM « A[i][j]; } ) Результаты выполнения программы: строка 1: 11 21 31 41 строка 2: 12 22 32 42 строка 3: 13 23 33 43 строка 4: 14 24 34 44 В основной программе матрица представлена двухмерным масси- вом с фиксированными размерами double А[4] [4]. Такой массив нельзя непосредственно использовать в качестве фактического пара- метра вместо формального параметра со спецификацией double *р[]. Поэтому вводится дополнительный вспомогательный массив указателей double *ptr f ]. В качестве начальных значений элементам
200 Язык Си++ этого массива присваиваются адреса строк матрицы, т.е. &а[0] &а[1], &л[2], &А[3], преобразованные к типу double*. Дальнейшее очевидно из текста программы. В следующей программе матрица формируется в основной про грамме как совокупность одномерных динамических массивов строг матрицы и динамического массива указателей на эти массивы-строки Элементы массива указателей имеют тип int*, с массивом указателен в целом связывается указатель int **pi. Для простоты опущены про верки правильности выделения памяти и выбрано фиксированное значение (т — з) порядка матрицы. Функция fill() присваиваем элементам квадратной матрицы значения "подряд": а[0][0] = о а[0] [1] =1 ...и т.д., т.е. a[i] [ j] » (i * n) + j, где n - порядоа матрицы. В иллюстративных целях указатель mat на массив указате лей на строки матрицы специфицирован в заголовке без использова ния квадратных скобок: int** mat. Первый из параметров функции fill О со спецификацией int п определяет размеры квадратной Mai рицы. Текст программы: //Р6-14.СРР - матрица как набор одномерных массивов tfinclude <iostream.h> // Функция, определяющая значения элементов матрицы void fill(int n, int** mat) { int k » 0; for (int i = 0; i < n; i++) for (int j == 0; j < n; j++) mat[i][j] = k++; } // Динамически© массивы для представления матрицы void main() { int **pi; // Указатель на массив указателей int m = 3; // Размеры массивов, т.е. порядок матрицы pi - new int* [mJ; // Вспомогательный массив указателей for (int i = 0; i < m; i++) pi[i] = new int [m] ; // Формируем строки (одномерные // массивы) fill(m, pi); // Заполнение матрицы for (i e 0; i < m; i++) // Цикл перебора строк { cout « '*\n строка ” « (i+1) « ”: ”; // Цикл печати элементов строки: for (int 3 = 0; j < m; 3++) cout « "\t" « pi[i][jl; } for (i = 0; i < m; i++) delete pi[ i 1;
Глава 6. Функции, указатели, ссылки 201 delete[] pi; Результаты выполнения программы: * строка 1: строка 2: строка 3: 0 12 3 4 5 6 7 8 Так как матрица создается как набор динамических массивов, то в конце программы помещены операторы delete для освобождения па- мяти. Многомерный массив с переменными размерами, сформирован- ный в функции, непосредственно невозможно вернуть в вызывающую ' программу как результат выполнения функции. Однако возвра- , щаемым функцией значением может быть указатель на одномерный массив указателей на одномерные массивы с элементами известной размерности и заданного типа. В следующей программе функция •inglejnatr () возвращает именно такой указатель, так как имеет тип int **. В тексте функции формируется набор одномерных масси- 1 вов с элементами типа int и создается массив указателей на эти одно- мерные массивы. Количество одномерных массивов и их длины определяются значением параметра функции, специфицированного как int п. Совокупность создаваемых динамических массивов пред- ставляет квадратную матрицу порядка н. Диагональным элементам ' матрицы присваиваются единичные значения, остальным - нулевые, , т.е. матрица заполняется как единичная диагональная. Локализован- ный в функции single jnatr () указатель int** р "настраивается" на создаваемый динамический массив указателей и используется в опера- торе возврата из функции как возвращаемое значение. В основной программе вводится с клавиатуры желаемое значение порядка матри- [ цы (int п), а после ее формирования печатается результат. Текст про- ; граммы: //Р6-15.СРР - единичная диагональная матрица с изменяемым // порядком 1 ftinclude <iostream.h> 11 Для ввода-вывода ftinclude <process.h> // Для exit О !' // Функция, формирующая единичную матрицу: int **single_matr(int n) // n - нужный размер матрицы ( // Вспомогательный указатель на формируемую матрицу: int** р; // Массив указателей на строки ~ одномерные массивы: р = new int* [п];
202 Язык Си++ if (р NULL) ( cout « "Не создан динамический массив!"; exit(1); } // Цикл создания одномерных массивов: for (int i “ 0; i < n; i++) { // Формирование строки элементов типа int: p[i] « new int [n]; if (p[i] « NULL) { cout « "He создан динамический массив!"; exit(1); } // Внутренний цикл заполнения строки: for (int 3 = 0; j < n; 3++) if (3 ! = i) p[i][3] « 0; else p[i][3] « 1; } return p; } void main() { int n; // Порядок матрицы cout « "\n Введите порядок матрицы: "; cin » n; int **matr; // Указатель для формируемой матрицы 11 Обращение к функции для создания единичной матрицы: matr - single_jnatr (п) ; for (int i == 0; i < n; i++) // Цикл перебора строк { cout « "\n строка " « (i + 1) « // Цикл печати элементов строки: for (int 3 “ 0; 3 < п; з++) cout « "\t" « matr[i][j); } // Очистка памяти от динамических массивов: for (i = 0; i < п; i++) delete matr[i]; delete[] matr; } Результат выполнения программы: Введите порядок матрицы: 4 <Enter> строка 1: 1 0 0 0 строка 2: 0 1 0 0 строка 3: 0 0 1 0 строка 4: 0 0 0 1
Глава 6. Функции, указатели, ссылки 203 Обратите внимание на тот факт, что динамические массивы соз- даются в функции, вызванной из основной программы, а доступ к ним выполняется в тексте основной программы. Здесь же освобождается память от динамических массивов перед окончанием программы. Следующая программа иллюстрирует передачу матрицы через па- раметры, создание внутри функции динамических массивов для пред- ставления еще одной матрицы и возврат в точку вызова указателя на вновь созданную матрицу. Функция reduction () решает следующую задачу: по заданной прямоугольной матрице с размерами п на т и номерам строки 0 < к < п + 1 и столбца 0 < / < т + 1 сформировать новую матрицу с размерами (п - 1) на (т - 1), исключив из исходной матрицы А-ю строку и /-й столбец. Текст программы: //Р6-16.СРР - создание матрицы по заданной матрице ftinclude <io3tream.h> // Функция, определяющая значения элементов матрицы: int **reduction(int n, int m, int **matr, int k, int 1) ( int **p; // Указатель на формируемую матрицу int ii, jj; p ж new (int *[n-l]); // Массив указателей на строки for (int i = 0; i < n - 1; i++) p[i] = new (int [m-1]); // Формирование i-й строки // Цикл перебора строк: for (i ж 0, ii = 0; i < п - 1; i++, ii++) { if (i «ж k) ii++; // Пропускаем k-ю строку // Цикл заполнения строки: for (int j ” 0, jj = 0; j < m - 1; j++, jj++) ( // Пропускаем 1-й элемент в строке: if (j ж« 1) jj++; p[i][jl = matrfii][jj]; } } return p; ) void main() ( int x[4][6] ж ( 11, 12, 13, 14, 15, 16, 21, 22, 23, 24, 25, 26, 31, 32, 33, 34, 35, 36, 41, 42, 43, 44, 45, 46 }; int *px[4]; px[0] = (int *)&x[0]; px[l] = (int *)fix[l]; px[2] ж (int *)&x[2] ; px[3] “ (int *)£x[3]; int n ж 3, m “ 5; int **ppm;
204 Язык Си++ ppm » reduction(4, 6, рх, 2, 3); for (int i « 0; i < n; i++) // Цикл перебора строк { cout « "\n строка " « (i + 1) « // Цикл печати элементов строки: for (int j “ 0; j < m; j++) cout « "\t" « pp»[i][j]; I for (i ® 0; i < n; i++) delete ppm[i]; delete[] ppm; Результат выполнения программы: строка 1: 11 12 13 15 16 строка 2: 21 22 23 25 26 строка 3: 41 42 43 45 46 6.6. Указатели на функции Прежде чем вводить указатель на функцию, напомним, что каждая функция характеризуется типом возвращаемого значения, именем и сигнатурой. Напомним, что сигнатура определяется количеством, по рядком следования и типами параметров. Иногда говорят, что сигна турой функции называется список типов ее параметров. При исполь зовании имени функции без последующих скобок и параметров имя функции выступает в качестве указателя на эту функцию, и его значе нием служит адрес размещения функции в памяти. Это значение адре са может быть присвоено другому указателю, и затем уже этот новый указатель можно применять для вызова функции. Однако в определе нии нового указателя должен быть тот же тип, что и возвращаемое функцией значение, и та же сигнатура. Указатель на функцию опреде ляется следующим образом: тип_функции (*имя_ухаэаФеля) (спецификация_парамвтро») ; Например: int (*funciptr) (char); - определение указателя fund ptr на функцию с параметром типа char, возвращающую значе- ние типа int Если приведенную синтаксическую конструкцию записать без первых круглых скобок, т.е. в виде int *fun(char); то компилятор воспримет ее как прототип некой функции с именем fun и параметром типа char, возвращающей значение указателя типа int *.
Глава 6. Функции, указатели, ссылки 205 Второй пример: char * (*func2Ptr) (char *, int); - определение указателя func2Ptr на функцию с параметрами типа указатель на char и типа int, возвращающую значение типа указатель на char. В определении указателя на функцию тип возвращаемого значе- ния и сигнатура (типы, количество и последовательность параметров) должны совпадать с соответствующими типами и сигнатурами тех функций, адреса которых предполагается присваивать вводимому указателю при инициализации или с помощью оператора присваива- ния. В качестве простейшей иллюстрации сказанного приведем про- грамму с указателем на функцию: //Р6-17.СРР - определение и использование указателей // на функции tinclude <iostream.h> // Для ввода-вывода void fl(void) // Определение fl { cout « "\пВыполняется fl О"; ) void f2 (void) // Определение f2 ( cout « "\пВыполняется f2()”; } void main() void (*ptr) (void) ; // ptr - указатель на функцию ptr “ f2; // Присваивается адрес f2() (*ptr) () ; // Вызов f2() по ее адресу ptr “ fl; // Присваивается адрес fl О (*ptr) () ; // Вызов fl() по ее адресу ptr(); // Вызов эквивалентен (*ptr)() Результат выполнения программы: ) Выполняется f2() Выполняется fl() Выполняется fl() В программе описан указатель ptr на функцию, и ему последова- тельно присваиваются адреса функций f2 и fl. Заслуживает внимания форма вызова функции с помощью указателя на функцию: (*имя__указателя) (список__фактических_параметров) ; Здесь значением имени_указателя служит адрес функции, а с по- мощью операции разыменования * обеспечивается обращение по ад- ресу к этой функции. Однако будет ошибкой записать вызов функции без скобок в виде *ptr(); Дело в том, что операция () имеет более высокий приоритет, нежели операция обращения по адресу *. Следо- вательно, в соответствии с синтаксисом будет вначале сделана попыт-
206 Язык Си++ ка обратиться к функции ptr(). И уже к результату будет отнесена операция разыменования, что будет воспринято как синтаксическая ошибка. При определении указатель на функцию может быть инициализи- рован. В качестве инициализирующего значения должен использо ваться адрес функции, тип и сигнатура которой соответствую! определяемому указателю. При присваивании указателей на функции также необходимо со- блюдать соответствие типов возвращаемых значений функций и сиг- натур для указателей правой и левой частей оператора присваивания. То же справедливо и при последующем вызове функций с помощью указателей, т.е. типы и количество фактических параметров, исполь- зуемых при обращении к функции по адресу, должны соответствовать формальным параметрам вызываемой функции. Например, только некоторые из следующих операторов будут допустимы: char fl(char) {...} // Определение функции char f2(int) (...) и Определение функции void f3(float) (...) и Определение функции int* f4(char *) {...) и Определение функции char (*ptl)(int); и Указатель на функцию char (*pt2)(int); и Указатель на функцию void (*ptr3)(float) « f3; и Инициализированный указатель void main() { ptl « fl; // Ошибка - несоответствие сигнатур pt2 = f3; // Ошибка * несоответствие типов // (значении и сигнатур) ptl = f4; // Ошибка * несоответствие типов ptl = f2; // Правильно pt2 = ptl; // Правильно char с = (*ptl) (44) ; // Правильно с “ (*pt2)('\t*); // Ошибка - несоответствие сигнатур ) Следующая программа иллюстрирует гибкость механизма вызо- вов функций с помощью указателей. //Р6-18.СРР - вызов функции по адресам через указатель tinclude <ioatream.h> // Функции одного типа с одинаковыми сигнатурами: int add(int n, int m) ( return n + m; ) int div(int n, int m) ( return n/m; } int mult (int n, int m) ( return n * m; ) int subt(int n, int m) { return n - m; ) void mainO ( int («par) (int, int) ; // Указатель на функцию
Гйава 6. Функции, указатели, ссылки 207 int а = 61 Ь = 2; char с = • +1 ; while (с ' ’) { cout « "\п Аргументы: а = •• « а « ", Ь = " « Ь; cout « ". Результат для с « \’” « с « « ” равен "; switch (с) { case par “ add; с = 1/’; break; case par = subt; c e ' *; break; case '*•: par » mult; c = '-’; break; case par = div; c « 'break; } cout « (a = (*par)(a,b)); // Вызов по адресу } } Результаты выполнения программы: Аргументы: Аргументы: Аргументы: Аргументы: а = 6, Ь « 2. Результат для а == 8, b “ 2. Результат для а « 4, b = 2. Результат для а « 8, b » 2. Результат для с “ ’+' равен с = ‘ / ’ равен с = ' *’ равен с “ ' -1 равен 8 4 8 6 Цикл продолжается, пока значением переменной с не станет про- бел. В каждой итерации указатель par получает адрес одной из функ- ций, и изменяется значение с. По результатам программы легко проследить порядок выполнения ее операторов. Указатели на функции могут быть объединены в массивы. Напри- мер, float (*ptrArray) (char) [4]; - описание массива с именем ptrArray из четырех указателей на функции, каждая из которых имеет параметр типа char и возвращает значение типа float. Чтобы обратиться, например, к третьей из этих функций, потребуется такой оператор: float а = (*ptrArray[2])('f'); Как обычно, индексация массива начинается с 0, и поэтому третий элемент массива имеет индекс 2. Приведенное описание массива ptrArray указателей на функции ие особенно наглядное и ясное. Для функций с большим количеством параметров и сложным описанием возвращаемых значений оно может стать и вовсе непонятным. Ситуация особенно усложняется, когда та- кое описание типа должно входить в более сложные описания, напри- мер, когда указатели функций и их массивы используются в качестве параметров других функций. Для удобства последующих применений
208 Язык Си++ и сокращения производных описаний рекомендуется с помощью спе цификатора typedef вводить имя типа указателя на функции*. typedef float. (*PTF) (float) ; typedef char *(*PTC)(char); typedef void (*PTFUNC) (PTF, int, float); Здесь ptf - имя типа "указатель на функцию с параметром типи float, возвращающую значение типа float", ртс -- имя типа "указатель на функцию с параметром типа char, возвращающую указатель на тип char", ptfdnc - имя типа "указатель на функцию, возвращающую пустое значение (типа void)". Параметрами для этой функции служат ptf - указатель на функцию float имя (float), выражение типа int и -выражение типа float. (В определение имени типа ptfdnc вошел только что определенный тип с именем ptf.) Введя имена типов указателей на функции, проще описать cooi ветствующие указатели, массивы и другие производные типы: PTF ptfloatl, ptfloat2[5]; // Указатель и массив // указателей на функции // float имя(float) PTC ptchar; // Указатель на функцию char *(char) PTFUNC ptfunc[8); // Массив указателей на функции Массивы указателей на функции удобно использовать при разра ботке всевозможных меню, точнее программ, управление которыми выполняется с помощью меню. Для этого действия, предлагаемые на выбор будущему пользователю программы, оформляются в виде функций, адреса которых помещаются в массив указателей на функ ции. Пользователю предлагается выбрать из меню нужный ему пункт (в простейшем случае он вводит номер выбираемого пункта) и по но меру пункта, как по индексу, из массива выбирается соответствующий адрес функции. Обращение к функции по этому адресу обеспечивает выполнение требуемых действий. Самую общую схему реализации такого подхода иллюстрирует следующая программа для обработки файлов: //Р6-19.СРР - массив указателей на функции #include <stdlib.h> // Для exit() #include <iostream.h> // Для cout, cin // Определение функций для обработки меню: void actl(char* name) ( cout « ”Действия no созданию файла " « name; } void асt2(char* name) ( cout « "Действия no уничтожению файла " « name; }
лава 6. Функции, указатели, ссылки <old act3(char* name) { cout « ° Действия no чтению файла ” « name; } ^oid асt4(char* name) { cout « "Действия no модификации файла " « name; } \oid act5(char* name) { cout « "Действия no закрытию файла."; exit(0); // Завершить программу } /( Тип MENU указателей на функции типа void (char *) ' trpedef void(*MENU)(char *); /f Инициализация таблицы адресов функций меню: М >NU MenuAct[5] = { actl, act2, act3, act4, act5 }; viid main() (int number; // Номер выбранного пункта меню char FileName[30]; // Строка для имени файла cout « "\п 1 - создание файла"; cout « "\п 2 - уничтожение файла”; cout « "\п 3 - чтение файла"; cout « "\п 4 - модификация файла"; cout « ”\п 5 - выход из программы"; while (1) // Бесконечный цикл { while (1) ( // Цикл продолжается до ввода правильного номера cout « "\п\пВведите номер пункта меню: "; cin » number; if (number >» 1 && number <= 5) break; cout « "\пОшибка в номере пункта меню!"; } if (number 5) { cout « "Введите имя файла: "; cin » FileName; // Читать имя файла } // Вызов функции по указателю на нее: (*MenuAct [number-1])(FileName); // Конец бесконечного цикла При выполнении программы возможен, например, такой диалог: 1 - создание файла 2 - уничтожение файла 3 - чтение файла 4 - модификация файла 5 - выход из программы Введите номер пункта меню: 3 <Enter> 14-3821
I 210 ЯзыкСи++ Введите имя файла: PROBA.TXT <Enter> Действия по чтению файла PROBA.TXT Введите номер пункта меню: 5 <Enter> Действия по Закрытию файла. Пункты меню повторяются, пока не будет введен номер 5 - выхо;| из программы. Указатели на функции - незаменимое средство языков Си и Си++, когда объектами обработки должны служить функции. Например^ создавая процедуры для вычисления определенного интеграла зада* ваемой пользователем функции или для построения таблицы значений произвольной функции, нужно иметь возможность передавать в про- граммы функции. Удобнее всего организовать связь между функцией, реализующей метод обработки (например, численный метод интегри- рования), и той функцией, для которой этот метод нужно применить, через аппарат параметров, в число которых входят указатели на функции. Рассмотрим задачу вычисления корня функции Дх) на интервале локализации [А ; В] для иллюстрации особенностей применения указа- теля функции в качестве параметра. Численный метод (целение попо- лам интервала локализации) оформляется в виде функции со следующим заголовком: float root (укааатель__на_функцию, float A, float В, float EPS) Здесь а - нижняя граница интервала локализации корня; в - верх- няя граница того же интервала; eps - требуемая точность определения корня. Введем тип "указатель на функцию", для которой нужно опре- делить корень: typedef float(*pointFunc)(float); Теперь можно определить функцию для вычисления корня задан- ной функции с указателем pointFunc. Прототип функции будет та- ким: float root(pointFunc F, float A, float B, float EPS); Приведем тестовую программу для определения с помощью root () корня функции у = х2 - 1 на интервале [0, 2]: //Р6-20.СРР - указатель функции как параметр функции tinclude <iostream.h>
лава 6. Функции, указатели, ссылки 211 include <stdlib.h> // Для функции exit() , / Определение типа указателя на функцию: lypedef float(*pointFunc)(float); // Определение функции для вычисления корня: float root(pointFunc F, float A, float B, float EPS) { float x, y, c, Fx, Fy, Fc; X A; у В; Fx (*F) (x) ; // Значение функции на левой границе Fy (*F) (у) ; // Значение функции на правой границе if (Fx * Fy > 0.0) { cout « "ХпНеверен интервал локализации”; exit(l); // Аварийное завершение программы } do ( с « (у - х)/2; // Центр интервала локализации Fc - (*F)(с); // Значение функции в с if (Fc * Fx > 0) ( х « с; Fx я Fc; } else { у я c; Fy » Fc; } } while (Fc !« 0 && у - x > EPS); return c; } #include <math.h> // Определение тестовой функции у = x * x - 1: float testfunc(float x) ( return x * x - 1; } void main() { float root(pointFunc, float, float, float); // Прототип float result; result = root(testfunc, 0.0, 2.0, le-5) ; cout « "\пКорень тестовой функции: ” « result; } Результат выполнения программы: Корень тестовой функции: 1 Опыт работы на языках Си и Си++ показал, что даже не новичок в области программирования испытывает серьезные неудобства, раз- бирая синтаксис определения конструкций, включающих указатели на функции. Например, не каждому сразу становится понятным такое определение прототипа функции [6, 11]: void qsort (void *base, siza_t nelem, size__t width, int (*fcap)(const void *pl, const void *p2)); Это прототип функции быстрой сортировки, входящей в стан- дартную для системы UNIX и для языка ANSI Си библиотеку функ<
212 ЯзыкСи++ ний. Прототип находится в заголовочном файле stdlib.h. Разберем элементы прототипа и напишем программу, использующую указа^ ную функцию. Функция qsortO сортирует содержимое таблицы од- нотипных элементов, постоянно вызывая функцию сравнения подготовленную пользователем. Для вызова функции сравнения ее адрес должен заместить указатель fcmp, специфицированный как формальный параметр. Итак, для использования qsortO програм мист должен подготовить таблицу сортируемых элементов в виде од номерного массива фиксированной длины и написать функцию, позволяющую сравнивать два любых элемента сортируемой таблицы Остановимся на параметрах функции qsortO: base указатель на начало таблицы сортируемых элементов (адрес 0-го элемента массива); nelem количество элементов в таблице (целая величина, не боль- шая размера массива); width размер элемента таблицы (целая величина, определяющая в байтах размер одного элемента массива); fcmp указатель на функцию сравнения, получающую в качестве параметров два указателя pl, р2 на элементы таблицы и возвращающую в зависимости от результата сравнения це- лое число: если *pl < *р2, fcmp возвращает целое < 0; если *р1 = *р2, fcmp возвращает 0; если *pi > *р2, fcnnp возвращает целое > 0. При сравнении символ ’’меньше чем" (<) означает, что после сор- тировки левый элемент отношения *р1 должен оказаться в таблице перед правым элементом *р2, т.е. значение *р1 должно иметь меньшее значение индекса в массиве, нежели *р2. Аналогично (но обратно) определяется расположение элементов при выполнении соотношения "больше чем" (>). В следующей программе функция qsortO используется для упо- рядочения массива указателей на строки разной длины. Упорядочение должно быть выполнено таким образом, чтобы последовательный перебор массива указателей позволял получать строки в алфавитном порядке. Сами строки в процессе сортировки не меняют своих поло- жений. Изменяются только значения указателей в массиве. //Р6-21.СРР - упорядочение с поиощью библиотечной функции // qsortO
Глава 6. Функции, указатели, ссылки 213 tinclude <iostream.h> tinclude <stdlib.h> // Для функции qsort() tinclude <string.h> 11 Для сравнения строк: strcmpO Il Определение функции для сравнения: int sravni(const void *a, const void *b) { unsigned long *pa (unsigned long *)a, *pb (unsigned long *)b; return stramp((char *)*pa, (char *)*pb); } void main() { char *pc[] = ( "Sine Cura - Синекура", "Pro Forma - Ради формы", "Differentia Specifics "\n\t\t Отличительная особенность", "Aiea Jacta Eat! - Жребии брошен!”, "Idem Per Idem -" "\n\t\t Определение через определяемое", "Fiat Lux! - Да будет свет!", "Multa Pan Cis - Многое в немногих словах" }; // Размер таблицы: int n sizeof(рс)/sizeof(рс[О]); cout « "\п До сортировки:" « hex; for (int i = 0; i < n; i++) cout « "\npc[" « i « "]" « (unsigned long)pc[i] « " -> " « pc[i]; // Вызов функции упорядочения: qsort((void *)pc, // Адрес начала сортируемой таблицы п, // Количество элементов сортируемой таблицы sizeof(рс[0]), // Размер одного элемента sravni // Имя функции сравнения (указатель) ) ; cout « "\n\n После сортировки:”; for (i = 0; i < n; i++) cout « "\npc[" « i « "]«" « (unsigned long)pc[i] « " -> " « pc[i]; } Результат выполнения программы: До сортировки: pc[0]"2de400ac -> Sine Cura - Синекура pc[l]-2de400cl -> Pro Forma - Ради формы pc[2]-2de400d8 -> Differentia Specifica - Отличительная особенность pc[3]»2de4010e -> Aiea Jacta Est! - Жребий брошен!
214 Язык Си++ pc[4]»2de4012f -> Idem Per Idem - Определение через определяемое pc[5]=2de40162 -> Fiat Lux! - Да будет свет! pc[6]-2de4017d -> Malta Pan Cis - Многое в немногих словах После сортировки: pc[0]"2de4010e -> Aiea Jacta Est! - Жребии брошен? pc[1]=2de400d8 -> Differentia Specifica - Отличительная особенность pc[2]=2de40162 -> Fiat Lux! - Да будет свет! pc[3]~2de4012f -> Idem Per Idem - Определение черев определяемое pc [4 ] ®2de4017d -> Multa Pan Cis - Многое в немногих словах pc[5]=2de400cl -> Pro Forma - Ради формы pc[6]»2de400ac -> Sine Cura - Синекура Обратите внимание на значения указателей pc[i], До сортировки разность между рс[1] Ирс[О] равна длине строки "Sine Cura “ Си- некура" и т.д. После упорядочения рс[О] получит исходное значение рс[3] и т.д. Для выполнения сравнения строк (а не элементов массива рс[]) в функции sravni() использована библиотечная функция strcupO, прототип которой в заголовочном файле string.h имеет вид: int strcmp(const char *sl, const char *s2); Функция stronp() поддерживается системой UNIX и выполняет беззнаковое сравнение строк, связанных с указателями si и s2. Срав- нение выполняется без учета регистров набора букв латинского алфа- вита. Функция выполняет сравнение посимвольно, начиная с начальных символов строк и до тех пор, пока не встретятся несовпа- дающие символы либо не закончится одна из строк. Прототип функции stronp () требует, чтобы параметры имели тип (const char *). Входные параметры функции sravni() имеют тип (const void *), как предусматривает определение функции qsort (). Необходимые преобразования для наглядности выполнены в два этапа. В теле функции sravni О определены два вспомогательных указателя типа (unsigned long' *), которым присваиваются значе- ния адресов элементов сортируемой таблицы (элементов массива рс[]) указателей. В свою очередь, функция strcmpO получает разыменования этих указателей, т.е. адреса символьных строк. Таким образом, выполняется сравнение не элементов массива char* рс[], а тех строк, адреса которых являются значениями pc[i]. Однако функ-
Глава 6. Функции, указатели, ссылки 215 ция qsortO работает с массивом рс[] и меняет местами только зна- чения его элементов. Последовательный перебор массива рс[] позво- ляет в дальнейшем получить строки в алфавитном порядке, что иллюстрирует результат выполнения программы. Так как pc[i] - указатель на некоторую строку, то его разыменование в операции вы- вода « в поток cout выполняется автоматически. Если не использовать вспомогательных указателей pa, pb, то функцию сравнения строк можно вызвать из тела функции sravniO таким оператором: return stramp((char *)(*(unsigned long *)a), (char *)(*(unsigned long *)b)); Здесь каждый родовой указатель (void *) вначале преобразуется к типу (unsigned long *). Последующее разыменование "достает" из четырех смежных байтов значение соответствующего указателя pc(i]. И уж затем преобразование (char *) формирует указатель на строку, который нужен функции stramp (). 6.7. Ссылки В языке Си++ ссылка определена как другое имя уже существую- щего объекта. Основные достоинства ссылок проявляются при работе с функциями, однако ссылки могут использоваться и безотносительно к функциям. Для определения ссылки используется символ &, если он употребляется в таком контексте: type& имя_ссылки инициализатор В соответствии с синтаксисом инициализатора, наличие которого обязательно, определение ссылки может быть таким; type& имя ссылки = выражение; ИЛИ type& имя^ссылки(выражение); Раз ссылка есть "другое имя уже существующего объекта", то в ка- честве инициализирующего выражения должно выступать имеющее значение леводопустимое выражение, т.е. имя некоторого объекта, имеющего место в памяти. Значением ссылки после определения с инициализацией становится адрес этого объекта. Примеры определе- ний ссылок:
216 Язык Си++ int L= 777; // Определена и инициализирована переменная L int& RL = L; // Значением ссылки RL является адрес // переменной L int& RI(O); // Опасная инициализация - значением ссылки RI // становится адрес объекта, в котором // временно размещено нулевое целое значение В определении ссылки символ не является частью типа, т.е. RL или RI имеют тип int и именно так должны восприниматься в программе. Итак, имя_ссылки определяет местоположение в памяти инициа- лизирующего выражения, т.е. значением ссылки является адрес объекта, связанного с инициализирующим выражением. Функционально ссылка ведет себя подобно обычной переменной того же, что и ссылка, типа. Для доступа к содержимому участка памяти, на который "смотрит" ссылка, нет необходимости явно выполнять разыменование, как это нужно для указателя. Если рассматривать переменную как пару "имя_переменной - значение—переменной", то инициализированная этой переменной ссылка может быть представлена парой "имя_ссылки - значение__переменной". Из этого становится понятной необходимость инициализации ссылок при их определении. Тут ссылки схожи с константами языка Си++. Раз ссылка есть имя, связанное со значением (объектом), уже размещенным в памяти, то, определяя ссылку, необходимо с помощью начального значения определить тот объект (тот участок памяти), на который указывает ссылка. После определения с инициализацией имя_ссылки становится еще одним именем (синонимом, псевдонимом, алиасом) уже существующего объекта. Таким образом, для нашего примера оператор RL -= 77; уменьшает на 77 значение переменной L. Связав ссылку (RL) с пере- менной (ь), мы получаем две возможности изменять значение пере- менной: RL ~= 88; или L = 88; Здесь есть аналогия с указателями, однако отсутствует необхо- димость в явном разыменовании, что обязательно при обращении к зна- чению переменной через указатель. Проиллюстрируем сказанное следующей простой программой:
Глава 6. Функции, указатели, ссылки 217 //Р6-22.СРР - обращение к значению переменной по ссылке # include <iostream.h> void main () {long z = 12345678L; //Выделяется память для переменной long & sz = z; //Определяется синоним для z cout « "\nsz = "«sz « ”\tz - "« z; z = 87654321L; cout « "\nsz = ”« sz « "\tz = ”«z; z = 0 ; cout « "\nsz — "« sz « ”\t\tz = ”«z; Результат выполнения программы: sz == 12345678 z = 12345678 sz = 87654321 z = 87654321 sz = 0 z = 0 Как видно из результатов, имена переменной и ссылки, "настроенной" на эту переменную, полностью равноправны и соотносятся с одним и тем же участком памяти. Присваивание значения переменной z приводит к изменению значения, связанного со ссылкой sz, и, наоборот, присваивание значения ссылке sz меняет значение переменной z. Ссылки не есть полноценные объекты, подобные переменным либо указателям. После инициализации значение ссылки изменить нельзя, она всегда "смотрит" на тот участок памяти (на тот объект), с которым она связана инициализацией. Ни одна из операций не действует на ссылку, а относится к тому объекту, с которым она связана. Можно считать, что это основное свойство ссылки. Таким образом, ссылка полностью аналогична исходному имени объекта. Конкретизируем и поясним сказанное. Пусть определены: double а[] = { 10.0, 20.0, 30.0, 40.0 }; // а- массив double *ра = а; //ра - указатель на массив double& га = а[0]; //га - ссылка на первый элемент массива double * & rpd = а; //Ссылка на указатель (на имя массива) Для ссылок и указателей из нашего примера соблюдаются равенства ра =- &га, *ра == га, rpd -= а, га = = а[0] . Тогда sizeof (ра) -= 4 - размер указателя типа double *; sizeof (ра) == 8 - размер элемента массива double а [0], связанного со ссылкой га при инициализации; sizeof {rpd) ==4 - размер указателя на массив с элементами double.
218 Язык Си++ Итак, результатом применения операции sizeof к ссылке является не ее размер, а размер именуемого ею объекта. Так как квадратные скобки [] есть обозначение бинарной опера- ции, то в соответствии с основным свойством ссылки оператор rpd[O] + rpd[2] + rpd[3]; относится к элементам массива а[]. После его выполнения а[0] = 80.0 Применив к ссылке операцию получения адреса s, определим не адрес ссылки, а адрес того объекта, которым инициализирована ссыл- ка. Можно рассмотреть и другие операции, но вывод один - каждая операция над ссылкой является операцией над тем объектом, с кото- рым она связана. Так как ссылки не есть настоящие объекты, то существуют огра- ничения при определении и использовании ссылок. Во-первых, ссылка не может иметь тип void, т.е. определение voids имя-ссылки запреще- но. Ссылку нельзя создать с помощью операции new, т.е. для ссылки нельзя выделить новый участок памяти. Не определены ссылки на другие ссылки. Нет указателей на ссылки и невозможно создать мас- сив ссылок. Например, все следующие определения ссылок ошибочны: long vl = 6, v2 = 4, v3 « 8; doubles ref; // voids refvar - vl; // longs refnew я new(long S); // longs refarr[3] { vl, v2, v3 longs S refref - v2; // Нет инициализации ссылки voids недопустимо для ссылки Ссылке не выделяют память }; // Массивов ссылок // не бывает Нет ссылок на ссылки Ссылка, определенная с типом type, должна быть инициализиро- вана либо объектом того же типа type, либо с помощью объекта, ко- торый может быть по умолчанию приведен к этому типу. После определения ссылка не может быть никаким способом "оторвана" от объекта инициализации и связана с другим объектом, т.е. значение ссылки не может изменяться. Однако один объект может быть адресован любым числом ссылок и указателей: long v = 7; // Определена переменная v longs refl - v; // Ссылка refl связана c v long* ptr Srefl; // Указатель ptr адресует v longs ref2 relf; // Ссылка ref2 указывает на v
lasa 6. Функции, указатели, ссылки 219 Теперь к значению переменной v можно добраться четырьмя спо- )бами: с помощью имени v; ссылки refl; разыменования указателя otr и ссылки ref2. В определении типа для ссылки можно использовать модификатор Mist, т.е. допустимо определять ссылку на константу: const types имя_ссылки инициализатор; В отличие от обычной ссылки (ссылки на переменную) ссылка на онстанту не позволяет изменить значение того объекта, с которым на связана. Например: ouble Euler « 2.718282; // Определили переменную oust doubles eRef Euler; // eRef - ссылка на константу При таких определениях: uler 0.0; // Допустимый оператор Ref » 0.0; // Ошибка, т.к. ссылка обмелена на константу При определении ссылок на константы инициализирующее выра- жение может быть не только леводопустимым и даже может не иметь ипа type. Например: :onst doubles refpi 38 3.141593;// Ссылка типа double // на константу типа double :onst doubles ref0 = 0; // Ссылка типа double // на константу 0 типа int В последнем определении инициализирующее выражение имеет гип int, отличный от типа double ссылки refO. Поэтому при инициали- *ации выполняется автоматическое преобразование типов. Если при определении ссылки на константу в инициализаторе используется праводопустимое выражение, например, константа, то создается вре- менный объект, он инициализируется значением этого выражения, а ссылка становится его именем. В соответствии с общими принципами время жизни этого временного объекта определяется областью дей- ствия, в которой определяется ссылка. Итак, если инициализирующее выражение для ссылки на констан ту праводопустимое, то прежде всего вычисляется его значение. Сле- дующий шаг - применяется необходимое преобразование типа полученного значения выражения к типу ссылки. Затем создается вре- менная переменная нужного типа и туда заносится это значение. Ад- рес временной переменной используется в качестве значения инициализируемой константной ссылки. Иллюстрировать показан-
220 Язык Си++ ную цепочку действий можно таким образом. Пусть определение ссылки имеет вид: const typelS имя—ссылки выражение_Ъуре2; Так как некоторым аналогом ссылки является указатель, то действия по инициализации ссылки с обозначением имя.ссылки эквивалентны в некотором приближении следующей последовательности: typel temp; //Временная переменная temp = typel (выражение_Луре2); //Приведение типов и //присваив ание typel* const имя—указателя = Stemp; //Имитация // инициализации указателя как аналога ссылки Пример (определяется ссылка на константу). const doubles cdr = 11; //Справа константа типа int Интерпретация приведенного определения с помощью указателя: double temp; //Временная переменная temp temp - double(11); //Присваивание значения переменной temp double *const cdrp = Stemp; // Присваивание адреса //постоянному указателю Отметим, что такая интерпретация не совсем точна и отображает лишь последовательность действий. Полный аналог ссылки есть кон- стантный, т.е. неизменяемый указатель, который автоматически разыменовывается при каждом использовании. Таких константных указателей в Си++ нет - указатели автоматически разыменовываться "не умеют". При определении ссылок обязательна их инициализация. Однако в описаниях ссылок инициализация не обязана присутствовать. К таким описаниям ссылок относятся: • описания внешних ссылок (со спецификатором extern): floats ref; //Ошибка - нет инициализации extern floats ref2;//Допустимо - инициализируется //в другом блоке; • описания ссылок на компоненты класса; • описания (спецификации) формальных параметров функций; • описание типа возвращаемого функцией значения. С классами и их компонентами мы встретимся позже, описания внешних ссылок ничем не отличаются от описания внешних переменных или массивов, а вот соотношение ссылок и функций нужно рас-
Глава 6. Функции, указатели, ссылки 221 смотреть подробно. Действительно, в качестве основных причин включения ссылок в язык Си++ указывают необходимость повысить эффективность обмена с функциями через аппарат параметров и целе- сообразность возможности использовать вызов функции в качестве леводопустимого значения. При использовании ссылки в качестве формального параметра обеспечивается доступ из тела функции к со- ответствующему фактическому параметру, т.е. к участку памяти, вы деленному для фактического параметра. При этом параметр-ссылка обеспечивает те же самые возможности, что и параметр-указатель. Отличия состоят в том, что в теле функции для параметра-ссылки не нужно применять операцию разыменования *, а фактическим пара- метром должен быть не адрес (как для параметра-указателя), а обыч- ная переменная. Следующая программа иллюстрирует в сравнении доступ к объектам (переменным) из тела функции через указатели и ссылки: //Р6-23.СРР - сравнение ссылок с указателями в качестве // параметров ♦include <iostreas.h> // Функции, меняющие значения фактических параметров: void changePtr(double *а, double *b) { double c « *a; *a » *b; *b » c; ) void changeRef(doublet x, doublet y) { double z = x; x » у; у » z; void main() { double d = 1.23; // Выделяется память для переменной double е - 4.56; // Выделяется память для переменной changePtr(fid,fie); cout « ”\nd я ” « d « "\t\te ® " « е; changeRef(d,e); cout « "\nd = " « d « "\t\te * " « e; } Результат выполнения программы: d « 4.56 e = 1.23 d • 1.23 e = 4.56 В функции changePtr () параметры специфицированы как указа- тели. Поэтому в ее теле выполняется их разыменование, а при обра- щении к changePtr () в качестве фактических переменных
222 Язык Си++ используются адреса (&d, fie) тех переменных, значения которых нуж- но поменять местами. В функции changeRef () параметры специфи- цированы как ссылки. Ссылки обеспечивают доступ из тела функции к фактическим параметрам, в качестве которых используются обыч- ные переменные, определенные в вызывающей программе. В спецификации ссылки как формального параметра инициализа- ция необязательна, однако она не запрещена. Сложность состоит в том, что объект, имя которого используется для инициализации пара- метра-ссылки, должен быть известен при определении функции. Иначе параметр должен быть ссылкой на константу, и с его помощью можно будет передавать значения только внутрь функции, а не из нее. //Р6-24.СРР - праводопустимые выражения в качестве / / параметров tinclude <ioetream.h> int п » -55; void invert(intfi k = n) // Инициализация параметра-ссылки { cout « "\пЗначенив параметра в функции invert() : k = "; cout « k; k « -k; } void main() { int a * 22, b * 66; // Выделяется память для переменных invert (); // Изменяется знак умалчиваемого параметра cout « ”\nn ® ” « п; invert(а); cout « и\па я " « а; invert (а + Ь) ; // Переменные а и Ь не будут изменены cout « м\па * ** « а « "\tb « " « Ь; double d - 3.33; invert(int(d)); // Переменная d сохранит свое значение cout « "\nd = " « d; } Результаты выполнения программы: Значение параметра в функции invert к ж -55 п — 55 Значение параметра в функции invert к * 22 а я -22 Значение параметра в функции invert к = 44 а • -22 Ь » 66 Значение параметра в функции invert к * 3 d = 3.33 В программе для параметра-ссылки проиллюстрированы:
Глава 6. Функции, указатели, ссылки 223 • задание начального значения; • использование в качестве фактического параметра арифметиче- ского выражения а + ь; • использование в качестве фактического параметра значения другого типа с явным приведением типов int (d). В двух последних случаях при выполнении тела функции в него передаются значения вспомогательных временных переменных (локальные копии фактических параметров). В теле функции действия выполняются только с этими локальными копиями, и тем самым фак- тические параметры не могут быть изменены за счет выполнения функции. Подобно указателю на функцию определяется и ссылка на функ- цию: -гипофункции (& имя_ссылки) (спецификация_параметров) инициализирующее_выражение; Здесь тип_функции - это тип возвращаемого функцией значения, спецификация_параметров определяет сигнатуру функций, допусти- мых для ссылки, инициализирующее_выражение - включает имя уже известной функции, имеющей тот же тип и ту же сигнатуру, что и определяемая ссылка. Например, int ifunc (float, int); // Прототип функции int (£ iref)(float, int) - ifunc; // Определение ссылки iref - ссылка на функцию, возвращающую значение типа int и имеющую два параметра с типами float и int. Напомним, что исполь- зование имени функции без скобок (и без параметров) воспринимает- ся как адрес функции. Ссылка на функцию обладает всеми правами основного имени функции, т.е. является его синонимом (псевдонимом). Изменить зна- чение ссылки на функцию невозможно, поэтому указатели на функции имеют гораздо большую сферу применения, чем ссылки. Следующая программа иллюстрирует вызовы функции по основ- ному имени, по указателю и по ссылке: //Р6-25.СРР - ссылка и указатель на функцию linclude <iostream.h> void func(char с) // Определение функции { cout « "\n" «с; ) void main() { void (*pf) (char); // pf - указатель на функцию void (&rf)(char) « func; // rf - ссылка на функцию
224 Язык Си++ func(’A’) ; pf « func; (*pf) CB'I ; // Вызов по имени // Указателю присваивается адрес функции / / Вызов по адресу с помощью указателя if('O; ) // Вызов по ссылке Результат выполнения программы: А В С Определение ссылки может содержать ее инициализацию и в круг- лых скобках. В нашем примере была бы допустима и такая конструк- ция: void (&rf)(char)(func); // rf - ссылка на функцию Так же можно инициализировать и указатель на функцию: void (*pf) (char) (func) ; // pf - указатель на функцию Гораздо большими возможностями, чем ссылки на функции, об- ладают ссылки, формируемые как возвращаемый результат выполне- ния функции. Рассмотрим следующую программу: //Р6-26.СРР - ссылка и указатель на функцию linclude <iostrean.h> // Функция определяет ссылку на элемент массива // с максимальным значением: intfi rmax(int n, int d[]) { int in » 0; for (int i « 1; i < n; i++) in « d[in] > d[i] ? in : i; return d[in]; ) void nain() { int n » 4; int x[] « { 10, 20, 30, 14 }; cout « "\nrmax(n,x) « ” « rnax(n,x); rnax(n,x) » 0; for (int i = 0; i < n; i++) cout « " x[" « i « "] = " « x[i); ) Результат выполнения программы: rnax(n,x) * 30 x[0] ж 10 x[l] «= 20 x[2] = 0 x[3] ж 14
Глава 6. Функции, указатели, ссылки 225 В программе дважды используется обращение к паах(). Один из вызовов находится в левой части оператора присваивания, что было бы совершенно недопустимо для языка Си. С его помощью заносится нулевое значение в элемент х [2], вначале равный 30. Возвращение функцией ссылки позволяет организовать много- кратное вложенное обращение к нескольким функциям. В результате таких вложенных обращений один и тот же объект можно многократ- но изменять по разным законам. //Р6-27.СРР - вложенные вызовы функций, возвращающих ссылки tinclude <iostream.h> // Функция возводит в куб значение параметра и возвращает // его адрес: double& rcube(doubled z) (z==z*z*z; return z; } // Функция изменяет знак параметра и возвращает его адрес: doubled rinvert(doublet d) { d = -d; return d; } // Функция возвращает адрес параметра с максимальным / / значением: doublet rm ах (doublet х, double & у) { return х > у ? х : у; } // Функция печатает значение параметра и возвращает его // адрес: doublet rprint(char* name, double& e) { cout « name « e; return e; } void main() { double a = 10.0, b = 8.0; rprint("\nrcube (rinvert (rmax (a,b))) = ", rcube(rinvert(rmax(a,b)))) = 0.0; cout « "\na « •' « a « ",\tb = " « b; rcube(rinvert(rmax(a,b))) = 0.0; cout « ”\na « ” « a « '* ,\tb = " « b; } Результат выполнения программы: rcube (rinvert (rmax (a,b))) = -1000 a = 0, b = 8 a = 0, b = 0 15-w.
226 Язык Си++ * Присваивание rprinto = 0.0 позволяет по цепочке передам нулевое значение аргументам функций и изменяет значение того п аргументов (а) функции rmax (), который вначале был максимальным Последующее присваивание rcube() = 0.0 обнуляет значение пари метра ь. В следующей программе введены две функции, возвращаюши ссылки на элементы двухмерных массивов с изменяемыми размерами Функция eiem() позволяет по заданным значениям индексов "добираться" до конкретного элемента. Особого выигрыша примем* ние такой функции не дает, она еще раз иллюстрирует возможное! и функций, возвращающих значения ссылок. Функция maxeiemO во । вращает адрес (ссылку) максимального элемента двумерного массива Используя ее в левой части оператора присваивания, можно замени 11 значение максимального элемента. Именно это и выполняется в цикл основной программы. Результаты ее работы и комментарии в теки поясняют сказанное. Текст программы: //Р6-28.СРР - ссылки на элементы "двухмерных11 массивов linclude <iostream.h> // Функция возвращает ссылку на обозначенный элемент // матрицы: floats elem(float **matr, int k, int 1) { return matr[k][ 1] ; } // Функция заполняет матрицу значениями от 1 до 9: void matrix(int п, int п, float **pmatr) { int k = 0; for (int i » 0; i < n; i++) for (int j » 0; j < m; j++) elem(pmatr,i,j) « k++ % 9 + 1; } // Функция выбирает адрес максимального элемента матрицы: floats maxelem(int п, int m, float **pmatr) { int im = 0, jm = 0; for (int i » 0; i < n; i++) for (int j = 0; j < m; j++) { if (pmatr[im][jm] >= pmatr[i][j]) continue; im = i; jm = j; } return pmatr[im][jm]; } // Функция печатает матрицу no строкам: void matr_print(int n, int m, float **pmatr)
Глава 6. Функции, указатели, ссылки 227 { for (int i = 0; i < n; i++) // Цикл перебора строк { cout « "\n строка w « (i + 1) « // Цикл печати элементов строки: for (int j = 0; j < m; j++) cout « "\t" « pmatr[i] [ j] ; } ) void main() { float z[3][4]; float *ptr[] = { (float *)&z[01, (float *) &z[l], (float *)&z[2] }; matrix(3,4,ptr); // Заполняем матрицу matr_print(3,4,ptr); // Печать исходной матрш”: for (int i = 0; i < 4; i++) // Обнулим 4 максимальных maxelem(3,4,ptr) =0.0; // элемента matr__print (3,4 ,ptr) ; // Печать измененной матрицы } Результат выполнения программы: строка 1: 1 2 3 4 строка 2: 5 6 7 8 строка 3: 9 1 2 3 строка 1: 1 2 3 4 строка 2: 5 0 0 0 строка 3: 0 1 2 3 6.8. Перегрузка функций Цель перегрузки функций состоит в том, чтобы функция с одним именем по-разному выполнялась и возвращала разные значения при обращении к ней с разными по типам и количеству фактическими па- раметрами. Например, может потребоваться функция, возвращающая максимальное из значений элементов одномерного массива, переда- ваемого ей в качестве параметра. Массивы, использованные как фак- тические параметры, могут содержать элементы разных типов, но пользователь функции не должен беспокоиться о типе результата. Функция всегда должна возвращать значение того же типа, что и тип массива - фактического параметра. Для обеспечения перегрузки функций необходимо для каждого имени определить, сколько разных функций связано с ним, т.е. сколь- 15*
228 Язык Си++ ко вариантов сигнатур допустимы при обращении к ним. Предполо жим, что функция выбора максимального значения элемента из мае сива должна работать для массивов типа int, long, float, double. В этом случае придется написать четыре разных варианта функции с одним и тем же именем. В следующей программе эта задача решена: //Р6-29.СРР - перегрузка функции tinclude <iostream.h> long max__element(int n, int array[]) // Функция для { int value « array[0]; // массивов с элементами типа for (int i « 1; i < n; i++) // int value = value > array[i] ? value : array[i]; cout « "\пДля (int) : return long(value); ) long max__element (int n, long array[}) // Функция для { long value ж array[0]; // массивов с элементами типа for (int i « 1; i < n; i++) // long value = value > array[i] ? value : array[i]; cout « и\пДля (long) : "; return value; ) double max__element(int n, float array[}) // Функция для { float value = array[0]; // массивов с элементами типа for (int i e 1; i < n; i++) // float value = value > array[i] ? value : array[i]; cout « "\пДля (float) ”; return double(value); ) double max_element (int n, double array [}) // Функция для ( double value = array[0]; // массивов с элементами типа for (int i = 1; i < n; i++) // double value « value > array[i] ? value : array[i]; cout « и\пДля (double) "; return value; ) void main() { int x[] = { 10, 20, 30, 40, 50, 60 ); long f[] = { 12L, 44L, 5L, 22L, 37L, 30L }; float у[J « { 0.1, 0.2, 0.3, 0.4, 0.5, 0.6 }; double z[] == { 0.01, 0.02, 0.03, 0.04, 0.05, 0.06 ); cout « ,,Max_elem (6,x) = ” « max_element(6,x); cout « ”max_elem(6,f) » " « max__element(6,f) ; cout « Hmax_elem(6,y) = " « max__element(6,y) ; cout « ”max_elem(6,z) = ” « max__element (6,z) ; )
Глава 6. Функции, указатели, ссылки 229 Результат работы программы: Для (int) max_elem(6,x) = 60 Для (long) max_elem(6,f) = 44 Для (float) max_elem(6,y) = 0.6 Для (double): max_elem{6,z) = 0.06 В программе для иллюстрации независимости перегруженных функций от типа возвращаемого значения две функции, обрабатывающие целые массивы (int, long), возвращают значение одного типа long; функции, обрабатывающие естественные массивы (double, float), обе возвращают значение типа double. Распознавание перегруженных функций при вызове выполняется по их сигнатурам. Перегруженные функции поэтому должны иметь одинаковые имена, но спецификации их параметров должны различаться по количеству и(или) по типам, и(или) по расположению. При использовании перегруженных функций нужно с осторож- ностью задавать начальные значения их параметров. Предположим, мы следующим образом определили перегруженную функцию умножения разного количества параметров: double multy (double х) { return х * х * х; } double multy (double х, double у) { return х * у * у; } double multy (double х, double у, double z) { return x * у * z; } Каждое из следующих обращений к функции multyQ будет однозначно идентифицировано и правильно обработано: multy{0.4) multy (4.0z 12.3) multy(0.le-6f 1.2e4, 6.4) Однако добавление в программу такой функции прототипа с умалчиваемыми значениями параметров: double multy (double а = 1.0, double b = 1.0, double c = 1.0, double d = 1.0); { return a*b+c*d; ) навсегда запутает любой компилятор при попытках обработать, на- пример, такой вызов: multy(0.1е-6, 1.2е4)
230 Язык Си++ 6.9. Шаблоны функций Цель введения шаблонов функций ~ автоматизация создания функций, которые могут обрабатывать разнотипные данные. В отли чие от механизма перегрузки, когда для каждой сигнатуры опреде- ляется своя функция, шаблон семейства функций определяется один раз, но это определение параметризуется. Параметризовать в шаблон* функций можно тип возвращаемого функцией значения и типы любых параметров, количество и порядок размещения которых должны бып фиксированы. Для параметризации используется список параметрон шаблона. В определении шаблона семейства функций используется служеб ное слово template. Для параметризации используется список фор мальных параметров шаблона, который заключается в угловые скобки о. Каждый формальный параметр шаблона обозначается служебным словом class, за которым следует имя параметра (идентификатор). Пример определения шаблона функций, вычисляю щих абсолютные значения числовых величин разных типов: template <class type> type abs(type x) ( return x > 0 ? x : ~x; } Шаблон семейства функций состоит из двух частей - заголовка шаблона: template <список_параметров__шаблона> и из обыкновенного определения функции, в котором тип возвращас мого значения и типы любых параметров обозначаются именами па раметров шаблона, введенных в его заголовке. Те же имена параметров шаблона могут использоваться и в теле определения функции для обозначения типов локальных объектов. В качестве еще одного примера рассмотрим шаблон семейства функций для обмена значений двух передаваемых им параметров. template Cclass Т> void swap (Т* х, Т* у) { Т z « *х; *х = *у; *у = z; } Здесь параметр т шаблона функций используется не только в за головке для спецификации формальных параметров, но и в теле опре деления функций, где он задает тип вспомогательной переменной z.
Глава 6. Функции, указатели, ссылки 231 Шаблон семейства функций служит для автоматического форми- рования конкретных определений функций по тем вызовам, которые транслятор обнаруживает в тексте программы. Например, если про- граммист употребляет обращение abs(-10.3), то на основе приве- денного выше шаблона компилятор сформирует такое определение функции: double abs(double х) { return х > 0 ? х : -х; } Далее будет организовано выполнение именно этой функции и в точку вызова в качестве результата вернется числовое значение 10.3. Если в программе присутствует приведенный выше шаблон се- мейства функций swap () и появится последовательность операторов: long к = 4, d = 8; swap (&к, &d); то компилятор сформирует определение функции: void swap(long* х, long* у) { long z я *x; *x = *y; *y я z; } Затем будет выполнено обращение именно к этой функции н зна- чения переменных k, d поменяются местами. Если в той же программе присутствуют операторы: double а = 2.44, Ь = 66.3; swap (&а, &Ь); то сформируется и выполнится функция void swap (double* х, double* у) { double z = *x; *x = *y; *y я z; } Проиллюстрируем сказанное о шаблонах на более конкретном примере. Рассмотрим следующую программу, в которой вспомним некоторые возможности функций, возвращающих значение типа "ссылка”. Но тип ссылки будет определяться параметром шаблона: //Р6-30.СРР - шаблон функций для поиска в массиве finclude <iostream.h> // Функция определяет ссылку на элемент с максимальным // значением: template <class type> type& rmax(int n, type d[]> { int im = 0;
232 Язык Си++ for (int i и 1; i < n; i++) im = d[im] > d[i] ? im : i; return d[im]; } void main() { int n = 4; int x[] = { 10, 20, 30, 14 } ; // Аргумент - целочисленный массив: cout « "\nnnax(n,x) = " « rmax(n,x); rmax(n,x) = 0; // Обращение с целочисленным массивом for (int i = 0; i < n; i++) cout « "\tx[" « i « "] « " « x[i]; float arx[] = { 10.3, 20.4, 10.5 }; // Аргумент - массив float: cout « ”\nrmax(3,arx)«'’ « rmax(3,arx) ; rmax(3,arx) = 0; // Обращение с массивом типа float for (i = 0; i < 3; i++) cout « *\tarx[" « i « *'] " « arx[i]; 1 Результат выполнения программы rmax(n,x) = 30 x[0] = 10 x[l] = 20 x[2] = 0 x[3] « 14 max (3,arx)=20.4 arx[0] = 10.3 arx[l] = 0 arx[2] = 10.5 В программе используются два разных обращения к функции тах(). В одном случае параметр - целочисленный массив и возвра- щаемое значение - ссылка типа int. Во втором случае фактический параметр - имя массива типа float и возвращаемое значение имеет тип ссылки на float. По существу механизм шаблонов функций позволяет автоматизи- ровать подготовку определений перегруженных функций. При ис- пользовании шаблонов уже нет необходимости готовить заранее все варианты функций с перегруженным именем. Компилятор автомати- чески, анализируя вызовы функций в тексте программы, формирует необходимые определения именно для таких типов параметров, кото- рые использованы в обращениях. Дальнейшая обработка выполняется так же, как и для перегруженных функций. Можно считать, что параметры шаблона функций являются его формальными параметрами, а типы тех параметров, которые исполь- зуются в конкретных обращениях к функции, служат фактическими параметрами шаблона. Именно по ним выполняется параметрическая настройка и с учетом этих типов генерируется конкретный текст определения функции. Однако, говоря о шаблоне семейства функций.
Глава 6. Функции, указатели, ссылки 233 обычно употребляют термин ’’список параметров шаблона’’, не добавляя определения ’’формальных”. Перечислим основные свойства параметров шаблона. 1. Имена параметров шаблона должны быть уникальными во всем определении шаблона. 2. Список параметров шаблона функций не может быть пустым, так как при этом теряется возможность параметризации и шаблон функций становится обычным определением конкретной функции. 3. В списке параметров шаблона функций может быть несколько параметров. Каждый из них должен начинаться со служебного слова typename либо class. Например, допустим такой заголовок шаблона: template Cclass typel, class type2> Соответственно, неверен заголовок: template Cclass typel, type2, type3> 4. Недопустимо использовать в заголовке шаблона параметры с одинаковыми именами, т.е. ошибочен такой заголовок: template Cclass t, class t, class t> Рис. 6.1. Схема параметризации шаблона функций 5. Имя параметра шаблона (в наших примерах typel, type2 и т.д.) имеет в определяемой шаблоном функции все права имени типа, т.е. с его помощью могут специализироваться формальные параметры, определяться тип возвращаемого функцией значения и типы любых объектов, локализованных в теле функции. Имя параметра шаблона видно во всем определении и скрывает другие использования того же идентификатора в области, глобальной по отношению к дан- ному шаблону функций. Если внутри тела определяемой функции необходим доступ к внешним объектам с тем же именем, нужно применять операцию изменения области видимости. Следующая про-
234 Язык Си++ грамма иллюстрирует указанную особенность имени параметра шаб- лона функций: //Р6-31.СРР - параметр шаблона и внешняя переменная с // тем же именем tinclude <iostream.h> int N; // Инициализирована no умолчанию нулевым значением / / Функция определяет максимальное из двух значении // параметров template Cclass N> N max (N x, N y) { N a = x; cout « ЛпСчетчик обращении N = " « ++::N; if (a < y) a = y; return a; } void main() { int a = 12, b = 42; max(a,b); float z = 66.3, f = 222.4; max (z, f) ; } Результат выполнения программы: Счетчик обращении N == 1 Счетчик обращении N = 2 Итак, одно имя нельзя использовать для обозначения нескольких параметров одного шаблона, но в разных шаблонах функций могут быть одинаковые имена у параметров шаблонов. Ситуация здесь та- кая же, как и у формальных параметров при определении обычных функций, и на ней можно не останавливаться подробнее. Действи- тельно, раз действие параметра шаблона заканчивается в конце опре- деления шаблона, то соответствующий идентификатор свободен для последующего использования, в том числе и в качестве имени пара- метра другого шаблона. Все параметры шаблона функций должны быть обязательно ис- пользованы в спецификациях параметров определения функции. Та- ким образом, будет ошибочным такой шаблон: template <class A, class В, class С> В func(A n, С т) {В valu; ... } В данном неверном примере остался неиспользованным параметр шаблона с именем в. Его применений в качестве типа возвращаемого
Глава 6. Функции, указатели, ссылки 235 функцией значения и для определения объекта valu в теле функции Недостаточно. Определяемая с помощью шаблона функция может иметь любое количество непараметризованных формальных параметров. Может быть не параметризовано и возвращаемое функцией значение. На- пример, в следующей программе шаблон определяет семейство функций, каждая из которых подсчитывает количество нулевых элементов одно- мерного массива параметризованного типа: //Р6-32.СРР - прототип шаблона для семейства функций tinclude <iostream.h> template Cclass D> long countO (int, D*}; //Прототип шаблона void main{) ( int A[] = { 1, 0, 6, 0, 4, 10 }; int n = sizeof(A}/ sizeof A[0]; cout « "\ncountO (n, A) = ”« countO(n,A); float X[] « { 10.0, 0.0, 3.3, 0.0, 2.1 }; n = sizeof(X)/ sizeof X[0]; cout « "\ncount0 (n,X) = "CC count0(n,X); } //Шаблон функций для подсчета количества нулевых //элементов в массиве template Cclass Т> long countO(int size, T* array} ( long k = 0; for (int i = 0; i C size; i++) if (int (array [i]) == 0) k++; return k; Результат выполнения программы: CountO(n,A) = 2 countO(n,X) = 2 В шаблоне функций countO (} параметр т используется только в спецификации одного формального параметра array. Параметр size и возвращаемое функцией значение имеют явно заданные непарамет- ризованные типы. Как и при работе с обычными функциями, для шаблонов функций существуют определения и описания. В качестве описания шаблона функций используется прототип шаблона: template Ссписок_параметров_шаблона> тип имя_функции (спецификация_параметров);
236 Язык Си++ В списке параметров прототипа шаблона имена параметров не обязаны совпадать с именами тех же параметров в определении шаб- лона. Это и продемонстрировано в программе. При конкретизации шаблонного определения функции необходи- мо, чтобы при вызове функции типы фактических параметров, соот- ветствующие одинаково параметризованным формальным парамет- рам, были одинаковыми. Для определенного выше шаблона функций с прототипом template <class Е> void swap(E,E); недопустимо использовать такое обращение к функции: int n в 4; double d 36 4.3; swap(n,d); // Ошибка в типах параметров Для правильного обращения к такой функции требуется явное приведение типа одного из параметров. Например, вызов swap (double (n) ,d) ; // Правильные типы параметров приведет к конкретизации шаблонного определения функций с пара- метром типа double. При использовании шаблонов функций возможна перегрузка как шаблонов, так и функций. Могут быть шаблоны с одинаковыми име- нами, но разными параметрами. Или с помощью шаблона может соз- даваться функция с таким же именем, что и явно определенная функция. В обоих случаях "распознавание" конкретного вызова вы- полняется по сигнатуре, т.е. по типам, порядку и количеству фактиче- ских параметров.
Глава 7. СТРУКТУРЫ И ОБЪЕДИНЕНИЯ Из основных типов языка Си++ пользователь может конструиро- вать производные типы, двум из которых посвящена настоящая глава. Это структуры и объединения. Вместе с массивами и классами струк- туры и объединения отнесены к структурированным типам. В этой главе рассмотрим те их особенности, которые относятся к процедур- ным возможностям языка, т.е. рассмотрим структуры и объединения в том виде, какой оии унаследовали от языка Си. 7.1. Структура как тип и совокупность данных В данной главе будем считать, что структура - это объединенное в единое целое множество поименованных элементов в общем случае разных типов. Сравнивая структуру с массивом, следует отметить, что массив - это совокупность однородных объектов, имеющая общее имя - идентификатор массива. Другими словами, все элементы массива являются объектами одного и того же типа. Это не всегда удобно. Пусть, например, библиотечная (библиографическая) карточка ката- лога должна включать сведения, которые приведены для книг в списке литературы, помещенном в конце нашей книги. Таким образом, для каждой книги будет указываться следующая информация: • фамилия и инициалы автора; • заглавие книги; • место издания; • издательство; • год издания; • количество страниц. Если к библиографической карточке каталога нужно обращаться как к единому целому, то воспользоваться массивом для представле- ния всех ее данных весьма сложно. Все данные имеют разные длины и разные типы. Объединить такие разнородные данные удобно с по- мощью структуры. Каждая структура включает в себя один или несколько объектов (переменных, массивов, указателей, структур и
238 Язык Си++ т.д.), называемых элементами структуры. Сведения о данных, входя- щих в библиографическую карточку, с помощью структуры можно представить таким структурным типом: struct card ( char *author; // Ф.И.О. автора книги char *tltle; // Заголовок книги char *city; // Место издания char *firm; // Издательство int year; // Год издания int pages; // Количество страниц ); Такое определение вводит новый производный тип, который бу- дем называть структурным типом. В данном примере у этого струк- турного типа есть конкретное имя card. В соответствии с синтаксисом языка определение структурного типа начинается со служебного слова struct, вслед за которым по- мещается выбранное пользователем имя типа. Описания элементов, входящих в структуру, помещаются в фигурные скобки, вслед за ко- торыми ставится точка с запятой. Элементы структуры могут быть как базовых, так и производных типов. Например, в структурах типа card будут элементы базового типа int и производного типа char *. Определив структурный тип, можно определять и описывать кон- кретные структуры, т.е. структурированные объекты, например, так: card reel, rec2, гесЗ; Здесь определены три структуры (три объекта) с именами reel, гес2, гесЗ. Каждая из этих структур содержит в качестве элементов свои собственные данные char *title; char *city; состав которых определяет структурный тип с именем card. Если структура определяется однократно, т.е. нет необходимости в разных частях программы определять или описывать одинаковые по внутреннему составу структурированные объекты, то можно не вво- дить именованный структурный тип, а непосредственно определять структуры одновременно с определением их компонентного состава. Следующий оператор определяет две структуры с именами хх, yy, массив структур с именем ее и указатель pst на структуру: struct { char N[12] ; int value; } XX, YY, EE[8], *pst;
Глава 7. Структуры и объединения 239 В хх,YY и в каждый элемент массива ЕЕ [0], ее [7] входят в ка- честве элементов массив char N[12] и целая переменная value. Име- ни у соответствующего структурного типа нет. Для обращения к объектам, входящим в качестве элементов в кон- кретную структуру, чаще всего используются уточненные имена. Об- щей формой уточненного имени элемента структуры является следующая конструкция: имя_структуры. имя__элемента_структуры Например, для определенной выше структуры уу оператор УУ.value « 86; Присвоит переменной value значение 86. Для ввода значения переменной value структуры ЕЕ[4] можно использовать оператор cin » ЕЕ[4].value; Точно так же можно вывести в выходной поток cout значение пе- ременной из любой структуры. Другими словами, элемент структуры обладает правами объекта того типа, который указан в конструкции (в определении) структурного типа. Например, для переменных с име- нем value из структур ее [0],..., ЕЕ [7], хх, уу определен тип int. При определении структур возможна их инициализация, т.е. зада- ние начальных значений их элементов. Например, введя структурный тип card, можно следующим образом определить и инициализировать конкретную структуру: card dictionary = { "Hornby A.S.", "Oxford students\ dictionary of Current English", "Oxford", "Oxford University", 1984, 769 }; Такое определение эквивалентно следующей последовательности операторов: card dictionary; dictionary .author =* "Hornby A.S."; dictionary.title = "Oxford students dictionary of Current English"; dictionary.city = "Oxford"; dictionary.firm = "Oxford University"; dictionary.year = 1984; dictionary.pages = 769;
240 Язык Си++ Нужно еще раз обратить внимание на отличие имени конкретной структуры (в наших примерах dictionary, reel, гес2, гесЗ, XX, YY, ее [0],..., ее [7]) от имени структурного типа (в нашем случае card). С именем структурного типа не связан никакой конкретный объ- ект, и поэтому с его помощью нельзя сформировать уточненные имена элементов. Определение структурного типа вводит только шаблон (формат, внутреннее строение) структур. Идентификатор card в на- шем примере - это название структурного типа, т.е. "ярлык" или "этикетка" структур, которые будут определены в программе. Чтобы подчеркнуть отличие имени структурного типа от имени конкретных структур, имеющих этот тип, в англоязычной литературе по языку Си++ для обозначения структурного типа используется термин tag (ярлык, этикетка, бирка). В переводе на русский язык книги Б.Кернигана, Д.Ритчи [3] для обозначения структурного типа исполь- зована транслитерация тег. Переводчики с английского и авторы рус- скоязычных книг по языку Си++ не решились украсить этим термином описание синтаксиса языка Си++, поэтому мы будем гово- рить о структурном типе, не употребляя термина тег. Итак, в нашем примере внутренний состав объектов, представ- ляющих библиографические карточки, определен с помощью струк- турного типа с именем card. Тем самым вводится формат (набор) данных, входящих в будущие однотипные структуры, и этот формат, обозначенный именем card, имеет права типа, введенного пользова- телем. С его помощью в качестве примера выше определены конкрет- ные структуры (объекты) reel, гес2, гесЗ. Определение структурного типа может быть совмещено с опреде- лением конкретных структур этого типа: struct PRIM (char *name; long sun;} А, В, C; Здесь определен структурный тип с именем PRIM и три структуры а, в, с, имеющие одинаковое внутреннее строение. Повторяю, что будет ошибкой использовать имя структурного типа для именования элемента структуры: PRIM.sum = 800L; // Ошибочная конструкция С.sum 800L; // Верная конструкция Так как имя структурного типа обладает всеми правами имен ти- пов, то разрешено определять указатели на структуры: имя_структурного_типа *имя_указателя_на__структуру;
Глава 7. Структуры и объединения 241 Как обычно, определяемый указатель может быть инициализиро- ван. Значением каждого указателя на структуру может быть адрес структуры того же типа, т.е., грубо говоря, номер байта, начиная с которого структура размещается в памяти. Структурный тип задает ее размеры и тем самым определяет, на какую величину (на сколько бай- тов) изменится значение указателя на структуру, если к нему приба- вить I (или из него вычесть I). Например, после наших определений структурного типа card и структуры гес2 можно так записать определение указателя на струк- туру типа card: card *ptrcard » &rec2; Здесь определен указатель ptrcard и ему с помощью инициализа- ции присвоено значение адреса одной из конкретных структур типа card. После определения такого указателя появляется еще одна возмож- ность доступа к элементам структуры гес2. Ее обеспечивает операция доступа к элементу структуры, с которой в этот момент связан указатель. Формат соответствующего выражения таков: имя_ухазателя -> имя__элемента_структуры । Например, количество страниц книги, информация о которой < хранится в структуре гес2, будет значением выражения ptrcard -> pages S Вторая возможность обращения к элементу структуры с помощью I адресующего ее указателя - это разыменование указателя и формиро- вание уточненного имени такого вида: (*имя_указателя) . имя__элемента_структуры Обратите внимание на круглые скобки. Операция разыменования г ’ * ’ должна относиться только к имени указателя, а не к уточненному имени элемента структуры. Таким образом, следующие три выраже- ния эквивалентны: (♦ptrcard) .pages рtгса rd->pages rec2.pages Все они именуют один и тот же элемент int pages конкретной структуры гес2, имеющей тип card.
242 Язык Си++ Как и для других объектов, для структур могут быть определены ссылки: имя_структурного_типа& имя_ссылки_на_структуру инициализатор; Например, для введенного выше структурного типа prim можно таким образом ввести ссылки на структуры а, в: PRIM& refA » А; PRIM& refB(В); Для разнообразия ссылки refA и refB инициализированы по- разному. После таких определений refA есть синоним имени структуры а, refB есть другое имя для структуры в. Теперь возможны, например, такие обращения: • A. sum эквивалентно refA. sum; • *В. name эквивалентно *refB. name; • А. name эквивалентно refA. name. Вернемся к нашей задаче с библиотечными карточками и спроек- тируем простейшую базу данных, построив ее в виде двухсвязного списка структур (рис. 7.1), каждая из которых имеет такой формат: struct record { card book; // Структура с данными о книге record *prior; // На предыдущий элемент списка record *next; // На следующий элемент списка Структурный тип record предусматривает, что в каждую струк- туру входят три элемента: структура типа card и два указателя на структуры типа record. Предполагается, что до определения струк- турного типа record уже определен структурный тип card. Указатель на структуру может входить в определение того же структурного типа. Именно так в определение формата структуры record введены два указателя: • record *prior - указатель на предыдущий элемент в двухсвяз- ном списке структур; • record *next - указатель на следующий элемент в двухсвязном списке структур. Чтобы не усложнять нашу задачу создания библиотечной карто- теки, примем следующие ограничения и упрощения. Чтобы не про-
i Глава 7. Структуры и объединения 243 граммировать процедур ввода исходных данных, подготовим сведе- ния о книгах, включаемых в картотеку, в виде массива структур типа ' card. Массив структур типа card с исходными данными будем обра- батывать в цикле и включать каждую очередную запись о книге в двухсвязный список структур типа record. Включая записи о книгах в список, будем соблюдать алфавитный порядок по именам авторов. Чтобы не вводить средств для сравнения русских фамилий, а пользо- ваться стандартными библиотечными функциями сравнения строк, будем рассматривать только книги с фамилиями авторов на англий- ском языке. Для каждой новой карточки, включаемой в двухсвязный список, будем запрашивать необходимый объем памяти, т.е. сформи- руем список с помощью средств динамического выделения памяти. Начало списка будем сохранять в отдельном указателе структурного типа record *. Указатель prior для первого элемента списка и указа- тель next для последнего элемента списка будут иметь значение null. р N | Р начало —► | 0 | book | —{--► | -г | book | —|-► t_________________J N [-1- |book | 0 | Р N Рис. 7.1. Схема двухсвязного списка из трех элементов типа record: Р - указатель prior; N - указатель next; 0 - значение NULL Следующая программа соответствует приведенным соглашениям. До функции main () определены структурный тип card, функция printbook () для вывода на экран информации о выбранной книге, структурный тип record. Здесь же, как внешние данные, доступные для всех функций программы, определены и инициализированы эле- менты массива book [ ] структур типа card. //Р7-01.СРР - библиографическая картотека - двухсвязный / / список // Для функции strcmpQ сравнения строк: «include <string.h> «include <iostream.h> struct card ( // Определение структурного типа для книги char *author; // Ф.И.О. автора char *title; // Заголовок книги char *city; // Место издания char *firm; // Издательство int year; // Год издания int pages; // Количество страниц
244 Язык Си++ // Функция печати сведений о книге: void printbook(cards car) ( static int count = 0; cout « "\n" « ++count « ". " « car.author; cout « ' 1 « car. title « " « car. city; cout « " « car.firm « ", cout « "\n" « car.year « " « car.pages « " c."; struct record ( // Структурный тип для элемента списка card book; record *prior; record *next; }; // Исходные данные о книгах: card books[] = { // Инициализация массива структур: ( "Wiener R.S.", "Язык Турбо Си", "M", "Мйр", 1991, 384 ), { "Stroustrup В."," Язык Си++", "Киев", "ДиаСофт", 1993, 560 ), ( "Turbo C++.", "Руководство программиста "М", "ИНТКВ", н 1991, 394 ), { "Lippman S.B.", "C++ для начинающих", "М" , "ГЭЛИОН", 1993, 496 ); void main () ( record *begin 3 NULL, // Указатель начала списка *last 3 NULL, // Указатель на очередную запись ♦list; // Указатель на элементы списка // п - количество записей в списке: int n s sizeof(books) / sizeof(books[0]); // Цикл обработки исходных записей о книгах: for (int i = 0; i < n; i++) { // Создать новую запись (элемент списка): last 3 new(record); // Занести сведения о книге в новую запись: (♦last).book.author — books[i].author; (♦last).book.title 3 books[i].title; last->book.city — books[i].city; last->book.firm = books[i].firm; last->book.year - books[i].year; last->book.pages = books[i].pages; // Включить запись в список (установить связи) : if (begin = NULL) // Списка еще нет { last->prior - NULL; begin — last; last->next = NULL; )
Глава 7. Структуры и объединения 245 else { //Список уже существует list = begin; // Цикл просмотра списка - поиск места для // новой записи; while (list) {if (strcmp(last->book.author, list->book.author) < 0 { //Вставить новую запись перед list: if (begin == list) { //Начало списка: last->prior = NULL; begin = last; } else { //Вставка между записями: list~>prior->next = last; last->prior = list->prior; } list->prior = last; last~>next = list; //Выйти из цикла просмотра списка: break; } if (list -next = NULL) { //Включить запись в конец списка: last“>next = NULL); last~>prior = list; list“>next = last; //Выйти из цикла просмотра списка: break; //Перейти к следующему элементу списка: list = list->next; } //Конец цикла просмотра списка // (поиск места для новой записи) ) // Включение записи выполнено //Конец цикла обработки исходных данных } //Печать в алфавитном порядке библиографического списка: list = begin; cout « ' \n ’ ; while (list) {printbook (list->book); list = list->next; } }
246 Язык Си++ Р__________ N begin—»| О [book [0] | 0 ] Рис. 7.2. Последовательное формирование двухсвязного списка библиографической картотеки для программы Р7-01 .СРР Результаты выполнения программы Р7-01.срр: 1. Lippman S.B. C++ для начинающих.- М: ГЭЛИОН, 1993.- 496с. 2. Stroustrup В. Язык Си++.- Киев: ДиаСофт, 1993.- 560с. 3. Turbo C++. Руководство программиста.- М: ИНТКВ, 1991.- 394 с. 4. Wiener R.S. Язык Турбо Си.- М: Мир, 1991.- 384с. Изучая структуры, имеет смысл обратить внимание на их пред- ставление в памяти ЭВМ. В следующей программе определена струк- тура str и выведены значения адресов ее элементов: //Р7-02.СРР - размещение в памяти элементов структуры tinclude <iostream.h> void main () { struct ( long L; int il, i2; char c[4]; ) STR» { 10L, 20, 30, 'a', 'b' , »c', 'd' ); cout « "\nsizeof(STR) " « sizeof (STR) « hex; cout « M\nfiSTR.L = " « fiSTR.L; cout « "\nfiSTR.il = " « fiSTR.il; cout « "\nfiSTR.i2 = ” « &STR.12; cout « "\nfiSTR.c = " « fiSTR.c; )
Глава 7. Структуры и объединения 247 Результат выполнения программы: i < sizeof(STR) = 12 fiSTR.L = 0x8d800ff4 fiSTR.il = 0x8d800ff8 fiSTR.i2 » 0x8d800ffa fiSTR.C = 0x8d800ffc Результаты программы и соответствующая им схема на рис. 7.3 иллюстрируют основные соглашения о структурах: все элементы раз- мещаются в памяти подряд и сообща занимают именно столько места, сколько отведено структуре в целом. байты f4 f5 f6 f7 f8 f9 fa fb fc fd fe ff I I I I I i f I | a | b | c | d I | long L I int il| int i2| char c[4] | Рис. 7.3. Размещение в памяти конкретной структуры из программы Р7-02. СРР Размещение элементов структуры в памяти может в некоторых пределах регулироваться с помощью опций компилятора. Например, | в компиляторах ТС++ и ВС++ [4, 9] имеется возможность "выравни- Л вания" структур по границам слов. При этом каждая структура раз- ll мещается в памяти с начала слова, т.е. начинается в байте с четным Ц адресом. Если при этом структура занимает нечетное количество бай- ff тов, то в ее конец добавляется дополнительный байт, чтобы структура занимала целое количество слов. 4 Приведем еще некоторые сведения о структурах, которые не сразу Ч бросаются в глаза. Начнем с сопоставления структурного типа с ти- пами, вводимыми с помощью служебного слова typedef. Напомним, что конструкция typedef тил_даннмх идентификатор; вводит новое обозначение (идентификатор) для типа_данкых, кото- рый может быть как базовым, так и производным. В качестве такого производного типа можно использовать структурный тип: typedef struct PRIM { char *name; long sum; } NEWSTRUCT; NEWSTRUCT st, *ps, as[8]; PRIM prim_st, *prim_ps, prim_as[8);
248 Язык Си++ В этом примере введен структурный тип prim, и ему дополнитель- но с помощью typedef присвоено имя newstruct. В дальнейшем это новое имя типа использовано для определения структуры st, указате- ля на структуру ps и массива структур as [8]. С помощью структурно- го типа prim определены такие же объекты: структура prim_st, указатель prim_ps, массив структур prim_as[8]. Таким образом, основное имя структурного типа (prim) и имя, дополнительно введен- ное для него с помощью typedef (newstruct), совершенно равноправ- ны. При определении с помощью typedef имени для структурного типа у последнего может отсутствовать основное имя. Например, струк- турный тип можно ввести и следующим образом: typedef struct ( char *name; long sum; } STRUCT; STRUCT struct_st, *struct_ps, struct_as[8]; В данном примере имя типа struct вводится с помощью typedef для структуры, тип которой не поименован. Затем имя типа struct используется для определения объектов. Таким образом, имеются две возможности определения имени структурного типа, и программист вправе выбирать любую из них. В отношении элементов структур существует практически только одно существенное ограничение - элемент структуры не может иметь тот же самый тип, что и определяемый структурный тип. Таким обра- зом, следующее определение структурного типа ошибочно: struct mistake { mistake s; int m; }; // Ошибка! В то же время элементом определяемой структуры может быть указатель на структуру определяемого типа: struct correct { correct *рс; long f; }; // Правильно! Элементом определяемой структуры может быть структура, тип которой уже определен: struct begin ( int k; char *h; } strbeg; struct next ( begin beg; float d; }; Если в определении структурного типа нужно в качестве элемента использовать указатель на структуру другого типа, то разрешена та- кая последовательность определений [9, 21]: struct А; // Неполное определение структурного типа struct В ( struct А *ра; }; struct А { struct В *pb; };
Глава 7. Структуры и объединения 249 Неполное определение структурного типа а можно использовать в определении структурного типа в, так как определение указателя ра на структуру типа а не требует сведений о размере структуры типа а. Последующее определение в той же программе структурного типа а обязательно. Использование в структурах типа а указателей на структуры уже введенного типа в не требует пояснений. Рассматривая "взаимоотношение*1 структур и функций, видим две возможности: возвращаемое функцией значение и параметры функ- ции. Функция может возвращать структуру как результат: struct help { char *name; int number; ); help fund(void); // Прототип функции Функция может возвращать указатель на структуру: help *func2(void); // Прототип функции Функция может возвращать ссылку на структуру: helps func3(void); // Прототип функции Через аппарат параметров информация о структуре может пере- даваться в функцию либо непосредственно, либо через указатель, ли- бо с помощью ссылки: void fund (help str); // Прямое использование void func5 (help *pst) ; // С помощью указателя void func6 (helps rst); // С помощью ссылки Напомним, что применение ссылки на объект в качестве парамет- ра позволяет избежать дублирования объекта в памяти. В программе Р7-01.СРР при формировании двухсвязного списка, объединяющего структуры с данными о книгах, для новых записей память выделялась динамически, т.е. во время выполнения програм- мы: struct record ( catd book; record *prior; record *next ); record *last; last — new (record); Это позволяет обратить внимание на применение операции new к структурам. Итак, операндом для операции new может быть струк-
250 Язык Си++ турный тип. В этом случае выделяется память для структуры исполь зованного типа, и операция new возвращает указатель на выделеннун' память. Память может быть выделена и для массива структур, напри мер, так: last » new record[9] ; // Память для массива структур В этом случае операция new возвращает указатель на начало мае сива. Дальнейшие действия с элементами массива структур подчиняют ся правилам индексации и доступа к элементам структур. Например разрешен такой оператор: last[0].next « NULL; 7.2. Объединения разнотипных данных Со структурами ’’в близком родстве" находятся объединения, ко торые вводятся с помощью служебного слова union. Чтобы пояснить "степень родства" объединений со структурами, рассмотрим приве денное выше в программе Р7-02 .срр определение структуры str: struct { long L; int il, i2; char c[4]; } STR; Размещение этой структуры в памяти схематично изображено ня рис.7.3. Важно то, что каждый элемент структуры имеет свое соб ственное место в памяти и размещаются эти элементы последователь но. Определим очень похожее внешне на структуру str объединение uni: union ( long L; int il, i2; char c[4]; } UNI; Количество элементов в объединении с именем uni и их типы сов падают с количеством и типами элементов в структуре str. Но су ществует одно очень важное отличие, которое иллюстрирует рис. 7.4 - все элементы объединения имеют один и тот же начальный адрес. Участок памяти, выделенный объединению У - » байт | байт байт | байт <— int il -* 4- int i2 -* — char с[4] J Рис. 7.4. Схема размещения в памяти объединения UNI
Глава 7. Структуры и объединения 251 Следующая программа подтверждает сказанное: //Р7-03.СРР - размещение в памяти объединения # include <iostream.h> void main() { union { long L; int il, i2; char c[4]; } UNI = { 10L }; cout « "\nsizeof (UNI) ==" « sizeof (UNI) «hex; cout « "\n&UNI.L =" «&UNI.L; cout « "\n&UNl.il =" «&UNI.il; cout « "\n4UNI.i2 =’’ <<&UNI.i2; cout « "\n&UNI.c =" «SUNI.c; cout « "\nsizeof(UNI.il) =»” « sizeof (UNI. il) ; cout « "\nsizeof(UNI.L) =" « sizeof(UNI.L); Результат выполнения программы: sizeof(UNI) « 4 &UNI.L = 0x8d7d0ffc &UNI.il = 0x8d7d0ffc &UN1.12 = 0x8d7d0ffc &UNI.C = 0x8d7d0ffc sizeof(UNI.il) = 2 sizeof(UNI.L) = 4 Как подтверждают результаты выполнения программы, все эле- менты объединения uni имеют один начальный адрес. Размеры эле- ментов соответствуют их типам, а размер объединения определяется максимальным размером его элементов. Итак, объединение можно рассматривать как структуру, все эле- менты которой при размещении в памяти имеют нулевое смещение от начала. Тем самым все элементы объединения размещаются в одном и том же участке памяти. Размер участка памяти, выделяемого для объ- единения, определяется максимальной длиной его элементов (рис. 7.5). double D ♦•int I Рис. 7.5. Размещение в памяти объединения union { double D; int I; ) UDI;
252 Язык Си++ Как и для структур, для объединений может быть введен про граммистом производный тип, определяющий "внутреннее строение всех объединений, относящихся к этому типу. Если для структур ан глийский термин tag мы заменили на структурный тип, то для union type можно говорить об объединяющем типе. Поэтому будем гово рить о типе объединения: union имя_объединяжяцего_типа { элементы^объединения }; Пример объединяющего типа: union mixture { double d; long E[2]; intK[4); }; Введя тип объединения, можно определять конкретные объединс ния, их массивы, а также указатели и ссылки на объединения: mixture mA, mB[4] ; // Объединение и массив объединении mixture *pmix; // Указатель на объединение mixtures rmix — mA; // Ссылка на объединение Для обращения к элементу объединения можно использовать либ< уточненное имя: имя_объединения. имя_элемента либо конструкцию, включающую указатель: указатель_на__объединение->имЯ—элемента (♦указатель^на__объединение) . имя__элемента либо конструкцию, включающую ссылку: ссылка__на__объединение .имя_элемента Примеры: mA. d = 64.8; mB[2].Е[1] = 10L; pmix — &тВ[0] ; pmix->E[0] = 66; cin » (*pmix).К[1]; cin » rmix.E[0]; Заносить значения в участок памяти, выделенный для объединс ния, можно с помощью любого из его элементов. То же самое спра ведливо и относительно доступа к содержимому участка памяти выделенного для объединения. Если бы элементы объединения имели
Глава 7. Структуры и объединения 253 одинаковую длину и одинаковый тип, а отличались только именами, то использование объединения было бы подобно применению ссылок. Просто один участок памяти в этом случае имел бы несколько раз- личных имен: union { int ii, int jj } uni j ; unij.ii =15; // Изменяем содержимое cout « unij.jj; // Выводим содержимое Основное достоинство объединения - возможность разных трак- товок одного и того же содержимого (кода) участка памяти. Напри- мер, введя объединение union { float F; unsigned long К; } FK; можно занести в участок памяти, выделенный для объединения fk, вещественное число: FK.F = 3.141593; а затем рассматривать код его внутреннего представления как неко- торое беззнаковое длинное целое: cout « FK.K; (В данном случае будет выведено 1078530012.) Если включить в объединение символьный массив такой же дли- ны, что и другие элементы объединения, то получим возможность до- ступа к отдельным байтам внутреннего представления объединения. Например, определим объединение: union ( float F; unsigned long L; char H[4] ; } FLH; . Занеся в участок памяти, выделенный для объединения, веще- ственное число, например, так: FLH.F = 2.718282; можем получить значение кода его внутреннего представления с по- мощью уточненного имени flh.l и(или) значения кодов, находя- щихся в отдельных байтах: flh.h[0], flh.h[1], flh.h[2], flh.h[3J. Итак, основное назначение объединений - обеспечить возмож- ность доступа к одному и тому же участку памяти с помощью объек- тов разных типов. Необходимость в таком механизме возникает, например, для выделения из внутреннего представления (из кода) объ- екта определенной части. Например, следующее объединение с именем
254 Язык Си++ сс позволяет выделить из внутреннего представления целого числ.« его отдельные байты: union { char hh[2]; int ii; } cc; Здесь символьный массив char hh[2] и целая переменная int ii - элементы объединения - соответствуют одному участку памяти (рис.7.6). Участок памяти, выделенный объединению сс (Массив символов) <- hh[l] —hh[O] -» байт I байт <-------- il --------> (Целая переменная) Рис. 7.6. Размещение в памяти объединения сс Используем введенное объединение с именем сс для решения не большой конкретной задачи, связанной с доступом к буферу клавиа туры ПЭВМ типа IBM PC. В MS-DOS принято, что нажатие на любую клавишу клавиатуры ПЭВМ приводит к занесению в буфер клавиатуры (буфер клавиатуры - это специально зарезервированный участок памяти) целого двухбайтового числа. Каждый байт этого це лого числа имеет самостоятельное смысловое значение. Байт с млад шим адресом содержит так называемый ASCII-код клавиши, а старший (с большим адресом) содержит дополнительный код, назы ваемый скэн-кодом клавиши. В библиотеке компилятора Турбо Си (и ВС++) имеется специальная функция int bioskey{int b); позво ляющая получить доступ к буферу клавиатуры. Параметр int ь функции bioskeyO позволяет выбрать режим ее использования. Об ращение bioskey (1) проверяет наличие в буфере хотя бы одного ко да. Если буфер пуст, то bioskey (1) возвращает нулевое значение. Как только в буфере появится код (от нажатия клавиши), функция bioskey (1) возвратит ненулевое значение. Прочитать тот код, кото рый занесен в буфер, и очистить буфер от этого кода позволяет обра щение к функции bioskeyO с нулевым значением параметра. При обращении bioskey(0) функция выбирает из буфера клавиатуры очередной двухбайтовый код и возвращает его как целое число (двухбайтовое). Выделить из этого целого числа отдельные байты очень просто с помощью объединения. Следующая программа полу
Глава 7. Структуры и объединения 255 чает и выводит на экран значения кодов, поступающих в буфер кла- виатуры ПЭВМ, работающей под управлением MS-DOS: //Р7-04.СРР - объединение выделяет скэн и ASCII-коды // клавиш linclude <bios.h> // Для функции bioskeyO #include <iostream.h> void main() { union { char hh[2]; int ii; } cc; unsigned char sen, // Скэн-коды asc; // ASCII-коды cout « "\пВыход из программы no Ctrl+Z"; cout « "\n\nSCAN | ASCII"; do { // Цикл до ctrl+Z cout« "\n"; while (bioskey(l) = 0) ; //До появления кода cc.ii s bioskey(O) ; asc « cc.hh[0]; sen cc.hh[l]; cout « " ” « int(sen) « ” | "; cout « int(asc) « " " « asc; } // Выход из цикла no Ctrl+Z, когда asc=»26 // и scn«"44: while (asc != 26 | | sen •= 44) ; ) Результат выполнения программы: Выход SCAN 34 35 36 24 44 из программы по Ctrl+Z ASCII 103 g 104 h 106 j 111 о 26 Для ASCII-кода печатается не только числовое значение, но и со- ответствующий ему экранный символ. Обратите внимание на внутренний цикл while с проверкой значе- ния bioskey(l). Он прерывается только при появлении внешнего со- бытия - при нажатии на клавишу. Значение bios key (1) становится при этом отличным от 0, и следует переход к оператору с обращением bioskey (0). Возвращаемое целое значение заносится в элемент объединения cc.ii, а потом из объединения выбираются отдельные однобайтовые коды (sen, asc). Внешний цикл будет прерван при появлении кодов
256 Язык Си++ asc=26 и scn=44 (сочетание клавиш ctri+z). После этого програм ма прекращает выполнение. Как массивы, так и структуры могут быть элементами объедим ний, причем здесь возможны весьма разнообразные сочетания. В ка честве содержательного примера рассмотрим тип объединения reg? введенный в заголовочном файле dos .h компиляторов ТС++ и ВС+ * Объединение типа regs позволяет обращаться к регистрам процессе ра 80x86 двумя способами. Можно рассматривать регистры как 1G разрядные, а можно обращаться к отдельным 8-разрядным половинам регистров. Прежде чем пояснять сказанное, рассмотрим определении нужных типов из файла dos. h: struct WORDREGS { unsigned int ax, bx, ex, dx, si, di, eflag, flags; }; struct BYTEREGS { unsigned char al, ah, Ы, bh, cl, ch, dl, dh; union REGS { struct WORDREGS x; struct BYTEREGS h; }; Как следует из определений, regs ~ это тип, обеспечивающий объ единение двух структур. Структуры типа wordregs позволяют обра щаться к регистрам как к двухбайтовым беззнаковым целым Структуры типа byteregs обеспечивают доступ к отдельным байтам первых четырех регистров. Заметьте, что структуры wordregs и byteregs имеют разную длину в памяти. При их включении в одно объединение длина объединения равна длине, являющейся макси мальной из длин его элементов, т.е. s i zeof (REGS) “ s i zeof (WORDREGS) He вдаваясь в подробности, отметим, что объединения типа regs используются при обращении к библиотечным функциям обработки прерываний. Например, для вызова функции 8 прерывания 16 (0x10) можно использовать такой набор операторов: union REGS in, out; in. h. ah » 8; // Номер вызываемой функции in.h.bh » page; // Номер страницы видеопамяти int86(0xl0, tin, &out); // Вызов прерывания 16 *ch » out.h.al; // Код ASCII символа из видеопамяти *attr » out.h.ah; // Атрибуты символа из видеопамяти О возможностях функции int86() можно прочитать в описании стандартной библиотеки ТС++ или ВС++ (см. [6, 11]).
Глава 7. Структуры и объединения 257 При определении конкретных объединений разрешена их инициа- лизация, причем инициализируется только первый элемент объедине- ния. Примеры: union compound { long LONG; int INT[2]; char CHAR[4]; }; compound mixl = { 11111111 ); // Правильно compound mix2 = { ’a’, * b1 , ' c' , 'd' }; // Ошибка union { char CHAR[4]; long LONG; int INT[2] } mix = ('a*,'b',* c','d'}; // Правильно Для объединения mix инициализирован первый элемент - массив символов из четырех элементов, что вполне допустимо. При определении объединений без явного указания имени объ- единяющего типа (как в последнем примере для объединения mix) разрешено не вводить даже имени объединения. В этом случае соз- дается анонимное или безымянное объединение: union { int IN[5]; char GH[10] ) = { 1, 2, 3, 4, 5 }; К элементам анонимного объединения можно обращаться как к отдельным объектам, но при этом могут изменяться другие элементы объединения: IN[0] = 10; // Изменятся значения СН[0], СН[1] СН[9] 3 'а1; // Изменится значение IN[4] Разрешено формировать массивы объединений и инициализиро- вать их: compound mixture[] ж { IL, 2L, 3L, 4L }; Здесь для каждого элемента mixture [i] введенного массива из че- тырех объединений типа compound инициализация выполнена для первого компонента объединения, т.е. начальное значение явно полу- чил каждый элемент mixture[i] .long. Доступ к внутренним кодам этих значений возможен также через элементы mixture[i] .iNT[j] и mixture[i] .CHAR[к]. . -»-WI
258 ЯзыкСи++ 7.3. Битовые поля структур и объединений Внутри структур и объединений могут в качестве их компонентов (элементов) использоваться битовые поля. Каждое битовое поле пред- ставляет целое или беззнаковое целое значение, занимающее в памяти фиксированное число битов (в компиляторе ВС++ от 1 до 16 бит). Би трвые поля могут быть только элементами структур, объединений (и, как увидим в дальнейшем, классов), т.е. битовые поля не могут появ ляться как самостоятельные объекты программ. Битовые поля не имеют адресов, т.е. для них не определена операция ’ fi’, нет указате лей и ссылок на битовые поля. Они не могут объединяться в массивы. Назначение битовых полей - обеспечить удобный доступ к отдельным битам данных. С помощью битовых полей можно формировать объ- екты с длиной внутреннего представления, не кратной байту. Это по зволяет плотно "упаковывать” информацию и тем самым экономить память, например, при работе с однобитовыми флажками. Определение структуры с битовыми полями имеет такой формат: struct { тиП—поля имя^поля: ширина—поля; ТИП—поля имЯ—поля: ширина—поля; ) имя—структуры; Здесь тип-Поля - один из базовых целых типов int, unsigned int (сокращенно unsigned), signed int (сокращенно signed), char, short, long и их знаковые и беззнаковые варианты. (В языке Си стандарт ANSI до пускает только знаковый или беззнаковый вариант типа int.) ИМЯ—поля идентификатор, выбираемый пользователем; ширина-поля целое неотрицательное десятичное число, значение которого обычно не должно превышать длины слова конкретной ЭВМ. Таким образом, диапазон возможных значений ширины_поля су- щественно зависит от реализации. В компиляторах ТС++ и ВС++ ши- рина-поля может выбираться в диапазоне от 0 до 16. Пример определения структуры с битовыми полями: struct ( int а: 10; int b:14; } хх, *рх;
Глава 7. Структуры и объединения 259 Для обращения к битовым полям используются те же конструкции, что и для обращения к обычным элементам структур: имя_структуры. нмя_поля указатель—на_СФруктуру-> имя_поля ссылка_на_структуру. имя_поля (*указатель_на„структуру). имя_поля Например, для введенной структуры хх и указателя рх допустимы такие операторы: хх.а = 1; рх = &хх; рх->Ь = 48; От реализации зависит порядок размещения полей структуры ь памяти ЭВМ. Поля могут размещаться как справа налево, так и слева направо. Кроме того, реализация определяет, как размещаются в памяти битовые поля, длина которых не кратна длине слова и(или) длине байта (рис. 7.7). Для компиляторов, работающих на IBM PC, поля, разме- щенные в начале описания структуры, имеют младшие адреса. Именно такое размещение изображено на рис. 7.7. 7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0 z z z z Z~Z|2Tz~Z Z ZZZZ р., — int Ь:14 —— 1 » 76 5 4321076 5 432 10 Z Z[Z Z Z Z Z Z Z Z р ....... int а: 10 — 76543210 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 zzz z z z z z|z z z zYFxx|xxxxxxTx ...int Ь;14 »j<—- int a: 10 > Рис. 7.7. Варианты размещения в памяти битовых полей структуры В компиляторах часто имеется возможность изменять размещение битовых полей, выравнивая их по границам слов или выполняя плотную упаковку. Некоторые возможности влиять на размещение битовых полей в памяти имеются на уровне синтаксиса самого языка Си++. Во-первых, при определении битового поля разрешается не указывать его имя. В этом случае (когда указаны только двоеточие и ширина поля) в структуру вводятся неиспользуемые (недоступные) биты, формирующие промежуток между значимыми полями. Например:
260 Язык Си++ struct { int а:10; int :6; int b:14; } yy; 76543210765432 076543210765432 10 Z Z Z Z Z Z|Z z z z z z z z L - int b: 14 » Puc. 7.8. Структура с безымянным полем В структуре уу между полем int а: 10 и полем int b:14 разме- щаются 6 бит, не доступных для использования. Их назначение - вы- равнивание полей по плану программиста (рис. 7.8). Битовые поля в объединениях используются для доступа к нуж- ным битам того или иного объекта, входящего в объединение. На- пример, следующее объединение позволяет замысловатым способом сформировать код символа Ъ' (равный 68): union { char sitab; struct { int x:5; int у:3; } hh; } cod; cod.hh.x = 4; cod.hh.y = 2; cout « cod.simb; // Выведет на экран символ *D* Рис. 7.9 иллюстрирует формирование кода 68, соответствующего символу 'o’. Номера битов: Битовые поля: Элемент char: 7 6 5 4 3 2 10 ---j-j-j--j-(-j-!- 01000100 ------------1-1-[ II-1--1- cod. hh. у —Н«-cod. hh. х-> < 1 " char sinb-► Рис. 7.9. Объединение со структурой из битовых полей Для иллюстрации особенностей объединений и структур с бито- выми полями рассмотрим следующую программу: / / Р 7 - 0 5 .СРР - битовые поля, структуры t объединения tinclude <iostream.h>
Глава 7. Структуры и объединения 261 // Функция упаковывает в один байт остатки от деления // на 16 двух целых чисел - параметров: unsigned char cod(int a,int b) { union { unsigned char z; struct { unsigned int x:4; // Младшие биты unsigned int y:4; // Старшие биты } hh; } un; un.hh.x я a % 16; un.hh.y » b % 16; return un.z; // Функция изображает на экране двоичное представление // байта-параметра: void binar(unsigned char ch) ( union ( unsigned char ss; struct ( unsigned aO: 1 ; unsigned al: 1 ; unsigned a2:l; unsigned a3:1 ; unsigned a4:l; unsigned a5:1 ; unsigned аб: 1 ; unsigned a 7 :1; ) byte; } codr- cod, ss » ch; // Занести значение параметра в объединение // Выводим биты внутреннего кода Значения параметра: cout « "\nHOMEPA БИТОВ: 76543210"; cout « "\пЗначеиия битов:"; cout « "\t” « cod.byte.a7 « ” " « cod.byte.аб; cout « ” 11 « cod.byte.a5 « " " « cod.byte.a4; cout « 11 11 « cod.byte.a3 « " ” « cod.byte.a2; cout « " " « cod.byte.al « " •' « cod.byte.aO; cout « "\n"; } void main() ( int k; int m, n; cout « "\nm я ”; cin » m; cout « "n = cin » n; k я cod(m,n); cout « "cod я » « k ; binar(kJ; }
262 Язык Си++ Возможный результат выполнения программы: m = 1 <Enter> n « 3 <Enter> cod » 49 НОМЕРА БИТОВ: 76543210 Значения битов: 00110001 Результат еще одного выполнения программы: m “ 0 <Enter> n “ 1 <Enter> cod « 16 НОМЕРА БИТОВ: 76543210 Значения битов: 00010000 Комментарии в тексте программы и приведенные результаты об ь ясняют особенности программы. В функциях cod() ИЫпаг() исполь зованы объединения, включающие структуры с битовыми полями. В функции cod () запись данных выполняется в битовые поля структур и hh, входящей в объединение un, а результат выбирается из того объединения un, но как числовое значение байта. В функции binar () обратное преобразование - в нее как значение параметра передаем и байт, содержимое которого побитово "расшифровывается" за счс< обращения к отдельным полям структуры byte, входящей в объеди нение cod. Одно из принципиальных отличий языка Си++ от языка Си возможность включения в структуры и объединения не только дан ных, но и функций. В этом случае структурный тип или объединяй» щий тип становится определением класса. Подробно о классах речь пойдет в гл. 9.
Глава 8. ПРЕПРОЦЕССОРНЫЕ СРЕДСТВА В.1. Стадии и команды препроцессорной обработки В интегрированную среду подготовки программ на Си++ или в компилятор языка как обязательный компонент входит препроцессор. Назначение препроцессора - обработка исходного текста программы до ее компиляции. Препроцессорная обработка в соответствии с требованиями стан- ’ Дарта языка Си++ [2] включает несколько стадий, выполняемых по- следовательно. Конкретная реализация транслятора может объединять несколько стадий, но результат должен быть таким, как f если бы они выполнялись последовательно: f • все системно-зависимые обозначения (например, системно- | зависимый индикатор конца строки) перекодируются в стан- । дартные коды; | • каждая пара из символов ' \' и "конец строки" убираются, и тем самым следующая строка исходного файла присоединяется к строке, в которой находилась эта пара символов; • в тексте распознаются директивы препроцессора, а каждый комментарий заменяется одним символом пустого промежутка; • выполняются директивы препроцессора и производятся макро- подстановки; • ESC-последовательности в символьных константах и символь- ных строках, например, '\п* заменяются на их эквиваленты (на соответствующие числовые коды); • смежные символьные строки конкатенируются, т.е. соединяются в одну строку. Знакомство с перечисленными задачами препроцессорной обра- ботки объясняет некоторые соглашения синтаксиса языка. Например, становится понятным смысл утверждений: каждая символьная строка может быть перенесена в файле на следующую строку, если использо- вать символ ' \' или "две символьные строки, записанные рядом, вос- принимаются как одна строка".
264 Язык Си++ Рассмотрим подробно стадию обработки директив препроцессора При ее выполнении возможны следующие действия: • замена идентификаторов (обозначений) заранее подготовлен ными последовательностями символов; • включение в программу текстов из указанных файлов; • исключение из программы отдельных частей ее текста (условная компиляция); • макроподстановка, т.е. замена обозначения параметризованным текстом, формируемым препроцессором с учетом конкретных параметров (аргументов). Для управления препроцессором, т.е. для задания нужных дей ствий, используются команды (директивы) препроцессора, каждая и < которых помещается на отдельной строке и начинается с символа # Определены следующие препроцессорные директивы: #define, #include, #undef, #if, #ifdef, #ifndef, #else, #endif, #line, tferror #pragma, #. Директива #define имеет несколько модификаций. Они предусмат ривают определение макросов или препроцессорных идентификато ров, каждому из которых ставится в соответствие некоторая символьная последовательность. В последующем тексте программы препроцессорные идентификаторы заменяются на заранее запланиро ванные последовательности символов. Директива #include позволяет включать в текст программы текст из выбранного файла. Директива #undef отменяет действие команды #define, которая определила до этого имя препроцессорного идентификатора. Директива #if и ее модификации #ifdef, #ifndef совместно с дирек тивами #else, #endif, #elif позволяют организовать условную обработ- ку текста программы. Условность состоит в том, что компилируется не весь текст, а только те его части, которые так или иначе выделены с помощью перечисленных директив. Директива #line позволяет управлять нумерацией строк в файле с программой. Имя файла и начальный номер строки указываются не посредственно в директиве #line. Директива #еггог позволяет задать текст диагностического сооб- щения, которое выводится при возникновении ошибок. Директива #pragma вызывает действия, зависящие от реализации. Директива # ничего не вызывает, так как является пустой дирек тивой, т.е. не дает никакого эффекта и всегда игнорируется. Рассмотрим возможности перечисленных команд при решении ти- пичных задач, поручаемых препроцессору.
Глава 8. Препроцессорные средства 265 8.2. Замены в тексте Для замены идентификатора заранее подготовленной последова- тельностью символов используется директива (обратите внимание на пробелы): #define идентификатор строка^замещемия Директива может размещаться в любом месте обрабатываемого текста, а ее действие в обычном случае распространяется от точки размещения до конца текста. Директива, во-первых, определяет иден- тификатор как процессорный. В результате обработки все вхождения определенного командой #define идентификатора в текст программы заменяются строкой замещения, окончанием которой обычно служит признак конца той строки, где размещена команда #define. Символы пробелов, помещенные в начале и в конце строки замещения, в под- становке не используются. Например: Исходный текст Idefine begin ( #define end ) void main() begin операторы end Результат препроцессорной обработки void main() ( операторы ) В данном случае программист решил использовать в качестве операторных скобок идентификаторы begin, end. Компилятор языка Си++ не может обрабатывать таких скобок, и поэтому до компиля- ции препроцессор заменяет все вхождения этих идентификаторов стандартными скобками ( и ). Соответствующие указания програм- мист дал препроцессору с помощью директив #define. Если строка_замещеиия оказывается слишком длинной, то, как любую символьную строку языка Си++, ее можно продолжить в сле- дующей строке текста пры^раммы. Для этого в конце продолжаемой строки помещается символ »\* (обратная наклонная черта). В ходе одной из стадий препроцессорной обработки этот символ вместе с последующим символом конца строки будет удален из текста про- граммы. Пример: tdefine STROKA "\n Muiturn, non multa - \ многое, но немного!"
266 Язык Си++ cout « STROKA; На экран будет выведено: Mui turn, non multa - многое, но немного! С помощью команды #define удобно выполнять настройку про граммы. Например, если в программе требуется работать с массива ми, то их размеры можно явно определять на этапе препроцессорной обработки: Исходный текст #define К 40 void main() { int М[К] [К] ; float А [К] , В [К] [К] Результат препроцессорной обработки void main () { int М[40] [40]; float А[40], В[40][40]; При таком описании очень легко изменять предельные размеры сразу всех массивов, изменив только одну константу в команд» #define. Те же возможности в языке Си++ обеспечивают константы, опре деленные в тексте программы. Например, того же результата можж достичь,записав: void main() { const int k “ 40; int M[k][k]; float A[k], B[k][k]; Именно в связи с расширением возможностей констант в язык< Си++ по сравнению с языком Си команда #define используется реже. Предусмотренные директивой #define препроцессорные замены не выполняются внутри строк, символьных констант и комментариев т.е. не распространяются на тексты, ограниченные кавычками (”) апострофами (') и разделителями (/*, */), В то же время строка заме щения может содержать перечисленные ограничители, например, так как это было в замене препроцессорного идентификатора stroka. Если в программе нужно часто печатать или выводить на экран дисплея значение какой-либо переменной и, кроме того, снабжать эту
Глава 8. Препроцессорные средства 267 печать одним и тем же пояснительным текстом, то удобно ввести со- кращенное обозначение оператора печати. Например, так: I define РК cout « "\п Номер элемента N « " « N « ' . ’ После этой директивы использование в программе оператора РК; будет эквивалентно (по результату) оператору из строки замещения. Например, последовательность операторов int N =» 4 ; РК; приведет к выводу такого текста: Номер элемента N « 4. Если в строку замещения входит идентификатор, определенный в другой команде #define, то в строке замещения выполняется следую- щая замена (цепочка подстановок). Например, программа, содержа- щая команды: Idefine К 50 Idefine РЕ cout « "\пКоличество элементов К « " « К РЕ; выведет на экран такой текст: Количество элементов К = 50 Обратите внимание, что идентификатор к внутри строки замеще- ния, обрамленной кавычками ("), не заменен иа 50. Строку замещения, связанную с препроцессорным идентификато- ром, можно сменить, приписав уже определенному идентификатору новое значение другой командой #define: Idefine M 16 ,, . // Идентификатор M определен как 16 Idefine M 'C ... // M определен хак символьная константа 'С Idefine М "С" // М определен как символьная строка ... //с двумя элементами: 'С’ и ’\0’ Однако при такой смене значений препроцессорного идентифика- тора компилятор ВС++ выдает предупреждающее сообщение на каж- дую следующую директиву #define: Warning . . . : Redefinition of lMt is not identical
268 Язык Си++ Замены в тексте можно отменять с помощью команды: «undef идентификатор После выполнения такой директивы идентификатор для препро- цессора становится неопределенным и его можно определять повтор- но. Например, не вызовут предупреждающих сообщений директивы: «define М 16 «undef М «define М 'С1 «undef М #define М "С" Директиву #undef удобно использовать при разработке больших программ, когда они собираются из отдельных "кусков текста", напи- санных в разное время или разными программистами. В этом случае могут встретиться одинаковые обозначения разных объектов. Чтобы не изменять исходных файлов, включаемый текст можно "обрамлять" подходящими директивами #define - #undef и тем самым устранять возможные ошибки. Приведем пример: А и 10; // Основной текст «define А X А « 5; II Ьъл'хыьжюхи. текст *undef А В « А; // Основной текст При выполнении программы в примет значение 10, несмотря на наличие оператора присваивания А « 5; во включенном тексте. 8.3. Включение текстов из файлов Для включения текста из файла используется команда #include. имеющая две формы записи: «include <имя_файла> // Имя в угловых скобках «include "имя_файла" // Имя в кавычках
Глава 8. Препроцессорные средства 269 Если имя_файла - в угловых скобках, то препроцессор разыскива- ет файл в стандартных системных каталогах. Если имя_фаила заклю- чено в кавычки, то вначале препроцессор просматривает текущий каталог пользователя и только затем обращается к просмотру стан- дартных системных каталогов. Начиная работать с языком Си++, пользователь сразу же сталки- вается с необходимостью использования в прох^раммах средств ввода- вывода. Для этого в начале текста программы помещают директиву: ^include <iostream.h> Выполняя эту директиву, препроцессор включает в программу средства связи с библиотекой ввода-вывода. Поиск файла iostream. h ведется в стандартных системных каталогах. По принятому соглашению суффикс .h приписывается тем фай- лам, которые нужно помещать в заголовке программы, т.е. до испол- няемых операторов. Кроме такого в некоторой степени стандартного файла, каким яв- ляется iostream.h, в заголовок программы могут быть включены любые другие файлы (стандартные или подготовленные специально). Заголовочные файлы оказываются весьма эффективным сред- ством при модульной разработке крупных программ, когда связь между модулями, размещаемыми в разных файлах, реализуется не только с помощью параметров, но и через внешние объекты, глобаль- ные для нескольких или всех модулей. Описания таких внешних объ- ектов (переменных, массивов, структур и т.п.) помещаются в одном файле, который с помощью директив #include включается во все мо- дули, где необходимы внешние объекты. В тот же файл можно вклю- чить и директиву подключения библиотеки функций ввода-вывода. Заголовочный файл может быть, например, таким: *include <iostream.h> // Включение средств обмена extern int ii, jj, XI; // Целые внежмие переменные extern float ДА, ВВ; // Вещественные внежмие переменные В практике программирования на Си++ обычна и в некотором смысле обратная ситуация. Если в программе используется несколько функций, то иногда удобно текст каждой из них хранить в отдельном файле. При подготовке программы пользователь включает в нее тексты используемых функций с помощью команд #include.
270 Язык Си++ В качестве примера рассмотрим задачу обработки строк, в кото- рой используем функции обработки строк, текст каждой из которых находится в отдельном файле. Задача об инвертировании слов в предложении. Ввести с клавиату- ры заканчивающееся точкой предложение, слова в котором отделены друг от друга пробелами. Записать каждое слово предложения в об- ратном порядке (инвертировать слово) и напечатать полученное предложение. Для простоты реализации ограничим длину вводимого предложения 80 символами. Тогда программа решения сформулиро- ванной задачи может быть такой: //Р8-01.СРР - препроцессорное формирование текста программы tinclude <iostream.h> tinclude "invert.срр" // Функция инвертирования строк tinclude "cone.срр" // Функция соединения строк void main() { char slovo[81], sp[81], c = ' *ptr “ slovo; sp[0] “ '\0’; // Очистка массива для нового предложения cout « "\пВведите предложение с точкой в конце:\п"; do { cin » slovo; // Читается слово из входного потока invert(slovo); // Инвертировать слово с » slovo[0]; // Убрать точку в начале последнего слова: if (с “ 1.') ptr « £slovo[l]; if (sp[0] '\01) conc(sp," \0"); // Пробел перед словом cone(spfptr); // Добавить слово в предложение } while(с ' . ') ; // Конец цикла чтения сопс(зр,".\0"); // Точка в конце предложения cout « "\п" « зр; // Вывод результата } В файле invert.срр текст функции: //invert.срр “ функция инвертирования строки void invert(char *е) { char s; for (int m “ 0; e[m] !» '\01; m++); for (int i“0, j“m-l;i<j; i++, j—) ( s « e[i]; e[i] » e[j]; e[j] = s; } } В файле cone. срр текст функции:
Глава 8. Препроцессорные средства 271 //CONC.CPP - функция соединения двух строк void cone(char *cl, char *с2) ( for (int m = 0; cl [m] ? = *\0’; m++); //m - длина первой строки без символа ’\0' for (int i = 0; c2[i] ! = '\0'; i++) cl[m+i] = c2[i]; cl[m+i] = '\0'; ) Возможный результат выполнения: Введите предложение с точкой в конце: А ШОРОХ ХОРОШ. <Enter> А ХОРОШ ШОРОХ. В программе в символьный массив slovo считывается из входного потока (с клавиатуры) очередное слово; sp - формируемое предложение, в конец которого всегда добавляется точка. Переменная char с-первый символ каждого инвертированного слова. Для последнего слова предложения этот символ равен точке. При добавлении этого слова к новому предложению точка отбрасывается с помощью изменения значения указателя ptr. Использованы директивы #include, включающие в программу средства ввода-вывода и тексты функций инвертирования строки invert () и конкатенации строк сопс (). Обратите внимание, что длина массива - первого из параметров функции сопс() должна быть достаточна велика, чтобы разместить результирующую строку. Препроцессор добавляет тексты всех функций в программу из файла Р8-11. СРР и как единое целое передает на компиляцию. 8.4. Условная компиляция Условная компиляция обеспечивается в языке Си++ набором ко- манд, которые, по существу, управляют не компиляцией, а препроцес- сорной обработкой: #if константное__выражение # ifdef идентификатор #ifndef идентификатор #else #endif #elif Первые три команды выполнят проверку условий, две следую- щие - позволяют определить диапазон действия проверяемого уело-
272 Язык Си++ вия. Команду #elif рассмотрим несколько позже. Общая структура применения директив условной компиляции такова: #if ... текст_1 #else текст_2 #endif Конструкция #else текст_2 не обязательна. Текст_1 включается в компилируемый текст только при истинности проверяемого условия Если условие ложно, то при наличии директивы #else на компиляции* передается текст_2. Если директива #else отсутствует, то весь текст о i #if до #endif при ложном условии опускается. Различие между формами команд #if состоит в следующем. В первой из перечисленных директив #if проверяется значение константного целочисленного выражения. Если оно отлично от нуля то считается, что проверяемое условие истинно. Например, в результате выполнения директив: #if 5+4 текст_1 #endif текст_1 всегда будет включен в компилируемую программу. В директиве #ifdef проверяется, определен ли с помощью команды #define к текущему моменту идентификатор, помещенный после #ifdef Если идентификатор определен, то текст_1 используется компилятором В директиве #ifndef проверяется обратное условие - истинным считается неопределенность идентификатора, т.е. тот случай, когдв идентификатор не был использован в команде #define или его определение было отмечено командой #undef. Условную компиляцию удобно применять при отладке программ для включения или исключения контрольных печатей. Например, #define DE 1 #ifdef DE cout «"Отладочная печать"; #endif Таких печатей, появляющихся в программе в зависимости oi определенности идентификатора DE, может быть несколько и, убрав директиву #define DE 1, сразу же отключаем все отладочные печати Файлы, предназначенные для препроцессорного включения в мо дули программы, обычно снабжают защитой от повторного включе
Глава 8. Препроцессорные средства 273 ния. Такое повторное включение может произойти, если несколько модулей, в каждом из которых запланировано препроцессорное включение одного и того же файла, объединяются в общий текст про- граммы. Например, такими средствами защиты снабжены все заголо- вочные файлы (подобные iostream.h) стандартной библиотеки. Схема защиты от повторного включения может быть такой: // Файл с именам filename #ifndef _FILE_NAME ... // Включаемый текст файла filename #define -FILE-NAME 1 #endif Здесь -FILE—name - зарезервированный для файла filename пре- процессорный идентификатор, который не должен встречаться в дру- гих текстах программы. Для организации мультиветвлений во время обработки препро- цессором исходного текста программы введена директива #elif константное выражение Структура исходного текста с применением этой директивы такова: #if тек СТ—ДЛЯ—i f #elif выражение—1 текст—1 #elif выражение—2 текст—2 #else текст—ДЛЯ—случая—el se #endif Препроцессор проверяет вначале условие в директиве #if, если оно ложно (равно 0) - вычисляет выражение..!, если выражение-1 равно 0 - вычисляется выраженив-2 и т.д. Если все выражения ложны, то в компилируемый текст включается текст_для_случая—else. В про- тивном случае, т.е. при появлении хотя бы одного истинного выраже- ния (в #if или в начинает обрабатываться текст, расположенный непосредственно за этой директивой, а все остальные директивы не рассматриваются. Таким образом, препроцессор обрабатывает всегда голько один из участков текста, выделенных командами условной компиляции. IX 3821
274 Язык Си++ 8.5. Макроподстановки средствами препроцессора Макрос, по определению, есть средство замены одной последова тельности символов другой. Для выполнения замен должны быть за даны соответствующие макроопределения. Простейшее макроопреде ление мы уже ввели, рассматривая директиву #define идентификатор строка~_аамецекия Такая директива удобна, однако она имеет существенный недо статок - строка замещения фиксирована. Большими возможностями обладает следующее макроопределение с параметрами «define имя(список_параметров) строка_вамещекия Здесь имя - имя макроса (идентификатор), список__параметров список разделенных запятыми идентификаторов. Между именем мак роса и списком параметров не должно быть пробелов. Классический пример макроопределения: «define max(a,b) (а < b ? b : а) позволяет формировать в программе выражение, определяющее мак симальное из двух значений аргументов. При таком определении вхождение в программу max(X,Y) заменяется выражением (X < Y ? Y : X) а использование max(Z,4) приведет к формированию выражения (Z < 4 ? 4 : Z) В первом случае при истинном значении X < Y возвращается зна- чение Y, иначе - значение х. Во втором примере z сравнивается с кон- стантой 4 и выбирается большее из значений. Не менее часто используется определение «define ABS(X) (X < 0 ? -(X) : X) С его помощью в программу можно вставлять выражение для определения абсолютных значений переменных. Конструкция
Глава 8. Препроцессорные средства 275 ABS(Е - Z) заменяется выражением (Е - Z < 0 ? - (Е - Z) : E-Z ) В результате вычисления которого определяется абсолютное значение выражения е - z. Сравнивая макросы с функциями, наиболее часто отмечают, что в отличие от функции, определение которой всегда присутствует в од- ном экземпляре, коды, формируемые макросом, вставляются в про- грамму столько раз, сколько раз используется макрос. В этом отношении макросы подобны встраиваемым (inline) функциям, но в отличие от встраиваемых функций подстановка для макроса выпол- няется всегда. Обратим внимание на еще одно отличие: функция опре- делена для данных того типа, который указан в спецификации ее параметров и возвращает значение только одного конкретного типа. Макрос пригоден для обработки параметров любого типа, допусти- мых в выражениях, формируемых при обработке строки замещения. Тип получаемого значения зависит только от типов параметров и от Самих выражений. Таким образом, макрос может заменять несколько функций. Например, приведенные макросы тах() и abso верно ра- ботают для параметров с целыми или плавающими типами, а резуль- тат зависит только от типов параметров. Механизм перегрузки и шаблоны функций позволяют решать те же задачи, что и макросы Именно поэтому в отличие от языка Си в программах на Си++ мак- росредства используются реже. Покажем на примере некоторые ограничения и возможности макроопределений с параметрами: //Р8-02.СРР - особенности макроопределении с параметрами Idefine max(a,b) (а < b ? b : а) •define t(e) e*3 Idefine PRNT(c) cout « "\n” « #c « " равно cout « c; Idefine E x*x •include <iostreaa.h> void main() ( int x; x x 2; PRNT(max(++x, ++x)); PRNT(t(x)) ; PRNT(t(x + x)) ; PRNT(t(x + x)/3) ; PRNT(E);
276 Язык Си++ В результате выполнения программы получен следующий, объяс- няемый ниже, результат: тах(++х, ++х) равно 5 t(x) равно 15 t(x + х) равно 20 t(х + х)/3 равно 10 Е равно 25 Обратите внимание на то, что в макроопределении prnt строка замещения включает два оператора, разделенных точкой с запятой, причем в строке замещения имеются пробелы. При подстановке пара- метров макроса в строку замещения запрещены подстановки внутрь кавычек, апострофов или ограничителей комментариев. В случае не- обходимости параметр макроса можно заключить в строке замещения в кавычки ("")• Для этого используется специальная операция #, ко- торая записывается непосредственно перед параметром. В макроопре- делении print (Е) будет сформирована последовательность операторов cout « ”\n” « "Е" « " равно cout « Е; Последующая подстановка приведет к следующему результату: cout « "\п" « "Е" « ” равно ”; cout « х*х; Именно поэтому в результатах слева от слова "равно" печатается изображение параметра, использованного в print. Рассмотрим результаты подробнее. В операторе PRINT (max (Нх, ++х)) ; печатается значение выражения (++х < ++х ? ++х : ++х), после вычисления которого х увеличивается на 3, т.е. становится рав- ным 5. В операторе PRiNT(t (х)); печатается значение выражения х*з. В операторе print (t(x + х));-значение выражения х + х*з. Чтобы подстановка t(e) утраивала значение любого аргумента, сле- дует использовать скобки вокруг параметра в строке замещения, т.е. записать #define t (е) (в) *3. При таком определении t(e) в опера- торах PRINT(t(x + х> > ; PRINT(t(x + х))/3;
Глава 8. Препроцессорные средства 277 сформировались бы выражения (х + х)*3, равное 30 и (х + х)*3/3, равное 10. Приведенные примеры показывают, что для устранения неодно- значных или неверных использований макроподстановок параметры в строке замещения и ее саму по крайней мере полезно заключать в скобки. В заключение приведем еще некоторые сведения о работе препроцессора и его директивах. При препроцессорной обработке исходного текста программы каждая строка обрабатывается отдельно. Напоминаем, что возможно "соединение" строк: если в конце строки стоит символ а за ним - символ перехода на новую строку ’ \п' , то эта пара символов исключается и следующая строка непосредственно присоединяется к текущей строке. Анализируя полученные строки, препроцессор распознает лексемы. Лексемами для препроцессора являются: • лексемы языка Си++; • имена файлов; • символы, не определенные иным способом. Аргументы вызова макроса - лексемы, разделенные запятыми. Аргументы макрорасширениям не подвергаются. В последовательности лексем, образующей строку замещения, предусматривается использование двух операций - ’ и ' , первая из которых помещается перед параметром, а вторая - между любыми двумя лексемами. Операция , уже использованная выше в программе Р8-02.СРР, требует, чтобы текст, замещающий данный параметр в формируемой строке, заключался в двойные кавычки. Например, для определения #define sm(zip) cout « #zip обращение (макровызов) sm (сумма) ; приведет к формированию оператора cout « "сумма" ;. Операция ' # ’ , допускаемая только между лексемами строки замещения, позволяет выполнять конкатенацию лексем, включаемых в строку замещения. Определение #define abc (a,b,c,d) a(b##c##d) позволит сформировать выражение sin(x+y) , если использовать макровызов abc (sin, х+у) . Введено еще несколько препроцессорных директив: #if defined
278 Язык Си++ Препроцессорная операция defined позволяет по-другому записать директивы условной компиляции: #if defined есть аналог tfifdef И #if !defined есть аналог #ifndef. Для нумерации строк можно использовать директиву #line константа которая указывает компилятору, что следующая ниже строка текста имеет номер, определяемый целой десятичной константой. Команда может определять не только номер строки, но и имя файла: #line константа "имя_файла" Директива #еггог последовательность_лексем приводит к выдаче диагностического сообщения в виде последова- тельности лексем. Естественно применение директивы #еггог совместно с условными препроцессорными командами. Например, определив некоторую препроцессорную переменную name #define NAME 5 в дальнейшем можно проверить ее значение и выдать сообщение, если у name другое значение: #if (NAME !=5) #error NAME должно быть равно 5 ’ Сообщение будет выглядеть так: fatal: имя_файла номер_строки #error directive: NAME должно быть равно 5 ! Допустима пустая директива: # не вызывающая никаких действий. Команда #pragma последовательность_лексем определяет действия, зависящие от конкретной реализации компилятора. Например, в ТС++ и ВС++ входит вариант этой директивы для извещения компилятора о наличии в тексте программы команд ассем- блера.
Глава 8. Препроцессорные средства 279 8.6. Встроенные (заранее определенные) макроимена Существуют встроенные (заранее определенные) макроимена, до- ступные препроцессору во время обработки. Они позволяют получить следующую информацию: ___LINE___ десятичная константа - номер текущей обрабатываемой стро- ки файла с программой Си++. Принято, что номер первой строки исходного файла равен 1. ___FILE___ строка символов - имя компилируемого файла. Имя изменяет- ся всякий раз, когда препроцессор встречает директиву #include с указанием имени файла. После окончания включе- ния файла по команде #include восстанавливается предыдущее значение макроимени___file__. _____DATE_ строка символов в формате: "месяц число год", определяющая дату начала обработки исходного файла. ___TIME___ строка символов вида "часы:минуты:секунды", определяющая время начала обработки препроцессором текущего исходного файла. ___STDC___ к онстанта, равная 1, если компилятор работает в соот- ветствии с ANSI-стандартом. В противном случае значение макроимени____stdc__ не определено. Проект стандарта Си++ предполагает, что наличие имени____stdc___опреде- ляется реализацией. ____cpluaplua и мя, определенное равным 1 при компиляции программы на языке Си++. В остальных случаях этот макрос не определен. В конкретных реализациях набор предопределенных имен шире. Например, в препроцессор ВС++ дополнительно включены: • всорт_____- имя, определенное равным 1, если в компиляторе имеется оптимизатор;
280 Язык Си++ • вс plus flu s - числовое значение соответствует версии компилятора; • cdecl_____- идентифицирует порядок передачи параметров функциям, значение 1 соответствует порядку, принятому в язы- ках Си и Си++ (в отличие от языка Паскаль); • console_____- определено для 32-разрядного компилятора и установлено в 1 для программ консольного приложения; • dll__ - соответствует работе в режиме Windows DLL; • __msdos___- равно 1 для 16-разрядных компиляторов ВС++, устанавливается в 0 для 32-разрядного компилятора; • - доступен только для 32-разрядного компилятора; • __overiay__ - равно 1 в оверлейном режиме; • FASCAL__-противоположен________CDECL__; • stdc__ - установлен в 1, если компилятор удовлетворяет стандарту ANSI; • tcflusflus_____- числовое значение соответствует версии компилятора; * __-Templates - означает, что ВС++ поддерживает шаблоны; • tls____- определен как истинный для 32-разрядного компи- лятора; • __turboc__ - числовое значение, равное 0x0400 для компиля- тора ВС++ 4.0; • Windows_____- означает генерацию кода для Windows; • WIN32_____- определен для 32-разрядного компилятора и уста- новлен в 1 для консольных приложений и GUI.
Глава 9. КЛАСС КАК АБСТРАКТНЫЙ ТИП Совокупность принципов проектирования, разработки и реализа ции программ, которая базируется на абстракции данных, предусма тривает создание новых типов данных, с наибольшей полнотой отоб ражающих особенности решаемой задачи. Одновременно с данным! для каждого типа вводится набор операций, удобных для обработка этих данных. Таким образом, создаваемые пользователем абстракт ные типы данных могут обеспечить представление понятий предмет- ной области решаемой задачи. В языке Си++ программист имеет возможность вводить собственные типы данных и определять опера- ции над ними с помощью классов. 9.1. Класс как расширение понятия структуры Класс - это производный структурированный тип, введенный программистом на основе уже существующих типов. Механизм клас- сов позволяет создавать типы в полном соответствии с принципами абстракции данных, т.е. класс задает некоторую структурированную совокупность типизированных данных и позволяет определить набор операций над этими данными. Простейшим образом класс можно определить с помощью кон- струкции: ключ_класса имя_класса { список_компонвнтов }; где ключ_класса - одно из служебных слов class, struct, union; имя__класса - произвольно выбираемый идентификатор; спи- сок_компонентов - определения и описания типизированных данных и принадлежащих классу функций. В проекте стандарта языка Си++ указано, что компонентами класса могут быть данные, функции, классы, перечисления, битовые поля, дружественные функции, дружественные классы и имена типов. Вначале для простоты будем считать, что компоненты класса - это типизированные данные (базовые и производные) и функции. Заклю-
282 Язык Си++ ценный в фигурные скобки список компонентов называют телом класса. Телу класса предшествует заголовок. В простейшем случае заголовок класса включает ключ класса и его имя. Определение класса всегда заканчивается точкой с запятой. Отметим терминологические трудности, связанные с классами. Все компоненты класса в английском языке обозначаются термином member (член, элемент, часть). Функции, принадлежащие классу, обозначают термином member functions, а данные класса имеют название data members. В русском языке терминология, относящаяся к классам, недостаточно устоялась, поэтому имеются многочисленные расхождения. Принадлежащие классу функции называют методами класса или компонентными функциями. Данные класса называют компонентными данными, полями данных или элементами данных (объектов) класса. В качестве ключа_класса можно использовать служебное слово struct, но класс отличается от обычного структурного типа по крайней мере возможностью включения компонентных функций. Например, следующая конструкция простейшим образом вводит класс ’’комплексное число”: struct complexl // Вариант класса "комплексное число" { double real; //Вещественная часть double imag; //Мнимая часть //Определить значение комплексного числа: void define (double re = 0.0, double im = 0.0) {real = re; imag = im; } //Вывести на экран значение комплексного числа: void display(void) { cout « "real = " « real; cout « ", imag = "«imag; } }; В отличие от структурного типа в класс (тип) complexl, кроме компонентных данных (real, imag) , включены две компонентные функции define () и display () . Недостатков в нашем простейшем определении класса комплексных чисел несколько. Однако отложим их объяснение и устранение, а обратим еще раз внимание на тот факт, что класс (как и его частный случай - структура), введенный пользователем, обладает правами типа. Следовательно, можно определять и описывать объекты класса и создавать производные типы: complexl XI, Х2, D; //Три объекта класса complexl complexl *point = &D; //Указатель на объект класса
Глава 9. Класс как абстрактный тип 283 //complexl complexl dim [8]; //Массив объектов класса complexl complexl &Name = 2; //Ссылка на объект класса complexl и Т.Д. (Класс ’’комплексное число” очень полезен в прикладных программах, и поэтому в компиляторах языка Си++ он включен в стандартные библиотеки. Библиотечный класс complex становится доступным при включении в программу заголовочного файла complex. h (см. прил. 6.).) Итак, класс - это тип, введенный программистом. Каждый тип служит для определения объектов. Для описания объекта класса ис- пользуется конструкция: имя.класса имя_объекта; В определяемые объекты (класса) входят данные (элементы), соот- ветствующие компонентным данным класса. Компонентные функции класса позволяют обрабатывать данные конкретных объектов класса. Но в отличие от компонентных данных компонентные функции не тиражируются при создании конкретных объектов класса. Если перейти на уровень реализации, то место в памяти выделяется именно для элементов каждого класса. Определение объекта класса предусматривает выделение участка памяти и деление этого участка на фрагменты, соответствующие отдельным элементам объекта, каждый из которых отображает отдельный компонент данных класса. Таким образом, и в объект xl, и в объект dim [ 3 ] класса complexl входит по два элемента типа double, представляющих вещественные и мнимые части комплексных чисел. Как только объект класса определен, появляется возможность об- ращаться к его компонентам, во-первых, с помощью "квалифици- рованных” имен, каждое из которых имеет формат: имя_объекта.имя_класса :: имя.компонента Имя класса с операцией указания области действия 1::1 обычно может быть опущено, и чаще всего для доступа к данным конкретного объекта заданного класса (как и в случае структур) используется уточненное имя: имя—объекта.имя—элемента При этом возможности те же, что и при работе с элементами структур. Например, можно явно присвоить значения элементам объектов класса complexl:
284 Язык Си++ XI.real « dim[3].real » 1.24; XI.image » 2.3; dim[3].image » 0.0; Уточненное имя принадлежащей классу (т.е. компонентной) функ- ции имя__объекта. обращение_к_компонентной_функции обеспечивает вызов компонентной функции класса для обработки данных именно того объекта, имя которого использовано в уточнен- ном имени. Например, можно таким образом определить значения компонентных данных для определенных выше объектов класса complexl: XI. define О; // Параметры выбираются по умолчанию: / / real«=«0.0, imag»0.0 Х2.define(4.3,20.0); // Комплексное число 4.3 4- i*20.0 С помощью принадлежащей классу complexl функции display () можно вывести на экран значения компонентных данных любого из объектов класса. (Разумно выполнять вывод только для объектов, которым уже присвоены осмысленные значения.) Например, следую- щий вызов принадлежащей классу complexl функции: Х2. display () ; приведет к печати real = 4.3, imag =20.0 Другой способ доступа к элементам объекта некоторого класса предусматривает явное использование указателя на объект класса и операции косвенного выбора компонента ('->'): указатель_на_объект_класса -> имя_элемента Определив, как сделано выше, указатель point, адресующий объ- ект D класса complexl, можно следующим образом присвоить значе- ния данным объекта D: point->real » 2.3; // Присваивание значения элементу // об'икта D point->imag = 6.1; // Присваивание значения элементу // объекта D Указатель на объект класса позволяет вызывать принадлежащие классу функции для обработки данных того объекта, который адре- суется указателем. Формат вызова функции:
Глава 9. Класс как абстрактный тип 285 указатель__на__об’ъект_класса -> обрацвние_к_компонентной__функции Например, вызвать компонентную функцию display () для дан- ных объекта D позволяет выражение point->display () ; В качестве второго примера рассмотрим класс, описывающий то- вары на складе магазина. Компонентами класса будут: • название товара; • оптовая (закупочная) цена; • розничная (торговая) наценка; • функция ввода данных о товаре; • функция печати (вывода на дисплей) сведений о товаре с указа- нием розничной цены. Определение класса: //GOODS.СРР - класс "товары на складе магазина" tinclude <iostream.h> struct goods // Определение класса "товары" ( char пале[40]; // Наименование товара float price; // Оптовая (закупочная) цена static int percent; // Торговая наценка, в % // Компонентные функции: void Input() // Ввод сведений о товаре ( cout « "Наименование товара: cin » name; cout « "Закупочная цена: "; cin » price; ) void Display () // Вывод данных о продаваемом товаре ( cout « "\п" « пате; cout « ", розничная цена: "; cout « long(price * (1.0 + goods::percent * 0.01)); ) ) ; Торговая наценка определена как статический компонент классг Статические компоненты классов не "дублируются" при создании объектов класса, т.е. каждый статический компонент существует в единственном экземпляре. Доступ к статическому компоненту возмо- жен только после его инициализации. Для инициализации использует- ся конструкция; тип имя__класса: : имя__компонента инициализатор;
286 Язык Си++ В нашем примере может быть такой вариант: int goods::percent = 12; Это предложение должно быть размещено в глобальной области (global scope) после определения класса. Только при инициализации статический компонент класса получает память и становится доступ- ным. Для обращения к статическому компоненту используется квали- фицированное имя: имя__класса: :имя-компонента Кроме того^статический компонент доступен "через" имя конкрет- ного объекта: имя_объекта.имя_класса: .'имя-компонента либо имя-объекта.имя_компонента В следующей программе иллюстрируются перечисленные возмож- ности и особенности классов со статическими компонентами, а также используется массив объектов: //Р9-01.СРР - массив объектов класса goods ^include <iostream.h> ttinclude "goods.срр" // Текст определения класса // Инициализация статического компонента: int goods::percent ® 12; void main(void) { goods wares [5] » { { "Мужской костюм", 190000 ), { "Косметический набор", 27600 }, { "Калькулятор", 11000 } ); int k » sizeof(wares) / sizeof(wares[0]); cout « "\пВведите сведения о товарах:\n"; for (int i = 3; i < k; i++) wares[i].Input(); cout « "\пСписок товаров при наценке " « wares(0].percent « "%"; for (i » 0; i < k; i++) wares[i].Display(); // Изменение статического компонента: goods:'.percent = 10; cout « "\п\пСписок товаров при наценке " « wares[0].goods::percent « "%"; goods *pGoods » wares; for (i « 0; i < k; i++) pGoods++->Display(); )
Глава 9. Класс как абстрактный тип 287 Результаты выполнения программы: Введите сведения о товарах: Наименование товара: Сигареты <Enter> Закупочная цена: 780 <Enter> Наименование товара: Кроссовки <Enter> Закупочная цена: 28400 <Enter> Список товаров при наценке 12% Мужской костюм, розничная цена: 212800 Косметический набор, розничная цена: 30912 Калькулятор, розничная цена: 12320 Сигареты, розничная цена: 873 !фоссовки, розничная цена: 31808 Список товаров при наценке 10% Мужской костюм, розничная цена: 209000 Косметический набор, розничная цена: 30360 Калькулятор, розничная цена: 12100 Сигареты, розничная цена: 858 IQpoccoBKH, розничная цена: 31240 Обратите внимание на инициализацию первых элементов массива wares [5] объектов класса goods. В списках значений не отражено существование статического компонента. Точно так же при вводе данных компонентной функцией input О не изменяется значение ста- тического компонента. Он ведь один для всех объектов класса! Для иллюстрации разных способов доступа к компонентам классов опре- делен указатель pGoods на объекты класса goods. Он инициализиро- ван значением адреса первого элемента массива объектов &wares[0]. В цикле указатель с операцией »->» используется для вызова компо- нентной функции display (). После каждого вызова указатель изме- няется - настраивается на следующий элемент массива, т.е. на очередной объект класса goods. 9.2. Конструкторы, деструкторы и доступность компонентов класса В определениях класса complexl и класса goods есть недостатки, которые легко устранить. Первый из них - отсутствие автоматической инициализации создаваемых объектов. Для каждого вновь созда- ваемого объекта класса complexl необходимо вызвать функцию define () либо явным образом с помощью уточненных имен присваи-
288 Язык Си++ вать значения данным объекта, т.е. переменным real и imag. Еще два способа использованы в предыдущей программе. Часть объектов класса goods получила начальные значения при инициализации, ко- торая выполняется по правилам, относящимся к структурам и масси- вам. Объектам wares[3] и wares[4] значения присвоены с помощью явного вызова компонентной функции input О. Для инициализации объектов класса в его определение можно яв- но включать специальную компонентную функцию, называемую кон- структор. Формат определения конструктора в теле класса может быть таким: имя__класса (список_формалышх__параметров) { операторы_тела_конструктора }; Имя этой компонентной функции по правилам языка Си++ долж- но совпадать с именем класса. Такая функция автоматически вызы- вается при определении или размещении в памяти с помощью оператора new каждого объекта класса. Основное назначение кон- структора - инициализация объектов. Для класса complexl можно ввести конструктор, эквивалентный функции define(), но отличаю- щийся от нее только названием: complexl(double re ® 0.0, double im ® 0.0) { real » re; imag « im; ) В соответствии с синтаксисом языка для конструктора не опреде- ляется тип возвращаемого значения. Даже тип void недопустим. С по- мощью параметров конструктору могут быть переданы любые данные, необходимые для создания и инициализации объектов класса. В конструктор complexl () передаются значения элементов объекта "комплексное число". По умолчанию за счет начальных значений па- раметров формируется комплексное число с нулевыми мнимой и ве- щественной частями. В общем случае конструктор может быть как угодно сложным. Например, в классе "матрицы" конструктор будет выделять память для массивов, с помощью которых представляется каждая матрица - объект данного класса, а затем инициализировать эти массивы. Размеры матриц и начальные значения их элементов та- кой конструктор может получать через аппарат параметров, как и значения составных частей комплексного числа в конструкторе complexl (). Для класса "товары на складе магазина" конструктор можно определить следующим образом: goods (char *new__name, float new_price) ( name « new__name; // Наименование товара
Глава 9. Класс как абстрактный тип 289 price « new_prxce; // Закупочная цена I В конструкторе можно было бы изменять и значение заранее ини- циализированного статического компонента percent, однако в рас- сматриваемом примере это не делается. Второй недостаток классов complexl и goods, введенных с по- мощью служебного слова struct, - это общедоступность компонент. В любом месте программы, где "видно" определение класса, можно с помощью уточненных имен (например, имя_объекта. real или имЯ-Объвкта. xmag) или с помощью указателя на объект и операции косвенного выбора •->' получить доступ к компонентным данным этого объекта. Тем самым не выполняется основной принцип аб- стракции данных - инкапсуляция (сокрытие) данных внутри объектов. В соответствии с правилами языка Си++ все компоненты класса, введенного с помощью ключа класса struct, являются общедоступ- ными (public). Для изменения видимости компонент в определении класса можно использовать спецификаторы доступа. Спецификатор доступа - это одно из трех служебных слов private (собственный), public (общедоступный), protected (защищенный), за которым поме- щается двоеточие. Появление любого из спецификаторов доступа в тексте определения класса означает, что до конца определения либо до другого спецификатора доступа все компоненты класса имеют ука- занный статус. Защищенные (protected) компоненты классов нужны только в слу- чае построения иерархии классов. При использовании классов без по- рождения на основе одних классов других (производных), применение спецификатора protected эквивалентно использованию спецификатора private. Применение в качестве ключа класса служебного слова union при- водит к созданию классов с несколько необычными свойствами, кото- рые нужны для весьма специфических приложений. Пример такого приложения - экономия памяти за счет многократного использования одних и тех же участков памяти для разных целей. В каждый момент времени исполнения программы объект-объединение содержит только один компонент класса, определенного с помощью union. Все компо- ненты этого класса являются общедоступными, но доступ может быть изменен с помощью спецификаторов доступа protected (защищенный), ргка!е(собственный), public (общедоступный). Изменить статус доступа к компонентам класса можно и с по- мощью использования в определении класса ключевого слова class. Все компоненты класса, определение которого начинается со служеб- 19-ЗК21
290 Язык Си++ ного слова class, являются собственными (private), т.е. недоступными для внешних обращений. Так как класс, все компоненты которого не доступны вне его определения, редко может оказаться полезным, то изменить статус доступа к компонентам позволяют спецификаторы доступа private (собственный), public (общедоступный), protected (защищенный). Итак, для сокрытия данных внутри объектов класса, определенно го с применением ключа struct, достаточно перед их появлением в определении типа (в определении класса) поместить спецификатор private. При этом необходимо, чтобы некоторые или все принадлежа- щие классу функции остались доступными извне, что позволило бы манипулировать с данными объектов класса. Этим требованиям будет удовлетворять следующее определение класса "комплексное число”: //COMPLEX.СРР - определение класса "комплексное число" tinclude <iostream.h> // Класс с конструктором и инкапсуляцией данных: struct complex { // Методы класса (все общедоступные - public) : // Конструктор объектов класса: complex (double re » 1.0, double im ж 0.0) { real « re; imag « im; } // Вывести на дисплей значение комплексного числа: void display (void) ( cout « "real ® " « real; cout « ", imag » " « imag; ) // Получить доступ к вещественной части числа: double fire(void) { return real; } // Получить доступ к мнимой части числа: double Aim(void) { return imag; } // Данные класса (скрыты от прямых внешних обращений) : private: // Изменить статус доступа на "собственный" double real; // Вещественная часть double imag; // Мнимая часть ) ; По сравнению с классом complexl в новый класс complex, кроме конструктора, дополнительно введены компонентные функции ге() и im(), с помощью которых можно получать доступ к данным объек- тов. Они возвращают ссылки соответственно на вещественную и мни- мую части того объекта, для которого они будут вызваны. Напомним, что для конструктора не задается тип возвращаемого значения. Существуют особенности и в вызове конструктора. Без яв-
Глава 9. Класс как абстрактный тип 291 ного указания программиста конструктор всегда автоматически вы- зывается при определении (создании) объекта класса. При этом ис- пользуются умалчиваемые значения параметров конструктора. Например, определив объект сс с неявным вызовом конструктора complex СС; получим при вызове cc.ro() значение 1.0. Функция cc.im() вернет ссылку на cc.imag, и этот элемент объекта сс будет иметь значение 0.0, заданное как умалчиваемое значение параметра конструктора. Итак, "конструктор превращает фрагмент памяти в объект, для которого выполнены правила системы типов" [2], т.е. в объект того типа, который предусмотрен определением класса. Конструктор существует для любого класса, причем он может быть создан без явных указаний программиста. Таким образом, для классов goods и complexl существуют автоматически созданные кон- структоры. По умолчанию формируются конструктор без параметров и кон- структор копирования вида Т::Т(const Tfi> где т - имя класса. Например, class F { .. . public: F(const F&); Такой конструктор существует всегда. По умолчанию конструк- тор копирования создается общедоступным. В классе может быть несколько конструкторов (перегрузка), но только один с умалчиваемыми значениями параметров. Нельзя получить адрес конструктора. Параметром конструктора не может быть его собственный класс, но может быть ссылка на него, как у конструктора копирования. Конструктор нельзя вызывать как обычную компонентную функ- цию. Для явного вызова конструктора можно использовать две раз- ные синтаксические формы: иия^класса имя_объвхта (фахтические__парам®тры__хонструхтора) ; имя__класса (фахтические_парам®тры_хонструхтора) ; 19’
292 Язык Си++ Первая форма допускается только при непустом списке фактиче- ских параметров. Она предусматривает вызов конструктора при определении нового объекта данного класса: complex SS(10.3,0.22}; // SS.real “ 10.3; // SS.imag = 0.22 complex ЕЕ (2.345); 11 EE.real = 2.345; / / no умолчанию EE . imag = 0.0 complex DD(); // Ошибка* Компилятор решит, что это // прототип функции без параметров, // возвращающей значение типа complex Вторая форма явного вызова конструктора приводит к созданию объекта, не имеющего имени. Созданный таким вызовом безымянный объект может использоваться в тех выражениях, где допустимо ис- пользование объекта данного класса. Например: complex ZZ=complex(4.0,5.0); Этим определением создается объект zz, которому присваивается значение безымянного объекта (с элементами real “ 4.0, imag = 5. о), созданного за счет явного вызова конструктора. Существуют два способа инициализации данных объекта с по- мощью конструкторов. Первый способ, а именно передача значений параметров в тело конструктора, уже продемонстрирован на приме- рах. Второй способ предусматривает применение списка инициализа- торов данных объекта. Этот список помещается между списком параметров и телом конструктора: имя-класса (списох^параметров) : список_имициализаторов_компоивитных__даш<ых { тело__хонструктора } Каждый инициализатор списка относится к компоненту и имееч вид: имя^хомпонента^данямс (выражение) Например: class AZ { int ii; float ее; char cc; public: AZ (int in, float an, char cn) : ii(5), ее (ii * en + in), cc(cn) { }
Глава 9. Класс как абстрактный тип 293 }; AZ А(2,3.0, ' d’) ; // Создается именованный объект А // с компонентами A.ii = 5, // А.ее а» 17r А.со =“ 'd' AZ X = AZ(0f2.Оt 1z'); // Создается безымянным объект, в // котором ii = 5, ее « 10, // cc = ’ z1 , и копируется // в объект X Перечисленные особенности конструкторов, соглашения о стату- сах доступа компонентов и новое понятие "деструктор" иллюстриру- ют следующее определение класса "символьная строка": //STROKA.СРР “ файл с определением класса "символьная // строка" tinclude <зtring.h> // Для библиотечных строковых функции tinclude <iostream.h> class stroka { // Скрытые от внешнего доступа данные: char *ch; // Указатель на текстовую строку int len; // Длина текстовой строки public: // Общедоступные функции: // Конструкторы объектов класса: // Создает объект как новую пустую строку: stroka(int N = 80) // Строка не содержит информации: 1еп(0) { ch = new char[N + 1] ; // Память выделена для массива ch[0] = »\0» ; } // Создает объект по заданной строке: stroka (const char *arch) { len — strlen(arch); ch = new char[len+l]; strcpy(ch,arch); ) intfi len__str (void) // Возвращает ссылку на длину строки { return len; } char *string(void) // Возвращает указатель на строку { return ch; } void display(void) // Печатает информацию о строке { cout « "\пДлина строки: " « len; cout « "ХпСодержимое строки: " « ch; } // Деструктор - освобождает память объекта: ~stroka() { delete [] ch; }
294 Язык Си++ В следующей программе создаются объекты класса stroka и вы водится информация на дисплей об их компонентах: //Р9-02.СРР - программа с классом "символьные строки" ^include "stroka.срр" // Текст определения класса void main() { stroka LAT("Non Malta, Sad Multum!"); stroka RUS("He иного, но многое!"); stroka СТР(20); LAT.display(); cout « "\nB объекте RUS: " « RUS. string() ; CTP.display() ; ) Результат выполнения программы: Длина строки: 22 Содержимое строки: Non Malta, Sad Multum! В объекте RUS: Не много, но иногое! Длина строки: 0 Содержимое строки: Так как класс stroka введен с помощью служебного слова class, то элементы char *ch и int len недоступны для непосредственного об- ращения. Чтобы получить значение длины строки из конкретного объекта, нужно использовать общедоступную компонентную функ- цию len_str(). Указатель на строку, принадлежащую конкретному объекту класса stroka, возвращает функция string(). У класса stroka два конструктора - перегруженные функции, при выполнении каждой из которых динамически выделяется память для символьного массива. При вызове конструктора с параметром int N массив из N + 1 элементов остается пустым, а длина строки устанавливается равной 0. При вызове с параметром char *arch длина массива и его содер- жание определяются уже существующей строкой, которую адресует фактический параметр, соответствующий указателю-параметру arch. Заслуживает внимания компонентная функция ^strokaO. Это де- структор. Объясним его назначение и рассмотрим его свойства. Динамическое выделение памяти для объектов какого-либо класса создает необходимость в освобождении этой памяти при уничтожении объекта. Например, если объект некоторого класса формируется как локальный внутри блока, то целесообразно, чтобы при выходе из блока, когда уже объект перестает существовать, выделенная для него память была возвращена системе. Желательно, чтобы освобождение
Глава 9. Класс как абстрактный тип 295 памяти происходило автоматически и не требовало вмешательства программиста. Такую возможность обеспечивает специальный ком- понент класса - деструктор (разрушитель объектов) класса. Для него предусматривается стандартный формат: -имя_класса() { операторы_тела_деструктора }; Название деструктора в Си++ всегда начинается с символа тильда за которым без пробелов или других разделительных знаков по- мещается имя класса. У деструктора не может быть параметров (даже типа void). Деструктор не имеет возвращаемого значения (даже типа void). Вызов деструктора выполняется неявно, автоматически, как только объект класса уничтожается. В нашем примере в теле деструк- тора только один оператор, освобождающий память, выделенную для символьного массива при создании объекта класса stroka. 9.3. Компонентные данные и компонентные функции При определении класса в его теле описываются и (или) опреде- ляются данные класса и принадлежащие ему функции. Компонентные данные. Определение данных класса внешне анало- гично обычному описанию объектов базовых и производных типов. Класс в этом отношении полностью сохраняет все особенности струк- турных типов. Именно поэтому данные класса могут быть названы его элементами. Элементы класса могут быть как базовых, так и про- изводных типов, т.е. компонентными данными служат переменные, массивы, указатели и т.д. Как обычно, описания элементов одного типа могут быть объединены в одном операторе. Например: class point ( float х, у, т; long а, Ь, с; }; В отличие от обычного определения данных при описании эле- ментов класса не допускается их инициализация. Это естественное свойство класса, так как при его определении еще не существует уча- стков памяти, соответствующих его компонентным данным. Напоми- наем, что память выделяется не для класса, а только для объектов класса. Для инициализации компонентных данных объектов должен использоваться автоматический или явно вызываемый конструктор соответствующего класса. Существуют различия между обращениями к компонентным данным класса из принадлежащих ему функций и из
296 Язык Си++ других частей программы. Как уже показано на примерах, классы complex, goods, stroka,принадлежащие классу функции, имеюч полный доступ к его данным, т.е. для обращения к элементу класса ич тела компонентной функции достаточно использовать только имя компонента. Например, в одном конструкторе класса stroka использо ван оператор: ch = new char[len+l]; За простотой такого обращения к данным класса из его компо нентных функций скрывается механизм неявного отождествления имен компонентный данных класса с элементами именно того объекта класса для которого вызывается компонентная функция. Например, при таком определении объекта line класса stroka: stroka line(20); значения присваиваются именно переменным line.len и line.ch. Для доступа к компонентным данным из операторов, выполняемых вне определения класса, непосредственное использование имен элемен тов недопустимо. Смысл такого запрета определяется упомянутым механизмом привязки данных класса к конкретным объектам. Напом ним, что по существу в определение класса не вводятся его данные, а только обозначается возможность их формирования при определении конкретных объектов класса. Явно размещается в памяти не класс, и конкретный объект класса. В отведенной для объекта области памяти выделяются участки, соответствующие компонентным данным (элеме нтам объекта). Для обращения к элементу объекта, как мы уже говорили нужно использовать операции выбора компонентов класса ('.1 или ' -> ’) Первая из них позволяет сформировать уточненное имя по известномх имени объекта: имя_объекта.имя_элемента Вторая операция обеспечивает обращение к компонентным данным объекта по заданному указателю на объект: указатель__на_обт»ект->имя_элемента Подытожим особенности компонентов класса. Хотя внешне компо нентные данные класса могут быть подобны данным, определенным г блоке или в теле функции, но существуют некоторые существенньи отличия. Данные класса не обязательно должны быть определены или описаны до их первого использования в принадлежащих классх функциях. То же самое справедливо и для принадлежащих класс \
Глава 9. Класс как абстрактный тип 297 > функций, т.е. обратиться из одной функции класса к другой можно до ее определения внутри тела класса. Все компоненты класса ’’видны’’ во всех операторах его тела. 1 Именно поэтому, кроме областей видимости ’’файл", ’’блок’’, "функция", в Си++ введена особая область видимости "класс". Статические компоненты класса. Статический элемент данных класса уже использовался в классе goods (см. Р9-01 .СРР), где ко всем объектам класса относилась переменная static int percent - «торговая наценка». Рассмотрим эту возможность подробнее. Итак, . каждый объект одного и того же класса имеет собственную копию 5 данных класса. Можно сказать, что данные класса тиражируются при каждом определении объекта этого класса. Отличаются они друг от друга именно по "привязке" к тому или иному объекту. Это не всегда соответствует требованиям решаемой задачи. Например, при форми- ровании объектов класса может потребоваться счетчик объектов. Если объекты создаются и при этом сцепляются в цепочку, образуя связный Список, то для просмотра всего списка удобно иметь указатель на начало Списка. Для добавления нового объекта в конец такого списка нужен ,! указатель на последний элемент списка (на последний объект, ‘ включаемый в список). Такие указатели на первый и последний объекты Списка, а также уже упомянутый счетчик объектов можно сделать Компонентами класса, но иметь их нужно только в единственном числе | (каждый). ? Чтобы компонент класса был в единственном экземпляре и не ' тиражировался при создании каждого нового объекта класса, он должен 1 быть определен в классе как статический, т.е. должен иметь атрибут i Static. Некоторые возможности статических компонентов уже были ? продемонстрированы на примере класса "товары на складе магазина". Статические компоненты класса после инициализации можно использовать в программе еще до определения объектов данного класса. Такую возможность для общедоступных данных предоставляет Квалифицированное имя компонента. Когда определен хотя бы один объект класса, к его статическим компонентам можно обращаться, как К обычным компонентам, т.е. с помощью операций выбора компонентов класса ('.' и '->'). Здесь возникает одно затруднение. На статические данные класса распространяются правила статуса доступа. Если статические данные имеют статус private или protected, то к ним Извне можно обращаться через компонентные функции. При каждом вызове такой компонентной функции необходимо указать имя некоторого объекта. К моменту обращения к статическим данным класса объекты класса могут быть либо еще не определены, либо их может
298 Язык Си++ быть несколько и каждый пригоден для вызова компонентных функ- ций. Без имени объекта обычную компонентную функцию вызвать нельзя в соответствии с требованиями синтаксиса. Но какой объект выбрать для вызова, ведь каждый статический элемент класса един- ственный в нем? Хотелось бы иметь возможность обойтись без имени конкретного объекта при обращении к статическим данным класса. Такую возможность обеспечивают статические компонентные функ- ции. Статическая компонентная функция сохраняет все основные осо- бенности обычных (нестатических) компонентных функций. К ней можно обращаться, используя имя уже существующего объекта класса либо указатель на такой объект. Дополнительно статическую компо- нентную функцию можно вызвать, используя квалифицированное имя: имя__класса: : имя__статической__функции С помощью квалифицированного имени статические компонент- ные функции можно вызывать до определения конкретных объектов класса и не используя конкретных объектов. В следующей программе класс points определяет точку в трехмерном пространстве и одно- временно содержит статический счетчик N таких точек. Обращение к счетчику обеспечивает статическая компонентная функция count (). //Р9-03.СРР - статические компоненты класса ^include <iostream.h> class point3 // Точка * трехмерном пространстве { double х, т; // Координаты точки static int N; // Количество точек (счетчик) public: // Конструктор инициализирует значения координат: points(double хп « 0.0, double уп « 0.0, double zn « 0.0) { N++; х = хп; у = уп; z = zn; } // Обращение к счетчику: static int& count() { return N; } } ; /I Внешнее описание и инициализация статического элемента: int point3::N = 0; void main(void) { cout « "\nsizeof (point3) » 11 « sizeof (points) ; points A(0.0,1.0,2.0); cout « "\nsizeof (A) « 11 « sizeof(A); points В(3.0,4.0,5.0); cout « "\nOnpeделены ” « points::count() « " точки.";
Глава 9. Класс как абстрактный тип 299 point3 С(6.0, 7.0, 8.0); cout « "\п Определены" « B.count() « "точки."; } Результаты выполнения программы: sizeof(point3) = 24 sizeof(А) = 24 Определены 2 точки. Определены 3 точки. Обратите внимание, что размер типа points равен размеру одно- го объекта этого класса. Память выделена для трех элементов типа double, и никак не учтено наличие в классе статического компонента int N. Как уже говорилось, в отличие от обычных компонентных данных статические компоненты класса необходимо дополнительно описывать и инициализировать вне определения класса как глобальные переменные. Именно таким образом в программе Р9-03.СРР получает начальное значение статический элемент класса point3::N. Так как N ~ собственный компонент класса, то последующие обращения к нему' возможны только с помощью дополнительной общедоступной функции. В данном примере это статическая функция countO . Попытка обратиться к компоненту N с помощью квалифицированного имени points: : N будет воспринята как ошибка, так как для ы определен статус private. Нестатический компонент класса может быть указателем или ссылкой на объект того же класса. Такая возможность позволяет формировать связные списки (цепочки) объектов одного класса. Статическим компонентом класса может быть указатель на объект класса. Это позволяет однозначно определить начало связного списка объектов класса, например, при моделировании списка можно ввести класс такой структуры: class list {... //Собственные компоненты public: static list *begin; //Начало связного списка ); list *list::begin = NULL; //Инициализация статического //компонента
300 Язык Си++ Полное определение класса, моделирующего список, здесь не при- водится, так как его компонентные функции удобно строить с исполь- зованием указателя this, который будет введен в следующих пара- графах. Указатели на компоненты класса. Две специфичные операции языка Си++ ' и ' ->*' предназначены для работы с указателями на компоненты класса. Прежде чем объяснить их особенности, отме- тим, что указатель на компонент класса не является обычным указа- телем, унаследованным языком Си++ от языка Си. Обыкновенный указатель предназначен для адресации того или иного объекта (участка памяти) программы. Указатель на компонент класса не мо- жет адресовать никакого участка памяти, так как память выделяется не классу, а объектам этого класса при их создании. Таким образом, указатель на компонент класса при определении не адресует никакого конкретного объекта. Каким же образом определяются (описываются) указатели на компоненты классов? Как эти указатели получают зна- чения? Чем являются эти значения указателей? Какими возможностя- ми обладают указатели на компоненты класса? Для каких целей они используются? Почему и как с указателями на компоненты класса ис- пользуются две операции разыменования ('. *1 и 1 ->*') ? Компоненты класса, как уже многократно повторялось, делятся на две группы - компоненты-данные и компоненты-функции. Указатели на компоненты класса по-разному определяются для данных и функ- ций (методов) класса. Начнем с указателей на принадлежащие классу функции. Их определение имеет следующий формат: тип__возвращаемого__функциеи__значения (имя__класса: : *имя__укаэателя__на__метод} (спецификация__параметров__функции) ; Например, выше в классе complex ("комплексное число") опреде- лены методы (компонентные функции) doublet re () , doublet im (). Вне класса можно следующим образом описать указатель ptcom: doublet (complex::*ptCom)(); Описав указатель ptcom на компонентные функции класса complex, можно почти обычным образом задать его значение: ptCom = tcomplex::re; // "Настройка" указателя Теперь для любого объекта А класса complex complex А(10.0,2.4); // Определение объекта А
Глава 9. Класс как абстрактный тип 301 можно таким образом вызвать принадлежащую классу функцию ге (): (A.*ptCom)() «11.1; // Изменится вещественная часть А ccut « (A.*ptCom)(); // Вывод на печать A.real Изменив значение указателя ptCom = (complex::im; // "Настройка” указателя можно с его помощью вызывать другую функцию того же класса: cout « (A.*ptCom) () ; // Вывод значения мнимой части А complex В ® А; // Определение нового объекта В (B.*ptCom) 3.0; // Изменение значения мнимой части В В данных примерах определен и использован указатель на компо- нентную функцию без параметров, возвращающую значение типа doubled Его не удастся настроить на принадлежащие классу complex функции с другой сигнатурой и другим типом возвращаемого значе- ния. Для обращения к компонентной функции display(), указатель ptDisp нужно ввести следующим образом: void (complex::*ptDisp)(void); Настроив указатель ptDisp на вещественную функцию display () класса complex, можно вызвать эту функцию для любого объекта это- го класса: ptDisp = (complex: :display; // "Настройка" указателя B.*ptDisp(); // Вызов функции display() для объекта В Формат определения указателя на компонентные данные класса: тип_дамяых (имя_класса: : *имя_указателя) ; В определение указателя можно включить его явную инициализа- цию, используя адрес компонента: (имя класса: :имя__компоиеита При этом компонент класса должен быть общедоступным (public). Например, попытка определить и использовать указатель на длину строки (компонент int len) класса stroka: int(stroka::*plen) — (stroka::len; окажется неверной, так как компонент len класса stroka по умолча- нию имеет атрибут private.
302 Язык Си++ В определенном выше простейшем классе complexl есть общедоступные компонентные данные типа double, поэтому следующее определение указателя pdat будет вполне правильным: complexl comp; comp.real=lб.О; comp.imag=33.4; double (complexl::*pdat) = fccomplexl::comp.imag; cout « "\ncomp.imag - " <<comp.*pdat; pdat = &complexl::comp.real; //Перестройка указателя cout « "\ncomp. real = "«comp. *pdat; Результаты выполнения приведенных операторов: comp.image - 33.4 comp.real = 16 Указатель на компоненты класса можно использовать в качестве фактического параметра при вызове функции. В приведенных примерах мы использовали операцию разыменования указателей на компоненты класса: имя_обт>екта. * указатель—на_компонент_данных имЯ—Объекта.*указатель_на_иетод (параметры) Слева от операции ’ . *' кроме имени конкретного объекта может помещаться ссылка на объект. Если определен указатель на объект класса и введены указатели на компоненты того же класса, то доступ к компонентам конкретных объектов можно получить с помощью бинарной операции доступа к компонентам класса через указатель на объект: указатель^на-ОбъекТ-Класса - >*указатель_на компонент—данных указатель_на„объект—класса - >*указатель—на-метод (параметры) Первым (левым) операндом должен быть указатель на объект класса, значение которого - адрес объекта класса. Второй (правый) операнд - указательна компонент класса. Результат выполнения операции - это либо компонент данных, либо компонентная функция класса. Если второй операнд - это леводопустимый компонент данных, то и результат применения операции (а также операции ’.*’) есть /-значение. Например, определим и инициализируем указатель pcml на компонент данных класса complexl: double (complexl::*pcml) = &complexl::real Определим и инициализируем указатель pcomplexl на объекты класса complexl:
[Глава 9. Класс как абстрактный тип 303 complexl СМ(10.2,-6.4); ' complexl *pcomplexl » &О4; Теперь, применяя операцию 1 ->* *, получим /-значение: pcomplexl->*pcml » 22.2; Приведенный оператор присваивания изменяет значение веще- ственной части комплексного числа, представленного объектом 04 Класса complexl. Если справа от операции »->*» находится инициализированный указатель на компонентную функцию для того объекта, на который "настроен" левый операнд, то выполнится обращение к соответ- ствующему методу: complex А(22.2,33.3); // Объект класса complex *pComplex = fcA; // Указатель класса void (complex::*pdisplay)(); // Указатель на компонентную // функцию pdisplay = £ complex; .-display; // "Настройка" указателя (pComplex->*pdi3play)(); // Вызов компонентной функции // через указатель на объект // класса и указатель на // компонентную функцию В данном примере на экран выводится сообщение: real = 22.2, imag =33.3 9.4. Определение компонентных функций Компонентная функция должна быть обязательно описана в теле класса. В отличие от обычных (глобальных) функций компонентная функция имеет доступ ко всем компонентам класса (с любым статусом доступа). Функция-компонент класса имеет ту же область видимости, что и класс, к которому она относится. Как уже говорилось в главе, посвященной функциям, в языке Си++ программист может влиять на компилятор, предлагая ему оформить ту или иную функцию как подставляемую (встраиваемую). Для этих целей в определении функции указывается служебное слово (спецификатор) inline. При определении классов их компонентные функции также могут быть специфицированы как подставляемые. Кроме явного использо-
304 ЯзыкСи++ вания служебного слова inline для этого используется следующее со- глашение. Если определение (не только прототип) принадлежащей классу функции полностью размещено в классе (в теле класса), то эта функция по умолчанию считается подставляемой. Именно таким об- разом определены компонентные функции классов complexl, goods, complex, stroka, points, использованных ранее в качестве примеров. Все функции перечисленных классов воспринимаются компьютером как подставляемые, т.е. при каждом вызове этих функций их код '’встраивается" непосредственно в точку вызова. Как уже говорилось в связи с обсуждением особенностей подставляемых функций, это не всегда удобно, так как подставляемыми могут быть не всякие функ- ции. (Подставляемая функция не может быть рекурсивной, не может содержать циклы, переключатели и т.д.) Это ограничение весьма су- щественное, поэтому существует второй способ определения принад- лежащих классу функций. Он состоит в том, что внутри тела класса помещается только прототип компонентной функции, а ее определе- ние - вне класса, как определение любой другой функции, входящей в программу. При таком внешнем определении компонентной функции она также может быть снабжена спецификатором inline, но при этом опять возникнут указанные ограничения, накладываемые на подстав- ляемые функции. При внешнем определении компонентной функции программист "должен сообщить" компилятору к какому именно классу она отно- сится. Для этого используется бинарная форма операции •::1 (указа- ния области видимости). Формат ее использования в этом случае та- ков: имя_класса::имя_компонентной_функции Приведенная конструкция, называемая квалифицированным име- нем компонентной функции, означает, что функция есть компонент класса и лежит в области его действия. Именно такое определение привязывает функцию к классу и позволяет в ее теле непосредственно использовать любые данные класса (его объектов) и любые принад- лежащие классу функции. (Это относится и к собственным private- и к защищенным protected-компонентам.) Итак при внешнем определении компонентной функции в. теле класса помещается ее прототип: тип имя_функции (спецификация_и_инициализация_параметров) ; Вне тела класса компонентная функция определяется таким обра- зом:
Глава 9. Класс как абстрактный тип 305 тип имя_класса:: имя__фумкции (спецификация__формальных_параметров) { тало__принадлежащеи__классу__функции ) В качестве примера класса с внешним определением компонент- ных функций введем класс point, определяющий понятие "точка на экране дисплея". Разместим описание класса в отдельном файле с на- званием point.h: // POINT.Н - описание класса с внешними определениями / / методов tifndef POINTH «define POINTH 1 class point { // Точка на экране дисплея protected: // Защищенные данные класса: int х, у; // Координаты точки // Прототипы общедоступных компонентных функции: public: point(int xi « 0, int yi « 0); // Конструктор intfi givex(void); // Доступ к x intfi givey(void); // Доступ к у void show(void); // Изобразить точку на экране // Переместить точку в новое место экрана: // (хп == 0, уп =* 0 - умалчиваемые значения // параметров) void move(int хп e 0, int уп = 0); private: // Собственная функция класса: void hide (); // Убрать с экрана изображение точки }; «endif Так как описание класса point в дальнейшем планируется вклю- чать в другие классы, то для предотвращения недопустимого дубли- рования описаний в текст включена условная препроцессорная директива tifndef pointh. Препроцессорный идентификатор pointh определяется тут же с помощью директивы «define POINTH 1 Тем самым текст описания класса point может появляться в ком- пилируемом файле только однократно, несмотря на возможность не- однократных появлений директив «include "point.h". Обратите внимание на обычную необязательность имен формальных пара- метров при описании прототипов функций. В прототипе конструкто- 2Q-382I
306 Язык Си++ ра и в прототипе функции move () имена формальных параметров xi, yi, xn, уп можцо было бы опустить. Описание класса с внешним определением его компонентных функций дает возможность, не меняя интерфейс объектов класса с другими частями программы, по-разному определять его компонентные функции. В данном примере понятие ’’точка на экране дисплея” можно трактовать несколькими способами. Принципиально различных здесь два подхода - использование графического режима дисплея или работа с дисплеем в текстовом режиме. В обоих случаях в самом языке Си++ нет явно определяемых синтаксисом средств для работы с дисплеем. Нужно использовать возможности, предоставляемые библиотеками конкретных компиляторов. В настоящее время все компиляторы, ориен- тированные на ПЭВМ и рабочие станции, имеют библиотеки функций для работы с дисплеем в графическом режиме. Мы в примерах с графикой будем использовать только некоторые возможности библиотеки гра- фических функций компиляторов фирмы Borland (их список - в прил. 4). В пояснениях к программам будут кратко описаны возможности нескольких графических функций. Этого достаточно для понимания примеров и самостоятельного выполнения несложных упражнений с классами фигур на экране. Подробнее с графическими возможностями компиляторов Turbo C++, Borland C++ можно познакомиться по документации и по литературе'. Для работы с графической библиотекой компиляторов фирмы Borland в программу должен быть обязательно включен заголовочный файл graphics. h. После этого в тексте программы можно обращаться к библиотечным графическим функциям и использовать заранее подготовленные константы и структуры. Учитывая сказанное, опре- делим компонентные функции класса point следующим образом: //POINT.СРР - внешнее определение функций класса #ifdef POINTCPP #define POINTCPP 1 #include <graphics.h> //Связь с графической библиотекой #include "point.h" //Описание класса point point::point(int xi=0, int yi=0) { x=xi; y=yi;} //Определение данных объекта int& point::givex(void) { return x; } // Доступ к x int& point::givey(void) { return у; } // Доступ к у //Изобразить точку на экране: void point::show (void) 1 Прокофьев Б.П., Сухарев Н.Н., Храмов Ю.Е. Графические средства Turbo С и Turbo С ++. - М.: Финансы и статистика, СП Ланит, 1992. - 160 с.
Глава 9. Класс как абстрактный тип 307 { putpixel (x,y,getcolor()); } //Убрать с экрана изображение точки: void point::hide(void) ( putpixel (x,y, getbcolor(}); } //Переместить точку в новое место экрана: void point::move(int xn=0, int yn=0) ( hide () ; x = xn; у = yn; show() ; } *endif Как и текст в файле point. h, определения компонентных функций защищены условной препроцессорной директивой от дублирования. В определении методов класса point используются следующие графи- ческие функции: void putpixel (int х, int у, int color) изображает цветом color точку на экране дисплея с координатами (X, у) ; int getbcolor (void) возвращает номер цвета фона; int getcolor (void) возвращает номер цвета изображения. Обратите внимание, что механизм удаления точки с экрана полностью подобен процедуре ее изображения, но в качестве цвета рисования выбирается цвет фона. Для иллюстрации в данном определении класса конструктор point и прототип функции move () снабжены умалчиваемыми значениями параметров. Координаты создаваемой по умолчанию (без указания значений параметров) точки равны нулю. Туда же по умолчанию перемещается точка. Внешнее определение методов класса в противоположность встроен- ному позволяет модифицировать принадлежащие классу функции, не изменяя текста описания класса. Определив класс и его компонентные функции, приведем программу, иллюстрирующую работу с классом point: //Р9-04.СРР - работа с классом "точка на экране" ftinclude <graphics.h> // Прототипы графических функций finclude <contio.h> // Прототип функции getch ()
308 Язык Си++ #include "point.срр" // Определение класса point void main() { point A(200,50); // Создается невидимая точка А point B; H Невидимая точка В с нулевыми // координатами по умолчанию point D(500,200); // Создается невидимая точка D // Переменные для инициализации графики: int dr « DETECT, mod; initgraph (&dr, &mod, "c: \\borlandc\\bgi") ; A.show(); // Показать на экране точку А getch(); // Ждать нажатия клавиши В.show(); / / Показать на экране точку В getch(); D.show(); // Показать на экране точку D getch(); A.move(); // Переместить точку А get ch (); В. move (50,60) ; // Переместить точку В getch(); closegraph(); // Закрыть графический режим } Результаты выполнения программы показаны на рис. 9.1. Рис. 9.1. Последовательность изображений на экране при выполнении программы Р9-04 . СРР Несмотря на тог факт, что при определении методов класса кор- ректно используются обращения к функциям графической библиоте- ки, этого недостаточно, чтобы "заставить" дисплей перейти в графический режим. Переключение дисплея в графический режим вы- полняет специальная функция initgraph(), вызов которой осу- ществляется в основной программе. Ей всегда нужно передать три фактических параметра. Первый из них по соглашениям графической библиотеки проще всего инициализировать стандартным именем detect, которое определено в заголовочном файле graphics.h. Вто-
Глава 9. Класс как абстрактный тип 309 рой параметр - это переменная типа int. Третий параметр - символьная строка (указатель на нее), явно определяющая путь к графической библиотеке. В примере библиотека находится на диске С: в каталоге borlandc\bgi (подробнее см. [6,31]. Выполнение программы понятно из комментариев. Как объекты класса point создаются невидимые точки а, в, D, затем они изобра- жаются на экране и перемещаются по нему. В конце программы выполняется еще одна функция графической библиотеки closegraph () . Ее назначение - выйти из графического режима, т.е, восстановить исходный (текстовый) режим работы дисплея. В программе используется еще одна библиотечная функция getch () , прототип которой находится в заголовочном файле conio.h. При обращении к этой функции программа ’’останавливается" и ожидает ввода от клавиатуры любого символа. Тем самым у пользователя появляется возможность проследить смену изображений на дисплее. Функция getch () специфична для MS-DOS. 9.5. Указатель this Когда функция, принадлежащая классу, вызывается для обработки данных конкретного объекта, этой функции автоматически и неявно передается указатель на тот объект, для которого функция вызвана. Этот указатель имеет фиксированное имя this и незаметно для программиста ("тайно") определен в каждой функции класса следующим образом: имя_класса * const this = адрес__обрабатываемого_об’ьекта Имя this является служебным (ключевым) словом. Явно описать или определить указатель this нельзя и не нужно. В соответствии с неявным определением this является константным указателем, т.е. изменять его нельзя, однако в каждой принадлежащей классу функции он указывает именно на тот объект, для которого функция вызывается. Говорят, что указатель this является дополнительным (скрытым) параметром каждой нестатической компонентной функции. Другими словами, при входе в тело принадлежащей классу функции указатель this инициализируется значением адреса того объекта, для которого вызвана функция. Объект, который адресуется указателем this, становится доступным внутри принадлежащей классу функции именно с помощью указателя this. При работе с компонентами класса внутри
310 Язык Си++ принадлежащей классу функции можно было бы везде использовать этот указатель. Например, совершенно правильным будет такое опре- деление класса: struct S3 { int si; char sc; ss(int in, char cn) // Конструктор объектов класса ( this->si = in; this->sc = cn; } void print(void) // Функция вывода сведении об объекте { cout « "\n si = •• « this->si; cout « "\n sc = ” « this->sc; } ); В таком использовании указателя this нет никаких преимуществ, так как данные конкретных объектов доступны в принадлежащих классу функциях и с помощью имен данных класса. Однако в некото- рых случаях указатель this полезен, а иногда просто незаменим. В сле- дующем примере указатель this позволяет компилятору разобраться в ситуации, когда имя meaning компонента класса совпадает с именем формального параметра принадлежащей классу функции: //Р9-05.СРР - указатель "this" tinclude <iostream.h> class cell // Класс "числовой элемент" ( int static Amount; // Общее количество элементов int Number; // Порядковый номер элемента double Meaning; // Значение элемента public: // Конструктор: cell(double Meaning = 0.0) { // Меняем значение статического компонента: Amount++; this -> Number « Amount; // Компонент и одноименный параметр: this -> Meaning * Meaning; ) // Вывод сведений об объекте и количестве элементов: void display(void) ( cout « "\nNumber ® " « this->Number; // Лишний this: cout « ” Amount = " « this->Amount; cout « " Meaning = " « thi s->Meaning; ) ); // Инициализация статического компонента:
Глава 9. Класс как абстрактный тип 311 int cell::Amount = 0; void main(void) ( cell A; // Объект с умалчиваемым значением A.display () ; cell В(200.0); cell C(300.0) ; B.display(); C.display() ; ) Результат выполнения программы: Number = 1 Amount = 1 Meaning = 0 Number = 2 Amount = 3 Meaning = 200 Number = 3 Amount = 3 Meaning = 300 Снятие неоднозначности в теле принадлежащей классу функции между одинаковыми именами формального параметра и компонента класса можно осуществить и без использования указателя this. Гораз- до чаще для этой цели применяют операцию изменения видимости, т.е. используют выражение имя_класса: :имн_компонента В приведенном примере класса cell конструктор может быть и таким: cell (double Meaning = 0.0) { Amount++; Number = Amount; cell: .-Meaning = Meaning; // Устранение неоднозначности ) Почти незаменимым и очень удобным указатель this становится в тех случаях, когда в теле принадлежащей классу функции нужно явно задать адрес того объекта, для которого она вызвана. Например, если в классе нужна функция, помещающая адрес выбранного объекта класса в массив или включающая конкретный объект класса в список, то такую функцию сложно написать без применения указателя this. Действительно, при организации связных списков, звеньями которых должны быть объекты класса, необходимо включать в связи звеньев указатель именно на тот объект, который в данный момент обраба- тывается. Это включение должна выполнить некоторая функция- компонент класса. Однако конкретное имя включаемого объекта в момент написания этой принадлежащей классу функции недоступно, так как его гораздо позже произвольно выбирает программист, ис-
312 Язык Си++ пользуя класс как тип данных. Можно передавать такой функции ссылку или указатель на нужный объект, но гораздо проще использовать указатель this. Итак, повторим, когда указатель this использован в функции, принадлежащей классу, например с именем ZOB, то он имеет по умолчанию тип ZOB * const и всегда равен адресу того объекта, для которого вызвана компонентная функция. Если в программе для некоторого класса х определить объект X factor (5) ; то при вызове конструктора класса х, создающего объект factor, значением указателя this будет &factor. Для пояснения возможностей указателя this рассмотрим в качестве примера класс, объекты которого формируют (образуют) двухсвязный список. В следующем тексте определяется состав класса и описываются свойства его компонентов: //MEMBER.Н - "элементу двухсвязного списка” class member { //Адрес последнего элемента списка: static member * last_memb; member * prev; //На предыдущий элемент списка member *next //На следующий элемент списка char bukva; //Содержимое (значение) элемента списка public; //Функции для работы со списком: member (char сс) { bukva = сс; } //Конструктор void add (void); //Добавление элемента в конец списка //Вывод на дисплей содержимого списка: static void reprint (void); }; Из объектов класса member, как из звеньев, может формироваться двухсвязный список. Схема построения списка показана на рис. 9.2. В классе member имеется статический компонент-указатель last-membHa последний объект, уже включенный в список. Когда список пуст, значение last_memb должно быть равно нулевому указателю NULL. Связь между объектами как звеньями списка организуется с помощью указателей next и prev. Пустой список last_memb== NULL представлен на рис. 9.2. Выполняет "подключение" объекта к списку компонентная функция add () . Статическая функция reprint () позволяет "перебрать" звенья списка (объекта класса member) в порядке от конца к началу и вывести символы ("содержания") объектов на экран дис-
Глава 9. Класс как абстрактный тип 313 плея. Конструктор инициализирует компонент char bukva каждого создаваемого объекта. last_memb » NULL Р_______ N | р ) bukva|o|*-- this laet^mamb Пустой список 1 элемент Р_________N Р ________N | 0 | bukva ] —|--► | т I bukva | о | — this 2 элемента ------ - lastjawnb Рис. 9.2. Последовательность формирования списка из объектов класса member (р - previous (предыдущий), N - next (следующий)) Определим компонентные функции класса member: //MEMBER.СРР - определения функций класса member: #include <iostream.h> linclude <stdio.h> //Для описания нулевого указателя NULL // Определение класса с прототипами функций: #include "memder. h" // Добавление элемента в конец списка: void member::add(void) { if (lastjnemb =» NULL) this -> prev = NULL; else { last_memb-> next == this; this —> prev = last_memb; last_memb = this; this-next = NULL; //Вывод на дисплей содержимого списка: void member::reprint (void) { member *uk; //Вспомогательный указатель uk == lastjnemb; if uk == NULL) { cout « "\n Список пуст!"; return; } else cout « "\пСодержимое списка: \n; //Цикл печати в обратном порядке значений элементов //списка: while (uk !=NULL) {cout « uk->bukva « '\t'; uk = uk->prev; }
314 Язык Си++ Вне класса указатель lastjnemb до включения в список первого элемента инициализируется нулевым значением (null). Поэтому пер- вым шагом выполнения функции add будет проверка значения lastjnemb. Если он равен нулю, то в список включается первый эле- мент (объект), для которого указатель prev на предшествующий эле- мент должен быть нулевым. Для подключения объекта к уже существующему списку необходимо указателю next последнего в списке объекта присвоить значение указателя this (адрес добавляемого объекта). В качестве указателя на своего предшественника (prev) под- ключаемый объект получает значение lastjnemb. Затем последним становится обрабатываемый (только что подключенный) объект (lastjnemb » this;) и обнуляется его указатель next на последую- щий объект в списке. Компонентная функция reprint о описана в классе как статиче- ская. Это никак не сказывается на ее определении. Первое действие функции - "настройка" вспомогательного указателя uk на последний включенный в список объект. Его адрес всегда является значением указателя lastjnemb. Если список пуст, то на этом выполнение функ- ции завершается. В противном случае в цикле печатаются значения uk->bukva и указатель "перемещается" к предыдущему звену списка. В следующей программе инициализирован статический указатель lastjnemb, создаются объекты класса member, объединяются компо- нентной функцией add () в двухсвязный список, и этот список выво- дится на экран дисплея с помощью статической функции reprint (). //Р9-06.СРР - статические компоненты, указатель this tinclude <iostream.h> tinclude "member. срр” // Определение класса member // Инициализация статического компонента (указателя) : member *member::lastjnemb = NULL; void main() { // Формирование объектов класса member: member A (1 a'); member В('b1); member C('c'); member D(’d’); // Вызов статической компонентной функции: member::reprint(); // Включение созданных объектов в двусвязный список: A.add(); B.add() ; C.add() ; D.addO; // Печать в обратном порядке Значений элементов списка: member::reprint(); }
Глава 9. Класс как абстрактный тип 315 Результат выполнения программы: Список пуст! Содержимое списка: d с Ь а Обратите внимание, что все компонентные данные класса member имеют статус собственных (private) и недоступны из других частей программы. Доступ к классу обеспечивают только компонентные функции, имеющие статус public. 9.6. Друзья классов Как уже сказано, механизм управления доступом позволяет выде- лять общедоступные (public), защищенные (protected) и собственные (private) компоненты классов. Защищенные компоненты доступны внутри класса и в производных классах. Собственные компоненты локализованы в классе и недоступны извне. С помощью общедоступ- ных компонентов реализуется взаимодействие класса с любыми час- тями программы. Однако имеется еще одна возможность расширить интерфейс класса. Ее обеспечивают дружественные функции. По определению [2], дружественной функцией класса называется функция, которая, не являясь его компонентом, имеет доступ к его защищенным и собственным компонентам. Функция не может стать другом класса "без его согласия". Для получения прав друга функция должна быть описана в теле класса со спецификатором friend. Именно при наличии такого описания класс предоставляет функции права доступа к защи- щенным и собственным компонентам. Пример класса с дружественной функцией: //Р9-07.СРР - класс с дружественной функцией tinclude <conio.h> // Для консольных функций в текстовом // режиме // Класс - "символ в заданном позиции экрана": class charlocus ( int х, у; // Координаты знакоместа на экране дисплея // Значение символа, связанного со знакоместом: char сс; // Прототип дружественной функции для замены символа: friend void friend^put(charlocus *, char); public: charlocus(int xi, int yi, char ci) // Конструктор { x » xi; у = yi; сс ж ci; }
316 ЯзыкСи++ void display (void) // Вывести символ на экран { gotoxy(x,y); putch(cc); } } ; // Дружественная функция замены символа в конкретном // объекте: void friend_put(charlocus *p, char c) { p->cc = c; } void main (void) ( charlocus D(20r4,'d'); // Создать объект charlocus 3(10,10,'s'); // Создать объект clrscr(); // Очистить экран D.display(); getch(); S.display(); getch(); friend_put(&D,’*'); D.display(); getch(); friend_put(&S,'#'); S.display(); getch(); ) Программа последовательно выводит на экран d (в позицию 20, 4), s (в позицию 10, 10), * (в позицию 20,4), # (в позицию 10, 10). Для работы с экраном в текстовом режиме использованы две функции из библиотеки Turbo С. Их прототипы находятся в заголо- вочном файле conio.h, где специфицированы так называемые "консольные" функции ввода-вывода. В других компиляторах языка Си++ эти функции могут быть определены иначе. void gotoxy (int х, int у) позволяет поместить курсор в позицию экрана с "координатами" х (по горизонтали) и у (по вертикали). В обычном (текстовом) режиме количество строк 25 (у меняется от 0 до 24), количество столбцов 80 (х меняется от 0 до 79). Позиция с координатами (0,0) соответствует левому верхнему углу экрана. void putch (int s) выводит на экран в местоположение курсора изображение символа, код которого определяется значением параметра з. int getch(void) также относится к библиотечным консольным функциям. Ее основное назначение - чтение кода из буфера клавиатуры без вывода соответствующего символа на экран. При вызове этой функции программа останавливает выполнение и ожидает сигнала от клавиатуры. Тем самым у пользователя появляется возможность проследить смену изображений на экране.
Глава 9. Класс как абстрактный тип 317 Более подробно с консольными функциями можно познакомиться по документации компилятора либо по имеющимся публикациям*. Функция friendjputo описана в классе char locus как друже- ственная и определена как обычная глобальная функция (вне класса, без указания его имени, без операции :: и без спецификатора friend). Как дружественная она получает доступ к собственным данным клас- са и изменяет значение символа того объекта, адрес которого будет передан ей как значение первого параметра. Выполнение основной программы очевидно. Создаются два объ- екта D и s, для которых определяются координаты мест на экране и символы (d, s). Затем общедоступная функция класса charlocus:: display () выводит символы в указанные позиции экрана. Функция friendjput заменяет символы объектов, что демонстрирует повтор- ный вывод на экран. Отметим особенности дружественных функций. Дружественная функция при вызове не получает указателя this. Объекты классов должны передаваться дружественной функции только явно через ап- парат параметров. При вызове дружественной функции нельзя ис- пользовать операции выбора: имЯ—Объекта. имя_функции И ухаэатель_на_об'ъект -> имя_функции Все это связано с тем фактом, что дружественная функция не яв- ляется компонентом класса. Именно поэтому на дружественную функцию не распространяется и действие спецификаторов доступа (public, protected, private). Место размещения прототипа дружествен- ной функции внутри определения класса безразлично. Права доступа дружественной функции не изменяются и не зависят от спецификато- ров доступа. В приведенном примере описание функции friendjput () помещено в разделе, который по умолчанию имеет ста- тус доступа private. Итак, дружественная функция • не может быть компонентной функцией того класса, по отношь - нию к которому определяется как дружественная; • может быть глобальной функцией (как в предыдущей програм- ме): class CL ( friend int f1 (...); ... }; int f1(...) ( тело_функции } 1 Бочков С.О., Субботин Д.М. Язык программирования Си для персонально- го компьютера - М.: Радио и связь, 1990 - 384 с.
318 Язык Си++ • может быть компонентной функцией другого ранее определен- ного класса: class CLASS {... char f2 (...); class CL {...friend char CLASS: : f 2 В примере класс class с помощью своей компонентной функции f 2 () получает доступ к компонентам класса CL. Компонентная функция некоторого класса (CLASS) может быть объявлена дружественной функцией другому классу (CL), если только определение этого первого класса размещено раньше, чем определение второго (CL). • может быть дружественной по отношению к нескольким клас- сам: // Предварительное неполное определение класса class CL2; class CL1 { friend void ff(CLl,CL2); ; class CL2 { friend void ff(CLl,CL2); ; void ff (CL1,cl,CL2,c2){ тело„функции } Использование механизма дружественных функций позволяет упростить интерфейс между классами. Например, дружественная функция позволит получить доступ к собственным или защищенным компонентам сразу нескольких классов. Тем самым из классов можно иногда убрать компонентные функции, предназначенные только для доступа к этим "скрытым" компонентам. В качестве примера рассмотрим дружественную функцию двух классов "точка на плоскости" и "прямая на плоскости". Класс "точка на плоскости" включает компонентные данные для задания координат (х, у) точки. Компонентными данными класса "прямая на плоскости" будут коэффициенты а, в, с общего уравнения прямой А * х + в * у + С = 0. Дружественная функция определяет уклонение заданной точки от заданной прямой. Если (а, Ь) - координаты конкретной точки, то для прямой, в уравнение которой входят коэффициенты а, в, с, уклонение вычисляется как значение выражения А*а + в*Ь + с. В следующей программе определены классы с общей дружественной функцией, в основной программе введены объекты этих классов и вычислено уклонение от точки до прямой: //Р9-08.СРР - классы с общей дружественной функцией linclude iostream.h> class line2; //Предварительное описание //Класс "точка на плоскости": }
Глава 9. Класс как абстрактный тип 319 class point2 ( float х, у; // Координаты точки на плоскости public: point2(float хп = 0, float уп » 0) // Конструктор { х = хп; у = уп; } friend float uclon(point2,line2); }; // Класс "прямая на плоскости": class line2 { float А, В, С; // Параметры прямой public: line2(float a, float b, float с) // Конструктор { A = a; B»b; C » c; } friend float uclon(point2,line2); }; // Внешнее определение дружественной функции float uclon(point2 p,line2 1) ( return l.A * p.x + l.B * p.y + l.C; } void main(void) ( point2 P(16.0,12.3); // Определение точки P line2 L(10.0,-42.3,24.0); // Определение прямой L cout « "\n Уклонение точки P от прямой L: ; cout « uclon(P,L); ) Результат выполнения программы: Уклонение точки Р от прямой L: -336.290009 В качестве упражнения можно вместо дружественной функции uclon () определить глобальную функцию с теми же параметрами. При этом в классы point2 и line2 придется ввести дополнительные компонентные функции для доступа к собственным данным. Класс может быть дружественным другому классу. Это означает, что все компонентные функции класса являются дружественными для другого класса. Дружественный класс должен быть определен вне тела класса, "предоставляющего дружбу". Например, так: class Х2 ( friend class XI; ... }; class Xl ( ... // Определение дружественного класса void f1 (...) ; void f2 (...) ; );
320 Язык Си++ В данном примере функции fl и f2 из класса xi являются друзья- ми класса Х2, хотя они описываются без спецификатора friend. Все компоненты класса доступны в дружественном классе. Друже- ственный класс может быть определен позже (ниже), нежели описан как дружественный. В качестве примера "дружбы” между классами рассмотрим класс pointN - "точка в ^-мерном пространстве” и дружественный ему класс vectorN - "радиус-вектор точки" ("вектор с началом в начале координат ^мерного пространства"). Все компоненты точки - ее размерность Npoint и массив координат х [Npoint] - собственные, и доступ к ним в классе vectorN возможен только за счет дружеских отношений. Конструктор класса pointN выделяет память для массива координат и инициализирует этот массив заданным значением пара- метра double d; Конструктор класса vectorN формирует объект "радиус-вектор" или просто "вектор" по двум объектам класса pointN, проверяя равенство их размерностей. Объекты класса pointN задают начало и конец вектора, который затем приводится к началу коорди- нат. Кроме конструктора в классе vectorN введена функция для опре- деления нормы вектора, которая вычисляется как сумма квадратов координат его конца. В основной программе сформирован вектор по двум точкам 2-мерного пространства, затем сделана неправильная попытка создать вектор из двух точек разной размерности. Текст про- граммы: //Р9-09.СРР - дружественные классы tinclude <iostream.h> tinclude <stdlib.h> // Для функции exit() // Класс "точка в N-мерном пространстве": class pointN ( int Npoint; // Размерность пространства double *х; / / Указатель на массив координат точки // Описание дружественного класса: friend class vectorN; public: pointN(int n, double d = 0.0); // Конструктор "точек" }; // Определение конструктора: pointN::pointN(int n, double d) ( Npoint 38 n; // Определение размерности // Выделение памяти для координат: х 38 new double [Npoint] ; for (int i » 0; i < Npoint; i++) x[i] 38 d; // Инициализация массива координат
Главв 9. Класс как абстрактный тип 321 //Класс «радиус-вектор»: class vectorN { //Указатель на массив координат конца вектора: double *xv; int Nvector; //Размерность пространства public: Nvector(pointN, pointN); //Конструктор «векторов» double norm(); //Норма вектора }; /Определение конструктора: vectorN:: vectorN(pointN beg, pointN end) { if (beg.Npoint ! « end. Npoint) // Проверка точек { cerr « "\пОшибка в размерностях точек!"; exit(l) ;//Завершение программы ) Nvector = beg.Npoint; //Размерность вектора xv = new double [Nvector] ; for (int i ® 0; i< Nvector; i++) xv[i] = end.x[i] - beg.x[i); //Определение координат ) double vectorN:norm() //Вычисление нормы вектора { double dd = 0.0; for (int i a= 0; i< Nvector; i++) dd + = xv[i] * xv[i]; return dd; ) void main(void) { pointN A(2,4.0); pointN В(2,2.0); vectorN V(A,B); cout <<"\пНорма вектора: «V.normO ; pointN X(3,2.0); vectorN Z(A,X); } Результат выполнения программы: Норма вектора: 8 Ошибка в размерностях точек! Обратите внимание, что за счет дружественного отношения между классами конструктор класса vectorN напрямую с помощью уточненных имен обращается к компонентам класса pointN. 2 Г1821
322 Язык Си++ 9.7. Расширение действия (перегрузка) стандартных операций Одной из привлекательных особенностей языка Си++ является возможность распространения действия стандартных операций на операнды, для которых эти операции первоначально в языке не пред- полагались. Например, если si и S2 - символьные строки, то их кон- катенацию (соединение) удобно было бы обозначить как si + S2. Однако бинарная операция + в обычном контексте языка Си++ пред- назначена для арифметических операндов и не предусматривает стро- ковых операндов. Никакой возможности распространить действие стандартной операции + на строки в виде символьных массивов или строковых констант в языке Си++ нет. Однако, если определить si и S2 как объекты некоторого класса, например, введенного в п. 9.2 класса stroka, то для них можно ввести операцию +, выполняемую по таким правилам, которые заранее выбрал программист. Для этих це- лей язык Си++ позволяет распространить действие любой стандарт- ной операции на новые типы данных, вводимые пользователем. Распространить операцию на новые типы данных позволяет механизм перегрузки стандартных операций. Чтобы появилась возможность использовать стандартную для языка Си++ операцию (например, ’ + ’ или 1 *•) с необычными для нее данными, необходимо специальным образом определить ее новое по- ведение. Это возможно, если хотя бы один из операндов является объ- ектом некоторого класса, т.е. введенного пользователем типа. В этом случае применяется механизм, во многом схожий с механизмом опре- деления функций. Для распространения действия операции на новые пользовательские типы данных программист определяет специальную функцию, называемую "операция-функция» (operator function). Фор- мат определения операции-функции: тип_возвращаемого_значения operator знак_операции (специфихация_параметров_операции~функции) { операторы_тела__операции-фукхции } При необходимости может добавляться и прототип операции- функции с таким форматом: тип_возврацаемогон3начения operator знак_операции (специфихация_параметров_операции--функции) ; И в прототипе, и в заголовке определения операции-функции ис- пользуется ключевое слово operator, вслед за которым помещен знак
Глава 9. Класс как абстрактный тип 323 операции. Если принять, что конструкция operator знак_операции есть имя некоторой функции, то определение и прототип операции- функции подобны определению и прототипу обычной функции языка Си++. Например, для распространения действия бинарной операции 1 * ’ на объекты класса т может быть введена функция с заголовком Т operator *(Т х, Ту) Определенная таким образом операция (в нашем примере опера- ция "звездочка") называется перегруженной (по-английски - overload), а сам механизм - перегрузкой или расширением действия стандартных операций языка Си++. Количество операндов у операции-функции зависит от арности операции и от способа определения функции. Операция-функция определяет алгоритм выполнения перегруженной операции, когда эта операция применяется к объектам класса, для которого операция- функция введена. Чтобы явная связь с классом была обеспечена, опе- рация-функция должна быть либо компонентом класса, либо она должна быть определена в классе как дружественная, либо у нее дол- жен быть хотя бы один параметр типа класс (или ссылка на класс). Начнем с последнего варианта. Если для класса т введена операция-функция с приведенным выше заголовком и определены два объекта а, в класса т, то выражение А * в интерпретируется как вызов функции operator *(А,в). В качестве содержательного примера распространим действие операции » + ' на объекты класса "символьные строки". Для этого ис- пользуется определенный выше в п. 9.2 класс stroka, в котором len - длина строки и ch - указатель на символьный массив с текстом стро- ки. В классе stroka два конструктора. Один для создаваемого объек- та выделяет память заданных размеров и оформляет ее как пустую строку. Второй формирует объект класса stroka по уже существую- щей строке, заданной в качестве фактического параметра. Вне класса определим операцию-функцию с заголовком strokafi operator +(strokat A, stroka& B), распространяющую действие операции ' + ' на объекты класса stroka. Определение операции-функции размещено ниже основной програм- мы, в которой используется выражение с операцией ’ +1, примененной к объектам класса stroka. Указанное размещение текста определения оцерации-функции потребовало применения ее прототипа, который помещен до функции main (). Текст программы: 21
324 Язык Си++ //Р9-10.СРР - расширение действия (перегрузка) операции "+’ //Определение класса "символьные строки": #include "stroka.срр" //Прототип функции для расширения действия операции stroka& operator + (stroka & A, stroka & В); void main (void) { stroka X("Qui"); stroka Y("Vivra"); stroka Z("Verra!"); stroka C; C = X + Y + Z + "- Поживем - увидим!"; C.display(); } //Расширение действия операции + на строковые операнды: stroka& operator + (stroka& a, stroka& b) { //Длина строки-результата: int ii - a.len_str () + b.len_str (); stroka *ps; //Вспомогательный указатель //Создаем объект в динамической памяти: ps = new stroka(ii); //Копируем строку из ’а’: strepy(ps->string()fa.string()); //Присоединяем строку из "b": strcat(ps->string(),b.string()); ps->len_str()=ii; //Записываем значение длины строки return *ps; //Возвращаем новый объект stroka } Результат выполнения программы: Длина строки: 36 Содержимое строки: Qui Vivra Verra! - Поживем - увидим! В программе операция функция, расширяющая действие операцш на операнды типа stroka&, используется трижды в одном выражени i X + Y + Z + "- Поживем - увидим!" Изобразительные достоинства такого вызова операции функции несомненны. Однако кроме такой сокращенной формы вызов, (с помощью выражения с операндами нужных типов) возможна и полна форма вызова: operator знак_операции (фактические_параметры);
Глава 9. Класс как абстрактный тип 325 Например, к тому же результату в нашем примере приведет сле- дующая последовательность операторов: С e operator + (X,Y); С = operator + (C,Z); С = operator + (С," - Поживем - увидим!"); Вторую возможность перегрузки бинарной операции представля- ют компонентные функции классов. Любая стандартная бинарная операция 1 е 1 может быть перегружена с помощью нестатической опе- рации-функции, входящей в число компонентов класса. В этом случае у нее должен быть только один параметр и заголовок может выгля- деть так: Т operator @(Т х) (здесь т - определенный пользователем тип, т.е. класс). В этом случае выражение деве объектами а, в класса т в качестве операндов ин- терпретируется как вызов функции a.operator е (в), причем в теле операции-функции выполняется обработка компонентов объекта- параметра в и того объекта а, для которого осуществлен вызов. При необходимости принадлежность компонентов объекту а в теле опера- ции-функции можно сделать явным с помощью указателя this. Проиллюстрируем особенности расширения действия бинарной операции с помощью компонентной функции. Введем операцию 1 +1 для точек на экране дисплея, определяемых классом point (см. выше). Для краткости упростим определение класса point, оставив только самые необходимые компоненты, но дополнительно введем опера- цию-функцию, расширяющую действие бинарной операции ' + *: //Р9-11.СРР - компонентная функция для перегрузки // операции ’+’ tinclude <graphics.h> // Графическая библиотека class pointl // Точка на экране ( // Защищенные компоненты (данные) класса: protected: int х, у; // Координаты точки // Общедоступные принадлежащие классу функции: public: pointl(int xi « О, int yi = О) // Конструктор { x = xi; у = yi; }; // Изобразить точку на экране: void show(void) { putpixel(x,y,getcolor()); }; // Прототип компонентной операции-функции: pointl operator +(pointl£ p);
326 Язык Си++ // Внешнее определение компонентной операции-функции: pointl pointl::operator +(pointl £p) { pointl d; d.x = this->x + p.x; d.y = this->y + p.y; return d; } #include <conio.h> // Для функции getch() void main() ( // Переменные для инициализации графики: int dr = DETECT, mod; pointl A(200,50); // Создаются еще невидимые точки // Точка создается с умалчиваемыми координатами: pointl В; pointl D(50,120); // Инициализация графической системы: initgraph (£dr, £mod, "с: \\borlandc\\bgi") ; A.show(); // Показать на экране точку А getch(); // Ждать нажатия клавиши В.show(); getch(); D.show(); getch(); В = A + D; 11 Неявное обращение к операции-функции В.show(); getch(); В = A.operator 4- (В) ; // Явный вызов операции-функции В.show(); getch(); closegraph(); // Закрыть графический реями ) Рис. 9.3. Последовательность изображений на экране при выполнении программы Р9-11. СРР
Глава 9. Класс как абстрактный тип 327 В результате выполнения программы на экран дисплея (рис. 93) выводятся последовательно точки: а(200, 50); в(0, 0); d(50, 120); в(250, 70), в(450, 220). Если операция-функция определена как принадлежащая классу, то вызвать ее явно можно с использованием имени объекта или указате- ля на объект и операции выбора компонентов (•->*, ' . ')• Другими словами в этом случае вызов операции-функции подобен вызову обычной компонентной функции класса. pointl *ptr = ЬА; // Указатель "настроен" на // объект А класса pointl В = ptr->operator + (D); // Операция '+' выполняется, как И А + D В предыдущей программе операция-функция для перегрузки опе- рации 1 +1 явно вызвана для объектов айв. Мы рассмотрели на примерах перегрузку бинарной операции с помощью компонентной операции-функции и с помощью глобальной операции-функции. Проиллюстрируем особенности оформления опе- рации-функции в виде дружественной функции класса. Для полноты изложения выполним перегрузку не бинарной, а унарной операции. Введем класс "радиус-вектор TV-мерного пространства" и определим для него операцию-функцию ' -1, изменяющую направление вектора на противоположное. //Р9-12.СРР - операция-функция как дружественная функция #include <iostream.h> // Класс "радиус-вектор": class vector { int N; // Размерность пространства double *х; // Указатель иа массив координат // Прототип операции-функции: friend vector^ operator -(vector £); public: vector (int n, double *xn) // Конструктор { N = n; x = xn; } // Компонентная функция печати вектора: void display (); }; 11 Определение компонентной функции: void vector::display() { cout « "\пКоординаты вектора:"; for (int i » 0; i < N; i++) cout « "\t" « x[i]; ) // Определение операции-функции:
328 Язык Си++ vectors operator - (vector £ v) { for (int i = 0; i < v.N; i++) v. x [ i ] = -v.x[i]; return v; } void main() // Иллюстрирующая программа ( // Определяем массив: double А[] = { 1.0, 2.0, , 3.0, 4.0 }; vector V(4,A) ; И Создан объект класса vector V. display О ; И Вывод на экран V « -V; И Перегруженная операция V. display () ; // Вывод на экран Результат выполнения программы: Координата вектора: 123 4 Координаты вектора: -1 -2 -3 -4 Итак, механизм классов дает возможность программисту опреде- лять новые типы данных, отображающие понятия решаемой задачи. Перегрузка стандартных операций языка Си++ позволяет сделать операции над объектами новых классов удобными и общепонятными. Но возникают два вопроса. Можно ли вводить собственные обозна- чения для операций, не совпадающие со стандартными операциями языка Си++? И все ли операции языка Си++ могут быть перегруже- ны? К сожалению (или как констатация факта), вводить операции с совершенно новыми обозначениями язык Си++ не позволяет. Ответ на второй вопрос также отрицателен - существует несколько опера- ций, не допускающих перегрузки. Вот их список: прямой выбор компонента структурированного объекта; . * обращение к компоненту через указатель на него; ?: условная операция; :: операция указания области видимости; sizeof операция вычисления размера в байтах; # препроцессорная операция; ## препроцессорная операция. Рассмотрим еще несколько важных особенностей механизма пере грузки (расширения действия) стандартных операций языка Си++. При расширении действия (при перегрузке) стандартных операций нельзя и нет возможности изменять их приоритеты (иначе компилятор окончательно запутается).
Глава 9. Класс как абстрактный тип 329 Нельзя изменить для перегруженных операций синтаксис выраже- ний, т.е. невозможно ввести унарную операцию = или бинарную опе- рацию ++. . Нельзя вводить новые лексические обозначения операций, даже f формируя их из допустимых символов. Например, возведение в степень | ** из языка Фортран нельзя ввести в языке Си++. * Любая бинарная операция @ определяется для объектов некоторого класса двумя существенно разными способами: либо как компонентная I функция с одним параметром, либо как глобальная (возможно, h . дружественная) функция с двумя параметрами. В первом случае х @ у I означает вызов х.operator @ (у) , во втором случае х @ у означает J вызов operator @(х,у). ( В соответствии с семантикой бинарных операций * [ ]', операции-функции с названиями operator =, operator [] , operator -> не могут быть глобальными функциями, а должны быть нестатиче- скими компонентными функциями. "Это гарантирует, что первыми операндами будут lvalue” [2]. Любая унарная операция *$' определяется для объектов некоторого класса также двумя способами: либо как компонентная функция без параметров, либо как глобальная (возможно, дружественная) функция J с одним параметром. Для префиксной операции ’$' выражение $z означает вызов компонентной функции z. operator $ () или вызов глобальной функции operator $(z) . Для постфиксной операции выражение z$ означает либо вызов компонентной функции z. opera tor $() или вызов глобальной функции | operator $ (z) . Синтаксис языка Си++ определяет некоторые встроенные операции над стандартными типами как комбинации других встроенных операций над теми же операндами. Например, для переменной long m = 0; выражение ++т означает m += 1, что в свою очередь означает выполнение выражения m = m + 1. Такие автоматические замены выражений не реализуются и не справедливы для перегруженных операций. Например, в общем случае определение operator *s () нельзя вывести из определений operator *= () и operator ~ . Нельзя изменить смысл выражения, если в него не входит объект класса, введенного пользователем. "В частности, нельзя определить операцию-функцию, действующую только на указатели" [2]. Невоз- можно для операнда m типа int изменить смысл выражения 2 + m < т.п.
330 Язык Си++ "Операция-функция, первым параметром которой предполагается основной (стандартный) тип, не может быть компонентной функци- ей". Для объяснения этого ограничения предположим, что аа - объект некоторого класса и для него расширено действие операции ' + •. При разборе выражения аа + 2 компилятором выполняется вы- зов операции-функции аа. operator +(2) ИЛИ operator +(аа,2). При разборе 2 + аа допустим вызов operator + (2, аа), но оши- бочен 2.operator +(аа). Таким образом, расширение действия опе- рации + на выражение стандартныи_тип + обчект_класса допусти- мо только с помощью глобальных операций-функций. При расширении действия операций приходится предусматривать всевозможные сочетания типов операндов. Например, определяя опе- рацию сложения ' + 1 для комплексных чисел, приходится учитывать сложение комплексного числа с вещественным и вещественного с комплексным, комплексного с целым и целого с комплексным и т.д. Если учесть, что вещественные числа представлены несколькими ти- пами (float, double, long double) и целые числа имеют разные типы (int, long, unsigned, char), то оказывается необходимым ввести большое ко- личество операций-функций. К счастью, при вызове операций- функций действуют все соглашения о преобразованиях стандартных типов параметров, и нет необходимости учитывать сочетания всех типов. В ряде случаев для бинарной операции достаточно определить только три варианта: • стандартный_тип, класс • класс, стамдартныи_тип • класс, класс. Например, для рассмотренного класса complex можно ввести как дружественные такие операции-функции: complex operator + (complex х, complex у) { return(complex(х.real + у.real, x.imag + у.imag)); ) complex operator + (double x, complex y) ( return(complex (x + y.real, y.imag)); ) complex operator + (complex x, double y) ( return(complex(x.real + y, x.imag)); } После этого станут допустимыми выражения в следующих опера- торах: complex СС(1.0,2.0); complex ЕЕ; ЕЕ = 4.0 + СС; ЕЕ = ЕЕ + 2.0;
Глава 9. Класс как абстрактный тип 331 ЕЕ = СС + ЕЕ; ЕЕ = СС + 20; //По умолчанию приведение int к double СС = ЕЕ + ’е’; //По уиолчанию приведение char к double Вместо использования нескольких (в нашем примере вместо трех) очень схожих операций-функций можно задачу преобразования стан- дартного типа в объект класса поручить конструктору. Для этого требуется только одно - необходим конструктор, формирующий объект класса по значению стандартного типа. Например, добавление в класс complex такого конструктора complex (double х) { real = х; imag =0.0; } позволяет удалить все дополнительные операции-функции, оставив только одну с прототипом: friend complex operator + (complex, complex); В этом случае целый операнд выражения 6 + ЕЕ автоматически преобразуется к типу double, а затем конструктор формирует комплексное число с нулевой мнимой частью. Далее выполняется операция-функция operator + (complex (double G) , ЕЕ); Вместо включения в класс дополнительного конструктора с одним аргументом можно в заголовке единственного конструктора ввести умалчиваемое значение второго параметра: complex (double re, double im = 0,0), (real re; imag = im; } Теперь каждое выражение с операцией ' +', в которое входит, кроме объекта класса complex, операнд одного из стандартных типов, будет обрабатываться совершенно верно operator + (complex (double (6), double(0)), ЕЕ). Однако такое умалчивание является частным решением и не для всех классов пригодно. Можно было бы в качестве умалчиваемого значения мнимой части взять и число, отличное от нуля, но поведение объектов класса complex при сложении с данными стандартных типов оказалось бы при этом довольно загадочным. Например, введя конструктор с прототипом complex (double re, double im = 10.0); при выполнении
332 Язык Си++ complex LL = complex (1.0,2.0) ; LL » LL + 4 + 5; получим ll = (10. 0, 22.0), так как два неявных обращения к кон- структору в выражении ll + 4 + 5 приводят К двум imag « 10. В отличие от всех других унарных операций операции ++ и — имеют, кроме префиксной формы еще и постфиксную. Это привело к особенностям при их перегрузке. В начальных версиях языка Си++ при перегрузках операций ++ и — не делалось различия между пост- фиксной и префиксной формами. Например, в следующей программе действие операции ++ распространено иа объекты класса pair с по- мощью дружественной операции-функции с одним параметром: friend pairs operator ++(pair S) ; Операция — перегружена с помощью компонентной операции- функции класса pair, не имеющей параметров: pair& pair :: operator — (); В компиляторе ТС++ реализованы первые варианты языка Си++, и поэтому он не различает постфиксного и префиксного применений операций ++, —. Текст программы: //Р9-13.СРР - перегрузка унарных операции ++, — #include <iostream.h> // Класс "пара чисел": class pair ( int N; // Целое число double х; // Вещественное число // Дружественная функция: friend pairs operator ++(pair S) ; public: pair (int n, double хп) // Конструктор { N » n; x = xn; } void display () { cout « "ХпКоординаты: N e " « N « "\tx » " « x; } pairs operator —(} // Компонентная функция { N -« 1; x -« 1.0; return *this; } }; pairs operator ++ (pairs P) // Дружественная функция { P.N +» 1; P.x +« 1.0; return P; }
Глава 9. Класс как абстрактный тип 333 void main () { pair Z(10,20.0); Z.display() ; ++Z; Z.display() ; —Z; Z.display() ; Z++; Z.display () ; Z—; Z.display () ; Результат выполнения программы на ТС++: Координаты: N = 10 х = 20 Координаты: N = 11 х = 21 Координаты: N = 10 х = 20 Координаты: N=llx=21 Координаты: N = 10 х = 20 Как наглядно демонстрируют результаты, компилятор ТС++ не учитывает префиксность и постфиксность перегруженных операций ++ и —. В современной версии языка Си++ принято соглашение, что перегрузка префиксных операций ++ и — ничем не отличается от перегрузки других унарных операций, т.е. глобальные и, возможно, дружественные функции operator ++() и operator —() с одним параметром некоторого класса определяют префиксные операции ++ и —. Компонентные операции-функции без параметров определяют те же префиксные операции. При расширении действия постфиксных операций ++ и -- операции-функции должны иметь еще один дополнительный параметр типа int. Если для перегрузки используется компонентная операция-функция, то она должна иметь один параметр типа int. Если операция-функция определена как глобальная (некомпонентная), то ее первый параметр должен иметь тип класса, а второй - тип int. Когда в программе используется соответствующее постфиксное выражение, то операция-функция вызывается с нулевым целым параметром. В следующей программе иллюстрируются возможности применения разных операций-функций для постфиксной и префиксной операций ++ и —:
334 Язык Си++ //Р9-14.СРР - необычная перегрузка унарных операций ++, — #include <iostream.h> // Класс "пара чисел": class pair { int N; // Целое число double х; // Вещественное число // Дружественная функция для префиксной операции: friend pairs operator ++(pairs); // Дружественная функция для постфиксной операции: friend pairs operator ++(pairs,int); public: pair (int n, double xn) // Конструктор { N » n; x “ xn; } void display () { cout « "\пКоординаты: N e " « N « " x e " « x; } // Компонентная функция (префиксная —): pairs operator —() ( N /« 10; x /» 10; return *this; ) // Компонентная функция (постфиксная —) : pairs operator —(int k) { N /= 2; x /« 2.0; return *this; ) im- pairs operator ++(pairs P) // Префиксная операция ++ { P.N *« 10; P.x *- 10; return P; } // Постфиксная операция ++: pairs operator ++ (pairs P,int k) ( P.N « P.N * 2 + k; P.x = P.x * 2 + k; return P; } void main() { pair Z(10,20.0); Z. display () ; ++Z; Z .display () ; —Z; Z.display () ; Z++; Z. display () ;
Глава 9. Класс как абстрактный тип 335 2—; Z.display() ; Результаты выполнения программы: Координаты: N = 10 х = 20 Координаты: N = 100 х * 200 Координаты: N ® 10 х « 20 Координаты: №20 х = 40 Координаты: №10 х = 20 Для демонстрации полной независимости смысла перегруженной операции от ее традиционного (стандартного) значения в операциях- функциях для префиксных операций ++ соответствуют увеличению в 10 раз, а — уменьшению в 10 раз. Для постфиксных операций ++ определили как увеличение в 2 раза, а — как уменьшение в 2 раза. Попытки использовать в постфиксных операциях-функциях значение дополнительного параметра int к подтверждает его равенство 0.
Глава 10. НАСЛЕДОВАНИЕ И ДРУГИЕ ВОЗМОЖНОСТИ КЛАССОВ 10.1. Наследование классов Начиная рассматривать вопросы наследования, нужно отметить, что обоснованно введенный в программу объект призван моделиро- вать свойства и поведение некоторого фрагмента решаемой задачи, связывая в единое целое данные и методы, относящиеся к этому фраг- менту. В терминах объектно-ориентированной методологии объекты взаимодействуют между собой и с другими частями программы с по- мощью сообщений. В каждом сообщении объекту передается некото- рая информация. В ответ на сообщение объект выполняет некоторое действие, предусмотренное набором компонентных функций того класса, которому он принадлежит. Таким действием может быть из- менение внутреннего состояния (изменение данных) объекта либо пе- редача сообщения другому объекту. Каждый объект является конкретным представителем класса. Объ- екты одного класса имеют разные имена, но одинаковые по типам и внутренним именам данные. Объектам одного класса для обработки своих данных доступны одинаковые компонентные функции класса и одинаковые операции, настроенные на работу с объектами класса. Таким образом, класс выступает в роли типа, позволяющего вводить нужное количество объектов, имена (названия) которых программист выбирает по своему усмотрению. Объекты разных классов и сами классы могут находиться в отно- шении наследования, при котором формируется иерархия объектов, соответствующая заранее предусмотренной иерархии классов. Иерархия классов позволяет определять новые классы на основе уже имеющихся. Имеющиеся классы обычно называют базовыми (иногда порождающими), а новые классы, формируемые на основе базовых, - производными (порожденными), иногда классами-потомка- ми или наследницами. Производные классы "получают наследство" - данные и методы своих базовых классов - и, кроме того, могут по- полняться собственными компонентами (данными и собственными
Гпава 10. Наследование и другие возможности классов 337 методами). Наследуемые компоненты не перемещаются в производ- ный класс, а остаются в базовых классах. Сообщение, обработку ко- торого не могут выполнить методы производного класса, автоматически передается в базовый класс. Если для обработки сооб- щения нужны данные, отсутствующие в производном классе, то их пытаются отыскать автоматически и незаметно для программиста в базовом классе (рис. 10.1). Рис. 10.1. Схема обработки сообщений в иерархии объектов: 1 - обработка сообщения методами производного класса; 2 - обработка сообщения методами базового класса. Если класс "точка (позиция) на экране" считать базовым классом, то на его основе можно построить класс "окно на экране". Данными этого класса будут две точки: • точка, определяющая левый верхний угол; • точка, определяющая размеры окна, т.е. смещения вдоль коор- динатных осей относительно левого верхнего угла. Методы класса "окно на экране": • сместить окно вдоль оси х на dx; • сместить окно вдоль оси Y на dy; • сообщить значение координаты X левого верхнего угла; • сообщить значение координаты Y левого верхнего угла; • сообщить размер окна вдоль оси X; • сообщить размер окна вдоль оси Y. Конструктор окна на экране: 22-3821
338 Язык Си++ • создать окно иа экране с заданным именем по двум точкам, определяющим левый верхний угол окна и его размеры. Деструктор окна на экране: • уничтожить окно с заданным именем. Обратите внимание, что две точки по-разному используются в классе "окно на экране". Первая из них - это абсолютные координаты точки на экране, вторая - интерпретируется просто как пара чисел, определяющая размеры окна. Таким образом, если первая точка имеет координаты (4,3), а вторая (0,0), то это соответствует пустому окну (окну с нулевыми размерами). Наименьшее окно, в которое можно вывести один символ (или один пиксель в графическом режиме), должно иметь размеры (1,1) независимо от положения левого верхнего угла. При наследовании некоторые имена методов (компонентных функций) и (или) компонентных данных базового класса могут быть по-новому определены в производном классе. В этом случае соответ- ствующие компоненты базового класса становятся недоступными из производного класса. Для доступа из производного класса к компо- нентам базового класса, имена которых повторно определены в про- изводном, используется операция •:: * указания (уточнения) области видимости. Любой производный класс может, в свою очередь, становиться ба- зовым для других классов, и таким образом формируется направлен- ный граф иерархии классов и объектов. В иерархии производный объект наследует разрешенные для наследования компоненты всех базовых объектов. Другими словами, у объекта имеется возможность доступа к данным и методам всех своих базовых классов. Наследование в иерархии классов может отображаться и в виде дерева, и в виде более общего направленного ациклического графа. До- пускается множественное наследование - возможность для некоторого класса наследовать компоненты нескольких никак не связанных меж- ду собой базовых классов. Например, класс "окно на экране" и класс "сообщение" совместно могут формировать новый класс объектов "сообщение в окне". При наследовании классов важную роль играет статус доступа (статус внешней видимости) компонентов. Для любого класса все его компоненты лежат в области его действия. Тем самым любая принад- лежащая классу функция может использовать любые компонентные данные и вызывать любые принадлежащие классу функции. Вне клас-
Глава 10. Наследование и другие возможности классов 339 са в общем случае доступны только те его компоненты, которые имеют статус public. В иерархии классов соглашение относительно доступности ком- понентов класса следующее. Собственные (private) методы и данные доступны только внутри того класса, где они определены. Защищенные (protected) компоненты доступны внутри класса, в котором онц определены, и дополнительно доступны во всех произ- водных классах. Общедоступные (public) компоненты класса видимы из любой точки программы, т.е. являются глобальными. Если считать, что объекты, т.е. конкретные представители клас- сов, обмениваются сообщениями и обрабатывают их, используя ме- тоды и данные классов, то при обработке сообщения используются, i во-первых, общедоступные члены всех классов программы; во- j вторых, защищенные компоненты базовых и рассматриваемого клас- | сов и, наконец, собственные компоненты рассматриваемого класса. (Собственные компоненты базовых и производных классов, а также защищенные компоненты производных классов недоступны для со- общения и не могут участвовать в его обработке. Еще раз отметим, что на доступность компонентов класса влияет не только явное использование спецификаторов доступа (служебных слов) - private (собственный), protected (защищенный), public (общедо- ступный), но и выбор ключевого слова class, struct, union, с помощью которого объявлен класс. Определение производного класса. В определении и описании про- изводного класса приводится список базовых классов, из которых он if непосредственно наследует данные и методы. Между именем вводи- | мого (нового) класса и списком базовых классов помещается двоето- | чие. Например, при таком определении j class S: X, Y, Z { ... }; | класс s порожден классами x,y,z, откуда ои наследует компоненты. - Наследование компонента не выполняется, если его имя будет исполь- зовано в качестве имени компонента в определении производного класса s. Как уже говорилось, по умолчанию из базовых классов на- следуются методы и данные со спецификаторами доступа - public (общедоступные) и protected (защищенные). В порожденном классе зти унаследованные компоненты получают статус доступа private, если новый класс определен с помощью ключе- вого слова class, и статус доступа public, если новый класс определен 22*
340 Язык Си++ как структура, т.е. с помощью ключевого слова struct. Таким образом, при определении класса struct J: х, z { ... }; любые наследуе- мые компоненты классов X, z будут иметь в классе J статус общедо- ступных (public). Пример: class В { protected: int t; public: char u; }; class E: В { ... ); // t, u наследуются как private struct S: В { ... }; // t, u наследуются как public Явно изменить умалчиваемый статус доступа при наследовании можно с помощью спецификаторов доступа - private, protected и public. Эти спецификаторы доступа указываются в описании производного класса непосредственно перед нужными именами базовых классов. Если класс в определен так, как показано выше, то можно ввести сле- дующие производные классы: class М: protected В { ... }; class Р: public В ( ... }; class D: private В { ... }; struct F: private В (...}; struct G: public В ( ... }; // t, u наследуются как // protected // t - protected, u - public // t, u наследуются как // private // t, u наследуются как // private // t - protected, u - public Соглашения о статусах доступа при разных сочетаниях базового и производного классов иллюстрирует табл. 10.1. Обратите внимание на тот факт, что ни базовый класс, ни произ- водный не могут быть объявлены с помощью ключевого слова union. Таким образом, объединения не могут использоваться при построе- нии иерархии классов. Чтобы проиллюстрировать некоторые особенности механизма на- следования, построим на основе класса point (см. п. 9.4) производный класс spot (пятно). Наследуемые компоненты класса point: • int х, у - координаты точки на экране; • point() - конструктор; • givex (), givey () - доступ к координатам точки; • show () - изобразить точку; • move {) - переместить точку. Дополнительно к наследуемым компонентам в класс spot введем: радиус пятна (rad); его видимость на экране (vis ==» 0, когда изоб-
Глава 10. Наследование и другие возможности классов 341 Таблица 10.1 Статусы доступа при наследовании Доступ в базовом классе Спецификатор доступа перед базовым классом Доступ в производном классе struct class public отсутствует public private protected отсутствует public private private отсутствует недоступны недоступны pnblic public public public protected public protected protected private public недоступны недоступны public protected protected protected protected protected protected protected private protected недоступны недоступны private public недоступны недоступны public private private private protected private private private private private недоступны недоступны раження нет на экране, vis 1 - изображение есть на экране); приз- нак сохранения образа в оперативной памяти (tag 0 - битовый образ не хранится, tag 1 - битовый образ хранится в памяти); указатель pspot на область памяти, выделенную для хранения бито- вого образа изображения. // SPOT.СРР “ классt наследующий данные и метода // класса POINT «ifndef SPOT «define SPOT 1 «include "point.срр" // Определение класса point class spot: // 'public' позволит сохранить статусы доступа для // наследуемых компонентов класса POINT: public point ( // Статус доступности данных в производных классах: protected: int rad; // Радиус пятна (изображения) int vis; // Видимость пятна на экране int tag; // Признак сохранения образа в памяти void *pspot; II Указатель на область памяти для и изображения (для битового образа)
342 Язык Си++ public: // Конструктор класса SPOT: spot(int xi, int yi, int ri): // Вызов конструктора базового класса: point(xi,yi) ( int size; vis = 0; tag » 0; rad = ri; // Определить размеры битового образа: size = imagesize(xi-ri,yi-ri,xi+ri,yi+ri); // Выделить память для битового образа: pspot ” new char[size]; } *spot() // Деструктор класса SPOT ( hide (); // Убрать с экрана изображение пятна tag « 0; // Сбросить признак сохранения в памяти delete pspot; // Освободить память, где находился // битовый образ } void show() // Изобразить пятно на экране дисплея ( // Если битового образа нет в памяти: if (tag « 0) ( // Нарисовать окружность на экране: circle(х,у,rad); // Закрасить пятно floodfill(х,у,getcolor()); // Запомнить битовый образ в памяти: getimage (x-rad,y-rad,x+rad,y+rad,pspot) ; tag = 1; I else // Перенести изображение из памяти на экран: putimage(x-rad,y-rad,pspot,XOR_PUT) ; vis I i » 1; void hide() // Убрать с экрана изображение пятна { if (vis ж» 0) // Нечего убирать return; и Стереть изображение с экрана: putimage (x-rad,y-rad,pspot,XOR_PUT) ; vis = 0; I // Переместить изображение: void move(int xn, int yn) ( hide(); // Убрать старое изображение с экрана // Изменить координаты центра пятна:
Глава 10. Наследование и другие возможности классов 343 х = хп; у » ул; show () ; // Вывести изображение в новом месте 1 // Изменить размер изображения пятна: void vary(float dr) ( float a; int size; hide(); // Убрать старое изображение с экрана tag “ 0; // Освободить память битового образа: delete pspot; // Вычислить новый радиус: а = dr * rad; if (а <® 0) rad e 0; else rad ж (int)а; // Определить размеры битового образа: size = imagesize(x-rad,y-rad,x+rad,y+rad); // Выделить память для нового образа: new oharfsize]; show () ; / / Изобразить пятно на экране I int& giver(void) // Доступ к радиусу пятна ( return rad; } ); ttendif В классе spot явно определены конструктор, деструктор *spot() и пять методов: show () вывести на экран изображение пятна, затем перенести его битовый образ в память; hide () убрать с экрана изображение пятна; move () переместить изображение в другое место на экране; vary () изменить (уменьшить или увеличить) изображение на экра- не; giver () обеспечить доступ к радиусу пятна. Из класса point класс spot наследует координаты (х, у) точки (центра пятна) и методы givex (), givey О. Методы point: :show(), point: :move() заменены в классе spot новыми функциями с такими же именами, а функция point: :hide () не наследуется, так как в клас- се point она имеет статут собственного компонента (private). Конструктор spot () имеет три параметра - координаты центра (xi, yi) и радиус пятна на экране (ri). При создании объекта класса
344 Язык Си++ spot вначале вызывается конструктор класса point, который по зна- чениям фактических параметров, соответствующих xi, yi, определяет точку - центр пятна. Эта точка создается как безымянный объект класса point. (Конструктор базового класса всегда вызывается и вы- полняется до конструктора производного класса.) Затем выполняются операторы конструктора spot(). Здесь устанавливаются начальные значения признаков vis, tag, и по значению фактического параметра, соответствующего формальному параметру ri, определяется радиус пятна rad. С помощью стандартной функции imagesize () из графи- ческой библиотеки graphics, ыв вычисляется объем памяти (вспомогательная переменная size), требуемый для сохранения пря- моугольного (квадратного) участка экрана, на котором предполагает- ся изобразить пятно. Выделение участка основной памяти нужного объема выполняет стандартная операция new, операнд которой - это массив типа char из size элементов. Выделенная память связывается с указателем pspot, имеющим в классе spot статус protected. На этом работа конструктора заканчивается. В функциях show () - изобразить пятно на экране, vary () - изме- нить размер изображения и hide О - убрать изображение пятна с эк- рана используются возможности графических функций: circle(х,у,rad) нарисовать окружность с центром в точке с координатами (х, у) и радиусом rad; floodfill(х,у,с) закрасить ограниченную область, которой принадлежит точ- ка с координатами (х, у), цветом, определенным параметром с; getcolor() определить текущий цвет изображений; getimage (xl, yl, х2, у2, pnt) поместить в заранее выделенный участок основной памяти, связанный с указателем pnt, битовый образ прямоугольного участка экрана, выделенного координатами левого верхнего (xi, yi) и правого нижнего (х2, у2) углов; putimage (xl, yl , pnt, op) изобразить на экране битовый образ, ранее сохраненный в памяти с помощью функции getimage (); xl, yl - координаты размещения на экране левого верхнего угла, pnt - указатель на область памяти, где хранится нужное изображение; ор - параметр, определяющий правила выбора цвета для каждого изображаемого пикселя. Выбор цвета осуществляется с учетом
Глава 10. Наследование и другие возможности классов 345 имеющегося на экране пикселя и сохраненного в памяти. Па- раметр ор определяет правило сочетания этих цветов в соот- ветствии с табл. 10.2. Необходимо обратить внимание на одну особенность режима XOR-PUT. Если в этом режиме изображе- ние вывести на экран в то же место, где уже было то же самое изображение, то изображение исчезнет с экрана. Именно так убирает с экрана пятно функция hide О. Флажок видимости пятна на экране vis необходим для распознавания необходи- мости повторного применения функции putimage (). Функция show О выполняется в разных режимах в зависимости от значения признака tag записи изображения в память. Если значение tag равно 0, то рисуется и закрашивается окружность, затем ее образ переписывается в память функцией getimageO и устанавливается в 1 значение tag. Если значение tag равно 1, то образ переносится на эк- ран из той области основной памяти, где он сохранялся,- при помощи функции putimage (). Действия функции move О понятны из комментариев в тексте про- граммы. Таблица 10.2 Правила выбора цвета при размещении на экране битового образа с помощью функции putimage () Значение параметра ОР Условное обозначение в graphics,h Смысл преобразования 0 COPY_PUT Копия без всяких условий 1 XOR_PUT Исключающее ИЛИ 2 OR—PUT Включающее ИЛИ (дизъюнкция) 3 AND—PUT Логическое И (конъюнкция) 4 NOT_PUT Копия с инверсией изображения Особенностью функции vary () является необходимость не только изменить размеры изображения, но и заново сохранить его битовый образ в основной памяти. При изменении размеров изображения нуж- но изменять и размеры памяти доя его образа. Прежде чем привести пример программы с классом spot, необхо- димо ввести дополнительные сведения о деструкторах. Особенности деструкторов. Итак, конструктор вызывается при создании каждого объекта класса и выполняет все необходимые опе- рации как доя выделения памяти доя данных объекта, так и доя ее инициализации. Когда объект уничтожается при завершении про-
346 Язык Си++ граммы или при выходе из области действия определения соответ- ствующего класса, необходимы противоположные операции, самая важная из которых - освобождение памяти. Эти операции могут и должны выполняться по-разному в зависимости от особенностей кон- кретного класса. Поэтому в определение класса явно или по умолча- нию включают специальную принадлежащую классу функцию - деструктор. Деструктор имеет строго фиксированное имя вида: -имя_класса У деструктора не может быть параметров (даже типа void), и де- структор не имеет возможности возвращать какой-либо результат, даже типа void. Статус доступа деструктора по умолчанию public (т.е. деструктор доступен во всей области действия определения класса). В несложных классах деструктор обычно определяется по умолча- нию. Например, в классе point деструктор явно не определен, и ком- пилятор предполагает, что он имеет вид -point() { }; В классе spot деструктор явно определен: -spot () { hide (); tag « 0; delete [] pspot; } Его действия: убрать с экрана изображение пятна, обратившись к функции spot::hide() ; установить в нуль признак tag наличия в памяти битового образа пятна; освободить память, выделенную при создании объекта для битового образа пятна и связанную с конкрет- ным экземпляром указателя pspot. Деструкторы не наследуются, поэтому даже при отсутствии в про- изводном классе (например, в классе spot) деструктора он не пере- дается из базового (например, из point), а формируется компилято- ром как умалчиваемый со статусом доступа public. Этот деструктор вызывает деструкторы базовых классов. В рассматриваемом примере это будет выглядеть примерно так: public: -spot() ( -point(); ) В любом классе могут быть в качестве компонентов определены другие классы. В этих классах будут свои деструкторы, которые при уничтожении объекта охватывающего (внешнего) класса выполняют- ся после деструктора охватывающего класса. Деструкторы базовых классов выполняются в порядке, обратном перечислению классов в определении производного класса. Таким
Глава 10. Наследование и другие возможности классов 347 образом порядок уничтожения объекта противоположен по отноше- нию к порядку его конструирования. Вызовы деструкторов для объектов класса и для базовых классов выполняются неявно и не требуют никаких действий программиста. Однако вызов деструктора того класса, объект которого уничтожает- ся в соответствии с логикой выполнения программы, может быть яв- ным. Это может быть, например, случай, когда при создании объекта для него явно выделялась память. Примером целесообразности явного вызова деструктора может служить класс spot. Объяснив основные принципы работы деструкторов, приведем программу для работы с объектами класса spot: //РЮ-01 .СВР - наследование классов и их деструкторы linclude <graphics.h> // Связь с графической библиотекой linclude <conio.h> // Прототип функции getch() linclude “spot.cpp" // Определение класса spot void main() { // Переменные для инициализации графики: int dr “ DETECT, mod; // Инициализация графической системы: initgraph(&dr,4mod,"с:\\borlandc\\bgi"); ( //В этом блоке создаются и используются объекты // класса spot spot А(200,50,20); // Создается невидимое пятно А spot D (500,200,30); // Создается невидимое пятно D А. show () ; / / Изобразить пятно А на экране getch(); // Ждать нажатия клавиши D.show(); // Изобразить пятно D на экране getch(); A.move(50,60); // Переместить пятно А gatch(); D.vary(3); // Изменить размеры пятна D getch(); / / Ждать нажатия клавиши I // При выходе из блока для каждого объекта автоматически // вызывается деструктор, освобождающий выделенную // память closegraph(); // Закрыть графический режим } // Конец программы Изменение состояний экрана при выполнении программы иллю- стрирует рис. 10.2. Принципиальным отличием этой программы от приведенных вы- ше программ Р9-04.СРР, рэ-и.срр для работы с объектами класса point является наличие внутреннего блока, что связано с наличием в классе spot деструктора, при выполнении которого вызывается ком-
348 Язык Си++ понентная функция hide О, использующая функции графики. Эти функции могут выполняться только в графическом режиме, т.е. до выполнения функции closegraph(). Если построить программу без внутреннего блока так же, как упомянутые программы с классом point, то деструктор по умолчанию будет вызываться только при окончании программы, когда графический режим уже закрыт и вы- полнение любых графических функций невозможно. Указанной оши- бочной ситуации можно избежать двумя путями: либо вызывать деструктор явно для уничтожения объектов А и D, а потом закрывать графический режим, либо после инициализации графики ввести внут- ренний блок, в котором определены объекты А и D и при выходе из которого они уничтожаются, для чего деструктор дважды вызывается автоматически. Графический режим закрывается во внешнем блоке, когда объекты a, d уже уничтожены и обращения к деструктору *spot О не нужны. В программе реализовано второе решение. • • • • • • е Рис. 10.2. Последовательность изображений на экране при выполнении программы РЮ-01. СРР В качестве несложного упражнения можно удалить скобки 1 (}', выделяющие внутренний блок, и убедиться, что при выполнении из- мененной программы будет выдаваться сообщение об ошибке в гра- фической системе: BGI Error: Graphics not initialized (use 'initgraph') Второе решение - явный вызов деструкторов без добавления вло- женного блока: getch(); D.vary(3) getch () ; A.spot: :~spot() ; getch(); D.spot::*spot() ; closegraph(); // Изменить размеры пятна D 11 Ждать нажатия клавиши // Уничтожать обпьект А // Ждать нажатия клавиши // Уничтожать об*мкт D // Закрыть графический режим // Конец программы
Глава 10. Наследование и другие возможности классов 349 В данном варианте класса spot при уничтожении объекта с по- мощью деструктора его изображение удаляется с экрана функцией hide (). 10.2. Множественное наследование и виртуальные базовые классы Класс называют непосредственным (прямым) базовым классом (прямой базой), если он входит в список базовых при определении класса. В то же время для производного класса могут существовать косвенные или непрямые предшественники, которые служат базовыми для классов, входящих в список базовых. Если некоторый класс а яв- ляется базовым для вив есть база для с, то класс в является непосред- ственным базовым классом для с, а класс а - непрямой базовый класс для с (рис. Ю.З). Обращение к компоненту ха, входящему в А и уна- следованному последовательно классами вис, можно обозначить в классе с либо как А::ха, либо как в: :ха. Обе конструкции обеспечи- вают обращение к элементу ха класса А. а (базовый класс - прямая база для в) t в (производный от а класс - прямая база для с) ? с (производный класс - с прямой базой в и косвенной а) Рис. 10.3. Прямое и косвенное наследование классов Иерархию производных классов удобно представлять с помощью направленного ациклического графа (НАГ), где стрелкой изображают отношение "производный от". Производные классы принято изобра- жать ниже базовых. Именно в таком порядке их объявления рассмат- ривает компилятор и их тексты размещаются в листинге программы. Класс может иметь несколько непосредственных базовых классов, тх может быть порожден из любого числа базовых классов, например, class XI ( ... }; class Х2 ( ... }; class ХЗ ( ... }; class Yl: public Xl, public X2, public X3 { ... };
350 ЯзыкСи++ Наличие нескольких прямых базовых классов называют множе- ственным наследованием. В качестве примера рассмотрим производ- ный класс "окружность, вписанная в квадрат". Базовыми классами будут: окружность (circ) и квадрат (square). Приведем вначале их определения: //CIRC.СРР - определение класса "окружность11 #include <graphics.h> class circ { int xc, ус, // Координаты центра rc; // Радиус окружности public: 11 Конструктор: circ(int xi, int yi, int ri) { xc = xi; yc “ yi; rc = ri; ) // Изобразить окружность на экране: void show() { circle(xc,yc,rc); } // Убрать с экрана изображение окружности: void hide() { int bk, cc; bk = getbkcolor(); // Цвет фона сс = getcolor(); // Цвет изображения setcolor(bk); // Сменить цвет рисования // Рисуем окружность цветом фона: circle(xc,ус,гс); // Восстановить цвет изображения: setcolor(сс); } }; //SQUARE.СРР - определение класса "квадрат" ftinclude <graphics.h> class square ( int xq, yq, // Координаты центра квадрата Iq; // Длина стороны квадрата // Вспомогательная функция рисования: void rissquare(void) { int d “ lq/2; line(xq-d,yq-d,xq+d,yq-d) ; line (xq-d, yq+d, xq+d, yq+d) ; line (xq-d,yq-d,xq-d,yq+d) ; line(xq+d,yq-d,xq+d,yq+d) ; } public:
Глава 10. Наследование и другие возможности классов 351 // Конструктор: square(int xi,int yi,int li) ( xq e xi; yq « yi; Iq = li; ) // Изобразить квадрат на экране: void show() { rissquare(); ) // Убрать с экрана изображение квадрата: void hide() ( int bk, cc; bk e getbkcolor() ; 11 Цвет фона cc “ getcolor(); // Цвет изображения setcolor(bk); // Сменить цвет рисования rissquareO; // Рисуем квадрат цветом фона setcolor(сс); // Восстановить цвет изображения ) ); В следующей программе на основе классов circ и square создан производный класс "окружность в квадрате" с именем circsqrt: //РЮ-02.СРР - окружность в квадрате - множественное / / насле дов анис tinclude <conio.h> // Для функции getch() tinclude "square.срр" 11 Определение класса "квадрат" tinclude "circ.срр" // Определение класса "окружность" // Производный класс - "окружность, вписанная в квадрат"; // Класс circsqrt наследует только методы обоих базовых // классов. В нем нет наследуемых данных. class circsqrt : public circ, public square { public: // Конструктор: circsqrt(int xi, int yi, int ri): circ(xi,yi,ri), // Явно вызываются конструкторы square(xi,yi,2*ri) 11 базовых классов О // Изобразить на экране окружность в квадрате: void show(void) ( circ::show(); square::show(); } // Убрать с экрана изображение: void hide() { square::hide(); circ::hide(); } ); void main() ( int dr « DETECT, mod; initgraph(&dr,&mod,"c:\\borlandc\\bgi"); circsqrt Al (100,100,60);
352 Язык Си++ circsqrt F4(400,300,50); Al.show(); getch(); F4.show(); getch(); F4.hide(); getch(); Al.hideO; getch (); closegraph(); ) Определения базовых классов должны предшествовать их исполь- зованию в качестве базовых. Поэтому тексты из файлов square. срр и circ.cpp включены в начало программы, после чего описывается класс circsqrt. В производном классе circsqrt телом конструктора служит пустой оператор. Выполнение конструктора circsqrt () сво- дится к последовательному вызову конструкторов базовых классов. При этом за счет соответствующего выбора параметров центры обеих фигур (квадрата и окружности) совпадают. Кроме того, длина сторо- ны квадрата выбирается равной удвоенному радиусу окружности (параметр 2 * ri), и тем самым окружность оказывается вписанной в квадрат. В основной программе формируются два объекта Al, F4 класса circsqrt. Они последовательно выводятся на экран дисплея (рис. 10.4) и в обратном порядке убираются с экрана. Рис. 10.4. Последовательность изображений на экране при выполнении программы РЮ-02. СРР При множественном наследовании никакой класс не может боль- ше одного раза использоваться в качестве непосредственного базово- го. Однако класс может больше одного раза быть непрямым базовым классом: class X ( ...; f(); ... }; class Y: public X ( ... }; class Z: public X { ... }; class D: public Y, public Z { ... };
Глава 10. Наследование и другие возможности классов 353 В данном примере класс х дважды опосредованно наследуется классом D. Особенно хорошо это видно в направленном ациклическом графе (НАГ): ___^х Проиллюстрированное дублирование класса соответствует вклю- чению в производный объект нескольких объектов базового класса. В нашем примере существуют два объекта класса х, и поэтому для устранения возможных неоднозначностей вне объектов класса d нуж- но обращаться к конкретному компоненту класса х, используя пол- ную квалификацию: D::Y::X::f() ИЛИ D: :Z: :Х: :f (). Внутри объекта класса D обращения упрощаются: У : : X: : f () ИЛИ Z : : X: : f (), но тоже содержат квалификацию. В качестве содержательного примера с дублированием непрямого базового класса рассмотрим программу, в которой определен класс •potelli - круглое пятно, вписанное в эллипс. Класс spotelli непо- средственно базируется на классах spot и ellips, каждый из которых базируется, в свою очередь, на классе point. Таким образом, point дважды входит в spotelli в качестве непрямого базового класса, т.е. дублируется. Класс point (точка на экране) уже рассматривался. Текст его определения находится в файле point. срр (см. п. 9.4), Опре- деление производного от класса point класса spot (круглое пятно на экране) находится в файле spot. срр (см. п. 10.1). На базе класса point можно следующим образом определить класс "эллипс": //ELLIPS.СРР - класс "эллипс" ♦ifndef ELLIPS ♦define ELLIPS 1 ♦include "point.срр" // Определение класса point class ellips : public point ( protected: int rx,ry; // Радиусы эллипса public: // Конструктор: ellips (int xc, int yc, int rx, int ry): point (xc,yc) ( this->rx “ rx; this->ry » ry; } void show() // Изобразить на экране эллипс ( ellipse(х,у,0,360,rx,ry); return; } // Убрать с экрана изображение эллипса: 23-3821
354 Язык Си++ void hide () { int ccz bk; cc » getcolor() ; bk ж getbkcolor(); setcolor(bk); ellipse(x,y,0,360,rx,ry); setcolor(cc); ) ; tendif Как уже отмечалось, определение базового класса должно пред шествовать его упоминанию в списке базовых классов. Поэтому в на чале текстов spot.срр и ellips.cpp помещена препроцессорная директива включения текста определения класса point. В классе ellips конструктор предусматривает задание четырех параметров: координаты центра (хс, ус) и радиусы вдоль осей (гх, гу). Координаты хс, ус используются в качестве параметров при вы зове конструктора базового класса point. Чтобы различить компо ненты гх, гу класса ellips и обозначенные теми же идентификатора ми формальные параметры конструктора, используется указатель this В классе ellips две общедоступные функции show о - изобразить эллипс на экране дисплея; hide() - убрать с экрана изображение эл- липса. Текст программы: //РЮ-03.СРР - круглое пятно в эллипсе - множественное // наследование с дублированием базовых // классов (дублируется класс point) tinclude "spot.срр" tinclude "ellips.cpp" // Производный класс - дважды косвенно наследуюции // класс point: class spotelli: public spot, public ellips ( // Вспомогательная функция: int min(int valuel, int value2) ( return ( (valuel < value2) ? valuel : value2); } public: 11 Конструктор: spotelli(int xi,int yi,int rx,int ry) ellips(xi,yi,rx,ry), spot(xi,yi,min(rx,ry)) {} // Вывести изображение на экран дисплея: void show() ( spot::show();
Глава 10. Наследование и другие возможности классов 355 ellips::show(); } void hide О // Убрать изображение с экрана дисплея { spot::hide(); ellips::hide(); } ); linclude <conio.h> // Для функции getch () void main() ( int dr « DETECT, mod; initgraph(£dr,&mod,"c:\\borlandc\\bgi"); { spotelli Al (100,100,20,80); spotelli F4 (400,300,230,100); Al.show(); getch(); F4.show(); getch(); F4.hide(); getch() ; Al.hide() ; ) closegraph(); ) В классе ellips, в классе spot н в классе spotelli наследуются данные х, у класса point - координаты точки на экране. В классе point они определены как защищенные (protected) и сохраняют тот же статус доступа в производных классах, где определяют координаты центров: пятна (класс spot), эллипса (класс ellips) и эллипса с пят- ном (класс spotelli). Класс spot мы уже разбирали. Рис, 10.5. Последовательность изображений на экране при выполнении программы РЮ-03 .СРР Конструктор класса spotelli не выполняет никаких дополни- тельных действий - последовательно вызываются конструкторы клас- са ellips и класса spot, причем центры создаваемых фигур совпадают, а в качестве радиуса пятна выбирается меньший из радиу- сов эллипса. Используемая в этом случае функция min() определена 23:
356 Язык Си++ по умолчанию как встроенная (inline) собственная (private) функция класса spotelli. Чтобы отличать одинаково обозначенные функции, унаследован- ные классом spotelli из классов spot и ellips, при вызове show() и hide () используются полные квалифицированные имена, в которых применена операция 1::1. Функция main () не содержит ничего нового. Описаны два объекта Al, F4 класса spotelli, которые последовательно изображаются на экране и "стираются" с него. Чтобы устранить дублирование объектов непрямого базового класса при множественном наследовании, этот базовый класс объяв- ляют виртуальным. Для этого в списке базовых классов перед именем класса необходимо поместить ключевое слово virtual. Например, класс х будет виртуальным базовым классом при таком описании: class X ( ... f() ; ... ); class Y: virtual public X ( ... ); class Z: virtual public X ( ... ); class D: public Y, public Z ( ... ); Теперь класс d будет включать только одни экземпляр х, доступ к которому равноправно имеют классы Y и z. Графически это очень на- глядно: Y^^^ __*Z Иллюстрацией сказанного может служить иерархия классов в сле- дующей программе: //РЮ-04.СРР - множественное наследование с виртуальным // базовым классом ♦include <iostream.h> class base // Первичный (основной) базовый класс ( int jj; char сс; char w[10J; public: base(int j ж 0, char c = •*’) { jj “ j; cc » c; ) ); class dbase: public virtual base ( double dd; public: dbase(double d я 0.0) : base()
Глава 10. Наследование и другие возможности классов 357 { dd « d; } }; class fbase: public virtual base { float ff; public: fbase(float f » 0.0): base() ( ff « f; ) ); class top: public dbase, public fbase ( long tt; public: top(long t « 0): dbase(), fbase() ( tt - t; } ); void main() ( cout « "\пОснояной базовый класс: sizeof(base) = ” « sizeof(base); cout « "\пНепосредствекная база: sizeof(dbase) = ” « sizeof(dbase); cout « " ^Непосредственная база: sizeof (fbase) = 11 « sizeof(fbase); cout « "\пПроиЗводный класс: sizeof(top) » " « sizeof(top); ) Результаты выполнения программы: Основной базовым класс: sizeof(base) = 13 Непосредственная база: sizeof(dbase) e 23 Непосредственная база: sizeof(fbase) » 19 Производный класс: sizeof(top) » 33 Основной базовый класс base в соответствии с размерами своих компонентов стандартных типов int и char [11] имеет размер 13 байт. Создаваемые на его основе классы dbase и fbase занимают соответ- ственно 23 и 19 байт. (В dbase входит переменная типа double, зани- мающая 8 байт, наследуется базовый класс base, для которого требуется 13 байт, и 2 байта нужны для связи в иерархии виртуальных классов.) Производный класс top включает: один экземпляр базового класса base (13 байт); данные и связи класса dbase (10 байт); данные и связи класса fbase (6 байт); компонент long tt (4 байта). Если в той же программе убрать требование виртуальности (атрибут virtual) при наследовании base в классах dbase и fbase, то результаты будут такими:
358 Язык Си++ Основной базовый класс: sizeof(base) e 13 Непосредственная база: sizeof(dbase) « 21 Непосредственная база: sizeof(fbase) “ 17 Производный класс: sizeof(top) e 42 Обратите внимание, что размеры производных классов при от- сутствии виртуальных базовых равны сумме длин их компонентов и длин унаследованных базовых классов. "Накладные расходы" памяти здесь отсутствуют. При множественном наследовании один и тот же базовый класс может быть включен в производный класс одновременно несколько раз, причем и как виртуальный, н как невиртуальный. Для иллюстра- ции этого положения изобразим направленный граф, а затем приве- дем структуру соответствующей ему иерархии классов: class X { ... ); class Y: virtual public X { ... }; class Z: virtual public X ( ... }; class B: virtual public X { ... }; class C: virtual public X { ... }; class E: public X ( ... ); class D: public X ( ... }; class A: public D, public B, public Y, public Z, public C, public E ( ... }; В данном примере объект класса а включает три экземпляра объ- ектов класса х: один виртуальный, совместно используемый классами в, Y, с, z, и два невиртуальных относящихся соответственно к классам D и е. Таким образом, можно констатировать, что виртуальность класса в иерархии производных классов является не свойством класса как такового, а результатом особенностей процедуры наследования. Возможны и другие комбинации виртуальных и невиртуальных базовых классов. Например: class ВВ { ... } ; class AA: virtual public ВВ class СС: virtual public ВВ class DD: public AA, public { ... ); ( ... ) ; CC, public virtual BB ( ... } ;
Глава 10. Наследование и другие возможности классов 359 Соответствующий НАГ имеет вид: При использовании наследования и множественного наследования могут возникать неоднозначности при доступе к одноименным ком- понентам разных базовых классов. Простейший и самый надежный способ устранения неоднозначностей - использование квалифициро- ванных имен компонентов. Как обычно, для квалификации имени компонента используется имя класса. Следующий пример иллюстри- рует упомянутую неоднозначность н ее разрешение с помощью ква- лифицированных имен компонентов: class X ( public: int d; ... ); class Y ( public: int d; ... }; class Z: public X, public Y, ( public: int d; d X::d + Y::d; }; 10.3. Виртуальные функции и абстрактные классы К механизму виртуальных функций обращаются в тех случаях, когда в базовый класс необходимо поместить функцию, которая должна по-разному выполняться в производных классах. Точнее, по- разному должна выполняться не единственная функция из базового класса, а в каждом производном классе требуется свой вариант этой функции. Например, базовый класс может описывать фигуру на экране без конкретизации ее вида, а производные классы (треугольник, эллипс и т.п.) однозначно определяют ее формы и размеры. Если в базовом классе ввести функцию для изображения фигуры на экране, то выпол- нение этой функции будет возможно только для объектов каждого из производных классов, определяющих конкретные изображения.
360 Язык Си++ До объяснения возможностей виртуальных функций отметим, чъ- классы, включающие такие функции, играют особую роль в объектно ориентированном программировании. Именно поэтому они нося i специальное название - полиморфные. Рассмотрим теперь, как ведут себя при наследовании невиртуальны, компонентные функции с одинаковыми именами, типами возвращаемы \ значений и сигнатурами параметров. Если в базовом классе определена некоторая компонентная функция то такая же функция (с тем же именем, того же типа и с тем же набором и типами параметров) может быть введена в производном классе Рассмотрим следующее определение классов: //BASE.DIR - определения базового и производного классов struct base ( void fun (int i) ( cout « "\nbase::i = ”«i; } }; struct dir: public base ( void fun (int i) ( cout « "\ndir::i = ”« i; } }; В данном случае внешне одинаковые функции void fun (int) определены в базовом классе base и в производном классе dir. В теле класса dir обращение к функции fun (}, принадлежащем классу base, может быть выполнено с помощью полного квалифи цированного имени, явно включающего имя базового классы base: :fun() . При обращении в классе dir к такой же (по внешнем' виду) функции, принадлежащей классу dir, достаточно использован имя fun О без предшествующего квалификатора. В программе, где определены и доступны оба класса base и di» обращения к функции fun () могут быть выполнены с помощью ук.1 зателей на объекты соответствующих классов: //РЮ-05.СРР - одинаковые функции в базовом и производном классах tinclude <iostream.h> tinclude "base.dir" //Определения классов void main (void) ( base B, *bp = &B; dir D, *dp = &D; base *pbd = &D; bp->fun(l); // Печатает : base::i = 1 dp->fun(5); // Печатает : dir::i = 5
Глава 10. Наследование и другие возможности классов 361 pbd->fun(4); // Печатает : base::! « 4 Результаты выполнения программы: base::i = 1 dir::i = 5 base::i = 4 В программе введены три указателя на объекты разных классов. Следует обратить внимание на инициализацию указателя pbd. В ней адрес Объекта производного класса (объекта D) присваивается указателю на Объект его прямого базового класса (base*) . При этом выполняется Огандартное преобразование указателей, предусмотренное синтаксисом |эыка Си++. Обратное преобразование, т.е. преобразование указателя На объект базового класса в указатель на объект производного класса, Невозможно (запрещено синтаксисом). Обращения к функциям классов base и dir с помощью указателей Ьр и dp не представляют особого Интереса. Вызов pbd->fun() требуется прокомментировать. Указатель и* имеет тип base*, однако его значение - адрес объекта D класса dir. Какая же из функций base: :fun() или dir: :fun() вызывается |ри обращении pbd->fun() ? Результат выполнения программы Показывает, что вызывается функция из базового класса. Именно такой Вызов предусмотрен синтаксисом языка Си++, т.е. выбор функции {Невиртуальной) зависит только от типа указателя, но не от его значения. "Настроив" указатель базового класса на объект производного класса, Це удается с помощью этого указателя вызвать функцию из производ- ного класса. Вернемся к упомянутому выше примеру с фигурой в виде базового Класса с названием figure. Пусть в этом классе определена компо- нентная функция void show () . Так как внешний вид фигуры в базовом Классе еще не определен, то в каждый из производных классов нужно Включить свою функцию void show () для формирования изображения На экране. Если оставаться в рамках проиллюстрированного в Примере с классами base и dir механизма, то доступ к функции show () Производного класса возможен только с помощью явного указания области видимости: имя_производного_.класса: : show() либо с использованием имени конкретного объекта: имЯ—Объекта—произволного_класса. show ()
362 Язык Си++ В обоих случаях выбор нужной функции выполняется при написа- нии исходного текста программы и не изменяется после компиляции. Такой режим называется ранним или статическим связыванием. Большую гибкость (особенно при использовании уже готовых би- блиотек классов) обеспечивает позднее (отложенное), или динамиче- ское связывание, которое предоставляется механизмом виртуальных функций. Любая нестатическая функция базового класса может быть сделана виртуальной, если в ее объявлении использовать специфика- тор virtual. Прежде чем объяснить преимущества динамического свя- зывания, приведем пример. Опишем в базовом классе виртуальную функцию и введем два производных класса, где определим функции с такими же прототипами, но без спецификатора virtual. В следующей программе в базовом классе base определена виртуальная функция void vfun (int). В производных классах dirl, di г 2 эта функция подменяется (override), т.е. определена по-другому: //РЮ-06.СРР - виртуальная функция в базовом классе finclude <iostream.h> finclude <conio.h> struct base { virtual void vfun(int i) { cout « "\nbase::i « " « i; | In- struct dirl: public base { void vfun (int i) { cout « ”\ndirl::i » " « i; | In- struct dir2: public base { void vfun (int i) { cout « "\ndir2::i « ” « i; | In- void main(void) ( base B, *bp к &B; dirl DIf *dpl « &D1; dir2 D2, *dp2 = &D2; bp->vfun(l); // Печатает: base::i « 1 dpl->vfun(2); // Печатает: dirl::i = 2 dp2->vfun(3); // Печатает: dir2::i = 3 bp « 6D1; bp->vfun(4); 11 Печатает: dirl::i « 4 bp = &D2; bp->vfun{5); // Печатает: dir2::i = 5 I Результат выполнения программы: base::i = 1
Глава 10. Наследование и другие возможности классов 363 dirl: : 1 «8 2 di г 2 : : i » 3 dirl::i = 4 dir2::x = 5 В примере надо обратить внимание на доступ к функциям vfun () через указатель Ьр на базовый класс. Когда Ьр принимает значение адреса &в объекта класса base, то вызывается функция из базового класса. Затем Ьр последовательно присваиваются значения ссылок на объекты производных классов &D1, &D2 и выбор соответствующего экземпляра функции vfun () каждый раз определяется именно объектом. Таким образом, интерпретация каждого вызова виртуальной функции через указатель на базовый класс зависит от значения этого указателя, т.е. от типа объекта, для которого выполняется вызов. В противо- положность этому интерпретация вызова через указатель невиртуальной функции зависит только от типа указателя (это было показано в преды- дущем примере с функцией fun ()) . Виртуальными могут быть не любые функции, а только нестати- ческие компонентные функции какого-либо класса. После того как функция определена как виртуальная, ее повторное определение в Производном классе (с тем же самым прототипом) создает в этом классе новую виртуальную функцию, причем спецификатор virtual может не нспользов аться. В производном классе нельзя определять функцию с тем же именем и с той же сигнатурой параметров, но с другим типом возвращаемого значения, чем у виртуальной функции базового класса. Это приводит к ошибке на этапе компиляции. Если в производном классе ввести функцию с тем же именем и типом возвращаемого значения, что и виртуальная функция базового класса, ио с другой сигнатурой параметров, то эта функция производного класса не будет виртуальной. В этом случае с помощью указателя на базовый класс при любом значении этого указателя выполняется обращение к функции базового класса (несмотря на спецификатор virtual и при- сутствие в производном классе похожей функции). Сказанное иллюстрирует следующая программа: //РЮ-07. СРР - некоторые особенности виртуальных функций (include <хоstrearn.h> (include <conio.h> struct base ( virtual void fl(void) { cout « "\nbase::fl"; } virtual void f2 (void) { cout « " \nbase: : f2; )
364 Язык Си++ virtual void f 3 (void) ( cout « "\nbase::f3"; ) In- struct dir: public base ( // Виртуальная функция: void fl(void) { cout « "\ndir::fl"; ) // Ошибка в типе функции: // int f2(void) { cout « ”\ndir::f2"; ) // Невиртуальная функция: void f3(int i) { cout « ”\ndir: : f 3: : i « ”« i; ) In- void main (void) ( base B, *pb « &B; dir D, *pd « &D; pb->fl() ; pb->f2() ; pb->f3() ; pd->fl() ; pd->f2() ; // Ошибка при попытке без параметра вызвать dir: :f3(int) : 11 pd->f3() ; pd->f3(0) ; pb « &D; pb->fl(); pb->f2(); pb->f3() ; // Ошибочное употребление или параметра, или указателя: // pb->f3(3); I Результат выполнения программы: base::£1 base::f2 base::f3 dir::fl base::f2 dir::f3::i « 0 dir::fl base::f2 base::f3 Обратите внимание, что три виртуальные функции базового клас- са по-разному воспринимаются в производном классе, dir:: fl о - виртуальная функция, подменяющая функцию base: :fl(). Функция base::f2() наследуется в классе dir так же, как и функция base: :f3(). Функция dir: :f3(int) - совершенно новая компонент-
Глава 10. Наследование и другие возможности классов 365 ная функция производного класса, никак не связанная с базовым классом. Именно поэтому невозможен вызов f3 (int) через указатель на базовый класс. Виртуальные функции base: : f2 () и base: :f3() оказались не переопределенными в производном классе dir. Поэтому при всех вызовах без параметров f3 () используется только компо- нентная функция базового класса. Функция dir: :f3(int) иллюстрирует соглашение языка о том, что если у функции производного класса набор параметров отличается от набора параметров соответствующей виртуальной функции базового класса, то это не виртуальная функция, а новый метод производного класса. Завершая рассмотрение примера, еще раз подчеркнем, что при подмене виртуальной функции требуется полное совпадение сигнатур имен и типов возвращаемых значений функций в базовом и производном классах. Как уже было упомянуто, виртуальной функцией может быть только нестатическая компонентная функция. Виртуальной не может быть глобальная функция. Функция, подменяющая виртуальную, в производном классе может быть описана как со спецификатором virtual, так и без него. В обоих случаях она будет виртуальной, т.е. ее вызов возможен только для конкретного объекта. Виртуальная функция может быть объявлена дружественной (friend) в другом классе. Механизм виртуального вызова может быть подавлен с помощью явного использования полного квалифицированного имени. Таким образом, при необходимости вызова из производного класса виртуаль- ного метода (компонентной функции) базового класса употребляется полное имя. Например, struct base { virtual int f(int j) {return j * j; ) }; struct dir: public base { int f(int i) {return base::f(i*2); } ); Абстрактные классы. Абстрактным классом называется класс, в котором есть хотя бы одна чистая (пустая) виртуальная функция. Чистой виртуальной называется компонентная функция, которая имеет следующее определение: virtual тип имя—функции (список^формальных_параметров) = 0; В этой записи конструкция "= 0" называется "чистый спецификатор". Пример описания чистой виртуальной функции:
366 Язык Си++ virtual void fpure (void) « 0; Чистая виртуальная функция "ничего не делает" и недоступна для вызовов. Ее назначение - служить основой для подменяющих ее функций в производных классах. Исходя из этого становится понятной невоз- можность создания самостоятельных объектов абстрактного класса. Абстрактный класс может использоваться только в качестве базового для производных классов. При создании объектов такого производного класса в качестве подобъектов создаются объекты базового абстракт- ного класса. Предположим, что имеется абстрактный класс: class В { protected: virtual void func(char) » 0; void sos(int); }; На основе класса в можно по-разному построить производные классы: class D: public В { void func(char); }; class E: public В { void sos(int); ); В классе D чистая виртуальная функция func () заменена конкретной виртуальной функцией того же типа. Функция В::sos () наследуется классом D и доступна в нем и в его методах. Класс D не абстрактный. В классе е переопределена функция в: :sos(), а виртуальная функция в: : func () унаследована. Тем самым класс Е становится абстрактным и может использоваться только как базовый. Как и всякий класс, абстрактный класс может иметь явно определенный конструктор. Из конструктора возможен вызов методов класса, но любые прямые или опосредованные обращения из конструктора к чистым виртуальным функциям приведут к ошибкам во время выполнения программы. Механизм абстрактных классов разработан для представления общих понятий, которые в дальнейшем предполагается конкрети- зировать. Эти общие понятия обычно невозможно использовать непосредственно, но на их основе можно, как на базе, построить частные производные классы, пригодные для описания конкретных объектов.
Глава 10. Наследование и другие возможности классов 367 Например, из абстрактного класса "фигура" можно сформировать класс "треугольник", "окружность" и т.д. i В качестве примера рассмотрим программу, в которой иа основе базового класса point построен абстрактный класс figure, В классе figure определены: конструктор, чистая виртуальная функция j show () для вывода изображения фигуры, например, на экран дисплея. Кроме того, в класс входят методы hide о - убрать изображение фи- гуры с экрана дисплея и moved - переместить изображение фигуры в заданную точку экрана. Функции hided и move о обращаются к чис- той виртуальной функции show О. Однако реальное выполнение r show О возможно только после создания производного класса, в ко- тором чистая виртуальная функция showd будет подменена компо- * нентной функцией для изображения конкретной фигуры. Определение абстрактного класса figure (в файле figure. срр): //FIGURE. СРР - абстрактный класс на бане класса i linclude "point.срр" class figure: public point ( public: // Конструктор абстрактного класса figure: figure(point p) : point(p.givex 0, p.givey 0) ( ) // Чистая виртуальная функция для будущего // изображения фигур: virtual void show О « 0; // Функция для удаления изображения фигуры: void hided { int bk, сс; bk s getbkcolorO ; cc s getcolord ; setcolor(bk); show(); // Обращение к «шстой виртуальной функции setcolor(сс); void move (point p) // Перемещение фигуры в точку "р" { hide(); х • р.givex(); у « р.givey(); show(); ) J; На базе класса figure определим неабстрактные классы: //ELLIPS.FIG - конкретный класс "эллипс" на основе figure class ellips : public figure ( int rx,ry; public: // Конструктор: ellips (point d, int radx, int rady): figure (d)
368 Язык Си++ { rx » radbc; ry « rady; } void show О { ellipse(х,у,0,360,rx,ry); return; } }; //CIRC. FIG - конкретный класс ’’окружность" class circ: public figure { int radius; public: // Конструктор: circ (point e, int rad): figure (e) ( radius e rad; } void show() { circle(x,у,radius); } }; В следующей программе используются все три класса: //Р10-08.СРР - абстрактные классы и чистые виртуальные // функции ♦include <graphics.h> ♦include "figure.срр" ♦include "circ.fig" ♦include "ellips.fig" ♦include <conio.h> // Для функции getch() void main(void) { point A(100,80), B(300,200); circ C(A,60); ellips E(B,200,100) ; { // Переменные для инициализации графики: int dr « DETECT, mod; // Инициализация графической системы: initgraph (&dr, £mod, "c: \\borlandc\\bgi") ; // Изобразить точку - point: :show() : A.show(); getch(); // Изобразить точку - point: :show() : В.show(); getch(}; 11 Показать окружность - circ::show(): C.show(); getch(); // Показать эллипс - ellips::show(): E.show(); getch(); // Совместить фигуры - circ: :figure: :move() : C.move(B); getch(); // Убрать эллипс - ellips::figure::hide (): E.hide(); getch(); // Убрать окружность - circ::figure::hide ():
Глава 10. Наследование и другие возможности классов 369 С.hide(); getch(); ) closegraph(); Графические результаты выполнения показаны на рис. 10.6. Рис. 10.6. Последовательность изображений на экране при выполнении программы РЮ-08. СРР В программе на базе класса figure определены два производных класса: circ (окружность) и ellips (эллипс). Для обоих классов унаследованный класс point определяет центры фигур. В обоих классах определены конкретные методы show () и из абстрактного класса figure унаследованы функции move () и hide () . Комментарии к операторам основной программы содержат полные (квалифицированные) имена исполняемых функций. По сравнению с обычными классами абстрактные классы поль- зуются "ограниченными правами". Как говорилось, невозможно создать объект абстрактного класса. Абстрактный класс нельзя употреблять для задания типа параметра функции или в качестве типа возвращаемого функцией значения. Абстрактный класс нельзя использовать при явном приведении типов. В то же время можно определять указатели и ссылки на абстрактные классы. Объект абстрактного класса не может быть формальным параметром функции, однако, формальным параметром может быть указатель абстрактного класса. В этом случае появляется возможность передавать в вызываемую функцию в качестве фактического параметра значение указателя на производный объект, заменяя им указатель на абстрактный базовый класс. 24-зи!
370 Язык Си++ Используя эту возможность, сформируем односвязный список, в элементы которого будут включаться объекты разных классов, производных от одного абстрактного класса. В качестве базового абстрактного класса выберем введенный выше класс figure. Список в целом будет описываться классом chain. В класс chain входят (рис. 10.7) в качестве компонентов: статический указатель на последний элемент, уже включенный в список (last), статический указатель на начало списка (begin), указатель в объекте на следующий элемент списка (next), указатель в объекте на абстрактный базовый класс figure (prig) . Параметр конструктора класса chain пусть имеет тип указателя на абстрактный базовый класс figure. В качестве фактических параметров будем использовать ссылки на конкретные объекты классов, производных от абстрактного класса figure. Тем самым с односвязный список включаются (см. рис. 10.7) конкретные фигуры (окружность - класс circ, эллипс - класс ellips). begin —> | next | pfig| --► circ last —» | next | pfig | —► ellips NULL Puc. 10.7. Схема односвязного списка (класс chain), объединяющего объекты разных классов Текст программы со всеми включениями и определениями: //РЮ-09.СРР - односвязный список объектов разных классов linclude <stdlib.h> //NULL, malloc,... #include <conio.h> //getch() ,... //linclude <iostream.h> //cout,... linclude "point.срр” //Базовый класс для figure //Абстрактный класс, производный от point: linclude "figure.срр" //Класс, производный от абстрактного figure: linclude "circ.fig" // Класс, производный от абстрактного figure: linclude "ellips.fig" //Объекты класса - фигуры, включенные в односвязный // список: class chain { //Объект - элемент в односвязном списке //Указатель на последний элемент в списке: static chain *last;
Глава 10. Наследование и другие возможности классов 371 // Указатель в объекте на следующий элемент: chain *next; public: // Указатель на фигуру, входящую в элемент списка: figure *pfig; // Указатель на начало списка: static chain *begin; // Конструктор: chain(figpre *р); // Функция изображает все фигуры списка: static void showAll(void); }; // Конец определения класса // Внешнее описание и инициализация статических // компонентов класса: chain *chain::begin « NULL; // Начало списка chain *chain::last « NULL; // Последний элемент в списке void chain::showAll(void) // Изображение элементов списка { chain *uk — begin; // Настройка на начало списка while (uk NULL) // Цикл до конца списка ( uk->pfig->show{); // Нарисовать конкретную фигуру uk = uk->next; // Настройка на следующий элемент ) ) // Конструктор создает и включает в список объект, // связав его с конкретной фигурой из класса, производного // от абстрактного: chain:: chain (figure *р) // р - адрес включаемой фигуры { if (begin =“» NULL) // Определили начало списка begin » this; else i last->next = this; // Свявь с предыдущим элементом pfig = p; // Запоминаем адрес включаемой фигуры next « NULL; // Пометим окончание списка last « this; // Запоминаем адрес последнего элемента // списка } void main() { point А(100,80), В(300,200); circ С(А,60); ellips Е(В,200,100); { // Переменные для инициализации графики: int dr « DETECT, mod; // Инициализация графической системы: initgraph(£dr,&mod,"с:\\borlandc\\bgiH); A.show(); getch(); // Изобразить точку - point::show() В.show(); getch(); // Изобразить точку // Показать окружность - circ::show(): 94*
372 Язык Си++ С.show(); getch(); // Включить в список первый элемент - окружность С: chain са(£С); Е.show(); getch(); // Показать эллипс - ellips::show() chain се(£Е); // Включить в список эллипс // Совместить фигуры ~ circ: : figure::move О : С.move (В) ; getch (); // Убрать эллипс - ellips :: figure::hide О : Е.hi de(); getch(); // Убрать окружность - circ::figure::hide О: С.hide(); getch(); // Изобразить все фигуры из списка: chain::showAll(); getch(); closegraph(); Рис, 10.8. Последовательность изображений на экране при выполнении программы Р10-09. СРР Статическая компонентная функция chain: :showAll() обеспечи- вает вывод на экран изображений всех конкретных фигур, включен- ных в односвязный список. Важно отметить последовательность передач управления в этой функции. Указатель uk типа chain * по- зволяет обратиться к компоненту pfig - указателю на абстрактный базовый класс figure. После выполнения конструктора chain () зна- чением pfig является ссылка на объект некоторого производного от figure класса. Именно оттуда выбирается соответствующая функция show О, подменяющая чистую виртуальную функцию figure::show(). Тем самым на экран выводится изображение кон-
Глава 10. Наследование и другие возможности классов 373 кретной фигуры. Например, функция circ::show() изобразит окружность ИТ.Д. В основной программе формируются точки А, в и на них, как на центрах, создаются объекты С (окружность) и Е (эллипс). В графи- ческом режиме выводятся на экран и убираются с экрана изображения всех созданных объектов. Затем функцией ShowAllO рисуются все объекты, включенные в список. Результат выполнения программы показан на рис. 10.8. 10.4. Локальные классы Класс может быть определен внутри блока или другого класса, например внутри тела функции. Такой класс называется локальным. Локализация класса предполагает недоступность его компонентов вне области определения класса (вне тела функции или блока, в котором он описан или определен). Локальный класс не может иметь статических данных, так как компоненты локального класса не могут быть определены вне текста класса. Внутри локального класса разрешено использовать из объемлющей его области только имена типов, статические (static) переменные, внешние (extern) переменные, внешние функции и элементы пере- числений. Из того, что запрещено, важно отметить переменные автома- тической памяти. Существует еще одно важное ограничение для локальных классов их компонентные функции могут быть только встроенными (inline). Для иллюстрации особенностей локальных классов рассмотрим следующую задачу. Пусть требуется определить класс "квадрат”. Ограничимся квадратами, стороны которых параллельны осям прямоугольной декартовой системы координат. Исходными данными для задания каждого конкретного квадрата (объекта класса "квадрат") будут координаты центра и размер стороны. Внутри класса "квадрат" определим локальный класс "отрезок". Исходными данными для опре- деления каждого конкретного отрезка будут координаты его концов. Четыре отрезка с соответствующим образом выбранными концевыми точками составят квадрат. Именно таким образом можно будет изобра- зить квадрат на экране (как изображения четырех отрезков). //РЮ-10.СРР - внешние, локальные и глобальные классы finclude <conio.h> linclude "point.срр" //Внешний класс "точка"
374 Язык Си++ class square // Глобальный класс "квадрат" { class segment // Локальный класс "отрезок" { point pn, pk; // Точки начала и конца отрезка public: // Конструктор отрезка segment(point pin « point(0,0), point pik « point(0,0)) { pn.givex() « pin.givex(); pn.givey() « pin.givey(); pk.givex() « pik.givex(); pk.givey() « pik.givey(); ) // Доступ к граничным точкам отрезка: point& beg(void) ( return pn; } points end(void) { return pk; } void showSeg() // Изобразить отрезок на экране { line (pn.givex(), pn.givey(), pk.givex(), pk.givey()); ) } ; // Конец определения класса segment segment ab, be, cd, da; // Отрезки - стороны квадрата public: // Конструктор квадрата: square (point ci « point(0,0), int di « 0) { // Вершимы квадрата - локальные объекты // конструктора: point a, : b, с, d; а. giv< «() SB ci.givex() - di/2; а. giv< iy() ж ci. giv< »y() - di/2; b. giv< «() ж ci.givex() + di/2; b. giv< yO ж ci. giv< »У0 - di/2; c. giv< »X() ж ci.giv< »x() + di/2; c.givey () ж ci.giv< »y() + di/2; d. giv< «() ж ci. giv< »x() - di/2; d. giv< iy() ж ci. giv< •y() + di/2; // Граничные точки отрезков: ab.begO « a; ab.< and () « b; be.beg () « b; be.< and () « c; cd.beg() « c; cd.< and() = d; da .beg () « d; da.' and() « a; ) void showSquare(void) // Изобразить квадрат { ab.showSeg(); be.showSeg(); cd.showSeg(); da.showSeg(); ) } ; // Конец определения класса "квадрат
Глава 10. Наследование и другие возможности классов 375 void main() ( // Переменные для инициализации графики: int dr « DETECT, mod; // Инициализация графической системы: initgraph(fidr,(mod,Mc:\\borlandc\\bgi"); point pl(80,120); point p2(250,240); square A(pl,30); square B(p2,140); A.showSquare(); getch(); В.showSquare(); getch(); closegraph(); ) Результат в графическом виде показан на рис. 10.9. Рис. 10.9. Изображения на экране при выполнении программы РЮ-10. СРР Отметим некоторые особенности программы. Класс "квадрат" (square) включает в качестве данных четыре стороны - отрезки аь, be, cd, da, каждый из которых есть объект локального класса segment. Конструктор класса square () по заданным центру квадрата и размеру стороны определяет значения точек-вершин, а уже по ним формирует отрезки - стороны квадрата. 10.5. Классы и шаблоны Шаблоны, которые иногда называют родовыми или параметризо- ванными типами, позволяют создавать (конструировать) семейства родственных функций и классов [2], [9]. Как уже говорилось в главе 6, шаблон семейства функций (function template) определяет потенциально неограниченное множество родг ственных функций. Он имеет следующий вид: template <список__параметров_шаблона> определение__фумкции
376 Язык Си++ Здесь угловые скобки являются неотъемлемым элементом опреде- ления. Список параметров шаблона должен быть заключен именно в угловые скобки. Аналогично определяется шаблон семейства классов: template <список_параметров_шаблона> определение_класса Шаблон семейства классов определяет способ построения отдель- ных классов подобно тому, как класс определяет правила построения и формат отдельных объектов. В определении класса, входящего в шаблон, особую роль играет имя класса. Оно является не именем от- дельного класса, а параметризованным именем семейства классов. Как уже отмечалось в связи с шаблонами функций, определение шаблона может быть только глобальным. Следуя авторам языка и компилятора Си++ [2, 9], рассмотрим векторный класс (в число данных входит одномерный массив). Какой бы тип ни имели элементы массива (целый, вещественный, с двойной точностью и т.д.), в этом классе должны быть определены одни и те же базовые операции, например доступ к элементу по индексу и т.д. Если тип элементов вектора задавать как параметр шаблона класса, то система будет формировать вектор нужного типа (и соответствую- щий класс) при каждом определении конкретного объекта. Следующий шаблон позволяет автоматически формировать клас- сы векторов с указанными свойствами: //TEMPLATE .VEС - шаблон векторов template Colass Т> // Т - параметр шаблона class Vector { Т *data; // Начало одномерного массива iht size; // Количество элементов в массиве public: Vector(int); // Конструктор класса vector -Vector () { deleted data; ) // Деструктор // Расширение действия (перегрузка) операции "!]": Т& operator!] (int i) { return data!i]; } } ; // Внешнее определение конструктора класса: template Cclass Т> Vector <Т>::Vector(int n) { data « new T[n]; size = n; } ; Когда шаблон введен, у программиста появляется возможность определять конкретные объекты конкретных классов, каждый из ко-
Глава 10. Наследование и другие возможности классов 377 торых параметрически порожден из шаблона. Формат определения объекта одного из.классов, порождаемых шаблоном классов: имя__параметризованиого__класса <фактические__параметры_шаблоиа> имя_об*»екта (параметры__коиструхтора) ; В нашем случае определить вектор, имеющий восемь веществен- ных координат типа double, можно следующим образом: Vector <double> Z(8); Проиллюстрируем сказанное следующей программой: //РЮ-11. СРР - формирование классов с помощью шаблона «include "template.vec" // Шаблон классов "вектор" «include <iostream.h> main() { // Создаем объект класса "целочисленный вектор" : Vector <int> Х(5) ; // Создаем объект класса "символьный вектор" *. Vector <char> С(5); // Определяем компоненты векторов: for (int i « 0; i < 5; i++) { X[i] = i; C[i] « 'A' + i;} for (i « 0; i < 5 ; i++) cout « " " « X[i] « ' ' « C[i]; } Результат выполнения программы: 0A IB 2 С 3D 4 Е В программе шаблон семейства классов с общим именем vector используется для формирования двух классов с массивами целого и символьного типов. В соответствии с требованием синтаксиса имя параметризованного класса, определенное в шаблоне (в примере vector), используется в программе только с последующим конкрет- ным фактическим параметром (аргументом), заключенным в угловые скобки. Параметром может быть имя стандартного или определенно- го пользователем типа. В данном примере использованы стандартные типы int и char. Использовать имя vector без указания фактического параметра шаблона нельзя - никакое умалчиваемое значение при этом не предусматривается.
378 Язык Си++ В списке параметров шаблона могут присутствовать формальные параметры, не определяющие тип, точнее - это параметры, для кото- рых тип фиксирован: //РЮ-12.СРР linclude <iostream.h> template Cclass T, int size = 64> class row { T *data; int length; public: row() { length » size; data « new T[size]; ) -row() { delete[] data; } T& operator [] (int i) { return datafi]; } } ; void nainO { row <float,8> if; row <int,8> ri; for (int i = 0; i < 8; i++) ( rf[i] = i; ri[i] = i * i; ) for (i = 0; i < 8; i++) cout « " " « rf[i] « ’ ' « ri[i]; } Результат выполнения программы: 0 0 1 1 2 4 3 9 4 16 5 25 6 36 7 49 В качестве аргумента, заменяющего при обращении к шаблону параметр size, взята константа. В общем случае может быть исполь- зовано константное выражение, однако выражения, содержащие пе- ременные, использовать в качестве фактических параметров шаблонов нельзя.
Глава 11. ВВОД-ВЫВОД В ЯЗЫКЕ СИ++ 11.1. Общие сведения о библиотеке потокового ввода-вывода Под ’’программированием на языке Си++" обычно понимается "про- граммирование в среде Си++". Дело в том, что никакая полезная программа не может быть написана на языке Си++» без привлечения библиотек, включаемых в конкретную среду (в компилятор) языка. Конкретная среда Си++, в которой разрабатывается программа, обычно обеспечивает программиста удобными средствами для работы с ее библиотеками. При этом по утверждению Б. Страуструпа: "Для исполь- зования библиотеки совсем не нужно знание методов, которые приме- нялись для ее реализации". Однако знание правил использования средств библиотеки совершенно необходимо. Самая незаменимая из этих биб- лиотек - библиотека ввода-вывода, так как средства ввода-вывода непосредственно в язык Си++ (так же как и в язык Си) не входят. В программах на языке Си++ можно равноправно использовать две биб- лиотеки ввода-вывода: стандартную библиотеку функций языка Си (стандарт ANSI С) и библиотеку классов, специально созданную для языка Си++. Библиотека функций языка Си становится доступной в программе, как только в ее заголовок будет включен файл stdio. h. Для обращения к функциям требуются сведения об их прототипах и соблю- дение соглашений стандарта (см. прил. 3). Подробную информацию можно получить из технической документации того компилятора, с которым вы работаете. На протяжении всей книги в программах постоянно использовалась препроцессорная директива: finclude <iostream.h> Назначение указанного в директиве заголовочного файла iostream.h - связать компилируемую программу с одной из основных частей библиотеки ввода-вывода, построенной на основе механизма классов. Эти библиотека ввода-вывода почти стандартна, так как включена практически во все компиляторы Си++. Однако о стандарте
380 Язык Си++ библиотеки ввода-вывода Си++ можно говорить только неформально. Библиотека создана позже, чем появился язык, она разрабатывалась в некотором смысле независимо от создания языка Си++, не входит в формальное описание языка и написана на языке Си++. Потоки ввода-вывода. В соответствии с названием заголовочного файла iostream.h (stream - поток; "i" - сокращение от input - ввод; "о" - сокращение от output ~ вывод) описанные в этом файле средства ввода-вывода обеспечивают программиста механизмами доя извлечения данных из потоков и доя включения (внесения) данных в потоки. Поток определяется как последовательность байтов (символов) и с точки зрения программы не зависит от тех конкретных устройств (файл на диске, принтер, клавиатура, дисплей, стример и т.п.), с которыми ведется обмен данными. При обмене с потоком часто используется вспомогательный участок основной памяти - буфер потока (рис. 11.1 - буфер вывода, рис. 11.2- буфер ввода). Основная память Передача при заполнении буфера или по специальной команде “пересылка буфера” ♦ Внешний носитель информации Рис. 11.1, Буферизированный выходной поток В буфер потока помещаются выводимые программой данные перед тем, как они будут переданы к внешнему устройству. При вводе данных они вначале помещаются в буфер и только затем передаются в область памяти выполняемой программы. Использование буфера как
Глава 11. Ввод-вывод в языке Си++ 381 промежуточной ступени при обменах с внешними устройствами по- вышает скорость передачи данных, так как реальные пересылки осу- ществляются только тогда, когда буфер уже заполнен (при выводе) или пуст (при вводе). Работу, связанную с заполнением и очисткой буферов ввода- вывода, операционная система очень часто берет на себя и выполняет без явного участия программиста. Поэтому поток в прикладной про- грамме обычно можно рассматривать просто как последовательность байтов. При этом очень важно, что никакой связи значений этих бай- тов с кодами какого-либо алфавита не предусматривается. Задача программиста при вводе-выводе с помощью потоков - установить со- ответствие между участвующими в обмене типизированными объек- тами и последовательностью байтов потока, в которой отсутствуют всякие сведения о типах представляемой (передаваемой) информации. Внешний источник информации -------!------------------ Передача при пустом буфере ввода ♦ Буфер ввода Пересылки (извлечения) по командам прикладной программы Принимающие объекты Прикладная программа Основная память Рис. 11.2. Буферизированный входной поток Используемые в программах потоки логически делятся на три ти- па: • входные, из которых читается информация; • выходные, в которые вводятся данные; • двунаправленные, допускающие как чтение, так и запись. Все потоки библиотеки ввода-вывода последовательные, т.е. в каждый момент для потока определены позиции записи и (или) чте-
382 Язык Си++ ния, и эти позиции после обмена перемещаются по потоку на длину переданной порции данных. В соответствии с особенностями "устройства", к которому "присоединен" поток, потоки принято делить на стандартные, кон- сольные, строковые и файловые. Из перечисленных потоков мы не будем рассматривать только консольные потоки. Причин для этого несколько. Во-первых, консольные потоки отсутствовали в классических реализациях библиотеки потокового ввода-вывода [26]. Во-вторых, консольные потоки несовместимы с операционной средой Microsoft Windows и могут использоваться только при разработке программ, работающих под управлением МС-DOS (см., например [9, 21, 29, 30, 31]). Консольные потоки поддерживаются классом constream и обеспечивают удобный доступ к терминалу. В них есть возможности работы с клавиатурой и средства манипуляции с участками экрана и с экраном в целом. Если символы потока в совокупности образуют символьный массив (строку) в основной памяти, то это строковой поток. Если при использовании потока его символы размещаются на внешнем носителе данных (например, на диске или на магнитной ленте), то говорят о файловом потоке или просто о файле. Стандартные и консольные потоки соответствуют передаче данных от клавиатуры и к экрану дисплея. До сих пор во всех программах выполнялся обмен только со стандартными потоками. Рассмотрим их возможности подробнее. Однако предва- рительно остановимся на некоторых принципиальных особенностях потоковой библиотеки ввода-вывода языка Си++. Иерархия классов библиотеки ввода-вывода. В отличие от стандартной библиотеки (в которой находятся средства, например, для работы со строками, или математические функции), унаследованной компиляторами языка Си++ от языка Си, библиотека ввода-вывода Сп++ является не библиотекой функций, а библиотекой классов. Это первая "промышленная" библиотека классов, разработанная для распространения совместно с компиляторами. Именно эту библиотеку рекомендуют изучать, начиная знакомиться с принципами объектно ориентированного программирования [28]. Одним из базовых принципов ООП является предположение о том, что объекты «знают», что нужно делать при появлении обращения (сообщения) определенного типа, т.е. для каждого типа адресованного ему обращения объект имеет соответствующий механизм обработки. Если мы используем объект cout е представляющий выходной поток, то, как уже неоднократно показано на примерах, для каждого из базовых типов (int, long, double,...) этот объект cout выбирает соответствующую процедуру обработки и выводит
Глава 11. Ввод-вывод в языке Си++ 383 значение в соответствующем виде. Объект cout не может перепутать и вывести, например, целое число в формате с плавающей точкой. От таких ошибок, которые были возможны в языках Си и Фортран, когда программист сам определял форму внешнего представления, библиотека классов ввода-вывода хорошо защищена. Библиотека потоковых классов построена на основе двух базовых классов: ios и streambuf. Класс streambuf обеспечивает буферизацию данных во всех производных классах, которыми явно или неявно пользуется программист. Обращаться к его методам и данным из при- кладных программ обычно не нужно. Класс streambuf обеспечивает взаимодействие создаваемых потоков с физическими устройствами. Он обеспечивает производные классы достаточно общими методами для буферизации данных. Класс ios и производные классы содержат ука- затель на класс streambuf, но об этом можно до времени не вспоминать. Методы и данные класса streambuf программист явно обычно не использует. Этот класс нужен другим классам библиотеки ввода-вывода. Он доступен и программисту-пользователю для создания новых классов на основе уже существующего класса из iostream. Однако необхо- димость в построении таких производных классов возникает достаточно редко, и мы не будем рассматривать класс streambuf. Класс ios со- держит компоненты (данные и методы), которые являются общими как для ввода, так и для вывода. При работе с потоковой библиотекой ввода-вывода программист обычно достаточно активно использует следующие классы: ios istream - базовый потоковый класс; - класс входных потоков; ostream iostream istrstream - класс выходных потоков; - класс двунаправленных потоков ввода-вывода; - класс входных строковых потоков; ostrstream - класс выходных строковых потоков; strstrearn - класс двунаправленных строковых потоков (ввода- вывода); ifstream ofstream fstream - класс входных файловых потоков; - класс выходных файловых потоков; - класс двунаправленных файловых потоков (ввода- вывода); □onstream - класс консольных потоков.
384 Язык Си++ Диаграмма взаимозависимости перечисленных классов изображена на рис. 11.3. Следует отметить, что эта диаграмма потоковых классов упрощена. В реальной схеме присутствуют промежуточные классы и реализовано более сложное множественное наследие. Кроме того, программист, как упоминалось, обычно не учитывает наличия второго базового класса streambuf, и он не показан на схеме. Рис. 113. Упрощенная схема иерархии потоковых классов Как наглядно видно из диаграммы классов (см. рис. 11.3), класс ios является базовым для классов ©stream, istream, и опосредованно базовым для всех остальных потоковых классов. Все общие средства потоковых классов помещаются в класс ios. Например, при помощи методов и данных класса ios осуществляется управление процессом передачи символов из буфера в буфер. При выполнении этих действий необходимы, например, сведения о нужном основании счисления (восьмеричное, десятичное, шестнадцатеричное), о точности представ- ления вещественных чисел и т.д. (см. флаги). Класс ios содержит эти сведения, т.е. (методы) функции и данные, относящиеся к состояниям потоков и позволяющие менять их свойства. Потоковые классы, их данные и методы становятся видимыми и доступными в программе, если в нее включен нужный заголовочный файл: iostream.h strstrea.h fstream.h constrea.h - для классов ios, istream, ©stream, stream; - для классов i str stream, ostrstream, strstream; - для классов if stream, of stream, fstream; - для классов constream.
Глава 11. Ввод-вывод в языке Си++ 385 Так как класс ios является базовым для остальных потоковых классов, то включение в текст программы любого из заголовочных файлов strstrea.h, constrea.h или fstream.h автоматически под- ключает к программе файл iostream, h. Соответствующие проверки выполняются на этапе препроцессорной обработки. В заключение перечислим отличительные особенности применения механизма потоков. Потоки обеспечивают: • буферизацию при обменах с внешними устройствами; • независимость программы от файловой системы конкретной операционной системы; • контроль типов передаваемых данных; • возможность удобного обмена для типов, определенных поль- зователем. 11.2. Стандартные потолки для базовых типов Стандартные потолки ввода-вывода. У читателя может возникнуть законный вопрос: почему в программах предыдущих глав использо- вались потоки ввода-вывода cin,cout,сегг и ничего не требовалось знать о тех классах, к которым они относятся. Достаточно поместить в текст программы препроцессорную процедуру #include <iostream.h> и можно с помощью операции включения (записи) данных в поток << и извлечения (чтения) данных из потока >> выполнять обмен с дисплеем и клавиатурой ЭВМ. Объясняется это тем, что заголовочный файл iostream. h не только подключает к программе описания классов ios, istream, ostream, stream, но и содержит определения стандартных потоков ввода-вывода: cin - объект класса istream, связанный со стандартным буферизи- рованным входным потоком (обычно клавиатура консоли); cout ~ объект класса ©stream, связанный со стандартным буферизи- рованным выходным потоком (обычно дисплей консоли); сегг - объект класса ostream, связанный со стандартным небуфери- зированным выходным потоком (обычно дисплей консоли), в который направляются сообщения об ошибках; 25-383'
386 Язык Си++ clog - объект класса ©stream, связанный со стандартным буферизи- рованным выходным потоком (обычно дисплей консоли), в который с буферизацией направляются сообщения об ошибках Каждый раз при включении в программу файла iostream.h про- исходит формирование объектов cin,cout,cerr,clog, т.е. создаются соответствующие стандартные потоки, и программисту становятся доступными связанные с ними средства ввода-вывода. Программист может по своему усмотрению разорвать связь любого из перечисленных объектов с консолью и соединить его с тем или иным файлом, но стандартная (по умолчанию) связь устанавливается именно с кла- виатурой (поток cin) и дисплеем (потоки cout,cerr,clog). В том же файле iostream.h, где описаны классы istream, ©stream, для них определены оригинальные операции ввода и вывода данных. Операция ввода класса istream называется извлечением (чтением) данных из потока. Она обозначается с помощью символа операции сдвига вправо >>. Операция вывода класса ©stream называется вставкой, или включением (или записью), данных в поток. Она обозначается с помощью символа операции сдвига влево «. Роль операции извлечения и вставки конструкции << и >> играют по умолчанию только в случае, если слева от них находятся объекты соответственно классов iostream и ©stream: cin » имя„_об,ьекта_базового_типа cout « выражение—базового—типа cerr « выражение_базового_типа clog « выражение_базового_типа Выполнение операции » (извлечение из потока) заключается в преобразовании последовательности символов потока в значение ти- пизированного объекта, частным случаем которого является переменная базового типа int, long, double и т.д. При выполнении операции << (включение в поток) осуществляется обратное преобразование - типизированное значение выражения (int, float, char и т.д.) Трансформи- руется в последовательность символов потока. Примеры применения операций включения в поток и извлечения из потока типизированных значений уже приводились многократно. Хорошо бы теперь читателю удивиться некоторым особенностям ввода-вывода с помощью этих операций. Дело в том, что внешнее (визуальное) представление данных ни- как не похоже на те внутренние коды, которые используются для их хранения и обработки внутри ЭВМ. Вне ЭВМ это алфавитно-
Глава 11. Ввод-вывод в языке Си++ 387 цифровые изображения (чисел или символов), внутри ЭВМ это дво- ичные коды - последовательности битов (двоичных разрядов) регла- ментированной для каждого типа длины. При выполнении програм- мы операция вывода данных (например, на экран дисплея) предусмат- ривает преобразование двоичных кодов в символы алфавита, изобра- жаемые на внешнем устройстве. В операции ввода выполняется преобразование сигналов от клавишей клавиатуры в двоичные коды внутреннего представления данных. Чтобы отвлечься (абстрагиро- ваться) от особенностей аппаратурной реализации устройств ввода- вывода, программист, работая на таком языке, как Си++, использует входные и выходные потоки. Поток в обоих случаях - это последова- тельность байтов (двоичных кодов фиксированной длины). При вы- воде коды потока могут изображаться на экране в виде символов принятого алфавита. При вводе по кодам из потока формируются двоичные представления вводимых данных. Сложности и вопросы появляются у программиста при преобразовании данных (кодов) из внешнего (потокового) во внутреннее представление и обратно. Например, во внутреннем коде целое число может быть представ- лено двумя смежными байтами. Те же самые смежные байты можно рассматривать как внутренние коды двух литер (символов). (Именно так действует объединение union в языке Си++, позволяя по-разному интерпретировать внутренние коды данных.) Средства вывода, применяемые в языке, должны иметь возмож- ность распознать тип выводимых данных и в указанном примере по- местить в выходной поток либо код внешнего представления целого числа, либо два кода расположенных рядом символов. Для обеспече- ния такой возможности операция « включения в стандартный вы- ходной поток перегружена. Существуют ее варианты для типов char, unsigned short, signed short, signed int, unsigned int, signed long, unsigned long, float double, long double, char *,void *. Все они доступны после включения в программу файла iostream.h. Отметим, что операция включения определена только для указателей двух типов. Этого впол- не достаточно, так как все указатели, отличные от char *, автомати- чески приводятся к типу void *. Предопределенный обмен данными со стандартными потоками. Рассмотрим процесс ввода данных с применением операции извлече- ния. Например, как нужно в программе воспринять последователь- ность -2.3«+1, набираемую на клавиатуре? Это символьная строка, которую нужно разместить в массиве типа chard, или экспоненци- альное представление вещественного числа типа float, либо типа double? Набирая на клавиатуре последовательность цифр 1234, можно 25
388 Язык Си++ интерпретировать ее либо как целое число, либо как символьную строку, либо как значение вещественного числа. А как же правильно ее воспринять? В таких языках, как Фортран или Си, программист с помощью специальных средств форматирования должен указать правила пре- образования и формы представления вводимой и выводимой инфор- мации. В библиотеке потокового ввода-вывода Си++ возможность форматирования передаваемых данных также существует, но допол- нительно имеется и широко используется новый механизм автомати- ческого распознавания типов вводимых н выводимых данных. Он работает подобно механизму перегрузки функций. Потоковые объек- ты cin, cout, сегг, clog построены таким образом, что ввод и вывод выполняются по-разному в зависимости от типов правого операнда операций вставки « и извлечения ». В текстах программ мы уже не- однократно пользовались этим свойством объектов cin и cout. Опе- ратор cout « "\nl234 « " « 1234; выведет с новой строки последовательность символов 1234 , а затем целое число со значением 1234. И никакой подсказки от програм- миста не требуется! На основе анализа типа выражения, помещенного справа от операции включения «, в поток помещаются коды внешне- го представления данных соответствующих типов. Подобная ситуация и при вводе данных. По существу, операция » извлечения из потока не одна, а существуют три по-разному выпол- няемых операции: • для целых чисел; • для вещественных чисел; • для строк. Все они при чтении по умолчанию игнорируют ведущие пробелы, но затем выполняются по-разному, в зависимости от типа правого операнда. Иллюстрируя сказанное, рассмотрим, как будет воспринята одна и та же последовательность символов, набираемая на клавиату- ре, если справа от cin » поместить разные типы данных, но все они будут соответствовать одному и тому же участку памяти: //Р11-01.СРР - "стандартная" перегрузка операций «, » // для базовых типов linclude <iostream.h> void main () { union ( long integer;
Глава 11. Ввод-вывод в языке Си++ 389 char line[4]; float real; } mix; cout « "\п\пВведите целое число (mix.integer): cin » mix.integer; cout « "mix.integer-" « mix.integer; cout « "\nmix.line»" « mix.line; cout « "\nmix.real»" « mix.real; cout « "\п\пВведите строку (mix.line): "; cin : >> mix.line; cout « "mix.integer « " « mix.integer; cout « "\nmix.line » ,r « mix.line; cout « "\nmix.real » " « mix.real; cout « "\п\пВведите вещественное число (mix.real): "; cin : ►> mix.real; cout « "mix.integer « " « mix.integer; cout « "\nmix.line я " « mix.line; cout « "\nmix.real » " « mix.real; } Результат выполнения программы: Введите целое число (mix.integer): 888 <Enter> mix.integer » 888 mix.line « xV mix.real я 1.244353e-42 Введите строку (mix.line): 888 <Enter> mix.integer я 3684408 mix.line я 888 mix.real » 5.162955e-39 Введите вещественное число (mix.real): 888 <Enter> mix.integer я 1147011072 mix.line я mix.real я 888 В программе определено объединение mix, сопоставляющее один и тот же участок памяти длиной 4 байта с данными разных типов long, char [4], float. Элементы объединения mix. integer, mix. line, mix.real используются в качестве правых операндов операций извле- чения из потока » и включения в поток «. В зависимости от типа операнда одна и та же последовательность символов, набираемая на клавиатуре (в примере это 888), воспринимается и заносится в память либо как char [4], либо как long, либо как float. При выводе в поток
390 Язык Си++ cout одно и то же внутреннее значение участка памяти, отведенного для объединения mix, воспринимается и отображается на экране по- разному в зависимости от типа правого операнда. В библиотеке вво- да-вывода Си++ для обеспечения указанных возможностей использу- ется тот же самый механизм перегрузки. Объекты cout, cin с операциями «, » "знают”, как выполнять ввод-вывод значений раз- ных типов. Еще раз обратим внимание на результаты выполнения програм- мы. "Правильно" выводятся значения именно тех типов, которые вве- дены. "Неправильные" значения других типов не всегда понятны. Например, после ввода mix.real строка mix.line почему-то оказа- лась пустой. По-видимому, в первом байте массива char line [4] на- ходится код нуля. Объяснение "неправильных" результатов вывода требует рассмотрения внутренних представлений, которые различны на разных ЭВМ, для разных компиляторов и даже для разных испол- нений программы. Некоторые особенности операций вставки и извлечения. Обратите внимание, что операции » и « обеспечивают связи с потоками толь- ко в том случае, если они употребляются справа от имен потоковых объектов. В противном случае они как обычно обозначают операции сдвига. В соответствии с синтаксисом языка (см. табл. 2.4 "Приоритеты операций") операции сдвига «, » имеют невысокий приоритет. Им "предшествуют", например, все арифметические опе- рации, преобразования типов, скобки и др. Использование операций «, » для обозначения передач данных в потоки и из потоков не из- меняет их приоритета. Поэтому допустима, например, такая запись: cout «2+3+4; В результате выполнения этого оператора на экран будет выведе- но значение 9. Таким образом, арифметические выражения можно без скобок использовать в качестве правых операндов с операцией вклю- чения (вывода) данных в поток. Чтобы вывести в поток значение выражения, содержащего опера- ции более низкого ранга чем «, требуется применение скобок: cout « (а + Ь < с) ; Так как условная операция ?: и операции сравнения имеют более низкий приоритет, чем операция сдвига <<, то следующий оператор для отрицательного значения х cout « х < 0 ? -х : х;
Глава 11. Ввод-вывод в языке Си++ 391 никогда не сможет вывести абсолютное значение переменной х. Пра- вильная запись: cout « (х < 0 ? -х : х); Операции сдвига можно использовать в выводимых выражениях, но обязательно должны быть использованы скобки. Следующий опе- ратор cout « (2 « 1); выведет в поток (и на экран) значение 4. Выражения, в которые входят операции » и «, должны иметь значения. В соответствии с определением, находящимся в файле iostream.h, значением выражения cout « выражение является ссылка на объект cout, т.е. операция включения « возвра- щает ссылку на тот потоковый объект, который указан слева от нее в выражении. Следовательно, к результату выполнения операции вклю- чения можно вновь применить операцию «, как и к объекту cout. Таким образом рационально применять "цепочки” операций вывода в поток. Например, так: cout « "\пх * 2 ж " « х * 2; С помощью скобок можно следующим образом пояснить порядок вычисления этого выражения: (cout « "\пх * 2 = ") « х * 2; Если значением х служит 33, то в результате выполнения любого из этих операторов с новой строки на экран будет выведено: х * 2 = 66 Для к равного 1 после выполнения оператора cout « "\п к * 2 « ” « (к « 1) « " к « 2 ж " « (к « 2) ; результат на экране дисплея будет таким: к * 2 « 2 к «2 = 4 При записи цепочек операторов вывода нужно не забывать о при- оритете операций и помнить, что правила трактовки могут зависеть
392 Язык Си++ от реализации компилятора. Например, после выполнения следующих операторов: int k=l; cout « "\nk++ = " « k++ « " (k+=3) = " « (k+=3) « ”k++ = « « k++; на экране получим: k++ = 5 (k+=3) = 5 k++ = 1 Зависимость от компилятора результатов выполнения цепочки операций включения и необходимость аккуратно учитывать приоритеты операций приводят к следующей нарушенной в этом примере рекомендации [13]: изменяемая переменная не должна появляться в цепочке вывода более одного раза. "Цепочки" операций обмена можно формировать и при вводе данных (при их извлечении, т.е. чтении) из потока. Например, для int i, j , k, 1; следующий оператор: cin » i » j > k » 1; обеспечивает ввод последовательности целых значений переменных i, j, k, 1. Элементы этой последовательности во входном потоке должны разделяться обобщенными пробельными символами (пробелами знаками табуляции, символами перевода строк). Исходные данные можно ввести либо размещая их на экране в одну строку (извлечение h i потока cin происходит только после сигнала от клавиши Enter): 1234 <Enter> либо помещая каждое значение на отдельной строке, т.е. нажимам клавишу Enter после каждого вводимого значения: 1 <Enter> 2 <Enter> 2 <Enter> 4 <Enter> Отметим, что при вводе "в одну строку", когда значения разделяются обобщенными пробельными символами, количество чисел, набранных на клавиатуре, может превышать количество операций извлечения >> Например, тот же результат будет получен (введен), если набрать: 123456789 <Enter>
Глава 11. Ввод-вывод в языке Си++ 393 При вводе и выводе значений элементов массивов необходимо яв- но использовать их обозначения в виде индексированных переменных. Например, попытка выполнить операторы: float real[3] ~ ( 10.0, 20.0, 30.0 }; cout « real; Приведет к выводу на экран только адреса первого элемента массива, ибо имя массива воспринимается как указатель со значением адреса начала массива. Чтобы вывести значения элементов, их нужно явно обозначить с помощью индексирования: ©out « real[0] « " ” « real[l] « " '• « real(2]; При попытке ввести значения элементов массива с помощью опе- ратора cin » real; Получим сообщение об ошибке на этапе компиляции. Для операторов double е[5]; for (int i - 0; i < 5; i++) cin » e[i]; на клавиатуре необходимо набрать, например, такую последователь- ность значений: 0.01 0.02 0.03 0.04 0.05 <Enter> При вводе-выводе целых чисел существуют ограничения на длины внешнего и внутреннего представлений. Например, если при выпол- нении операторов int i; cin » i; cout « "\ni « ” « i; набрать на клавиатуре число 123456789, то результатом, выведенным на экран дисплея, будет (в конкретном случае) i “ -13035 Та же последовательность цифр 123456789, набираемая на клавиа- туре для длинного целого long g, будет безболезненно воспринята и выведена операторами: cin » g; cout « ”\ng « " « g;
394 Язык Си++ При выводе в стандартный поток правым операндом может быть любая константа, допустимая реализацией компилятора. Например, при выполнении оператора cout « 123456789; все будет в порядке - именно такое значение будет выведено на экран Оператор с недопустимой константой, например, такой: cout « 98765432100; приведет к выводу неверного числового значения. В соответствии с ограничениями реализации компилятора ВС++ справа от знака включения « можно записывать целые константы от 0 до 4294967295 (см. табл. 2.1). При вводе целочисленного значения его можно набрать на кла виатуре в восьмеричном или шестнадцатеричном виде, например, при выполнении операторов int N; cin » N; cout « "\nN - " « N; можно ввести восьмеричное значение 077777 и получить ответ в деся- тичном виде: N ж 32767 Введя шестнадцатеричное значение -0x7FFF, получим N « -32767 И Т.Д. Значения указателей (т.е. адреса) выводятся в стандартный поток в шестнадцатеричном виде, как мы это видели в гл. 5. Вывод числово го значения, например типа int, в шестнадцатеричном или восьмерич ном виде по умолчанию не выполняется. Для смены десятичного основания необходимо специальное "воздействие" на поток. Средства такого управления потоком будут рассмотрены позже, хотя в про грамме Р5-03.СРР мы уже использовали флаг hex, переключающий поток cout на вывод числовых значений в шестнадцатеричной форме. При вводе вещественных чисел допустимы все их формы записи, т.е. они могут быть представлены во входном потоке (введены с кла- виатуры) в форме с фиксированной точкой либо с указанием мантис- сы и порядка, т.е. в форме с плавающей точкой. Например, операторы float pi; cin » pi; cout « "pi « ’• « pi;
Глава 11. Ввод-вывод в языке Си++ 395 при вводе числового значения в виде 3.141593, или3.141593е0, или +3.141593, или 0.3141593е+1 всегда приведут к печати: pi = 3.141593 Если при вводе вещественного значения набрать на клавиатуре 3,1415^2^35897932385 <Enter> то в выходной поток cout опять будет выведено pi == 3.141593 Если вещественное значение слишком мало и при записи с фиксированной точкой значащая часть выходит за разрядную сетку, то вывод происходит в форме с плавающей точкой. Для последова- тельности float at; cin » at; cout « "\nat - " « at; При вводе с клавиатуры значения Я 0.00000000000000000000001 <Enter> г на экран будет выведено: J at = 1е-23 I Тот же результат будет выведен, если на клавиатуре набрать 1.0е—23. Наличие буфера в стандартном входном потоке создает некоторые особенности. В процессе набора данных на клавиатуре они отобра- жаются на экране, но не извлекаются из потока. Это дает возможность исправлять допущенные ошибки во вводимых данных до того, как значения будут выбраны из входного потока. Извлечение данных из потока, т.е. собственно выполнение операции cin », происходит только после нажатия клавиши Enter. При этом вся набранная строка переносится в буфер ввода, и именно из буфера ввода начинается "чтение’'. При извлечении числовых данных игнорируются начальные пробельные символы. Чтение начинается с первого непробельного символа и заканчивается при появлении нечислового символа. При вводе целых читаются символы десятичные цифры и, если число вводится в шестнадцатеричном виде (признак Ох), то буквы А, а, %b,c,c,D,d,E,e,F,f. Для вещественных чисел дополнительно может ^явиться символ е или е, как обозначение экспоненциальной части ела, и точка, отделяющая целую часть от дробной. Выборка из
396 Язык Си++ входного потока прекращается, как только очередной символ окажет- ся недопустимым. Например, если для операторов int К; float Е; cin » К » Е; cout « "К * " « К « " Е « " « Е; набрать на клавиатуре 1234.567 89 <Enter> то получим К ж 1234 Е « 0.567 Здесь точка явилась недопустимым символом для к, но корректно воспринята при вводе значения Е. Символы 89 из входного потока проигнорированы, так как извлечение закончилось при достижении пробела, а больше операция » не применяется. При вводе из стандартного потока вещественного значения можно набрать на клавиатуре большое целое число. Например, для операто- ров: double D; cin » D; cout « "D “ " « D; введя с клавиатуры 112233445566778899 <Enter> получим округленное значение: D = 1.1223340+17 Ввод-вывод массивов и символьных массивов-строк - это разли- чающиеся процедуры. Как известно, символьная строка всегда пред- ставляется как массив типа char, последним значащим элементом в котором является литера ’\о*. Именно до этой литеры оператор вы- вода переносит в выходной поток символы из памяти: char Н[] ж ”Qui pro quo - путаница"; cout « "\n" « H; На экране дисплея: Qui pro quo - путаница Если в той же программе добавить операторы
Глава 11. Ввод-вывод в языке Си++ 397 char *рН « Н; cout « ”\п" « pH; то результат не изменится - будет выведена строка, с которой связан указатель pH. Операция вывода «, "настроенная" на операнд типа char *, всегда выводит строку, а не значение указателя, связанного с этой строкой. Чтобы вывести не значение строки, а значение указате- ля, необходимо явное приведение типа. Например, оператор cout « ”\пуказаталь “ ” « (void *)рН; выведет не значение строки, начало которой есть значение указателя, а собственно адрес. Для указателя, не связанного с символьной стро- кой (т.е. не имеющего типа char *), и вывод указателя, и вывод ре- зультата его приведения к типу (void *) будут одинаковы. Например, выполнение операторов: int *pi,i ж 6; pi ж fii; cout « n\npi » " « pi; cout « "\n(void *)pi e ” « (void *)pi; приведет к печати одинаковых значений: pi = 0x1 Обе (void*)pi — 0x10бе Интересно, что в некоторых случаях операторы char сс[5] = ( ’a’, 'b', 'с1, 'd', ’е’ ); cout « "\пс » ” « сс; приведут к выводу сс == abcde но надеяться на это нельзя. При вводе строки на клавиатуре набираются любые символы до тех пор, пока не будет нажата клавиша Enter. Например, для опера- торов char line[255)r stroka[80]; cin » line » stroka; на клавиатуре может набираться любая последовательность симво- лов, пока не появится код от клавиши Enter. Система ввода-вывода переносит эту последовательность в буфер входного потока, а из бу- фера при выполнении каждой операции » извлечение происходит до ближайшего пробела. Вместо пробельного символа заносится код
398 Язык Си++ » \о ’, тем самым завершая строку. Если при выполнении этого опера- тора ввода набрать на клавиатуре: ERGO (следовательно) (лат.) получим line="ERGO", И stroka»»" (следовательно)". СИМВОЛЫ " (лат.)" не будут восприняты. Так как операции «, » "настроены" на заранее определенные ти- пы данных/то иногда возникают несоответствия, которых могло бы не быть при использовании библиотечных функций ввода scanf () и вывода printf () из стандартной библиотеки языка Си. Например, рассмотрим попытку вывести на экран (в поток cout) значение сим- вола, получаемого от клавиатуры с помощью библиотечной функции getch (), описанной в файле conio.h: cout « "От клавиатуры поступил символ: " « getch(); Если на клавиатуре нажать клавишу • в', то на экране получим: От клавиатуры поступил символ: 66 Дело в том, что функция getch () ввода символа от клавиатуры без отображения на экране возвращает значение типа int, т.е. имеет прототип: int getch(void); Поэтому печатается не изображение символа 'В', а его целочис- ленный код. Чтобы получить изображение символа, необходимо яв- ное приведение типа: cout « (char)getch(); 11.3. Форматирование данных при обменах с потоками Форматирование пересылаемых данных. Непосредственное при- менение операций вывода « (включение в поток) и ввода » (извлечение из потока) к стандартным потокам cout, cin, сегг, clog для данных базовых типов приводит к использованию "умалчивае- мых" форматов внешнего представления пересылаемых значений. На- пример, при выводе чисел каждое из них занимает ровно столько по-
Глава 11. Ввод-вывод в языке Си**** 399 1Иций, сколько необходимо для его представления. Это не всегда удобно и правильно. Например, выполнение операторов int il = 1t 12*2, i3 я 3, i4 = 4, i5 = 5; cout « "\n" « il « 12 « 13 « i4 « 15; приведет к такому результату: 12345 Для улучшения читаемости проще всего явно ввести разделитель- ные пробелы. Выполнив оператор oout « ”\n,,« il«‘ *« х2«' '« i3«’ ’« i4«' ’« i5; получим более наглядный результат: 1 2 3 4 5 Следующий шаг - добавление пояснительного текста и(или) сим- волов табуляции. Эти приемы мы уже неоднократно применяли в программах, но никак не изменяли формат самих выводимых значе- ний. Ширина (количество позиций) внешнего представления каждого числа выбирается автоматически, исходя из необходимого количества позиций. Единообразие не всегда устраивает пользователя програм- мы. Например, периодическую дробь 1.0 / 3.0 можно представить весьма различными способами: 0.3 О.3333 3.Зе-1 0.ЗЗЗЗЗЗЗеО Однако стандартное представление при выводе с помощью опера- тора cout « "\nl.o / 3.0 « " « 1.0 / 3.0; будет всегда одинаковым: 1.0 / 3.0 = 0.333333 Такое поведение выходного потока при использовании операции включения со значением типа double предусматривается по умолча- нию. Форматы представления выводимой информации и правила восприятия данных, вводимых из потока, могут быть изменены про- граммистом с помощью флагов форматирования. Эти флаги унасле- дованы всеми потоками библиотеки из базового класса ios. Флаги реализованы в виде отдельных фиксированных битов чисел типа long, поэтому несколько флагов с помощью логических битовых выраже- ний можно объединять, тем самым по-разному комбинируя свойства
400 ЯзыкСи++ потока. Перечислим флаги форматирования, объясняя их действия для тех значений, которые указаны справа от знаков присваивания: skipws « 0x0001 при таком значении флага операция извлечения из потока » будет игнорировать (пропускать) обобщенные пробельные символы; left « 0x0002 вывод значения с левым выравниванием (прижать к левому краю поля); right == 0x0004 вывод значения с правым выравниванием (это значение уста- навливается по умолчанию); internal » 0x0008 принятый в качестве заполнителя символ (по умолчанию про- бел) помещается между числовым значением и знаком числа либо признаком основания системы счисления (см. ниже ком- понент ios: :x_fill); dec = 0x0010 десятичная система счисления; oct » 0x0020 восьмеричная система счисления; hex = 0x0040 шестнадцатеричная система счисления; showbase = 0x0080 напечатать при выводе признак системы счисления (0х для шестнадцатеричных чисел, о - для восьмеричных чисел); showpoint “ 0x0100 при выводе вещественных чисел обязательно печатать деся- тичную точку и следующие за ней нули (даже для веществен- ного числа, имеющего нулевую дробную часть); uppercase = 0x0200 при выводе чисел использовать буквы верхнего регистра: сим- вол х и буквы abcdef для шестнадцатеричных цифр, указа- тель порядка Е для чисел с плавающей точкой; showpos =* 0x0400 печатать знак числа (символ * + ') при выводе положительных чисел; scientific « 0x0800 для вещественных чисел (типов float, double) использовать представление в формате с плавающей точкой (научное пред-
Глава 11. Ввод-вывод в языке Си++ 401 ставление), т.е. с указанием порядка и мантиссы, имеющей од- ну ненулевую (значащую) цифру перед точкой; fixed а 0x1000 для вещественных чисел (типов float, double) использовать представление в формате с фиксированной точкой, причем количество цифр дробной части определяется заданной по умолчанию точностью (см. ниже переменную xjprecision); unitbuf = 0x2000 очищать все потоки (выгрузить содержимое буферов) после каждого вывода (после включения в поток); Btdio = 0x4000 очищать потоки stdout, stderr (выгрузить содержимое бу- феров) после каждого вывода (после включения в поток). Все флаги форматирования в виде отдельных фиксированных би- тов входят в компонент класса ios: long x_flags; // Переменная представления флагов // форматирования Именно эта переменная, относящаяся к конкретному потоку, ана- лизируется при обменах и влияет на преобразование информации. В библиотеке классов ввода-вывода существуют принадлежащие классу ios функции flags () и setf () для проверки значений перечисленных флагов, для установки флагов и для их сбрасывания в исходные (умалчиваемые) состояния. Флаги могут обрабатываться как по от- дельности, так и группами, для чего используют дизъюнктивные вы- ражения, в которых флаги связаны побитовой операцией »| * (ИЛИ). Кроме флагов для управления форматом используются следующие компонентные переменные класса ios: int x_width задает минимальную ширину поля вывода; int x_jpr®cision задает точность представления вещественных чисел, т.е. мак- симальное количество цифр дробной части при выводе; int определяет символ заполнения поля вывода до минимальной ширины, определенной x_width. По умолчанию x_fill имеет значение пробела. 26-3821
402 Язык Си++ Для изменения компонентных переменных x_flags, x_width, x_fill, xjprecision программист может использовать общедоступ- ные функции класса ios: static long bitalloc () ; возвращаемое значение может быть использовано для уста- новки, очистки и проверки флагов. Функция предназначена для заданных пользователем флагов форматирования', char fill(); возвращает текущий символ заполнения незанятых (пустых) позиций поля вывода; char fill(char); заменяет символ заполнения значением параметра, возвра- щает предыдущий символ заполнения; long flags(); возвращает текущий набор битов флагов форматирования; long flags(long); устанавливает биты флагов форматирования в соответствии со значением параметра. Возвращает предыдущее значение флагов; int precision(); возвращает текущее значение точности представления при выводе вещественных чисел (типа float и double); int precision(int n); устанавливает по значению параметра п точность представле- ния вещественных чисел, возвращает предыдущее значение точности; long setf(long); устанавливает флаги в соответствии с тем, как они помечены в фактическом параметре. Возвращает предыдущую установку флагов; long setf(long _setbits, long _field) ; устанавливает флаги в соответствии со значениями пара- метров. Биты, помеченные в параметре _f ield, сбрасываются (очищаются), а затем устанавливаются те биты, которые от- мечены в параметре _setbits; long unsetf(long); сбрасываются (очищаются) все биты флагов, которые помече- ны в параметре. Функция возвращает предыдущее значение флагов; int width() ;
Глава 11. Ввод-вывод в языке Си** 403 возвращает установленное значение ширины поля; int width(int); устанавливает значение ширины поля в соответствии со зна- чением параметра; static int xalloc(); возвращает индекс массива до сих пор не использованных слов, которые можно использовать в качестве определенных флагов форматирования. Следующие компоненты (константы) класса ioa определены как статические, т.е. существуют в единственном экземпляре для класса в целом и требуют при обращении указания имени класса (ios::). В определении класса ios они описаны таким образом: static const long adjustfield; // left | right | internal static const long basefield; // dec | oct I hex static const long floatfield; // scientific | fixed Каждая из этих констант объединяет несколько установленных битов флагов форматирования. Эти константы удобно использовать в том случае, когда перед установкой флага требуется сбросить все фла- ги, которые не могут быть одновременно с ним установлены. Для сбрасывания флагов константа используется в качестве второго пара- метра функции setf (). Объяснять тонкости применения перечисленных компонентов класса ios нужно на примерах, причем понимание смысла и значи- мости отдельных компонент приходит только с учетом их взаимосвя- зей. В следующей программе демонстрируются основные принципы форматирования с помощью компонентных функций класса ios. От- метим, что определение класса ios включается в программу автома- тически, так как файл iostream.h содержит описания классов, производных от класса ios. //Р11-02 .СРР - форматирование выводимой информации ^include <strstrea.h> void main () ( char name[] s "Строка длиной 52 (зааола ” "в поле шириной 58 позиции.”; cout « "\n\n"; cout.width(58); // Ширина поля вывода для потока cout // Символ заполнения пустых позиций поля: cout.fill(1 $1); // Первый вывод строки в поток cout:
404 ЯзыкСи++ cout « паше « endl; cout.width(58); // Убрать нельзя // Заполнитель между знаком и значением: cout.setf(ios::internal); double dd - -33.4455; cout « dd « endl; // Вывод вещественного значения cout.width(58); // Убрать нельзя // Смена выравнивания: cout.aetf(ios::left,ios::adjustfield); // Символ заполнения пустых позиции поля: cout.fill(•#'); // Второй вывод строки в поток cout: cout « папе « endl; long пп ж 90000; // Пестнадцатеричное значение 0xl5f90 // Смена основания счисления: cout.setf(ios::hex,ios:zbasefield); // Выводить признак основания счисления: cout.setf(ios::showbase); // Переход на верхний регистр: cout.setf(ios::uppercase); cout.width(58); /У Убрать нельзя cout « nn « endl; 11 Вывод целого значения типа long cout.width(58); // Убрать нельзя // Смена выравнивания: cout.setf(ios::internal,ios::adjustfield); / / Символ заполнения пустых позиций поля: cout.fill(1 $1) ; cout.unsetf(0x0200); // Переход на нижний регистр cout « nn « endl; // Вывод целого значения типа long ) Результат выполнения программы: $$$$$$Строка длиной 52 символа в поле шириной 58 позиций. -$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$33.4455 Строка длиной 52 символа в поле шириной 58 позиций. ###### 0X15F90***#***O**»»»mH*«»**«**«OO«OO*»O»***OO» 0x$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$15f90 Прокомментируем программу и результаты. По умолчанию ши- рина поля вывода устанавливается равной длине принятого представ- ления выводимого значения. Поэтому действие функции width О
Глава 11. Ввод-вывод в языке Си** 405 однократное, и ее нужно при каждом выводе значения явно использо- вать, если умалчиваемое значение ширины поля вывода не устраивает программиста. Функция устанавливает символ заполнения Пустых позиций поля. При первом выводе строки name [ ] по умолча- f иию установлено выравнивание по правому краю поля, и символы ' $ • помещены слева от содержимого строки. Перед выводом значения вещественной переменной dd функцией setf() установлен флаг ( internal. Под его влиянием символ заполнения разместился между знаком • -1 и числовым значением 33.4455. Ширина поля явно указа- на в 58 позиций. Перед вторым выводом строки name[] "под влиянием" второго параметра (adjustfield) функции setf () сброшены флаги right и * internal и явно установлен флаг left выравнивания по левому краю. Изменен символ заполнения пустых позиций 1 #'. Перед выво- J дом длинного целого числа пп установлено основание системы счис- ( ления (basefield - сбрасывает флаги оснований счисления; hex - явно устанавливает шестнадцатеричное основание). Установлены флаги showbase и uppercase и ширина поля вывода. 1 Число 90000 выведено в шестнадцатеричном виде, признаком ох < обозначено основание системы счисления, для изображения шестнад- цатеричных цифр и признака основания используются прописные I, буквы. Так как при переходе к выравниванию по левому краю флаг * internal оказался сброшенным, то символ заполнения '#* размещен не после признака основания счисления ох, а заполняет правую пустую часть поля. Заключительный вывод значения пп, равного ' 90000, выполнен с флагами internal и left. Для перехода на нижний регистр использована функция unsetf() с явным значением флага uppercase. 4 Манипуляторы. Несмотря на гибкость и большие возможности управления форматами с помощью компонентных функций класса ios, их применение достаточно громоздко. Более простой способ из- менения параметров и флагов форматирования обеспечивают мани- пуляторы, к возможностям которых мы перейдем. Манипуляторами называют специальные функции, позволяющие программисту изменять состояния и флаги потока. Особенность ма- нипуляторов и их отличие от обычных функций состоит в том, что их имена (без параметров) и вызовы (с параметрами) можно использо- вать в качестве правого операнда для операции обмена (« или »). В качестве левого операнда в этом выражении, как обычно, использует-
406 Язык Си++ ся поток (ссылка на поток), и именно на этот поток оказывает влияние манипулятор. Прежде чем переходить к перечислению манипуляторов и их свойств, напомним, что мы уже пользовались некоторыми из них (см. гл. 5, программу Р5-03.срр). Например, манипулятор hex позволяет устанавливать шестнадцатеричное основание счисления выводимых в поток cout числовых значений. Выполнив последовательность операторов: cout « "ХпДесятичное число: "« 15 « hex; cout « "ХпШестнадцатеричяое представление: "« 15; получим на экране: * Десятичное число: 15 Шестнадцатеричное представление: OxF Как наглядно показывает результат, применение манипулятора hex изменило одно из свойств потока cout. Десятичная целая кон- станта 15 воспринимается и выводится на экран в шестнадцатеричном виде. В качестве примера каждый манипулятор автоматически (без явного участия программиста) получает ссылку на тот поток, с которым он используется в выражении. После выполнения манипулятора он возвращает ссылку на тот же поток. Поэтому манипуляторы можно использовать в цепочке включений в поток или извлечений из потока При выполнении манипулятора никаких обменов данными с потоком не происходит, но манипулятор изменяет состояние потока. Например, выполнив оператор с манипуляторами hex, oct, dec: cout « 15 « hex « 15 « oct « 15 « dec « 15; в качестве результата получим: 150xF01715 Манипуляторы библиотеки классов ввода-вывода языка Си++ де лятся на две группы: манипуляторы с параметрами и манипуляторы без параметров. Манипуляторы без параметров: dec - при вводе и выводе устанавливает флаг десятичной системы счисления; hex - при вводе и выводе устанавливает флаг шестнадцатеричной системы счисления;
Глава 11. Ввод-вывод в языке Си++ 407 oct - при вводе и выводе устанавливает флаг восьмеричной си- стемы счисления; ws - действует только при вводе и предусматривает извлечение из входного потока пробельных символов (пробел, знаки табуляции '\t' и '\v, символ перевода строки '\п', сим- вол возврата каретки ’\г', символ перевода страни- цы \f’); endl - действует только при выводе, обеспечивает включение в выходной поток символа новой строки и сбрасывает буфер (выгружает содержимое) этого потока; ends - действует только при выводе и обеспечивает включение в поток нулевого признака конца строки; flush - действует только при выводе и очищает выходной поток, т.е. сбрасывает его буфер (выгружает содержимое буфера). Обратите внимание, что не все перечисленные манипуляторы дей- ствуют как на входные, так и на выходные потоки, из действует толь- ко при вводе; endl, ends, flush - только при выводе. Манипуляторы dec, hex, oct, задающие основание системы счис- ления, изменяют состояние потока, и это изменение остается в силе до следующего явного изменения. Манипулятор endl рекомендуется использовать при каждом вы- воде, который должен быть незамедлительно воспринят пользовате- лем. Например, его использование просто необходимо в таком операторе: cout « "Ждите! Идет набор статистики." « endl; При отсутствии endl здесь нельзя гарантировать, что сообщение пользователю не останется в буфере потока cout до окончания набо- ра статистики. Рекомендуется с помощью манипулятора flush сбрасывать буфер входного потока при выводе на экран подсказки до последующего ввода информации: cout « "Введите название файла:" « flush; cin » fileName; // Здесь filename - символьный массив Манипуляторы с параметрами определены в файле iomanip.h: setbase(int n) устанавливает основание (п) системы счисления. Значениями параметра п могут быть: 0, 8, 10 или 16. При использовании
408 Язык Си++ параметра 0 основание счисления при выводе выбирается де- сятичным. При вводе параметр 0 означает, что целые десятич- ные цифры из входного потока должны обрабатываться по правилам стандарта ANSI языка Си; resetiosflags(long L) сбрасывает (очищает) отдельные флаги состояния потоков ввода и вывода на основе битового представления значения параметра L (сбрасывает флаги в соответствии с единичными битами); setiosflags(long L) устанавливает отдельные флаги состояния (форматные биты) потоков ввода-вывода на основе битового представления зна- чения параметра L (устанавливаются флаги в соответствии с единичными битами параметра); setfill(int n) значение параметра п в дальнейшем используется в качестве кода символа-заполнителя, который помещается в незанятых позициях поля при вводе значения. (См. компонент xefill класса ios.); setprecision(int n) определяет с помощью значения параметра п точность пред- ставления вещественных чисел, т.е. максимальное количество цифр дробной части числа при вводе и выводе. (См. компо- нент x^jprecision класса ios.); setw(int n) значение параметра п задает минимальную ширину поля вы- вода. (См. компонент x_width класса ios.) С помощью манипуляторов можно управлять представлением ин- формации в выходном потоке. Например, манипулятор setw(int п) позволит выделить для числового значения поле фиксированной ши- рины, что удобно при печати таблиц. Итак, для управления форматом вывода (включения в поток) класс ios имеет следующие компоненты: • компонент (атрибут) xef lags; • функции доступа к атрибуту xeflags: flags () и self (); • атрибуты управления форматом: x_width, xjprecision, x_f ile; • принадлежащие классу функции: width (), precision (), file О; • манипуляторы (вставляемые в цепочки обмена).
Глава 11. Ввод-вывод в языке Си++ 409 11.4. Ввод-вывод для типов, определенных пользователем При передачах данных базовых типов потоки cin, cout «знают» как выполнять преобразования значений разных типов. Напомним, что при перегрузке функций конкретная реализация выбирается в со- ответствии с сигнатурой, т.е зависит от фактических параметров. Подобным образом в выражениях cin » операнд и cout « операнд каждому типу правого операнда соответствует свое правило преобразования данных. Эти правила заранее оформлены в виде процедур (функций) специального вида, называемых операциями- функциями”. Определения этих функций находятся в библиотеке ввода- вывода, а прототипы размещены в заголовочном файле iostream.h. Чтобы использовать операции обмена >> и « с данными произ- водных типов, определяемых пользователем, необходимо расширить действие указанных операций, введя новые операции-функции. Каждая из операций обмена « и » бинарная, причем левым операндом служит объект, связанный с потоком, а правый операнд должен иметь желаемый тип. Этот бинарный характер операций обмена отражает спецификация параметров соответствующих операций-функций. Первый параметр - ссылка на объект потокового класса (тип istreamfi либо ostreamfi), второй параметр - ссылка или объект желаемого типа. Тип возвращаемого значения должен быть ссылкой на тот поток, для которого предназначена операция. Таким образом, формат операции- функции для перегрузки операций таков: ostreamfi operator « (ostreamfi out, новый_тип имя) { ... // Любые операторы для параметра нового__типа out « ... // Вывод значений нового_типа return out; // Возврат ссылки на объект класса ostream } Здесь новый__тип - тип, определенный пользователем. Таким обра- зом, если определить структурный тип (класс): struct point // Точка трехмерного евклидова пространства ( float х; // Декартовы координаты точки float у; float z; }
410 Язык Си++ то для типа point можно определить правила вывода (по умолчании) на экран дисплея) значений, например, с помощью такой операции- функции: ©streams // Тип возвращаемого значения operator « (©streams t, point d) { return t « "\nx = ” « d.x « ’’ у = " « d.y « " z = " « d.z; } Отметим, что в теле функции с "названием" operator «, операция « используется в бинарном выражении с объектом типа ostream& в качестве левого операнда. Здесь правыми операндами для каждой операции включения в поток служат значения стандартных (базовых) типов - символьные строки и вещественные числа типа float. Результат операции t « "\пх = ” -ссылка на объект типа ostream. Эта ссылка используется в качестве левого операнда при выводе « d.x и т.д. Так как d - имя формального параметра структурного типа point, то для обращения к компонентам структуры (x,y,z) используются уточненные имена d.x, d.y, d. z. В результате выполнения всей цепочки операций вывода в теле операции-функции формируется ссылка на объект типа ostream, и именно это значение ссылки возвращает в точку вызова оператор return. Тем самым операция включения в поток«. применяемая в вызывающей программе к операнду типа point, вернет ссылку на выходной поток. Таким образом, сохраняется возможность применения операции « несколько раз в одном выражении, как это разрешено для операндов базовых типов (т.е. допустимы цепочки вывода). Например, если, определив структуру point и введя для нее приведенную выше операцию-функцию operator <<() , выполнить программу void main () { point F = { 10.0, 20.0, 30.0 }; cout « "\пКоординаты точки: " «F; } то на экране дисплея получим: Координаты точки: х = 10.0 у = 20.0 z = 30.0 Как видите, в данном примере при выводе в поток cout равноправно используются и вывод значения базового типа char [ ], и вывод значений объекта F типа point, определенного пользователем.
Глава 11. Ввод-вывод в языке Си++ 411 Напомним, ЧТО класс ©stream, поток (объект) cout и "стандартные" режимы выполнения операции вывода (для базовых типов) определены в заголовочном файле iostream.h, который необ- ходимо поместить в начале текста программы до текста операции- функции operator « (). В качестве еще одного примера перегрузки (расширения действия) операции вывода «, рассмотрим следующую программу, в которой для представления почтового адреса используется структурный тип с названием address: //Р11-03.СРР - перегрузка операции вывода "«" «include <iostream.h> struct address // Почтовый адрес ( char*country; // Страна char *city; // Город char *street; // Улица int number_of_house; // Номер дома }; // Определение операции-функции, "распространяющей" // действие операции включения в поток « на операнд // типа address: ostreamfi // Тип возвращаемого значения operator « (ostreamfi out, address ad) ( out « "\nCountry: " « ad.country; out « "\nCity: " « ad.city; out « "\nStreat: " « ad.street; out « "\nHouse: " « ad. number_of_house; return out; void main О ( address ad ж { "Russia", "Moscow", "Arbat", 11 }; cout « "\пЗначекие структуры (почтовый адрес):"; cout « ad « "\n"; Результаты выполнения программы: Значение структуры (почтовый адрес): Country: Russia City: Moscow Street: Arbat House: 11 В теле операции-функции operator « () использовано несколь- ко операторов с выражениями, выводящими значения в поток вывода.
412 Язык Си++ В операторе return возвращается ссылка на него (имеющая тип ostreamfi). Как показано на примерах, перегрузка операции вывода « по- зволяет не только организовать с ее помощью вывод значений поль- зовательских типов, но и обеспечивает программиста широкими возможностями оформления результатов. К сожалению, расширить действие операции можно только на пользовательские типы и поэто- му невозможно непосредственно изменить формат вывода какого- либо из базовых типов. Например, не удастся ввести процедуру operator « (), с помощью которой при использовании операнда типа char * в стандартный поток будет выводиться и длина символь- ной строки, и ее содержимое. Чтобы решить указанную задачу, необходимо определить струк- туру, компонентами которой будут связанный со строкой указатель char * и целая переменная со значением, равным длине строки. Вот для такой структуры перегрузка операции вывода « вполне допусти- ма. В следующей программе это реализовано: //Р11-04.СРР - вывод информации о структуре-строке tinclude <iostream.h> tinclude <string.h> // Для работы co строковыми функциями // Определение класса (пользовательского типа) : struct string ( int length; char *line; }; / / Прототип операции-функции для перегрузки операции «: ostreamfi operator « (ostreamfi out, string str) ; void main() { string st; // Объект st класса string strcpy(st.line,"Содержимое строки."); st.length = strlen(st.line); cout «st; } ostreamfi // Тип возвращаемого значения operator « (ostreamfi out, string str) { out « "\n Длина строки: " « str.length; out « 11 \n Значение строки: " « str. line; return out; } Результат выполнения программы: Длина строки: 18 Значение строки: Содерзоосое строки.
Глава 11. Ввод-вывод в языке Си++ 413 Отметим, что в файле с тестом программы определение операции- функции помещено ниже, чем обращение к ней. Поэтому в вызы- вающую программу пришлось поместить прототип операции- функции. Для перегрузки (расширения действия) операции ввода » необхо- димо определить операцию-функцию вида: istreamf operator » (istreamfi in, яовий__тилЛ имя) ( // Любые операторы для параметра нового типа in » ... // Ввод значении нового типа return in; // Возврат ссылки на объект класса istream Здесь новый^тил - тип, определенный пользователем, т.е. некото- рый класс или его частный случай - структурный тип. Основное отли- чие от перегрузки операции вывода - необходимость в качестве второго параметра использовать ссылку. Для уже введенного выше структурного типа "точка трехмерного евклидова пространства" можно с помощью перегрузки операции ввода » очень изящно опре- делить последовательность ввода с консоли значений координат. На- пример, удобной может оказаться операция-функция, использованная в следующей программе: //Р11-05.СРР - перегрузка операции ввода » #include <iostream.h> struct point // Точка трехмерного евклидова пространства ( float х; float у; float z; ); istreamfi // Тип возвращаемого значения operator » (istreamfi in, pointfi d) { cout « “\n Введите три координаты точки: " « ”\nx = " ; in » d.x; cout « "y « "; in » d.y; ccut « 11 z « "; in » d.z; return in; } void main() { point D; cin » D; } При выполнении иа экране может быть, например, такой текст:
414 Язык Си++ Введите три координаты точки: х =* 100 <Enter> у = 200 <Enter> z = 300 <Enter> В предложенной операции-функции operator » () выполняется не только "чтение" (ввод) данных, набираемых пользователем на кла- виатуре (в примере 100, 200, 300), но и выводятся на экран соответ- ствующие подсказки, что упрощает использование программы. В данной программе операция-функция operator » () работает не только со входным потоком, ссылка на который передается как пара- метр, но и со стандартным выходным потоком cout, для которого обычным образом используется операция вывода «. Поэтому до определения операции-функции operator » () в текст программы необходимо ВКЛЮЧИТЬ заголовочный файл iostream.h. Следующая программа еще раз иллюстрирует сказанное. В про- грамме определен структурный тип employee, предназначенный для структур, содержащих сведения о некоторых "служащих": • фамилия — char name [50]; • оклад - long salary; • возраст - int age; Можно было бы ввести и другие компоненты, например, название отдела, должность, стаж работы и т.д., но для целей нашего примера это лишнее. В теле операции-функции operator » (), распростра- няющей действие операции ввода » на структуру типа employee, на экран (поток cout) выводятся подсказки и из потока типа istream считываются набираемые на клавиатуре данные. Текст программы: //Р11-06.СРР - перегрузка операций ввода » и вывода « tinclude <iostream.h> // Определение класса "служащий": struct employee // Определение класса "служащий" { char name[50]; // Фамилия long salary; // Оклад int age; // Возраст }; // Определение операции-функции, "распространяющей" // действие операции ввода » на операнд типа employee: istreamfi // Тип возвращаемого Значении operator » (istreamfi input, employee^ em) { cout « "\п\пВведите сведении о служащем:" « ”\пФамилии: ";
Глава 11. Ввод-вывод в языке Си++ 415 input » em.name; cout « "Оклад: input » em.salary; cout « "Возраст: "; input » em.age; return input; } //Прототип операции-функции для перегрузки операции «: cstreamfi operator « (ostreamfi, employee); void main(} ( employee E; // Определен объект класса employee cin » E; cout « E; ) // Определение операции-функции для перегрузки // операции «: озtгearn£ operator « (ostream£ out, employee e) ( out « "\пВведены следуюврга сведения о служащем:"; out « "\пИМя: " « е.паше; out « ", оклад: " « е.salary « " руб."? out « ", возраст: " « е.аде « " лет.\п"; return out; } Возможный результат выполнения программы: Введите сведения о служащем: Фамилия: Иванов <Enter> Оклад: 140000 <Enter> Возраст: 39 <Enter> Введены следующие сведения о служащем: Имя: Иванов, оклад: 140000 руб., возраст: 39 лет. В программе для структуры типа employee перегружены как опе- рация ввода », так и операция вывода «. 11.5. Функции для обмена с потоками Кроме операции включения (записи) в поток « и извлечения (чтения) из потока », в классах библиотеки ввода-вывода есть весьма полезные функции, обеспечивающие программиста альтернативными средствами для обмена с потоками.
416 Язык Си++ При выводе в качестве основного класса, формирующего выход- ные потоки, используется класс ©stream. В нем определены (ему при- надлежат) две функции для двоичного вывода данных: ostreamfi ostream::put(char сс); ostreamfi ostream::write(const signed char *array, int n); ostreamfi ostream::write(const unsigned char *array, int n); Функция put () помещает в тот выходной поток, для которого она вызвана, символ, использованный в качестве фактического парамет- ра. В этом случае эквивалентны операторы: cout « * Z' ; и cout.put('Z•); Функция write () имеет два параметра - указатель array на учас- ток памяти, из которого выполняется вывод, и целое значение п, определяющее количество выводимых из этого участка символов (байт). В отличие от операции « включения в поток функции put() и write () не обеспечивают форматирования выводимых данных. На- пример, если при выводе одного символа с помощью операции « можно, используя функцию width(), разместить его в поле из нужно- го количества позиций, то функция put() всегда разместит символ в одной позиции выходного потока. Флаги форматирования также не применимы к функциям put () и write (}. Так как функции put() и write о возвращают ссылки на объект того класса, для которого они выполняются, то можно организовать цепочку вызовов: char ss [ ] =* "Merci ” ; cout.put('\n*).write(ss,sizeof(ss)-1).put('!').put(•\n'); На экране (в потоке cout) появится: Merci < Если необходимо прочитать из входного потока строку символов, содержащую пробелы, то с помощью операции извлечения » это де- лать неудобно - каждое чтение строки выполняется до пробела, а ве- дущие (левые) пробельные символы игнорируются. Если мы хотим, набрав на клавиатуре строку: "Qui vivra verra - будущее покажет (лат.) ", ввести ее в символьный массив, то с помощью операции из-
Глава 11. Ввод-вывод в языке Си++ 417 влечения » это сделать несколько хлопотно, все слова будут читаться , отдельно (до пробела). Гораздо удобнее воспользоваться функциями бесформатного (двоичного) чтения. Функции двоичного (бесформатного) чтения данных принадлежат Потоку istream. Прежде чем перечислить их, отметим основное свой- ство двоичного чтения данных. Данные читаются без преобразования Их из двоичного представления в текстовое. Например, если во вход- ном потоке размещено представление вещественного числа 1. ЗЕ-з, то это будет воспринято как последовательность из шести байт, и читать эту последовательность с помощью функций двоичного ввода можно Только в символьный массив. Итак, функции чтения. Во-первых, это 6 перегруженных функций get (). Две из них имеют следующие прототипы: istreamfi get(signed char *array, int max_len, char«r\n'); istreamfi get(unsigned char *array, int maxlen, char='\n'); Каждая из этих функций выполняет извлечение (чтение) последо- вательности байтов из стандартного входного потока и перенос их в f символьный массив, задаваемый первым параметром. Второй пара- i метр определяет максимально допустимое количество прочитанных ’ байтов. Третий параметр определяет ограничивающий символ (байт), при появлении которого во входном потоке следует завершить чтение. ! По умолчанию третий параметр имеет значение '\п' - переход на следующую строку, однако при обращении к функции его можно за- давать и по-другому. Значение этого третьего параметра из входного ; потока не удаляется, он в формируемую строку (символьный массив) не переносится, а вместо него автоматически добавляется "концевой" символ строки • \0'. Если из входного потока извлечены ровно max_len - 1 символов, однако ограничивающий символ (например, по умолчанию 1 \п*) не встретился, то концевой символ помещается после введенных символов. Массив, в который выполняется чтение, должен иметь длину не менее шах_1еп символов. Если из входного потока не извлечено ни одного символа, то устанавливается код ошибки. Если до появления ограничивающего символа и до извлече- ния max_ien - 1 символов встретился конец файла EOF, то чтение прекращается как при появлении ограничивающего символа. Функция с прототипом istreamfi get(streambuf& buf, char = ’\n’); извлекает из входного потока символы и помещает их в буфер, опре- деленный первым параметром. Чтение продолжается до появления 27’3821
418 Язык Си++ ограничивающего символа, которым по умолчанию является ’\п', он может быть установлен явно любым образом. Три следующих варианта функции get() позволяют прочесть и. входного потока один символ. Функции istream& get (unsigned char& сс) ; istreamfi get(signed char& cc); присваивают извлеченный символ фактическому параметру и во < вращают ссылку на поток, из которого выполнено чтение. Функция int get(); получает код извлеченного из потока символа в качестве возвращае- мого значения. Если поток пуст, то возвращается код конца файл.> EOF. Функции "ввода строк": istreamS getline(signed char *array, int len, char= ‘\n’; istreams getline(unsigned char *array, int len, char» '\n', подобны функциям get о с теми же сигнатурами, но переносят i г входного потока и символ-ограничитель. Функция int peek () ; позволяет "взглянуть" на очередной символ входного потока. Точнее она возвращает код следующего символа потока (или EOF, если потоь пуст), но оставляет этот символ во входном потоке. При необходимое! 11 этот символ можно в дальнейшем извлечь из потока с помощью други \ средств библиотеки. Например, следующий цикл работает до кони., строки (до сигнала от клавиши Enter ): char cim; while (cin.peek() != ’\n’) { cin.get(cim); cout.put(cim) ; } Принадлежащая классу istream функция istream& putback (char cc); не извлекает ничего из потока, а помещает в него символ сс, который становится текущим и будет следующим извлекаемым из потока сим волом. Аналогичным образом функция int gcount ();
Глава 11. Ввод-вывод в языке Си++419 подсчитывает количество символов, которые были извлечены из входного потока при последнем обрашении к нему. Функция istreamS ignore (int n==l, int EOF) ; позволяет извлечь из потока и "опустить" то количество символов п, которое определяется первым параметром. Второй параметр определяет символ-ограничитель, при появлении которого выполнение функции нужно прекратить, даже если из потока еще не извлечены все п символов. Функции istream& read(signed char *array, int numb); istreams read(unsigned char *array, int numb); выполняют чтение заданного количества numb символов в массив array. Полезны следующие функции того же класса istream: lstream& seekg(long pos); устанавливает позицию чтения из потока в положение, опре- деляемое значением параметра. istreamfi seekg(long pos, seek.dir dir); выполняет перемещение позиции чтения вдоль потока в на- правлении, определенном параметром dir, принимающим значения из перечисления enum seek dir ( beg, cur, end ). Относительная величина перемещения (в байтах) определяется значением параметра long pos. Если направление определено как beg, то смещение от начала потока; cur - от текущей по- зиции; end - от конца потока; long tellg(} определяет текущую позицию чтения из потока. Подобные перечисленным функции класса os tream: long tellp() определяет текущую позицию записи в поток; ostream; seekp (long pos, seek.dir dir); аналогична функции seekgO , но принадлежит классу ostream и выполняет относительное перемещение позиции записи в поток; ostream& seekp(long pos); устанавливает абсолютную позицию записи в поток. 27’
420 Язык Си++ 11.6. Строковые потоки (обмены в основной памяти) Классы istrstream, ostrstream, strstream, определяемые в за головочном файле strstream, h (он в компиляторах под MS-DOS имее । более короткое название strstrea.h, так как длина имени файла в MS-DOS не может превышать 8 символов), предназначены для создании потоков, связанных с участками (областями) основной памяти Достаточно часто такие участки памяти определяются в программе как символьные массивы. Именно поэтому в обозначениях указанных классов используется аббревиатура (приставка) "str" - сокращение английского слова string (строка), а объекты этих классов называю i строковыми потоками. Строковый поток определяется и одновременно связывается < областью памяти с помощью конструктора объектов соответствующе! * > класса. Формат вызова конструктора: имя класса имя потока(параметры конструктора); Имя_класса в данном случае - это одно из имен istrstream, ostrstream, strstream. Имя потока - это идентификатор (произ вольно выбираемое программистом имя объекта данного класса). Ти пы параметров и их число различны для конструкторов разных клас сов. Входные строковые потоки. Эти потоки создаются с помощью такого конструктора класса istrstream: istrstream имя_потока (char *str); Обязательным параметром конструктора объектов класса istrstream является указатель str на уже существующий участок основной памяти. Например, следующие операторы char buf[40]; istrstream inBuf(buf); определяет входной строковый поток с именем inBuf и свяжут его с участком памяти, предварительно выделенным для символьного массива buf [ з . Теперь этот строковый поток inBuf может использоваться как левый операнд операции извлечения ». В следующей программе определена строка, содержащая символьнук > операцию "123.5 Salve»", затем определен и связан с этой строкой входной строковый поток instr. Из потока instr и тем са
Глава 11. Ввод-вывод в языке Си++ 421 мым из строки, адресуемой указателем stroka, разделенные пробела- ми значения переносятся в переменную real типа double и символь- ный массив array [10]. Текст программы: //Р11-07.СРР - строковые потоки, операция извлечения » // Автоматически включается файл iostream.h: linclude <strstrea.h> void main() ( // Выделена область памяти (строка): char *stroka » "123.5 Salve!"; // Создан входной строковый поток instr: istrstream instr(stroka); char array[10]; double real; // Извлекаем информацию из строкового потока: instr » real » array; // Вывод на экран: cout « "\narray » " « array « " real = " « real « endl; } Результат выполнения программы (на экране): array « Salve! real e 123.5 В следующем примере с помощью входного строкового потока выполняется чтение информации, передаваемой в качестве параметра командной строки функции main: //Р11-08.СРР - входной строковый поток; чтение аргумента // функции main () // Автоматически включается файл iostream.h: linclude <strstrea.h> void main(int argc, char *argv[]) // Определены аргументы ( char name[40]; // Выделяется область памяти // Создает строковый поток input: istrstream input(argv[l]); I/ Извлекаем информацию из строкового потока: input » name; // Вывод в стандартный поток (на экран): cout « "\пПри вызове аргумент = " « name « endl; } Если программа будет запущена на выполнение директивой: С:\>Р11-08.EXE FileName <Enter>
422 Язык Си++ то на экране появится сообщение: При вызове аргумент « FileName Извлечение информации из строкового потока с помощью опера ции » выполняется, начиная от первого непробельного символа до ближайшего пробела (точнее до ближайшего обобщенного пробель ного символа). Если необходимо читать и пробельные символы, то можно воспользоваться функциями бесформатного обмена get() и getiine(). Эти функции, вызываемые для символьных потоков, по зволяют организовать, например, копирование строк. Для копирова ния строк в библиотеке языка Си существует специальная функция strcpyO, определенная в заголовочном файле string.h. Копирова ние несложно организовать и с помощью циклического переноса сим волов из одного массива в другой. Однако интересный вариант копирования получается при использовании строковых потоков. На пример, в следующей программе выполняется "копирование" содер жимого строки, адресуемой указателем line, в заранее созданный символьный массив array[]. В соответствии с форматом функции getline() ее первый параметр - массив, в который ведется чтение, второй параметр - предельное количество читаемых символов, третий параметр - ограничивающий символ, после извлечения которого об- мен прекращается. Функция getlinel) вызывается для чтения из по- тока inpotok, связанного со строкой line. Текст программы: //Р11-09.СРР - копирование строки функцией getlineO. tfinclude <strstrea.h> void main() ( char *line « "000 111 \t222\n333\t444 555"; istrstream inpotok(line); char array[80]; inpotok.getline(array,sizeof(array),' \0’) ; cout « "\narray » " « array « endl; ) Результат выполнения программы: array ж 000 111 222 333 444 555 В результатах выполнения обратите внимание на влияние симво- лов '\t•, Ап', присутствующих как в исходной строке line, так и перенесенных в символьный массив array. Они, естественно, не выво- дятся на экран, а обеспечивают табуляцию и смену строки.
Глава 11. Ввод-вывод в языке Си++ 423 Безымянные входные строковые потоки. Конструктор класса istrstream позволяет создавать и связывать с заданной областью Памяти безымянные входные строковые потоки. Например, чтение Информации, переданной при запуске функции main() в командной Строке, обеспечит следующая программа: //Р11-10.СРР - безымянный входной строковый поток; чтение данных с помощью операции извлечения » linclude <strstrea.h> Void main (int Narg, char *arg[]) ( char path[80]; //Чтение из безымянного потока: istrstream (arg[0]) » path; cout « 'ЛпПолное имя программы: \n" « path « endl; » Результат выполнения программы, например, такой: Полное имя программы: D: \WWP\TESTPROG\P11-10.EXE По соглашениям языка и операционной системы arg[0] всегда содержит полное наименование программы с указанием диска и полного пути к каталогу, где находится ЕХЕ-файл. Вызов конструктора istrstream (arg [0 ]) создает безымянный объект - входной строковый поток, связанный с символьным массивом, адрес которого передается как значение указателя агд[О]. К этому потоку применима операция извлечения », с помощью которой полное наименование программы переносится в виде строки из arg [ 0 ] в символьный массив path [ ] . В следующей программе с одним и тем же участком памяти (строка, адресуемая указателем line) последовательно связываются три безымянных входных строковых потока. //Р11-11.СРР - чтение из строковых потоков linclude <strstrea.h> void main () { char *line = "000 111 \t222\n333\t444 555"; char array [80]; //Вспомогательный массив //Чтение из безымянного потока до пробела: istrstream(line) »array; cout « "\narray » "« array « endl; //Повторное чтение из безымянного потока: istrstream (line) »array; cout « "\narray = "« array « endl; //Вызов функции getline() для безымянного потока:
424 Язык Си++ istrstream(line).getline(array,sizeof (array),’\0 *); cout « "array = " «array «endl; Результат выполнения программы: array = ООО array = ООО array = ООО 111 222 333 444 555 Из первого безымянного потока данные в массив array извлекаютс ч (до первого пробела) операцией >>. Второй безымянный поток связан с той же строкой. Из него в массив array снова с начала строки читаете। та же самая информация. Третий безымянный поток читается с помощью • функции getline(), которая полностью копирует строку line а символьный массив array [ ] . Извлечение данных функцией getline () продолжается до появления нулевого признака конца строки "\0". Это> символ также переносится в массив array и служит признаком kohim созданной строки. Выходные строковые потоки. Эти потоки обычно создаются с по мощью такого конструктора класса ostrstream: ostrstream имя_потока (char *str, int len, int mode); Необязательное имя__потока - это идентификатор, произвольно выбираемый программистом. Указатель str должен адресовать уже существующий участок памяти. Параметр int len определяет разме ры этого участка (буфера). Последний параметр - индикатор режима обмена mode. Режим обмена строкового потока при выводе определяе« размещение информации в связанной с потоком строке. Для задани и конкретного режима используется флаг или дизъюнкция нескольких флагов: ios::out строковый поток создается для вывода, запись нформации ведется с начала строки; ios::ate позиция записи устанавливается в месте размещения нуле вого признака конца строки '\о' (запись в продолжение строки); ios::арр для строковых типов этот флаг действует аналогично флаг\ ios: : ate, но в конкретных реализациях могут быть отли чия, обычно отражаемые в документации.
Глава 11. Ввод-вывод в языке Си++ 425 Вместе с флагом ios: :out могут быть указаны (в дизъюнктивной форме) флаги ios: rate ИЛИ ios: :арр. В обоих случаях При формИро- |ании потока позиция записи устанавливается на нулевой признак • \01 конца строки (буфера потока), т.е. предполагается запись в ко- нец потока. По умолчанию выбирается ios::out, т.е. строковый поток соз- дается для вывода (записи) информации с начала строки (буфера). Начнем рассмотрение основных возможностей выходных строко- вых потоков с их безымянного варианта. В следующей программе аначение строки обязательного аргумента функции main с помощью Операции вставки « переносится в безымянный строковый поток, Связанный с символьным массивом path [*]: //Р11-12.СРР - запись в безымянный выходной строковый // лоток («) ^include <strstrea.h> Void main(int Narg, char *arg[]) { char path[80]; ostrstream(path,sizeof(path)) « arg[0] « '\0'; cout « "\пПолное имя программы: 11 « path « endl; » Результат выполнения программы: Полное имя программы: D:\WWP\TESTPROG\P11-12.EXE Так как операция включения « не переносит в выходной поток Признак конца строки *\о •, то его пришлось явным образом помес- тить в выходной поток (тем самым в буфер path [ ]). Функция write О применительно к выходному потоку позволяет записывать в него данные без форматирования, т.е. строка записы- вается вместе с пробельными символами и символом конца строки *\0*. Чтобы продемонстрировать особенности ее применения к безымянным выходным строковым потокам, рассмотрим следующую программу: //Р11-13.СРР - запись в безымянный выходной строковый // лоток; копирование строки с использованием // функции write О *include <strstrea.h> void main () { char latEJ « "Quod erat demonstrandum!"; char rus E ] ш ” - Что и требовалось доказать!11;
426 Язык Си++ char result[60]; ostrstream(result,sizeof(result)).write(lat,sizeof(lat)), ostrstream(result, sizeof(result),ios::ate).write(rus, sizeof (rus)). cout <<"\n « result « endl; ] Результат выполнения программы: Quod erat demonstrandum! - Что и требовалось доказать! В программе два безымянных потока, каждый из которых "настроен1 на символьный массив result [ ] . При создании первого безымянной • потока в качестве параметров конструктора указываются массив result [ ] и его размеры, т.е. длина массива в байтах. Функция write () "присоединяется" с помощью операции "точка" непосредственно к конструктору и тем самым вызывается для созданного им безымянного потока. В качестве фактических параметров функции write {) используются указатель 1st на строку и количество записываемых символов. Так как длина строки меньше длины буфера result, то буфер не заполняется целиком. При создании второго безымянного потока кроме буфера result и его длины, в конструкторе (в качестве третьего параметра) использован флаг ios:: ate, под действием которого поток создается как "дополняемый". Тем самым последующая запись в поток выполняется не с начала потока, а начиная с позиции окончания предыдущей записи ’ \о ’. Именно туда функция write () помещаем строку, адресованную указателем rus. Тем самым в MaccHBeresuit (] осуществляется конкатенация строк, что видно из результатов. Из массива result [] вывод в поток cout выполняется до появления символа конца строки ' \0' в массиве result. Следующая программа иллюстрирует особенности последовательного вывода в строковый поток данных разных типов с помощью операции включения: //Р11-14.СРР - вывод в строковый поток операцией « #include <strstrea.h> void main() { char buffer[180]; ostrstream outstring(buffer,sizeof(buffer), ios::out/ios::ate) outstring «\пБеэ явного включения разделителей» «"текст в потоке\п\»сливается\” : \п" ; outstring « 123456789 « -456 « +1.23456789; outstring « -0.123456789е+1 « +123.456789е-3 « ends;
Глава 11. Ввод-вывод в языке Си++ 427 cout « "\пи « buffer « endl; » Результат выполнения программы: Вез явного включения разделителей текст в потоке "сливается": 123456789-456 1.23456789-1.2345678 0.123457 Как показывают результаты, последовательное обращение к стро- ковому потоку приводит к записи "в продолжение”, т.е. указатель по- зиции записи при создании потока устанавливается на его начало, а затем перемещается на длину каждой новой записи. Никаких проме- жутков или разделителей между выводимыми данными не добавляет- ся. Более того, операция включения в поток « даже не переносит в него нулевой ограничитель конца строки '\0*. Поэтому его нужно добавлять явно, если в дальнейшем требуется использовать буфер по- тока в качестве строки. Числовая информация, включаемая в поток операцией «, форматируется. При этом знак + для чисел заменяется Пробелом, вещественные числа, заданные в экспоненциальной форме (в научной нотации), переводятся в форму с фиксированной точкой. Выполняется округление дробной части вещественного числа. Пере- численные и проиллюстрированные результатами особенности фор- матирования могут быть изменены с помощью средств управления форматом как и для стандартных потоков. Двунаправленные строковые потоки. Основной конструктор стро- ковых потоков, создаваемых как для чтения, так и для записи, имеет следующий формат: strstream имя_потока(char *buf, int lenBuf, int mode); где • buf - указатель на участок памяти (буфер потока, обычно сим- вольный массив), для которого создается поток; • lenBuf - длина в байтах участка памяти; • mode - индикатор режима обмена с создаваемым потоком. В ка- честве индикатора режима обмена используется дизъюнкция флагов, принадлежащих классу ios. Флаги ios: :in и ios: :out определяют направление обмена. Флаги ios::ate и ios:арр влияют на размещение указателя позиции чтения/записи в буфе- ре и т.д.
428 Язык Си++ В следующей программе с символьным массивом buffer [] связы- вается двунаправленный поток string. Затем последовательно вы- полняются операции записи в поток и чтения из потока. //Р11-15.СРР - ввод и вывод для двунаправленного // строкового потока ttinclude <strstrea.h> void main О ( char buffer[180]; char stroka[150], ss[150]; // Строковый поток string связан с массивом buffer: strstream string(buffer,sizeof(buffer),ios::in|ios::out); string « "В строковый поток записывается " "это предложение." « ends; // Чтение из строкового потока string в массив stroka: string.getline(stroka,sizeof(stroka),1\0') ; // Вывод в стандартный поток содержимого массива // stroka: cout « "\nstroka » " « stroka; // Возвращение позиции чтения/записи к началу потока // string: string.seekg(OL,ios::beg); // Чтение из строкового потока до пробельного символа: string » ss; cout « "\nss « " « ss; string » ss; cout « "\nss » " « ss; string.getline(ss fsizeof(ss),1\0'); cout « "\nss =» " « ss; ) Результат выполнения программы (на экране): stroka = В строковый поток записывается это предложение, ss » В S3 “ строковый зз = поток записывается это предложение. Обратите внимание, что функция getline () переносит из потока string даже ведущие пробельные символы. Комментарии в тексте программы и результаты ее выполнения достаточно подробно иллю- стрируют основные особенности работы с двунаправленными пото- ками. Отметим только необходимость при операциях « явно занести в поток признак конца строки (манипулятор ends), а при повторном чтении из потока - необходимость перейти к его началу.
Глава 11. Ввод-вывод в языке Си++ 429 Переход к началу потока для чтения из него выполняет функция seekg(), первый параметр которой (типа long) указывает нулевую величину смещения, а второй - положение, от которого это смещение отсчитывается. В классе ios определены три возможных начала отсчета: • ios: :Ьед - от начала потока (его буфера); • ios:: end - от конца потока; • ios:: cur - от текущей позиции чтения/записи. Обратите внимание, что для двунаправленного потока класса strstream определены два указателя позиций - позиции записи и по- зиции чтения. Именно поэтому в программе после окончания записи в поток string функция getline() выполняет чтение от его начала, и не требуется перевода указателя чтения (что делает функция seekp ()). Перегрузка операций ввода-вывода и использование строковых по- токов для межмодульного обмена. Строковые потоки можно исполь- зовать по-разному. С их помощью в участок памяти, связанный со строковым потоком, можно заносить разнотипную информацию, за- тем извлекать ее по нужным правилам. Строковый поток можно в этом случае сделать внешним и с его помощью осуществлять межмо- дульный обмен. В следующей программе с помощью дуального (двунаправленного) строкового потока выполняется обмен между функциями. Строковый поток с именем obmen и связанный с ним сим- вольный массив Link[] определены как глобальные и тем самым до- ступны во всех функциях файла с программой. В основной программе в строковый поток obmen заносятся данные из массива структур. Предварительно в поток записывается значение количества элементов массива. В функции result () выполняется чтение из строкового по- тока. При первом обращении к потоку "извлекается" значение коли- чества элементов, используемое затем для организации цикла чтения структур. Структурный тип с именем element и две операции- функции для перегрузки операций обмена » и « со строковыми по- токами определены до функции main (). Текст программы: //Р11-16.СРР - перегрузка операции обмена («, ») и // двунаправленный строковый поток ввода-вывода linclude <strstrea.h> const int lenLink « 200; // Глобальная константа char Link[lenLink]; // Глобальный символьный массив // Строковый поток obmen связан с массивом Link: strstream obmen(Link,sizeof(Link),ios::in|ios::out);
430 Язык Си++ struct element { int nk, nl; float zn; strstreamfi operator »(strstream& in, elementfi el) { in » el.nk; in » el.nl; in » el.zn; return in; ); strstreamfi operator «(strstreamfi out, element & el) ( out « 1 '« el.nk « • ' « el.nl « ' 1 « el.zn; return out; ); // Функция чтения из потока и вывода на экран: void result(void) { element zuar; int numb; obmen » numb; cout « H\nnumb “ •’ « numb; for(int j « 0; j < numb; j++) ( obmen » zuar; cout « "\nelement[" « j « •'] « "; cout « zuar.nk « ’\t’ « zuar.nl « '\t' « zuar.zn; ) ) void main() { char buffer[180]; const int numbeEl « 5; element arel[numbeEl] "(1,2, 3.45, 2, 3, 4.56, 22, 11, 45.6, 3, 24, 4.33, 3, 6, -5.3 }; // Запись в строковый поток: obmen « numbeEl; for (int i « 0; i < numbeEl; i++) obmen « arelfi]; obmen « 1\0 ' ; result () ; ) Результат выполнения программы: numb " 5 element[0] = 1 2 3.45 element[1] = 2 3 4.56 element[2] " 22 11 45.599998 element[3] « 3 24 4.33 element[4] " 3 6 -5.3
Глава 11. Ваод-выаод в языке Си++ 431 11.7. Работа с файлами Основное отличие внешней памяти ЭВМ от основной (иначе опе- ративной) памяти - возможность сохранения информации при отклю- чении ЭВМ. Информация во внешней памяти (на диске, на магнитных лентах и т.п.) сохраняется в виде файлов - именованных объектов, доступ к которым поддерживает (обеспечивает) операционная система ЭВМ. Поддержка операционной системы состоит в том, что в ней имеются средства: • создания файлов; • уничтожения файлов; • поиска файлов на внешнем носителе информации (на диске); • чтения и записи данных из файлов и в файлы; • открытия файлов; • закрытия файлов; • позиционирования файлов. Библиотека ввода-вывода Си++ включает средства для работы с > последовательными файлами. Логически последовательный файл ‘ можно представить как именованную цепочку (ленту, строку...) бай- тов, имеющую начало и конец. Последовательный файл отличается от файлов с другой организацией тем простым свойством, что чтение (или запись) из файла (в файл) ведутся байт за байтом от начала к концу. В каждый момент позиции в файле, откуда выполняется чтение и куда производится запись, определяются значениями указателей по- зиций записи и чтения файла. Позиционирование указателей записи и чтения (т.е. установка на нужные байты) выполняется либо автомати- : чески, либо за счет явного управления их положением. В стандартной ; библиотеке ввода-вывода Си++ имеются соответствующие средства. Рассматривая взаимосвязь файлов с потоками ввода-вывода, нуж- i но отметить существование следующих процедур: 1 - создание файла; 2 - создание потока; 3 - открытие файла; 4 - "присоединение" файла к потоку; 5 - обмены с файлом с помощью потока; 6 - "отсоединение" потока от файла; 7 - закрытие файла; 8 - уничтожение файла. Все перечисленные действия могут быть выполнены с помощью средств библиотеки классов ввода-вывода языка Си++. Однако су-
432 Язык Си++ шествует несколько альтернативных вариантов их выполнения. Крат- ко остановимся на наиболее простых и удобных механизмах реализа- ции указанных действий. Создание файла может быть на "нижнем уровне" выполнено с по- мощью библиотечной функции (из библиотеки ANSI С): int creat (const char *path, int anode); Прототип этой функции находится в заголовочном файле io.h. Функция create) по заданному имени файла path создает новый файл либо "очищает" и подготавливает для работы уже существую- щий. Параметр anode нужен только для вновь создаваемого файла. Файл создается для работы в таком режиме обмена, который соответ- ствует значению определенной в файлах fcnti.h и stdiib.h гло- бальной переменной _fnode (о_техт или o_binary). Значение о_техт определяет текстовый режим обмена с файлом, при котором в процес- се чтения из файла каждая пара символов cr (OxOD - конец строки), lf (ОхОА - переход к началу строки) преобразуется в один символ новой строки 1 \п1. При записи в файл в текстовом режиме обмена каждый символ новой строки '\п* преобразуется в пару cr, lf. Чтение из файла в текстовом режиме не может продолжаться, если обнаружен символ, обозначающий конец файла. В этом случае считается, что до- стигнут конец файла, т.е. выполнено условие eof. В двоичном режиме обмена o_binary преобразований символов не происходит, и их значения не анализируются. По умолчанию переменная __fmode устанавливается равной о_техт, т.е. файл создается для работы в текстовом режиме. Про- граммист может явно изменить режим обмена, используя последова- тельность: #includa <fcntl.h> _fnode=0_BINARY; Параметр anode определяет режим доступа к создаваемому файлу. Предопределены в заголовочном файле sys\stat.h следующие кон- станты-значения этого параметра: s_iwrite - разрешена запись в файл; S—IRjead - разрешено только чтение из файла; s_iread | s_iwrite - разрешены и чтение, и запись. Если файл с полным именем *path не существует, то он создается заново, для него устанавливаются по значению параметра anode ре-
Глава 11. Ввод-вывод в языке Си++ 433 жим доступа и по значению глобальной переменной _fmode выбирается текстовый или двоичный режим обмена. Если файл с полным именем *path уже существует и для него был определен режим доступа ’’только чтение" s„iread , то вызов функции creat() завершается неудачно, файл с указанным именем останется неизменным. Если файл с полным именем *path уже существует и для него установлен режим записи S_IWRITE, то функция creat() обнуляет длину файла, оставляя неизменными атрибуты его режимов. При успешном завершении функция create) возвращает неотри- цательное целое число - индивидуальный логический номер (дескриптор) файла. В противном случае возвращается значение -1. Пример создания файла с проверкой результата: linclude <io.h> //Для функций create() linclude <ргосеss.h> //Для функций exit() linclude <iostream.h> linclude <sys\stat.h> //Значения параметров amode char *fileName = ”EXAMPLE.CPP"; int fileNumb; fileNumb = creat(fileName,S_IWRITE); if (fileNumb = -1) { сегг « «Ошибка при создании файла»; exit(l); } Здесь файл с именем example, срр создается в текущем каталоге. Явно указан режим файла, предусматривающий только запись в файл. По умолчанию файл создается как текстовый о_техт. Потоки для работы с файлами создаются как объекты следующих классов: • of stream, h ~ для вывода (записи) данных в файл; • if stream - для ввода (чтения) данных из файла; • fstream -для чтения и для записи данных (двунаправленный обмен). Чтобы использовать эти классы, в текст программы необходимо включить дополнительный заголовочный файл f stream. h. После этого в программе можно определять конкретные файловые потоки, соответст- вующих типов (объекты классов ofstream, ifstream, fstream), например, таким образом: 28-382;
434 Язык Си++ ofstream outFile; // Определяется выходкой файловый поток ifstream inFile; // Определяется входной файловый поток fstream ioFile; // Определяется файловый поток для ввода // и вывода Создание файлового потока (объекта соответствующего класса) связывает имя потока с выделяемым для него буфером и инициализи- рует переменные состояния потока. Так как перечисленные классы файловых потоков наследуют свойства класса ios, то и переменные состояния каждого файлового потока наследуются из этого базового класса. Так как файловые классы являются производными от классов ostream (класс of stream), i stream (класс ifstream), stream (класс fstream), то они поддерживают описанный в предыдущих парагра- фах форматированный и бесформатный обмен с файлами. Однако прежде чем выполнить обмен, необходимо открыть соответствующий файл и связать его с файловым потоком. Открытие файла в самом общем смысле означает процедуру, ин- формирующую систему о тех действиях, которые предполагается вы- полнять с файлом. Существуют функции стандартной библиотеки языка Си для открытия файлов fopen (), open О. Но работая с файло- выми потоками библиотеки ввода-вывода языка Си++, удобнее поль- зоваться компонентными функциями соответствующих классов. Создав файловый поток, можно "присоединить" его к конкретно- му файлу с помощью компонентной функции open(). Функция open () унаследована каждым из файловых классов of stream, ifsream, fstream от класса fstreambase (для простоты он не показан на рис. 11.3). С ее помощью можно не только открыть файл, но и свя- зать его с уже определенным потоком. Формат функции: void open(const char *fileMame, int mode ж умалчижаемое_зяачеиие, int protection ж умалчиваемое__значеиие> ; Первый параметр - fileName - имя уже существующего или соз- даваемого заново файла. Это строка, определяющая полное или со- кращенное имя файла в формате, регламентированном операционной системой. Второй параметр - mode (режим) - дизъюнкция флагов, определяющих режим работы с открываемым файлом (например, только запись или только чтение). Флаги определены следующим об- разом: enum ios::open_mode ( in » 0x01, // Открыть только для чтения out ж 0x02, // Открыть только для записи
Глава 11. Ввод-вывод в языке Си++ 435 ate 3 0x04, // При открытии искать конец файла app ж 0x08, // Дописывать данные конец файла trunc ж 0x10, // Вместо существующего создать // новый файл nocreate ж 0x20, // Не открывать новый файл (Для // несуществующего файла функция // open выдаст ошибку) noreplace ж 0x40, // Не открывать существующий файл / / (Для существующего выходного // файла, не имеющего режимов ate // или арр, выдать ошибку) binary ж 0x80, // Открыть для двоичного (не // текстового) обмена Назначения флагов поясняют комментарии, однако надеяться, что именно такое действие на поток будет оказывать тот или иной флаг в конкретной реализации библиотеки ввода-вывода, нельзя. Как пишет автор языка Си++ (26], ’’смысл значений openjnode скорее всего зави- сит от реализации”. Например, различие между флагами ios: :ate и ios: :арр проявляется весьма редко, и часто они действуют одинако- во. Однако ниже в пояснениях к программе Р11-19.СРР приведен пример использования флага ios::арр в конструкторе класса of stream, где использование ios: :ate приведет к ошибке открытая файла. Умалчиваемое значение параметра mode зависит от типа поте ка, для которого вызывается функция open (). Третий параметр - protection (зпм/wma) - определяет защиту и достаточно редко используется. Точнее, он устанавливается по умол- чанию и умалчиваемое значение обычно устраивает программиста. Как обычно вызов функции ореп() осуществляется с помощью уточненного имени имя_об*»екта_класса. вызов^принадлежащей^классу^фунхции Итак, открытие и присоединение файла к конкретному файловому потоку обеспечивается таким вызовом функции open (): имя_потока.open(имя_файла, режим, защита); Здесь имя_потока - имя одного из объектов, принадлежащих классам ofstream, ifstream, fstream. Примеры вызовов для опреде- ленных выше потоков: outFile. open ("С: \\USER\\RESULT.DAT") ; inFile, open ("DATA. TXT") ; ioFile.open("CHANGE.DAT",ios::out); 28*
436 Язык Си++ При открытии файлов с потоками класса of stream второй параметр по умолчанию устанавливается равным ios::out, т.е. файл открывается только для вывода. Таким образом, файл C:\USER\RESULT.DAT после удачного выполнения функции open() будет при необходимости (если он не существовал ранее) создан, а затем открыт для вывода(записи) данных в текстовом режиме обмена и присоединен к потоку outFile. Теперь к потоку outFile может применяться, например, операция включения <<, как к стандартным выходным потокам cout, сегг. Поток inFile класса ifstream в нашем примере присоединяется функцией open () к файлу с именем data.txt. Этот файл открывается для чтения из него данных в текстовом режиме. Если файла с именем data.txt не существует, то попытка вызвать функцию inFile.open() приведет к ошибке. Для проверки удачности-завершения функции open () используется перегруженная операция ’. Если унарная операция ’ применяется к потоку, то результат ненулевой при наличии ошибок. Если ошибок не было, то выражение ’имя_потока имеет нулевое значение. Таким образом, можно проверить результат выполнения функции open (): if (’inFile) { сегг « "Ошибка при открытии файла!\п"; exit(1); } Для потоков класса fstream второй аргумент функции open () должен быть задан явно, так как по умолчанию неясно, в каком направлении предполагается выполнять обмен с потоком. В примере файл change.dat открывается для записи и связывается с потоком ioFile, который будет выходным потоком до тех пор, пока с помощью повторного открытия файла явно не изменится направление обмена с файлом или потоком. (Чтобы изменить режимы доступа к файлу, его нужно предварительно закрыть с помощью функции close (), унаследованной всеми тремя файловыми классами из базового класса fstreambase.) В классе fstreambase, который служит основой для файловых классов, имеются и другие средства для открытия уже существующих файлов.
Глава 11. Ввод-вывод в языке Си++ 437 Если файл явно создан с помощью библиотечной функции "нижнего уровня" create О, то для него определен дескриптор файла. Этот дескриптор можно использовать в качестве фактического пара- метра функции fstreambase: : attach (). При вызове ЭТОЙ функции используется уточненное имя, содержащее название того потока, ко- торый предполагается присоединить к уже созданному файлу с из- вестным дескриптором: tinclude <fstream.h> // Классы файловых потоков tinclude <sys\stat.h> // Константы режимов доступа к файлам char паше [20]; // Вспомогательный массив cin » папе; // Ввести имя создаваемого файла int descrip ж create(name,S_WRITE); // Создать файл if (descrip « -1) ( cout « "\n Ошибка при создании файла"); exit () ; ) // Определение выходного файлового потока*. ofstream fileOut; II Присоединение потока к файлу: fileOut.attach(descrip); if (!fileOut) { cerr « "\пОшибка присоединения файла!") exit(l); ) В классах ifstream, ofstream, fstream определены конструкто- ры, позволяющие по-иному выполнять создание и открытие файлов. Типы конструкторов для потоков разных классов очень похожи: имя_класса () ; создает поток, не присоединяя его ни к какому файлу; имя__класса (int fd) ; создает поток и присоединяет его к уже открытому файлу, де- скриптор которого используется в качестве параметра fd; имя_класса(int fd, char *buf, int); создает поток, присоединяя его к уже открытому файлу с де- скриптором fd, и использует явно заданный буфер (параметр buf); имя_класса(char * FileName, int mode, int * . ..); создает поток, присоединяет его к файлу с заданным именем Filename, а при необходимости предварительно создает файл с таким именем.
438 Язык Си++ Детали и особенности перечисленных конструкторов лучше изучать по документации конкретной библиотеки ввода-вывода. Работая со средствами библиотечных классов ввода-вывода, чаще всего употребляют конструктор без параметров и конструктор, в котором явно задано имя файла. Примеры обращений к конструкторам без параметров: ifstream fi; //Создает входной файловый поток fi ostream fo; // Создает выходной файловый поток fo fstream ff; // Создает файловый поток ввода-вывода ff После выполнения каждого из этих конструкторов файловый поток можно присоединить к конкретному файлу, используя уже упомянутую компонентную функцию open () : void open (char*FileName, int режим, int защита); Примеры: fi.open ("Filel.txt",ios::in); // Поток fi соединен // с файлом Filel.txt fi.close(); //Разорвана связь потока fi с файлом //Filel.txt fi.open("File2.txt"); //Поток fi присоединен к файлу //File2.txt fo.open("NewFile"; // Поток fo присоединяется к файлу //NewFile; если такой файл //отсутствует - он будет создан При обращении к конструктору с явным указанием в параметре имени файла остальные параметры можно не указывать, они выби- раются по умолчанию. Примеры: ifstream flowl ("File.l"); создает входной файловый поток с именем flowl для чтения данных. Разыскивается файл с названием File.l. Если такой файл существует, то конструктор завершает работу аварийно. Проверка: if (’flowl) сегг « "Не открыт файл File.l’"; ofstream flow2 ("File.2"); создается выходной файловый поток с именем flow2 для за- писи информации. Если файл с названием File.2 не су- ществует, он будет создан, открыт и соединен с потоком
Глава 11. Ввод-вывод в языке Си++ 439 flow2. Если файл уже существует, то предыдущий вариант бу- дет удален и пустой файл создается заново. Проверка: if (!flow2) cerr « "Не открыт файл File.2!11; fstream flow3 ("File.3") ; создается файловый поток fiow3, открывается файл File.3 и присоединяется к потоку flow3. Все файловые классы унаследовали от базовых классов функцию close (), позволяющую очистить буфер потока, отсоединить поток от файла и закрыть файл. Функцию close () необходимо явно вызывать при изменении режимов работы с файловым потоком. Автоматически эта функция вызывается только при завершении программы. В качестве иллюстрации основных особенностей работы с файла- ми рассмотрим несколько программ. //Р11-17.СРР - чтение текстового файла с помощью // операции » linclude <stdlib.h> // Для функции exit() linclude <fstream.h> // Для файловых потоков const int lenName * 13; // max длина имени файла // Длина вспомогательного массива: const int lenString ж 60; void main() { char source[lenName]; // Массив для имени файла cout « "\пВведите имя исходного файла: "; cin » source; ifstream inFile; // Входной файловый поток // Открыть файл source и связать его с потоком inFile: inFile.open(source); if (!inFile) // Проверить правильность открытия файла { cerr « "\пОшибка при открытии файла " « source; exit(l); // Завершение программы } // Вспомогательный массив для чтения: char string[lenString]; char next; cout « ”\n Текст файла:\n\n”; cin.get(); // Убирает код ив потока cin while(1), // Неограниченный цикл { // Ввод из файла одного слова до пробельного // символа либо EOF: inFile » string; // Проверка следующего символа: next ж inFile.реек();
440 Язык Си++ // Выход при достижении конца файла: if (next = EOF) break; // Печать с добавлением разделительного пробела: cout « string « " "; if (next = *\n*) // Обработка конца строки ( cout « '\n*; // 4 - смещение для первой страницы экрана: static int i “ 4; // Деление по страницам до 20 строк каждая: if (! (++i % 20)) ( cout « "\пДля продолжения вывода " "нажмите ENTER.\n" « endl; cin.get(); } ) ) ) Результат выполнения программы - постраничный вывод на эк- ран текстового файла, имя которого набирает на клавиатуре пользо- ватель по "запросу" программы. Размер страницы - 20 строк. В начале первой страницы - результат диалога с пользователем и поэтому из файла читаются и выводятся только первые 16 строк. Программа демонстрирует неудобства чтения текста из файла с помощью операции извлечения », которая реагирует на каждый об- общенный пробельный символ. Между словами, прочитанными из файла, принудительно добавлено по одному пробелу. А сколько их (пробелов) было в исходном тексте, уже не известно. Тем самым иска- жается содержащийся в файле текст. Читать пробельные символы по- зволяет компонентная функция getline () класса istream, наследуемая классом ifstream. Текст из файла будет читаться и вы- водиться на экран (в поток cout) без искажений (без пропусков про- белов), если в предыдущей программе чтение и вывод в поток cout организовать таким образом: while (1) // Неограниченный цикл ( inFile.getline(string,lenString); next ж inFile.peek(); if (next = EOF) break; cout « string; Следующая программа читает текстовый файл и разделяет его на две части - строки, не содержащие последовательности из двух симво- лов • //•, и строки, начинающиеся такой парой символов. Иначе го-
Глава 11. Ввод-вывод в языке Си** 441 воря, эта программа позволяет удалить из исходного текста програм- мы на языке Си++ комментарии, начинающиеся парой символов ' // ’ и заканчивающиеся признаком конца строки '\п'. В программе определены два выходных потока outtext и outcom, связанные соот- ветственно с создаваемыми заново файлами text.срр и comment. Имя входного файла с текстом анализируемой программы на языке Си++ определяет (вводит с клавиатуры) пользователь. С этим файлом "связывается" функцией ореп() входной поток inFile. Для проверки безошибочного открытия файлов проверяются значения выражений (?имя_потока). При истинности результата вызывается вспомога- тельная функция errorFO. Вспомогательная переменная int len, позволяет проследить за необходимостью перехода к новой строке в потоке out text, если во входном потоке inFile обнаружена пара символов 1 / / ’. Символы входного потока последовательно читаются в переменную simb и выводятся в нужный выходной поток. Если не встречен символ 1 /1, то все просто - вывод идет в поток out text. Так как обнаружение во входном отдельного символа ' /1 не есть признак начала комментария, то в этом случае анализируется следующий сим- вол, читаемый из входного потока в переменную next. Если next имеет значение 1/', то это начало комментария, и последующий вы- вод нужно вести в поток outcom, предварительно "закрыв" строку в потоке outtext символом '\п'. Комментарии в тексте программы поясняют остальные детали алгоритма. //Р11-18.СРР - выделение комментариев ив текста на Си**; // посимвольные чтение и запись из текстового // файла ttinclude <stdlib.h> ttinclude <fstream.h> void errorF(char *ss) // Вспомогательная функция { сегг « ”\пОшибка при открытии файла” « ’ ’ « ss « 1\п'; axit(l); } const int lenName » 23; // Длина массива для имени файла void main() { char progName[lenName]; // Массив для имени файла cout « "ХпВведите полное имя анализируемой программы: "; cin » progName; ifstream inFile; // Входной поток // Связываем входной поток с файлом программы: inFile. open (progName) ; > if (! inFile) errorF(progName);
442 ЯзыкСи++ char simb, last, next; // Вспомогательные переменные ofstream outtext, outcom; // Два выходных потока // Переменная для вычисления длин строк программы: int len — 0; outtext.open("text.срр",ios::ate) ; if ('outtext) errorF("text.cpp"); outcom.open("comment",ios::app); if ('outcom) errorF("comment"); while (inFile.get(simb)) // Читает символы до EOF { len++; // Длина очередной строки программы if (simb «ж '\n') len — 0; // Начнется новая строка программы if (simb !-'/') // Это не начало комментария // Вывод символа строки программы: outtext.put(simb); else / / Когда simb — ’ /1 - возможно начало // комментария { // Проверка на EOF: if (!inFile.get(next)) break; if (next — 1/1) { // Теперь уже точно комментарий if (len >« 1) // "Закрываем" строку программы: outtext.put(•\n'); outcom.put(simb); outcom.put(next); // Цикл до конца комментария, // т.е. до конца строки: do { // Чтение символа из файла: inFile.get(simb) ; // Запись символа в поток: outcom.put(simb) ; } while (simb!-'\n') ; } else // Вывод символов, не входящих // в комментарий: { outtext.put(simb) ; outtext.put(next); } } } 1
Глава 11. Ввод-вывод в языке Си** 443 Результат выполнения программы - два файла text.срр и comment в текущем каталоге, из которого "запущена" на выполнение программа. В первом файле - текст программы без комментариев, во втором - текст всех комментариев. В качестве примера можно выпол- нить программу для текста из файла pii-18.cpp, т.е. разобрать ис- ходный текст этой же программы. Для разнообразия при открытии файлов text, срр и comment в функциях open () использованы разные флаги, определяющие режим работы с соответствующим потоком. Результат одинаков - флаги ios::ate и ios::арр в этом случае неразличимы. Запись в файлы идет с их дополнением. После каждого нового выполнения програм- мы новая "порция" текстовой информации дописывается в каждый файл. Если необходимо, чтобы сохранялся в файлах только последний результат, второй параметр функции ореп() проще всего задавать по умолчанию. Как и для других потоков, для потоков, связанных с файлами, до- пустима перегрузка операций обмена. Для иллюстрации приведем следующую программу pii-19.cpp, в которой перегружена операция включения в поток «. Действие операции распространено на аргу- менты типа ofstraam£ И element, где element - пользовательский тип, а именно структура. В программе с помощью конструктора клас- са ofstream определяется поток filei и связывается с файлом две. Текст программы: //Р11-19.СРР - запись структур в файл перегруженной // операцией « ♦include <fstream.h> struct element { // Определение некоторой структуры int nk, nl; float zn; }; H Операции-функции, расширяквдая действие операции « ofstreamfc operator « (ofstreamt out, element el) { out « ’ ’« el.nk « ’ ' « el.nl « 1 1 « al.zn « ' \n' ; return out; } int main() ( const int numbeEl «5; // Количество структур в массиве element arel[numbeEl] { 1, 2, 3.45, 2, 3, 4.56, 22, 11, 45.6, 3, 24, 4.33, 3, 6, -5.3 }; // Определяем поток и связываем его с новым файлом АВС: ofstream filei ("abc11) ;
444 Язык Си++ if (!filel) { сегг « "Неудача при открытии файла АВС."; return 1; } // Запись в файл АВС массива структур: for (int i « 0; i < numbBl; i++) filel « arel[i]; ) Результат выполнения программы - создание файла с именем авс в текущем каталоге и запись в этот файл элементов массива из пяти структур element. Содержимое файла авс: 1 2 3.45 2 3 4.56 22 11 45.599998 3 24 4.33 3 6 -5.3 Файл авс создается заново при каждом выполнении программы. Чтобы файл создавался один раз и была возможность его дополнения, нужно добавить в конструктор второй параметр таким образом: of stream filel ("abc" , ios : :арр) ; В этом случае при двух последовательных выполнениях програм- мы результат в файле авс будет таким: 1 2 3.45 2 3 4.56 22 11 45.599998 3 24 4.33 3 6 -5.3 1 2 3.45 2 3 4.56 22 11 45.599998 3 24 4.33 3 6 -5.3
Глава 12. ОБРАБОТКА ОСОБЫХ (ИСКЛЮЧИТЕЛЬНЫХ) СИТУАЦИЙ В последних версиях компиляторов языка Си++ наконец появи- лись рекомендованные стандартом ANSI средства для обработки осо- бых ситуаций. Такие ситуации в Си++ называют исключительными ситуациями или исключениями (exceptions). Для компиляции приведен- ных ниже программ можно использовать, например, компиляторы Borland C++ версий 4.0 или 4.5, в которых реализованы все описан- ные средства. 12.1. Общие принципы механизма обработки исключений Механизм обработки особых ситуаций присутствовал в разных языках программирования до появления Си++. Один из примеров - язык ПЛ/1, в котором программисты могли работать как со встроен- ными (заранее запланированными) ситуациями, так и с ситуациями, создаваемымми (формируемыми) по указанию программиста при на- ступлении того или иного события. Типичные встроенные ситуации это “деление иа нуль”, “достижение конца файла”, “переполнение в арифметических операциях” и т.п. В языке Си++ практически любое состояние, достигнутое в процессе выполнения программы, можно заранее определить как особую ситуацию (исключение) и предусмот- реть действия, которые нужно выполнить при ее возникновении. Для реализации механизма обработки исключений в язык Си++ введены следующие три ключевых (служебных) слова: try (контроли- ровать), catch (ловить), throw (генерировать, порождать, бросать, по- сылать, формировать). Служебное слово try позволяет выделить в любом месте испол- няемого текста программы так называемый контролируемый блок: try { операторы }
446 Язык Си++ Среди операторов, заключенных в фигурные скобки могут быть: описания, определения, обычные операторы языка Си++ и специаль- ные операторы генерации (порождения, формирования) исключений: throw выражение__гемерахщи_исключекия; Когда выполняется такой оператор, то с помощью выражения, ис- пользованного после служебного слова throw* формируется специаль- ный объект, называемый исключением. Исключение создается как статический объект, тип которого определяется типом значения выра- жения1?емерации_исключения. После формирования исключения ис- полняемый оператор throw автоматически передает управление (и само исключение как объект) непосредственно за пределы контроли- руемого блока. В этом месте (за закрывающейся фигурной скобкой) обязательно находятся один или несколько обработчиков исключе- ний, каждый из которых идентифицируется служебным словом catch и имеет в общем случае следующий формат: catch (тип__исключения имя) { операторы } Об операторах в фигурных скобках здесь говорят как о блоке об- работчика исключений. Обработчик исключений (процедура обра- ботки исключений) внешне и по смыслу похож на определение функции с одним параметром, не возвращающей никакого значения. Когда обработчиков несколько, они должны отличаться друг от друга типами исключений. Все это очень похоже на перегрузку функций, когда несколько одноименных функций отличаются спецификациями параметров. Так как исключение передается как объект определенно- го типа, то именно этот тип позволяет выбрать из нескольких обра- ботчиков соответствующий посланному исключению. Проиллюстрируем сказанное примерами. Предварительно отме- тим, что механизм обработки исключений является весьма общим средством управления программой. Он может использоваться не только при обработке аварийных ситуаций, но и любых других со- стояний в программе, которые почему-либо выделил программист. Для этого достаточно, чтобы га часть программы, где планируется возникновение исключений, была оформлена в виде контролируемого блока, в котором выполнялись бы операторы генерации исключений при обнаружении заранее запланированных ситуаций. Демонстрацию свойств механизма обработки исключений начнем с несложной функции для определения наибольшего общего делителя (НОД) двух целых чисел.
Глава 12. Обработка особых (исключительных) ситуаций в Си++ 447 Классический алгоритм Евклида определения наибольшего об- щего делителя двух целых чисел (х, у) может применяться только при следующих условиях: • оба числа х и у неотрицательные; • оба числа х и у отличны от нуля. На каждом шаге алгоритма выполняются сравнения: • если х== у, то ответ найден; • если х < у, то у заменяется значением у - х; • если х > у, то х заменяется значением х - у. Следующая программа содержит функцию gcm() для определения наибольшего общего делителя, включающую контролируемый блок с проверкой исходных данных. В основной программе main о трижды вызывается функция gcm(). Два последних вызова выполнены с не- верными значениями параметров. //Р12-01.СРР - GCM - Greatest Common Measure linclude <iostream.h> // Определение функции с генерацией, контролем и // обработкой исключений: int GCMfint xt int у) { // Контролируемый блок: try { if (х==0 || у«0) throw "\nZERO!"; if (х < 0) throw "\nNegative parameter 1."; if (у < 0) throw "\nNegative parameter 2."; while (x !== y) { if (x > y) x == x - y; else У = У “ x; } return x; } // Конец контролируемого блока // Обработчик исключений стандартного типа "строка": catch (const char *report) ( сегг « report « ” x « " « x « ”, у « " « у; return 0; ) } // Конец определения функции void main() { // Безошибочный вызов: cout « "\nGCM(66,44) « " « GCM(66,44) ; // Нулевой параметр: cout « ”\nGCM(0j7) « " « GCM(0,7);
448 Язык Си++ // Отрицательный параметр: cout « "\nGCM(-12,8> « " « GCM(-12,8) ; } Результат выполнения программы: GCM(66,44) « 22 ZERO! х « 0, у = 7 GCM(0,7) « О Negative parameter 1. х = -12, у = 8 GCM(-12,8) - О Программа демонстрирует основные принципы применения меха- низма обработки исключений. Однако как генерация исключений, так и их обработка выполняются в одной и той же функции, что, не ти- пично для эффективного применения исключений. Однако прежде чем объяснять полезность исключений и очерчивать область их примене- ния рассмотрим текст программы и результаты ее выполнения. Служебное слово try определяет следующий за ним набор опера- торов в фигурных скобках как контролируемый блок. Среди операто- ров этого контролируемого блока три условных оператора анализируют значения параметров. При истинности проверяемого условия в каждом из них с помощью оператора генерации throw фор- мируется исключение, т.е. создается объект - символьная строка, имеющая атрибуты const char *. При выполнении любого из опе- раторов throw естественная последовательность исполнения операто- ров функции прерывается и управление автоматически без каких-либо дополнительных указаний программиста передается обработчику ис- ключений, помещенному непосредственно за контролируемым блоком (Это чуть-чуть похоже на оператор goto). Так как в данной программе обработчик исключений локализован в теле функции, то ему доступ- ны значения ее параметров (х, у). Поэтому при возникновении каждо- го исключения в поток вывода сообщений об ошибках cerr выводит- ся символьная строка с информацией о характере ошибки (нулевые параметры или отрицательные значения параметров) и значения па- раметров, приведшие к возникновению особой ситуации и к генера- ции исключения. Здесь же в составном операторе обработчика исключений выполняется оператор return о; Тем самым при ошиб- ках возвращается необычное нулевое значение наибольшего общего делителя. При естественном окончании выполнения функции gcm(), когда становятся равными значения х и у, функция возвращает значе- ние X.
Глава 12. Обработка особых (исключительных) ситуаций в Си++ 449 Так как по умолчанию и выходной поток cout, и поток сегг связы- ваются с Экраном дисплея, то результаты как правильного, так и ошибочного выполнения функции gcm() выводятся на один экран. Обратите внимание, что исключения (const char *) одного типа по- сылаются в ответ на разные ситуации, возникающие в функции. Хотя разобранная программа демонстрирует особенности меха- низма особых ситуаций (исключений), но в ней нет никаких преиму- ществ перед стандартными средствами анализа данных и возврата из функций. Все действия при возникновении особой ситуации (при не- верных данных) запланированы автором функции и реализованы в ее Теле. Использование механизма обработки исключений полезнее в тех случаях, когда автор функции только констатирует наличие особых ситуаций и предлагает программисту (использующему функцию) са- мостоятельно решать вопрос о выборе правил обработки исключений В вызывающей программе. В следующей программе используется другой вариант функции для определения наибольшего общего делителя (НОД) двух целых чисел, передаваемых в качестве аргументов. В теле указанной функ- ции gcm__new() нет контролируемого блока и обработчика исключе- ний, но сохранены “генераторы” исключений. Контролируемый блок И обработчик исключений перенесены в функцию main (). Все вызовы функции (верный и с ошибками в параметрах) помещены в контроли- руемый блок. //Р12-02.СРР - функция с генерацией, но без контроля // исключений Binclude <iostream.h> Ant G€M_NEW(int x, int у) // Определение функции ( if (х == О I| у == 0) throw "\nZERO!"; if (х < 0) throw "\nNegative parameter 1."; if (у < 0) throw "\nNegative parameter 2.H; while (x ?= y) { if (x > y)' x » x - y; else у ~ у - x; ) return x; ) // Контроль и обработка исключений в вызывающей программе void main() { try // Контролируемый блок { cout « ”\nGCM_NEW(66,44) = ” « GCM_NEW(66,44); cout « "\nGCM_NEW(0,7) = " « GCM_NEW (0,7) ; cout « »\nGCM_NEW(-12,8) = " « GCM_NEW(-12,8); } 29 3S421
450 Язык Си++ catch (const char *report) // Обработчик исключении { cerr « report; } } Результаты выполнения программы: GCM_NEW(66,44) « 22 ZERO! Как видно из результатов, программа прекращает работу при втором вызове функции после обработки первого исключения. Так как обработка исключения выполняется вне тела функции, то в обра- ботчике исключений недоступны параметры функции и тем самым утрачивается возможность наблюдения за их значениями, привел шими к особой ситуации. В целом программа демонстрирует возможность формировать ис- ключения в функции, а наблюдать за этими исключениями в вызы вающей программе. По сравнению с предыдущей программой в точке обработки исключения недоступны переменные х, у, локализованные в теле функции. Это снижение информативности можно устранить, введя специальный тип для исключения, т.е. генерируя исключение как информационно богатый объект введенного программистом класса. В следующей программе определен класс data с компонентами, позволяющими отображать в объекте-исключении как целочисленные параметры функции, так и указатель на строку с сообщением (о смыс ле события, при наступлении которого сформировано исключение). //Р12-03.СРР - исключения глобального пользовательского // типа #include <iostream.h> struct DATA // Глобальный класс объектов-исключений { int п, т; char *s; DATA(int x, int у, char *c) // Конструктор класса DATA { n « x; m = у; s == c;} } ; int GCM_ONE(int x, int у) // Определение функции (if (x«0 ||y«0) throw DATA(x,y, "\nZERO!11) ; if (x < 0) throw DATA(x,y, "\nNegative parameter 1."); if (y < 0) throw DATA(x,y, "\nNegative parameter 2."); while (x !=® y) { if (x > y) x « x - y; else у = у - x; }
Глава 12. Обработка особых (исключительных) ситуаций в Си++ 451 return х; void main() { try ( cout « "\nGCM_ONE(66,44) » " « GCM_ONE(66,44); cout « "\nGCM_ONE(0,7) » " « GCMjONE (0,7) ; cout « "\nGCM_ONE(-12,8) » " « GCMJDNE(-12,8); ) catch (DATA d) { cerr « d.s « " x=" « d.n « ", y=" « d.m; } ) Результат выполнения программы: GCM_ONE (66,44) » 22 ZERO! x « 0, у » 7 Отметим, что объект класса data формируется в теле функции при выполнении конструктора класса. Если бы этот объект не был исклю- чением, он был бы локализован в теле функции и недоступен в точке ее вызова. Но по определению исключений они создаются как вре- менные статические объекты. В данном примере исключения как безымянные объекты класса data формируются в теле функции, вы- зываемой из контролируемого блока. В блоке обработчика исключе- ний безымянный объект типа data инициализирует переменную (параметр) data d и тем самым информация из исключения становит- ся доступной в теле обработчика исключений, что демонстрирует ре- зультат. Итак, чтобы исключение было достаточно информативным, оно должно быть объектом класса, причем класс обычно определяется специально. В приведенной программе класс для исключений опреде- лен как глобальный, т.е. ои доступен как в функции gcm_one(), где формируются исключения, так и в основной программе, где выпол- няется контроль за ними и, при необходимости, их обработка. Внешне исключение выглядит как локальный объект той функции, где оно формируется. Однако исключение не локализуется в блоке, где ис- пользован оператор его генерации. Исключение как объект возникает в точке генерации, распознается в контролируемом блоке и передается в обработчик исключений. Только после обработки оио может исчез- нуть. Нет необходимости в глобальном определении класса объектов- исключений. Основное требование к нему - известность в точке фор- мирования (throw) и в точке обработки (catch). Следующий пример иллюстрирует сказанное. Класс (структура) data определен отдельно как внутри функции gcm_two(), так и в основной программе. Ника- 29*
452 Язык Си++ ких утверждений относительно адекватности этих определений явно не делается. Но передача исключений проходит вполне корректно, что демонстрируют результаты. //Р12-04.СРР - локализация определении типов (классов) // исключении finclude <iostream.h> xnt GCM_ONE(int x, int y) { struct DATA // Определение типа локализовано в функции ( int n, m; char * s; DATA(int x, int y, char *c) // Конструктор класса DATA { n « x; m « у; s « c;} }; if (x =» 0 || у =» 0) throw DATA(x,y, "\nZERO! ”) ; if (x < 0) throw DATA(x,y, "\nNegative parameter 1."); if (y < 0) throw DATA(x,y, "\nNegative parameter 2."); while (x y) { if (x > y) x = x - y; else у « у - x; return x; } void main () { struct DATA // Определение типа локализовано в main() ( int n, m; char *s; DATA(int x, int y, char *c) // Конструктор класса DATA { n = x; m«y; s « c;} }; try { cout « "\nGCM_ONE (66,44) « " « GCMONE (66,44) ; cout « "\nGCM_ONE(-12,8) = " « GCM_ONE (-12,8) ; cout « "\nGCM_ONE(0,7) * " « GCMjONE (0,7) ; } catch (DATA d) { cerr « d.s « " x =* " « d.n « ", у ж " « d.m;} } Результат выполнения программы: GCMJTWO (66,44) ж 22 Negative parameter 1. x = -12, у » 8
Глава 12. Обработка особых (исключительных) ситуаций в Си++ 453 12.2. Синтаксис и семантика генерации и обработки исключений Если проанализировать приведенные выше программы, то ока- жется, что в большинстве из них механизм генерации и обработки ис- ключений можно имитировать “старыми” средствами. В этом случае, определив некоторое состояние программы как особое, ее автор пре- .дусматривает анализ результатов выполнения оператора, в котором Это состояние может быть достигнуто, либо проверяет исходные дан- ные, использование которых в операторе может привести к возникно- вению указанного состояния. Далее выявленное состояние обрабаты- вается. Чаще всего при обработке выводится сообщение о достигну- том состоянии и либо завершается выполнение программы, либо вы- । ПОлняются заранее предусмотренные коррекции. Описанная схема ? имитации механизма обработки особых ситуаций неудобна в тех слу- чаях, когда существует “временной разрыв” между написанием частей программы, где возникает (выявляется) ситуация и где она обрабаты- вается. Например, это типично при разработке библиотечных функ- ций, когда реакции на необычные состояния в функциях должен . определять не автор функций, а программист, применяющий их в сво- f их программах. При возникновении аварийной (особой) ситуации в библиотечной (или просто заранее написанной ) функции желательно передать управление и информацию о характере. ситуации вызы- вающей программе, где программист может по своему предусмотреть | обработку возникшего состояния. Именно такую возможность в язы- | ке Си++ обеспечивает механизм обработки исключений. р Итак, исключения введены в язык в основном для того, чтобы дать возможность программисту динамически (run-time) проводить [ обработку возникающих ситуаций, с которыми не может справиться । исполняемая функция. Основная идея состоит в том, что “функция, | сталкивающаяся с неразрешимой проблемой, формирует исключение i в надежде на то, что вызывающая ее (прямо или косвенно) функция сможет обработать проблему” [26]. Механизм исключений позволяет переносить анализ и обработку ситуации из точки ее возникновения (throw point), в другое место про- граммы, специально предназначенное для ее обработки. Кроме того, из точки возникновения ситуации в место ее обработки (в список об- работчиков исключений) может быть передано любое количество не- обходимой информации, например, сведения о том, какие данные и действия привели к возникновению такой ситуации.
454 Язык Си++ Таким образом механизм обработки исключений позволяет реги- стрировать исключительные ситуации и определять процедуры их обработки, которые будут выполняться перед дальнейшим продолже- нием или завершением программы. Необходимо лишь помнить, что механизм исключений предназна- чен только для синхронных событий, то-есть таких, которые порож- даются в результате работы самой программы (к примеру, попытка прерывания программы нажатием Ctrl+C во время ее выполнения не является синхронным событием). Как уже объяснялось, применение механизма обработки исключе- ний предусматривает выделение в тексте программы двух размещен- ных последовательно обязательных участков - контролируемого блока, в котором могут формироваться исключения, и последователь- ности обработчиков исключений. Контролируемый блок идентифи- цируется ключевым словом try. Каждый обработчик исключений начинается со служебного слова catch. Общая схема размещения ука- занных блоков: try ( операторы контролируемого__блока } са tch (специфихация__исключения) { операторы__обработчика__исключаний } catch (спецификация__исключения) { операторы__обработчика__исключений } В приведенных выше программах использовалось по одному об работчику исключений. Это объясняется “однотипностью” форми руемых исключений (только типа const char * или только типи data). В общем случае в контролируемом блоке могут формироваться исключения разных типов и обработчиков может быть несколько Размещаются они подряд, последовательно друг за другом и каждый обработчик “настроен” на исключение конкретного типа. Специфи кация исключения, размещенная в скобках после служебного слова catch, имеет три формы: catch (тип имя) { ... } catch (тип) { ... } catch (...) { ... } Первый вариант подобен спецификации формального параметра в определении функции. Имя этого параметра используется в операто
Глава 12. Обработка особых (исключительных) ситуаций в Си++ 455 рах обработки исключения. С его помощью к ним передается инфор- мация из обрабатываемого исключения. Второй вариант не предполагает использования значения исклю- чения. Для обработчика важен только его тип и факт его получения. В третьем случае (многоточие) обработчик реагирует на любое исключение независимо от его типа. Так как сравнение “посланного” йсключения со спецификациями обработчиков выполняется последо- вательно, то обработчик с многоточием в качестве спецификации сле- дует помещать только в конце списка обработчиков. В противном случае все возникающие исключения “перехватит” обработчик с мно- готочием в качестве спецификации. В случае, если описать его не по- следним обработчиком, компилятор Borland C++, к примеру, выдаст сообщение об ошибке: The ' . . . ' handler must be last in function . . . () (Обработчик должен идти последним в функции ...0). Продемонстрируем некоторые из перечисленных особенностей обработки исключений еще одной программой: //Р12-05.СРР - исключения без передачи информации tinclude <iostream.h> class ZeroDivide {); // Класс без компонентов class Overflow {}; 11 Класс без компонентов // Определение функции с генерацией исключении: float div(float n, float d) (if (d — 0.0) throw ZeroDivide(); // Вызов конструктора double b = n/d; if (b > le+30) throw Overflow(); // Вызов конструктора return b; I float x » le-20, у » 5.5, z « le+20, w « 0.0; // Вызывающая функция с выявлением и обработкой исключений: void RR(void) { // Контролируемый блок: try { у » div(4.4,w); z « div(z,х); } // Последовательность обработчиков исключений: catch (overflow) { cerr « "\nOverflow"; z — 1е30; ) catch (zeroDivide) { cerr « "\nZeroDivide"; w « 1.0; ) )
456 Язык Си++ void main () { // Вызов функции div () с нулевым делителем w: RRO ; // Вызов функции div () с арифметическим переполнением: RRO ; cout « "\nResult: у = " «у; cout « "\nResult: z = " << z; Результат выполнения программы: ZeroDivide Overflow Result: у « 4.4 Result: z = le+30 В программе в качестве типов для исключений используются клас сы без явйо определенных компонентов. Конструктор ZeroDivide () вызывается и формирует безымянный объект (исключение) при по пытке деления на нуль. Конструктор Overflow () используется для создания исключений, когда значение результата деления превысив величину 1е+30. Исключения указанных типов не передают содержа тельной информации. Эта информация не предусмотрена и в соответ ствующих обработчиках исключений. При первом обращении к функции RRO значение глобальной переменной у не изменяется, так как управление передается обработчику исключений catch (ZeroD ivide) При его выполнении выводится сообщение, и делитель w (глобальная переменная) устанавливается равным 1.0. После обработчика исклю чения завершается функция RR(), и вновь в основной программе вы зывается функция rr() , но уже с измененным значением w. При этом обращение div(4.4,w) обрабатывается безошибочно, а вызов div(z,x) приводит к формированию исключения типа overflow. Его обработка в rr() предусматривает печать предупреждающего сооб щения и изменение значения глобальной переменной г. Обработчик catch (ZeroDivide) в этом случае пропускается. После выхода из RR() основная программа выполняется обычным образом и печата- ются значения результатов “деления”, осуществленного с помощью функции div(). Продолжим рассмотрение правил обработки исключений. Если при выполнении операторов контролируемого блока исключений не
Глава 12. Обработка особых (исключительных) ситуаций в Си++ 457 I возникло, то ни один из обработчиков исключений не используется, и ; управление передается в точку непосредственно после них. Если в контролируемом блоке формируется исключение, то де- j Лается попытка найти среди последующих обработчиков соответ- ствующий исключению обработчик и передать ему управление. После ; обработки исключения управление передается в точку окончания по- • следовательности обработчиков. Возврата в контролируемый блок не ; происходит. Если исключение создано, однако соответствующий ему J блок обработки отсутствует, то автоматически вызывается специаль- г ная библиотечная функция terminate О. Выполнение функции terminate {) завершает выполнение программы. । При поиске обработчика, пригодного для “обслуживания” исклю- I чения, оно последовательно сравнивается по типу со спецификациями исключений, помещенными в скобках после служебных слов catch. I Спецификации исключений подобны спецификациям формальных параметров функций, а набор обработчиков исключений подобен со- вокупности перегруженных функций. Если обработчик исключений (процедура обработки) имеет вид: catch (Т х) ( действия обработчика ) I [ Где т - некоторый тип, то обработчик предназначен для исключений в . виде объектов типа т. I Однако сравнение по типам в обработчиках имеет более широкий смысл. Если исключение имеет тип const т, const Tfi или Tfi, то про- цедура также пригодна для обработки исключения. Исключение “захватывается” (воспринимается) обработчиком и в том случае, если * тип исключения может быть стандартным образом приведен к типу формального параметра обработчика. Кроме того, если исключение есть объект некоторого класса т и у этого класса т есть доступный в точке порождения исключения базовый класс в, то обработчик catch (В х) { действия_обработчика } также соответствует этому исключению. Генерация исключений. Выражение, формирующее исключение, может иметь две формы: throw выраженив^генврации_исключеиия; throw; Первая из указанных форм уже продемонстрирована в приведен- ных программах. Важно отметить, что исключение в ней формируется
458 Язык Си++ как статический объект, значение которого определяется выражением генерации. Несмотря на то, что исключение формируется внутри функции как локальный объект, копия этого объекта передается зи пределы контролируемого блока и инциализирует переменную, ис пользованную в спецификации исключения обработчика. Копия объ екта, сформированного при генерации исключения, Существует, пока исключение не будет полностью обработано. В некоторых случаях используется вложение контролируемых блоков, и не всегда исключение, возникшее в самом внутреннем кон тролируемом блоке, может быть сразу же правильно обработано. В этом случае в обработчике можно использовать сокращенную форму оператора: throw; Этот оператор, не содержащий выражения после служебного ело ва, “ретранслирует” уже существующее исключение, т.е. передает его из процедуры обработки и из контролируемого блока, в который входит эта процедура, в процедуру обработки следующего (более вы сокого) уровня. Естественно, что ретрансляция возможна только для уже созданного исключения. Поэтому оператор throw; может исполь зоваться только внутри процедуры обработки исключений и разумен только при вложении контролируемых блоков. В качестве иллюстра ции сказанного приведем следующую программу с функцией compare (), анализирующей четность (или нечетность) значения цело го параметра. Для четного (even) значения параметра функция форми рует исключение типа const char *. Для нечетного (odd) значения создается исключение типа int, равное значению параметра. В визы вающей функции gg() - два вложенных контролируемых блока. Во внутреннем - два обработчика исключений. Обработчик catch (int n), приняв исключение, выводит в поток cout сообщение и ретрансли рует исключение, т.е. передает его во внешний контролируемый блок Обработка исключения во внешнем блоке не имеет каких-либо осо бенностей. Текст программы: //Р12-06.СРР - вложение контролируемых блоков // и ретрансляция исключений tinclude <iostream.h> void compare (int k) // Функция t генерирующая исключения { if (k%2 ?= 0) throw k; // Нечетное значение (odd) else throw 11 even"; // Четное значение (even) )
Глава 12. Обработка особых (исключительных) ситуаций в Си++ 459 // Функция с контролем и обработкой исключений: Void GG(int j) ( try ( try ( compare(j); } // Вложенный контролируемый блок catch (int n) { cout « "\nOdd"; throw; // Ретрансляция исключения } catch (const char *) ( cout « "XnEven"; } } (I Конец внешнего контролируемого блока // Обработка ретранслированного исключения: catch (int i) ( cout « "\nRasult ® " « i; } ) // Конец функции GG() void main() ( GG(4) ; GG(7) ; I Результат работы программы: Even Odd Result » 7 В основной программе функция GG(j вызывается дважды - с чет- ным и нечетным параметрами. Для четного параметра 4 функция пос- ле печати сообщения "Even" завершается без выхода из внутреннего контролируемого блока. Для нечетного параметра выполняются две процедуры обработки исключений из двух вложенных контролируе- мых блоков. Первая из них печатает сообщение “Odd” и ретранслиру- ет исключение. Вторая печатает значение нечетного параметра, снабдив его пояснительным текстом: "Result » 7". Если оператор throw; использовать вие контролируемого блока, то вызывается специальная функция terminate(), завершающая вы- полнение программы. При вложении контролируемых блоков исключение, возникшее во внутреннем блоке, последовательно “просматривает” обработчики, переходя от внутреннего (вложенного) блока к внешнему до тех пор, пока не будет найдена подходящая процедура обработки. (Иногда действия по установлению соответствия между процедурой обработки и исключением объясняют в обратном порядке. Говорят,что не ис- ключение просматривает заголовок процедуры обработки, а обра- ботчики анализируют исключение, посланное из контролируемого блока и последовательно проходящее через заголовки обработчиков.
460 Язык Си++ Однако это не меняет существа механизма.) Если во всей совокуп- ности обработчиков не будет найден подходящий, то выполняется аварийное завершение программы с выдачей, например, такого сооб- щения: ”Program Aborted”. Аналогичная ситуация может возникнуть и при ретрансляции исключения, когда во внешних контролируемых блоках не окажется соответствующей исключению процедуры обра- ботки. Используя следующие ниже синтаксические конструкции, можно указывать исключения, которые будет формировать конкретная функция: void my_funcl() throw(А,В) ( // Тело функции } void my_func2() throw() ( // Тело функции } В первом случае указан список исключений (А и в ~ это имена не- которых типов), которые может порождать функция my_funcl(). Ес- ли в функции my__funci () создано исключение, отличное по типу от А и в, это будет соответствовать порождению неопределенного исклю- чения и управление будет передано специальной функции unexpectedl) (см.п. 12.4). По умолчанию функция unexpected() за- канчивается вызовом библиотечной функции abort(), которая за- вершает программу. Во втором случае утверждается, что функция my_func2() не мо- жет порождать никаких исключений. Точнее говоря, “внешний мир” не должен ожидать от функции никаких исключений. Если некоторые другие функции в теле функции my_func2 О породили исключение, то оно должно быть обработано в теле самой функции my_func2(). В противном случае такое исключение, вышедшее за пределы функции my_func2(), считается неопределенным исключением, и управление передается функции unexpected (). Обработка исключений. Как уже было сказано, процедура обра- ботки исключений определяется ключевым словом catch, вслед за ко- торым в скобках помещена спецификация исключения, а затем в фигурных скобках следует блок обработки исключения. Эта процеду- ра должна быть помещена непосредственно после контролируемого блока. Каждая процедура может обрабатывать только одно исключе- ние заданного или преобразуемого к заданному типа, который указан в спецификации ее параметра. Рассмотрим возможные преобразова- ния при отождествлений исключения с процедурой обработки исклю- чений. Стандартная схема:
Глава 12. Обработка особых (исключительных) ситуаций в Си++ 461 try { /* Произвольный ход, лорождажлций исключения X */ } catch (Т х) { /* Некоторые действия, возможно с х */ } Здесь определена процедура обработки для объекта типа т. Как уже говорилось, если исключение х есть объект типа т, т&, const т или const т&, то процедура соответствует этому объекту х. Кроме того, соответствие между исключением х и процедурой обра- ботки устанавливается в тех случаях, когда тих- одного типа; т - доступный в точке порождения исключения базовый класс для х; т - тип “указатель” их- типа “указатель”, причем х можно преобразо- вать к типу т путем стандартных преобразований указателя в точке порождения исключения. Просмотр процедур обработки исключений производится в соот- ветствии с порядком их размещения в программе. Исключение обра- батывается некоторой процедурой в случае, если его тип совпадает или может быть преобразован к типу, обозначенному в спецификации исключения. При этом необходимо обратить внимание, что если один класс (например, alpha) является базовым для другого класса (например, beta), то обработчик исключения beta должен разме- щаться раньше обработчика alpha, в противном случае обработчик исключения beta не будет вызван никогда. Рассмотрим такую схему программы: class ALPHA {}; class BETA: public ALPHA (); void fl () ( . . . try (...) catch (BETA) // Правильно (...) catch (ALPHA) (...) ) void f2() ( . . . try (...) catch (ALPHA) // Всегда будет обработан и объект класса ( ... // ВЕТА, т.к. "захватываются" исключения ... // классов ALPHA и всех порожденных
462 Язык Си++ } //от него catch (ВЕТА) // Неправильно: заход в обработчик // невозможен! (. . .} } Если из контролируемого блока будет послано исключение типа ВЕТА, то во втором случае, т.е. в f 2 (), оно всегда будет захвачено обработчиком alpha, так как alpha является доступным базовым классом для ВЕТА. При этом компилятор Borland C++ выдает предупреж- дение: Handler for ’BETA’ hidden by previous handler for 'ALPHA' in function f2() (Обработчик ’beta' "не виден" за размещенным перед ним обработчиком 'ALPHA' в функции f2 ()) . Заметим, что для явного выхода из процедуры обработки исклю- чения или контролируемого блока можно также использовать оператор goto для передачи управления операторам, находящимся вне этой процедуры или вне контролируемого блока, однако оператором goto нельзя воспользоваться для передачи управления обратно - в процедуру обработки исключений или в контролируемый блок. После выполнения процедуры обработки программа продолжает выполнение с точки, расположенной после последней процедуры обработки исключений данного контролируемого блока. Другие процедуры обработки исключений для текущего исключения нс выполняются. try ( // Тело контролируемого блока } catch (спецификация.исключения) ( // Тело обработчика исключений } catch (спецификация.исключения) { // Тело обработчика исключении } // После выполнения любого обработчика // исполнение программы будет продолжено отсюда Как уже показано выше, язык Си++позволяет описывать набо| исключений, которые может порождать функция. Это описан! i- исключений помещается в качестве суффикса в заголовке определен! i функции или в ее прототипе. Синтаксис такого описания исключена следующий: throw (список^ -Идентмфикаторов.типов)
Глава 12. Обработка особых (исключительных) ситуаций в Си++ 463 где список_идентификаторов_типов - это ОДИН идентификатор типа или последовательность разделенных запятыми идентификаторов ти- пов. Указанный суффикс, определяющий генерируемые функцией ис- ключения, не входит в тип функции. Поэтому при описании указателей на функцию этот суффикс не используется. При описании указателя на функцию задают лишь возвращаемое функцией значение и типы аргументов. Примеры прототипов функций с указанием генерируемых исклю- чений: void f2 (void) throw (); // Функция# не порождающая // исключений void f3(void) throw(BETA); // Функция может порождать // только исключение типа ВЕТА void (*fptr) () ; // Указатель на функцию, возвращающую void fptr = f2; // Корректное присваивание fptr = f3; // Корректное присваивание В следующих примерах описываются еще некоторые функции с перечислением исключений: void f 1 О ; // Может порождать любые исключения void f 2 () throw () ; / / Не порождает никаких исключений void f3() throw (А, В*); // Может порождать исключения в виде // объектов классов, порожденных из // А или указателей на объекты // классов, наследственно // порожденных из В Если функция порождает исключение, не указанное в списке, про- грамма вызывает функцию unexpected (). Это происходит во время выполнения программы и не может быть выяснено на стадии ее ком- пиляции. Поэтому необходимо внимательно описывать процедуры обработки тех исключений, которые порождаются функциями, вызы- ваемыми изнутри (из тела рассматриваемой) функции. Особое внимание необходимо обратить на перегрузку виртуаль- ных функций тех классов, к которым относятся исключения. Речь идет о следующем. Пусть классы alpha и beta определены следующим об- разом: class ALPHA // Базовый класс для ВЕТА { public: virtual void print (void) { cout « ’’print: Класс ALPHA”; ) };
464 Язык Си++ class BETA: public ALPHA { public: virtual void print(void) ( cout « "print: Класс BETA"; } }; BETA b; // Создан объект класса BETA Теперь рассмотрим три ситуации: 1) ... try ( throw(b); // Исключение в виде объекта класса ВЕТА catch (ALPHA d) { d.printO; ) 2) ... try { throw(b); // Исключение в виде объекта класса ВЕТА catch (ALPHA & d) ( d.printO; ) 3) ... try { throw(b); // Исключение в виде объекта класса ВЕТА } catch (BETA d) { d.printO; ) В первом случае при входе в обработчик фактический параметр соответствующий формальному параметру alpha d, воспринимается как объект типа alpha, даже если исключение создано как объект класса beta. Поэтому при обработке доступны только компоненты класса alpha. Результатом выполнения этого фрагмента будет печать строки: print: Класс ALPHA Во втором случае во избежание потери информации использована передача значения по ссылке. В этом случае будет вызвана компо нентная функция print () класса beta, и результат будет таким: print: Класс ВЕТА
Глава 12. Обработка особых (исключительных) ситуаций в Си++ 465 Попутно отметим, что функция print О класса ветл будет вызы- ваться и в том случае, если она будет являться защищенным (protected) или собственным (private) компонентом класса beta. Так, если в опи- сании класса beta вместо ключевого слова public поставить protected или private, то результат не изменится. В этом нет ничего удивитель- ного, так как “права доступа к виртуальной функции определяются ее определением и не заменяются на права доступа к функциям, которые позднее переопределяют ее” [2]. Поэтому и в данном случае права до- ступа к функции print определяются правами, заданными в классе alpha (см. п. 10.3). Конечно, можно непосредственно “отлавливать” исключение в виде объекта класса beta, как показано в третьем примере. Однако в этом случае если функция print () будет входить в число защищенных или собственных компонентов класса beta, такой вызов функции print () окажется невозможным, и при компиляции будет выдано со- общение об ошибке. 12.3. Обработка исключений при динамическом выделении памяти i Конкретные реализации компиляторов языка Си++ обеспечивают i программиста некоторыми дополнительными возможностями для [' работы с исключениями. Здесь следует отметить предопределенные исключения, а также типы, переменные и функции, специально пред- назначенные для расширения возможностей механизма исключений. Достаточно распространенной особой ситуацией, требующей спе- циальных действий на этапе выполнения программы, является невоз- можность выделить нужный участок памяти при ее динамическом распределении. Стандартное средство для такого запроса памяти - это операция new или перегруженные операции, вводимые с помощью операций-функций operator new() и operator new[] (). По умол- чанию, если операция new не может выделить требуемое количество памяти, то она возвращает нулевое значение (null) и одновременно формирует исключение типа xalloc. Кроме того, в реализацию ВС++ включена специальная глобальная переменная _new_handler, значе- нием которой служит указатель на функцию, которая запускается на выполнение при неудачном завершении операции-функции operator new(). По умолчанию функция, адресуемая указателем _new_handler, завершает выполнение программы. |'3821
466 Язык Си++ Функция set__new__handler () позволяет программисту назначить собственную функцию, которая будет автоматически вызываться при невозможности выполнить операцию new. Функция set_new__handler () ОПИСЗНа В заголовочном файле new.h следующим образом: new__handler set_new_handler (new__handler my__handler) ; Функция set_new__handier о принимает в качестве параметра указатель my_handler на ту функцию, которая должна автоматически вызываться при неудачном выделении памяти операцией new. Параметр my_handier специфицирован как имеющий тип new_handier, определенный в заголовочном файле new.h таким обра- зом: typedef void (new * new_handler) () throw (xalloc) ; В соответствии с приведенным форматом new_handler - это ука- затель на функцию без параметров, не возвращающую значения (void) и, возможно, порождающую исключение типа xalloc. Тип xalloc - это класс, определенный в заголовочном файле except.h. Объект класса xalloc, созданный как исключение, передает ин- формацию об ошибке при обработке запроса на выделение памяти. Класс xalloc создан на базе класса xmsg, который выдает сообщение, определяющее сформированное исключение. Определение xmsg в за- головочном файле except.h выглядит так: class xmsg ( public: xmsg(const string fimsg); xmsg(const xmsg fimsg); -xmsg(); const string & why() const; void raise() throw(xmsg); xmsgfi operator»(const xmsg &src); private: string _FAR *str; 1; Класс xmsg не имеет конструктора по умолчанию. Общедоступ ный (public) конструктор: xmsg (string msg) предполагает, что с каж дым xmsg-объектом должно быть связано конкретное явно заданное сообщение типа string. Тип string определен в заголовочном файле cstring.h.
Глава 12. Обработка особых (исключительных) ситуаций в Си++ 467 Общедоступные (public) компонентные функции класса: void raise() throw(xmsg); вызов raise () приводит к порождению исключения xmsg. В частности, порождается *this. inline const string _FAR & xmsg::why() const { return *str; ); выдает строку, использованную в качестве параметра кон- структором класса xmsg. Поскольку каждый экземпляр (объект) клссса xmsg обязан иметь собственное сообщение, все его копии должны иметь уникальные сообщения. Вернемся к классу xalloc. Он описан в заголовочном файле except.h следующим образом: class xalloc : public xmsg ( public: xalloc(const string fasg, size__t size); // Конструктор size_t requested () const; void raise() throw(xalloc); private: size_t siz; }; Класс xalloc не имеет конструктора по умолчанию, поэтому каж- дое определение объекта xalloc должно включать сообщение, кото- рое выдается в случае, если не может быть выделено size байт памяти. Тип string определен в заголовочном файле cstring .h. Общедоступные (public) компонентные функции класса xalloc: void xalloc;:raise() throw(xalloc); Вызов raise () приводит к порождению исключения типа xalloc. В частности, порождается *this. inline size_t xalloc::requested() const { return siz; } Функция возвращает количество запрошенной для выделения памяти. Итак, поговорив о классах xalloc и xmsg, возвратимся к функции et_new_handler(). Ее можно вызывать многократно, устанавливая разные функции для реакции на нехватку памяти. В качестве возвра щаемого значения функция set_new_handler () возвращает указатель (тип new_handler) на функцию, которая была установлена с по? 'ощью set_new_handler () при ее предыдущем выполнении. Чтобы
468 Язык Си++ восстановить традиционную схему обработки ситуации нехватки па- мяти для операции new, следует вызвать функцию в виде set_new_handier (0). В этом случае все обработчики “отключаются” и накладывается запрет на генерацию исключений. Итак, если операция new не может выделить требуемого коли- чества памяти, вызывается последняя из функций, установленных с помощью set__new_handler (). Если не было установлено ни одной такой функции, new возвращает значение 0. Функция my_handler() должна описывать действия, которые необходимо выполнить, если new не может удовлетворить требуемый запрос. Определяемая программистом функция my__handler () должна вы- полнить одно из следующих действий: • вызвать библиотечную функцию abort () или exit (); • передать управление исходному обработчику этой ситуации; • освободить память и вернуть управление программе; • вызвать исключение типа xalioc или порожденное от него. Рассмотрим особенности перечисленных вариантов. Вызов функ- ции abort () демонстрирует следующая программа: //Р12-07.СРР " завершение программы в функции my_handler() tinclude <iostream.h> // Описание потоков ввода/вывода tinclude <new.h> // Описание функции set__new_handler () tinclude <stdlib.h> // Описание функции abort О // Функция для обработки ошибок при выполнении // операции new: void new__new__handler () { cerr « ’’Ошибка при выделении памяти! abort(); // Если память выделить невозможно, вызываем // функцию abort(), которая завершает программу // с выдачей сообщения "Abnormal program // termination” } void main(void) { // Устанавливаем собственный обработчик ошибок: aet__new_handler (new__new_handler) ; // Цикл с ограничением количества попыток выделения / / памяти: for (int п e 1; п <« 1000; п++) ( cout « п « ": "; new char [61440U]; // Пытаемся выделить 60 Кбайт cout « "Успех!" « endl;
Глава 12. Обработка особых (исключительных) ситуаций в Си++ 469 Несмотря на простоту программы, результаты ее работы будут сильно зависеть от того, откуда она будет запускаться на выполнение. Так, если ее запустить под DOS из интегрированной среды компиля- тора Borland C++ 3.1 (начиная с версии Borland C++ 4.0, интегриро- ванная среда разработки (IDE - Integrated Development Environment) * под DOS отсутствует), то, скорее всего, неудачей закончится уже пер- t вый запрос на выделение памяти, и результат будет таким: 1: Ошибка при выделении памяти? I Если запустить эту же программу непосредственно из командной I строки DOS (выйдя из интегрированной среды), то несколько первых I попыток выделения памяти будут удачными, и результат будет при- I мерно таким: I 1: Успех! I 2: Успех! I 3: Успех! Г 4: Успех! | 5: Успех! I 6: Успех! j 7: Ошибка при выделении памяти! [ Если же откомпилировать эту же программу под Microsoft Windows в режиме EasyWin компиляторов Borland C++ версий 4.0 или 4.5, то для программы будет отводиться не только вся физически до- ступная оперативная, но также и вся виртуальная память, определен- ная в этот момент в среде Windows. (Для справки: режим EasyWin дает возможность компилировать DOS-приложения, которые будут осу- ществлять ввод-вывод в стандартное окно Windows.) Так, на компьютере с оперативной памятью 8 Мбайт и виртуаль- ной памятью 15 Мбайт при свободных 16.401 Кбайте памяти (соглас- но информации из окна About оболочки Program Manager) работа программы, скомпилированной с помощью Borland C++ 4.5, прохо- дила следующим образом: сначала очень быстро было выделено 225 блоков по 60 Кбайт (что составляет в сумме 13,5 Мбайт!), после чего начался активный обмен данными с диском и было выделено еще 22 блока. Затем выделение каждого очередного блока происходило с частотой примерно 1 раз в 2-3 с, а при попытке выделить 257-й блок было получено сообщение об ошибке. Всего, таким образом, было выделено 256 * 60Кбайт = 15.360 Кбайт или 15 Мбайт памяти! Для демонстрации передачи управления исходному обработчику рассмотрим следующую программу (модификация программы Р12- 17. срр):
470 Язык Си++ //Р12-08.СРР #includa <iostream.h> // Описание потоков ввода/вывода Mincluda <new.h> // Описание функции set_new_handler() Minclude <atdlib.h> // Описание функции abort () / / Прототип функции “ старого обработчика ошибок // распределения памяти: void (*old_new_handler)О; void new_new_handler() // Функция для обработки окибок { cerr « "Ошибка при выделении памяти! ; if (old_new_handler) (*old_new_handler)(); abort(); // Если память выделить невозможно, вызываем // функцию abort(), которая завершает программу // с выдачей сообщения "Abnormal program // termination" } void main(void} { If Устанавливаем собственный обработчик ошибок: old_new__handler * aet_new_handler (nex_new__handler) ; / / Цикл с ограничением количества попыток выделения // памяти: for (int п » 1; п О 1000; п++) { cout « п « ": ”; new char[61440U]; // Пытаемся выделить 60 Кбайт cout « "Успех!" « endl; 1 } Ее отличие от предыдущей программы (Р12-07.СРР) состоит в том, что при установке собственного обработчика ошибок адрес ста- рого (стандартного) обработчика сохраняется как значение указателя old_new_handier. Этот сохраненный адрес используется затем в функции ДЛЯ обработки ошибок new_new_handler. С его помощью вместо библиотечной функции abort () вызывается “старый” обра- ботчик. Результаты выполнения программы при тех же условиях (в среде Windows): 1: Успех! 2: Успех! 244: Успех! Ошибка при выделении памяти! и затем сообщение в окне: "Program Aborted" (рис. 12.1)
Глава 12. Обработка особых (исключительных) ситуаций в Си++ 471 Если my__handler возвращает управление программе, new пытает- ся снова выделить требуемое количество памяти. Наилучшим выхо- дом из ситуации нехватки памяти будет, очевидно, освобождение внутри функции my_handi®r требуемого количества памяти и воз- вращение управления программе. В этом случае new сможет удовлет- ворить запрос, и выполнение программы будет продолжено. Рис. 12.1. Окно, выдаваемое функцией abort () при компиляции программы в режиме EasyWin В следующем примере при нехватке памяти освобождаются блоки памяти, выделенной ранее, и управление возвращается программе. Для этого в программе определена глобальная переменная-указатель на блок (массив) символов (char *ptr;). //РОЭ.СРР - освобождение памяти в функции my__handler () linclude <ioatream.h> // Описание потоков ввода-вывода linclude <new.h> // Описание функции aet__new_handler char *ptr; // Указатель на блок (массив) символов // Функция для обработки ошибок при выполнении // операции new: void new_new_handler() { сегг « "Ошибка при выделении памяти! delete ptr; // Если память выделить невозможно, удаляем / / последний выделенный блок и возвращаем // управление программе ) void main(void) { // Устанавливаем собственный обработчик ошибок: set_new__handler(new_new_handler) ; // Цикл с ограничением количества попыток выделения // памяти: for (int n « 1; n <e 1000; n++) { cout « n « ": ”; // Пытаемся выделить 60 Кбайт: ptr « new char[61440U]; cout « "Успех!" « endl;
472 Язык Си++ } set new_handler (0) ; // Отключаем все обработчики } Результат выполнения этой программы будет следующим (при за- пуске из командной строки DOS): 1: Успех! 2: Успех! 3: Успех! 4: Успех! 5: Успех! 6: Успех! * 7: Ошибка при выделении памяти! Успех! 8: Ошибка при выделении памяти! Успех! 9: Ошибка при выделении памяти! Успех! 10: Ошибка при выделении памяти! Успех! Действительно, результаты работы подтверждают сказанное: пос- ле неудачного выделения памяти освобождается последний выделен- ный блок, после чего операции new удается выделить очередной блок памяти такого же размера. Эта последовательность действий преры- вается только по достижении конца цикла. В случае компиляции в режиме EasyWin была отмечена и такая комбинация сообщений: 251: Успех! 252: Ошибка при выделении памяти! Успех! 253: Ошибка при выделении памяти! Успех! 254: Успех! 255: Успех! 256: Ошибка при выделении памяти! Успех! 257: Ошибка при выделении памяти! Успех! Это означает, что если программа, выполняемая под Windows, не может получить требуемое количество памяти, то имеет смысл повто- рить попытки через некоторое время. Если показанное в последнем примере освобождение памяти не- возможно, функция my_handler о обязана либо вызвать исключение, либо завершить программу. В противном случае программа, очевид- но, зациклится (после возврата из new_new_handler() попытка new выделить память опять окончится неудачей, снова будет вызвана
Глава 12. Оборотка особых (исключительных) ситуаций в Си++ 473 new_new_handler (), которая, не чистив память, вновь вернет управление программе и т.д.). Так, если в первом примере Р12-07.СРР из функции new_new handler () убрать вызов функции abort () , получавшаяся программа зациклится, и результат ее запуска будет примерно таким: 1: Успех ? 2: Успех! 3: Успех! 4: Успех! 5: Успех! 6: Успех! 7: Ошибка при выделении памяти(Ошибка при выделении памяти! Ошибка при выделении памяти!Ошибка при выделении памяти! Ошибка при выделении памяти!Ошибка при выделении памяти! Ошибка при выделении памяти(Ошибка при выделении памяти! Ошибка при выделении памяти! Ошибка при вы... Последняя из перечисленных задач, решаемых функцией, назначен- ной для обработки неудачного завершения операции new, предусмат- ривает генерацию исключения xalloc. Это исключение формирует и функция, которая по умолчанию обрабатывает неудачное завершение операции new. Рассмотрим на примере, какую информацию передает исключение типа xalloc и как эту информацию можно использовать. //Р12-10.СРР - обработка исключения типа xalloc ^include < except.h> // Описание класса xalloc ^include < iostream.h> // Описание потоков ввода/вывода #include <cstring.h> //Описание класса string void main (void) {try { //Цикл с ограничением количества попыток выделения //памяти: for (int n=l; n <= 1000; n++) {cout « n « ": "; new char [61440U]; //Пытаемся выделить 60 Кбайт cout « "Успех!" « endl; } } catch (xalloc X) { cout « "При выделении памяти обнаружено"; cout « "исключение"; <<X.why(); } }
474 Язык Си++ Результат выполнения программы (из командной строки DOS): 1: Успех! 2: Успех* 3: Успех! 4: Успех! 5: Успех! 6: Успех! 7: При выделении памяти обнаружено исключение Out of memory К сожалению, стандартный обработчик ошибок выделения памя- ти не заносит количество нехватившей памяти в компоненте siz класса xalioc (на что, между прочим, намекается в документации), поэтому даже если в тело обработчика исключений в последнем при- мере вставить дополнительно вызов функции requested, возвра- щающей siz, т.е.: cout « "Обнаружено исключение " « X.whyO ; cout « ” при выделении ”; cout « X.request() « " байт памяти"; то результат и в этом случае будет не очень информативным: 7: Обнаружено исключение Out of memory при выделении 0 байт памяти. Самым радикальным способом устранения этой некорректности реализации будет, вероятно, перегрузка операции new. Впрочем, эту возможность предоставим читателю, а сейчас покажем, как можно реализовать обработку ошибок операции new с помощью установки своей функции, которая будет порождать исключение xalioc с соот- ветствующими значениями компонентов. //Р12-11.СРР - "своя" функция обработки ошибок при // выполнении операции new и генерации xalioc «include <except.h> // Описание класса xalioc «include <iostream.h> // Описание потоков ввода/вывода «include <new.h> // Описание функции swt_new_handler «include <cstring.h> // Описание класса string «define SIZE 61440U // Функция для обработки ошибок при выполнении // операции new: void new_new__handler () throw (xalioc) ( // Если память выделить не удалось, формируем исключение // xalioc с соответствующими компонентами:
Глава 12. Обработка особых (исключительных) ситуаций в Си++ 475 throw(xalloc(string("Memory full"),SIZE)); ) void main(void) { // Устанавливаем собственный обработчик ошибок: set new_handler(new new^handler); tty // Контролируемый блок { for (int n « 1; n <“ 1000; n++) { cout « n « ": "; new char [SIZE]; // Пытаемся выделить 60 Кбайт cout « "Успех!" « endl; } catch (xalloc X) 11 Обработчик исключении { cout « "Обнаружено исключение " « X.whyO ; cout « ” при выделении "; cout « X.requested() « " байт памяти."; ) } Результат выполнения программы (из командной строки DOS): 1: Успех! 2: Успех! 3: Успех! 4: Успех! 5: Успех! 6: Успех! 7: Обнаружено исключение Memory full при выделении 61440 байт памяти. По-видимому, так по смыслу и должен работать встроенный об- , работник ошибок выделения памяти. Кроме вышеописанных вариан- тов, также может использоваться перегрузка операций new() и new[J() для определения каких-либо дополнительных проверок (о перегрузке см. п.9.7). 12 .4. Функции, глобальные переменные и классы поддержки механизма исключений Функция обработки неопознанного исключения. Функция void tareinata() вызывается в случае, когда отсутствует процедура для обработки некоторого сформированного исключения. По умолчанию ta»inate() вызывает библиотечную функцию abort(), что влечет ыдачу сообщения "Abnormal program termination" (или, при компи-
476 Язык Си++ ляции программы в режиме EasyWin в среде Windows, вывод окна, аналогичного изображенному на рис. 12.1) и завершение программы. Если такая последовательность действий программиста не устраивает, он может написать собственную функцию (terminate_function) и зарегистрировать ее с помощью функции set_terminate(). В этом случае terminate () будет вызывать эту новую функцию вместо функции abort (). Функция set_terminate() позволяет установить функцию, опре- деляющую реакцию программы на исключение, для обработки кото- рого нет специальной процедуры. Эти действия определяются в функции, поименованной ниже как terminate_func (). Указанная функция специфицируется как функция типа terminate_function. Такой тип в свою очередь определен в файле except.h как указатель на функцию без параметров, не возвращающую значения: typedef void (*terminate__function) () ; terminate_function set^terminate (terminate_f unction terminate^func) ; Функция set_terminate() возвращает указатель на функцию, которая была установлена с помощью set_terminate() ранее. Следующая программа демонстрирует общую схему применения собственной функции для обработки неопознанного исключения: //Р12-12.СРР - замена стандартной функции terminate() #include <stdlib.h> // Для функции abort() tinclude <except.h> // Для функции поддержки исключений #include <iostream.h> // Для потоков ввода-вывода // Указатель на предыдущую функцию terminate: void (*old_terminate)(); // Новая функция обработки неопознанного исключения: void new_terminate() { cout « ”\пВызвана функция new__terminate () "; // ... Действия, которые необходимо выполнить // ... до завершения программы abort(); // Завершение программы } void main() { // Установка своей функции обработки: old_terminate “ set_terminate(new_terminate) ; // Генерация исключения вне контролируемого блока: throw(25); }
Глава 12. Обработка особых (исключительных) ситуаций в Си++ 477 Результат выполнения программы: Вызвана функция пехиterminate () Вслед за этим программа завершается и выводит в окно сообше- нйе: "Program Aborted!" (рис. 12.1) Вводимая программистом функция для обработки неопознанного исключения, во-первых, не должна формировать новых исключений, во-вторых, эта функция должна завершать программу н не возвра- щать управление вызвавшей ее функции terminate (). Попытка тако- го возврата приведет к неопределенным результатам. Функция void unexpected () вызывается, когда некоторая функ- ция порождает исключение, отсутствующее в списке ее исключений. В свою очередь функция unexpected () по умолчанию вызывает функ- цию, зарегистрированную пользователем с помощью set—unexpected(). Если такая функция отсутствует, unexpected() вызывает terminate О. Функция unexpected () не возвращает значе- ния, однако может сама порождать исключения. Функция set_unexpected() позволяет установить функцию, определяющую реакцию программы на неизвестное исключение. Эти действия определяются в функции, которая ниже поименована как unexpected—func(). Указанная функция специфицируется как функ- ция типа unexpected-function. Этот тип определен в файле except.h как указатель на функцию без параметров, не возвра- щающую значения: typedef void ^unexpected—function)(); unexpected—function set—unexpected(unexpected—function unexpected—func); По умолчанию, неожиданное (неизвестное для функции) исключе- ние вызывает функцию unexpected (), которая, в свою очередь вызы- вает либо unexpected—func () (если она определена), либо terminate () (в противном случае). Функция set—unexpected () возвращает указатель на функцию, которая была установлена с помощью set-unexpected() ранее. Устанавливаемая функция (unexpected_func) обработки неизвестно- го исключения не должна возвращать управление вызвавшей ее функ- ции unexpected(). Попытка возврата приведет к неопределенным результатам.
478 Язык Си++ Кроме всего прочего, unexpected_func() может вызывать функ- ции abort (), exit () И terminate (). Глобальные переменные, относящиеся к исключениям: ___throwExceptionName содержит имя типа (класса) последнего исключения, порож- денного программой; ___throwFileName содержит имя файла с исходным текстом программы, в кото- ром было порождено последнее исключение; ___throwLineNumber содержит номер строки в исходном файле, в которой создано порождение исключения. Эти переменные определяются в файле except.h следующим об- разом: extern char * throwExceptionName; extern char * throwFileName; extern unsigned___throwLineNumber; Следующая программа демонстрирует возможности применения перечисленных глобальных переменных: //Р12-13.СРР - Использование глобальных переменных // __throwExceptionName,____throwFileName / / и___throwLineNumber linclude <except.h> // Описание переменных_________throwXXXX linclude <iostream.h> // Описание потоков ввода-вывода class А // Определяем класс А { public: void print() // Функция печати сведений об исключении { cout « "Обнаружено исключение 11; cout « ^throwExceptionName; cout « " в строке " «______throwLineNumber; cout « ” файла 1 " «_____throwFileName « "' " cout « endl; I class B: public A {J; // Класс В порождается из А class С: public А (}; // Класс С порождается из А С _с; // Создан объект класса С void f() // Функция может порождать любые исключения { try { // Формируем исключение (обмят класса С) : throw(_с);
Глава 12. Обработка особых (исключительных) ситуаций в Си++ 479 } catch (В X) // Здесь обрабатываются исключения типа В ( X.print(); } } void main () { try { f () ; ) // Контролируемый блок // Обрабатываются исключения типа А // (и порожденных от него): catch (А X) { X.print(); }; // Обнаружено исключение } Результат выполнения программы; Обнаружено исключение С в строке 22 файла 'Р12-13.СРР' Комментарии в тексте программы достаточно подробно описы- вают ее особенности. В выводимом на экран результате используются значения глобальных переменных. (Обратите внимание, что указан- ные результаты получаются при компиляции программы с заранее установленными опциями, о которых кратко сказано ниже в п. 12.7. 12 .5. Конструкторы и деструкторы в исключениях Когда выполнение программы прерывается возникшим исключе- нием, происходит вызов деструкторов для всех автоматических объ- ектов, появившихся с начала входа в контролируемый блок. Если исключение было порождено во время исполнения конструктора не- которого объекта, деструкторы вызываются лишь для успешно по- строенных объектов. Например, если исключение возникло при построении массива объектов, деструкторы будут вызваны только для полностью построенных объектов. Приведем пример законченной программы, иллюстрирующий по- ведение деструкторов при обработке исключений. //Р12-14.СРР - вызовы деструкторов при исключении tinclude <iostream.h> // Описание потоков ввода-вывода tinclude <new.h> // Описание функции set_new__handler tinclude <cstring.h> // Описание класса cstring // Определение класса "блок памяти" class Memory { char *ptr; public: Memory () // Конструктор выделяет 60 Кбайт памяти
480 Язык Си++ { ptr ш new char [61440U]; } ^Memory() // Деструктор очищает выделенную память { delete ptr; } }; // Определение класса "Набор блоков памяти": class BigMemory { static int nCopy; // Счетчик экземпляров класса + 1 Memory *MemPtr; // Указатель на класс Memory public: // Конструктор с параметром по умолчанию BigMemory(int n 3) { cout « endl « nCopy « " : "; MemPtr “ new Memory [n] ; cout « "Успех!"; // Если память выделена успешно, ++пСору; // увеличиваем счетчик числа экземпляров } ^BigMemory () // Деструктор очищает выделенную память { cout « endl « —nCopy « ": Вызов деструктора"; delete [] MemPtr; ) }; // Инициализация статического элемента: int BigMemory::nCopy “ 1; // Указатель на старый обработчик для new: void (*old_new_handler) () ; .// Новый обработчик ошибок: void new_new_handler() throw(xalloc) { // Печатаем сообщение... cout « "Ошибка при выделении памяти!"; // ... и передаем управление старому обработчику (*old_new_handler)(); ) void main(void) { // Устанавливаем новый обработчик: old_new_handler e set_new_handler (new__new_handler) ; try // Контролируемый блок { II Запрашиваем 100 блоков по 60 Кбайт: BigMemory Requestl(100); // Запрашиваем 100 блоков по 60 Кбайт: BigMemory Requests(100); // Запрашиваем 100 блоков по 60 Кбайт: BigMemory Requests(100); } catch (xmsg& X) // Передача объекта по ссылке { cout « "\пОбнаружено исключение " « X.whyO; cout « " класса " «________throwExceptionName;
Глава 12. Обработка особых (исключительных) ситуаций в Си++ 481 } aet_new handler (old new^handler) ; } Результат выполнения программы: 1: Успех < 2: Успех! 3: Ошибка при выделении памяти! 2: Вызов деструктора 1: Вызов деструктора Обнаружено исключение Out of Memory класса xalloc Заметим, что обычно вызовы деструкторов происходят по умол- чанию. Однако эти умалчиваемые вызовы можно отменить с по- мощью опции компилятора -xd (см. ниже п. 12.7). В этом случае результат выполнения программы будет таким: 1: Успех! 2: Успех! 3: Обнаружено исключение Out of Memory класса xalloc 12.в. Динамическая идентификация типов (RTTI) Недавнее добавление в проект стандарта языка Си++ механизма динамической идентификации типов (RTTI - Run-Time Type Identifi- cation) расширяет язык набором средств, позволяющих идентифици- ровать конкретные типы объектов во время выполнения программы, даже если известны только указатель или только ссылка на интере- сующий вас объект. (Отметим, что для использования средств дина- мического определения типов к программе необходимо подключить файл typeinfo. h.) Механизм динамического определения (идентификации) типов по- зволяет проверить, является ли некоторый объект объектом заданного типа, а также сравнивать типы двух данных объектов . Для этого ис- пользуется операция typeid, которая определяет тип аргумента и воз- вращает ссылку на объект типа const typeinfo, описывающий этот тип. В качестве аргумента typeid можно также использовать и имя не- которого типа. В этом случае typeid вернет ссылку на объект const typeinfo этого типа. Класс typeinfo содержит операции-функции operator— и operator!», которые используются для сравнении ти- пов объектов. Класс typeinfo также содержит компонентную функ- зг3821
482 Язык Си++ цию name(), возвращающую указатель на строку, содержащую ими типа аргумента. Операция typeid имеет две формы: typeid (выражение) typeid (иия_типа) Эта операция используется для динамического получения инфор мации о типах данных, typeid возвращает ссылку на объект типа const typeinfo. Возвращаемый объект представляет тип операнд;! оператора typeid. При динамической идентификации типов важным свойством типа является его полиморфность. Напомним, что понятие полиморфное™ введено для обозначения классов, имеющих по крайней мере одн\ виртуальную или чисто виртуальную функцию. Если операнд операции typeid есть разыменованный указатель или ссылка на полиморфный тип, typeid возвращает динамический тин объекта, получаемого по указателю или ссылке на него. Если операнд неполиморфный, typeid вернет объект, представляемый статическим типом. При этом вы можете использовать операцию typeid как < основными типами данных, так и с производными. Если операнд операции typeid суть разыменованный нулевой ука затель, порождается исключение Bad_typeid. Класс typeinfo описан в файле typeinfo.h (если не считать неко торых опущенных нами модификаторов, не влияющих на существо дела) следующим образом: class___rtti typeinfo { public: tpid * tpp; private: typeinfo(const typeinfo &); typeinfo £ operator»(const typeinfo £); public: virtual *typeinfo(); int operator»»(const typeinfo £) const; int operator!«(const typeinfo &) const; int before(const typeinfo £) const; const char * name() const; ); Общедоступная компонентная функция name О возвращает указа- тель на строку, определяющую тип операнда typeid. Конкретная длина
Глвва 12. Обработка особых (исключительных) ситуаций в Си++ 483 Возвращаемой строки может изменяться при каждом новом вызове функции. Компонентная функция before() производит лексикографическое сравнение названий (имен) типов двух заданных объектов. Для этого Необходимо использовать следующую конструкцию: typeid(Tl) .before(typeid(Т2)); Если имя типа Т1 “предшествует” имени типа Т2, то функция воз- вращает 1, в противном случае возвращается 0. Рассмотрим следующий вариант программы: //Р12-15.СРР ~ идентификация и сравнение типов объектов linclude <typeinfo.h> linclude <iostream.h> void main() ( char szString[10]; float floatVar; cout « typeid(szString).name() « ", " « typeid(floatVar).name(); cout « endl « typeid(szString).before(typeid(floatVar)); ) Результат выполнения программы: char *, float 1 В первой строке результата в текстовом виде напечатаны названия типов переменных szString И floatVar, а ВО второй строке - резуль- тат выполнения функции before(). Он равен 1. Это означает, что строка "char *” лексикографически предшествует строке "float", что соответствует латинскому алфавиту. Операции-функции operator=() и operator!=() обеспечивают сравнение двух типов данных. Пусть szString и floatVar описаны так, как в предыдущем при- мере, тогда •.. typeid (szString)—typeid(floatVar)... // Условие // HE выполнено ...typeid(szString)!«typeid(floatVar)... // Условие /I выполнено Ключевое слово___rtti и опция -rt. По умолчанию в Borland C++ динамическое определение типов (RTTI) всегда разрешено. Од- 31
484 Язык Си++ нако его можно разрешать или запрещать, используя ключ -rt (подробнее см. ниже п. 12.7). Если RTTI запрещено или аргументом typeid является указатель или ссылка на класс без виртуальных функ ций, typeid возвращает ссылку на объект const typeinfo, описы вающий объявленный тип указателя или ссылки, но не реальнын объект (т.е. указатель или ссылку на него). Если прн компиляции использовать ключ -rt- (то-есть отключит! динамическое определение типов) и некоторый базовый класс объяв лен___rtty, то все полиморфные базовые классы также должны бьп i. объявлены как__rtti. struct rtti { virtual si О ; }; // Полиморфизм struct rtti { virtual s2(); }; // Полиморфизм struct X: SI, S2 {}; Если отключен механизм RTTI (использован ключ -rt-), то ди намическое определение типов в порожденных классах может ока заться недоступным. При множественном наследовании способность к динамической идентификации типов порожденного класса зависит от типов и порядка следования базовых классов. Порядок следования оказывается важным в случае, если новый класс порождается как классами без виртуальных функций, так н классами с виртуальными функциями. Если компилировать указанные ниже строки с ключом -rt-, вы должны объявить х с модификатором_____rtti. В противном случае изменение порядка следования базовых классов для х вызовет ошибку при компиляции: Can't inherit non RTTX class from RTTX base 'SI' (Невозможно наследовать He-RTTI-класс из базового RTTI-класса •si*). struct____rtti SI { virtual func(); }; // класс struct S2 { }; // Неполиморфныи класс struct____rtti X: SI, S2 ( 1; В данном случае класс х явно объявлен как_rtti. Это сделано как раз для того, чтобы не зависеть от порядка следования базовых классов при описании х.
Глава 12. Обработка особых (исключительных) ситуаций в Си++ 485 В следующем примере класс х порождается из неполиморфных классов, поэтому его не требуется объявлять как_rtti. struct___rtti SI ( ); // Неполиморфные классы struct 32 { }; struct X: S2, S2 { ); // Порядок следования базовых // классов несущественен Обратите внимание на то, что применение______rtti или ключа -RT не преобразует статический класс в полиморфный класс. //Р12-16.СРР ~ динамическая идентификация типов для // полиморфных классов linclude <iostream.h> linclude <typeinfo.h> class___rtti Alpha ( // Alpha и порожденные из него - RTTI-классы virtual void func() {}; // Виртуальная функция делает }; // класс Alpha полиморфным class В : public Alpha (); //В - тоже RTTI int main(void) ( В Binst; // Копия класса В В *Bptr; // Указатель на класс В 1 Bptr « ABinst; // Инициализация указателя try { // Какого типа *Bptr? if (typeid(*Bptr) typeid(B)) cout « "Тип ” « typeid(*Bptr) .name () « " . " if (typeid(*Bptr) typeid(Alpha)) cout « "ХпУказатель не типа Alpha."; return 0; ) catch (Bad_typeid) ( cout « "Ошибка функции typeid()."; return 1; } Результат работы программы: тип в. Указатель не типа Alpha.
486 ЯзыкСи++ 12.7. Опции компилятора, относящиеся к обработке исключений и динамическому определению типов (RTTI) Доступ к установке перечисленных ниже опций можно получить либо с помощью задания ключей компилятору в командной строке, либо нз интегрированной среды разработки (IDE) Borland С++ 4.0/4.5, из меню Options(Project|C++ Options|Exception Handling/RTTI Если вы используете компилятор командной строки, то для того, чтобы установить некоторую опцию, необходимо задать соответ- ствующий ключ, а для того, чтобы ее отменить - тот же ключ, но с последующим символом ' . Например, для задания разрешения под- держки исключений (см. ниже) необходимо запустить компилятор с ключом -х, а для того, чтобы такую поддержку запретить - с ключом -Х-. —х Enable Exceptions - Разрешить поддержку исключений (По умолчанию = on, т.е. опция включена) Попытка использования исключений и процедур их обработ- ки без этой опции вызовет появление ошибки при компиля- ции: “Exception handling not enabled in function Выключение этой опции может быть полезно, например, если вы хотите удалить обработку исключений из вашей програм- мы (скажем, для переноса ее на другую платформу или пере- хода на другой компилятор). ~хр Enable Exception Location Information - Разрешить получение ин- формации о месте порождения исключения (По умолчанию = off, т.е. опция отключена) Эта опция предоставляет возможность получения информа- ции о месте возникновения исключительной ситуации (название файла и иомер строки) во время выполнении про- граммы (см. функции ______________throwFileName и __throwLineNumber). -xd Enable Destructor Cleanup - Разрешить вызов деструкторов (По умолчанию = on, т.е. опция включена) Если эта опция установлена, то в случае порождения исклю- чения для всех объектов, созданных между областями види-
Глава 12. Обработка особых (исключительных) ситуаций в Си++ 487 мости операторов catch и throw, происходит вызов соответ- ствующих деструкторов. Заметим, что для динамических объ- ектов автоматического вызова деструкторов не происходит н динамические объекты автоматически не освобождаются. -xf Enable Fast Exception Prologs - Разрешить генерацию “быстрого” начального кода (По умолчанию = off, т.е. опция отключена) Эта опция указывает компилятору, что необходимо осущест- вить подстановку ш/ше-выражений во все функции обработки исключений. Это повышает надежность программы, но уве- личивает ее размер (размер получающегося .ЕХЕ-модуля). За- метим, что выбор этой опции возможен лишь при отключенной опции Enable Compatible Exception. -хс Enable Compatible Exceptions ~ Разрешить совместимость исключе- ний (По умолчанию = off, т.е. опция отключена) ; Эта опция позволяет создавать с помощью Borland C++ .ехе и .dll модули, совместимые с .ЕХЕ-модулямн, созданными другими программами. Если вы установите эту опцию, то в исполняемый (.ехе) файл будет включена некоторая инфор- мация, касающаяся обработки исключений (даже при выклю- ченной опции Enable Exceptions). -rt Enable Runtime Type Information - Разрешить динамическое полу- чение информации о типах объектов t (По умолчанию = on, т.е. опция включена) Указание этой опции сообщает компилятору о необходимости построения кода, позволяющего производить определение ти- пов объектов во время нсполнения программы. Если вы ис- пользуете ключ -xd, то вам необходимо, как правило, указывать и эту опцию, поскольку в противном случае вы не сможете удалить указатель на класс с виртуальным деструк- тором. Поэтому обычно эта опция используется одновремен- но с опцией -xd (Enable Destructor Cleanup).
ПРИЛОЖЕНИЕ 1 ТАБЛИЦЫ КОДОВ ASCII Таблица П1.1 Коды управляющих символов (0 -ь 31) Символ Код 10 Код 08 Код 16 Клавиши Значение nul 0 0 00 Ae Нуль soh 1 1 01 AA Начало заголовка stx 2 2 02 AB Начало текста etx 3 3 03 Ac Конец текста eot 4 4 04 AD Конец передачи enq 5 5 05 AE Запрос ack 6 6 06 Ag. Подтверждение bel 7 7 07 AG Сигнал (звонок) be 8 10 08 АИ Забой (шаг назад) ht 9 11 09 AI Горизонтальная табуляция If 10 12 0A AJ Перевод строки vt 11 13 OB AK Вертикальная табуляция ff 12 14 ОС AL Новая страница cr 13 15 0D AM Возврат каретхн so 14 16 0E AN Выключить сдвиг si 15 17 OF AO Включить сдвиг die 16 20 10 AP Ключ связи данных del 17 21 11 AQ Управление устройством 1 dc2 18 22 12 AR Управление устройством 2 dc3 19 23 13 AS Управление устройством 3 dc4 20 24 14 АЦ, Управление устройством 4 nak 21 25 15 AU Отрицательное подтверждение syn 22 26 16 Av Синхронизация etb 23 27 17 Aw Конец передаваемого блока can 24 30 18 Ax Отказ em 25 31 19 AY Конец среды sub 26 32 IA AZ Замена esc 27 33 IB A[ Ключ fs 28 34 IC A\ Разделитель файлов gs 29 35 ID A] Разделитель группы rs 30 36 IE A A Разделитель записей US 31 37 IF A Разделитель модулей В графе "клавиши" обозначение А соответствует нажатию клави ши Ctrl, вместе с которой нажимается соответствующая "буквенная' клавиша, формируя код символа.
Приложение 1. Таблицы кодов ASCH 489 Таблица П1.2 Символы с кодами 32 +127 Символ Код 10 Код 08 Код 16 Символ Код 10 Код 08 Код 16 пробел 32 40 20 I 73 111 49 । 33 41 21 J 74 112 4A п 34 42 22 К 75 113 4B # 35 43 23 L 76 114 4C $ 36 44 24 М 77 115 4D % 37 45 25 N 78 116 4E & 38 46 26 О 79 117 4F 1 39 47 27 Р 80 120 50 ( 40 50 28 Q 81 121 51 ) 41 51 29 R 82 122 52 * 42 52 2А S 83 123 53 + 43 53 2В Т 84 124 54 f 44 54 2С и 85 125 55 - 45 55 2D V 86 126 56 46 56 2Е W 87 127 57 / 47 57 2F X 88 130 58 0 48 60 30 Y 89 131 59 1 49 61 31 Z 90 132 5A 2 50 62 32 [ 91 133 5B 3 51 63 33 \ 92 134 5C 4 52 64 34 1 93 135 5D 5 53 65 35 А 94 136 5E 6 54 66 36 95 137 5F 7 55 67 37 4 96 140 60 8 56 70 38 а 97 141 61 9 57 71 39 Ь 98 142 62 58 72 ЗА с 99 143 63 59 73 ЗВ d 100 144 64 < 60 74 ЗС е 101 145 65 ж 61 75 3D f 102 146 66 > 62 76 ЗЕ 9 103 147 67 63 77 3F h 104 150 68 е 64 100 40 i 105 151 69 А 65 101 41 j 106 152 6A В 66 102 42 k 107 153 6B С 67 103 43 1 108 154 6C D 68 104 44 m 109 155 6D Е 69 105 45 n 110 156 6E Р 70 106 46 о 111 157 6F G 71 107 47 P 112 160 70 Н 72 НО 48 q 113 161 71
490 Язык Си++ Таблица П1.2 (продолжение) Символ Код 10 Код 08 Код 16 г 114 162 72 в 115 163 73 t 116 164 74 U 117 165 75 V 118 166 76 W 119 167 77 X 120 170 78 Символ Код 10 Код 08 Код 16 У 121 171 79 Z 122 172 7А { 123 173 7В 1 124 174 7С } 125 175 7D 126 176 7Е del 127 177 7F Таблица П1.3 Символы с кодами 128 + 255 (Кодовая таблица 866 - MS-DOS) Символ Код 10 Код 08 Код 16 Символ Код 10 Код 08 Код 16 А 128 200 80 ь 156 234 9С В 129 201 81 э 157 235 9D В 130 202 82 ю 158 236 9Е г 131 203 83 я 159 237 9F д 132 204 84 а 160 240 АО Е 133 205 85 б 161 241 А1 Ж 134 206 86 в 162 242 А2 3 135 207 87 г 163 243 АЗ и 136 210 88 д 164 244 А4 й 137 211 89 е 165 245 А5 к 138 212 8А ж 166 246 А6 л 139 213 8В 9 167 247 А7 м 140 214 8С и 168 250 А8 н 141 215 8D й 169 251 А9 о 142 216 8Е к 170 252 АА п 143 217 8F л 171 253 АВ р 144 220 90 м 172 254 АС с 145 221 91 н 173 255 AD т 146 222 92 о 174 256 АЕ У 147 223 93 п 175 257 AF ф 148 224 94 176 260 ВО X 149 225 95 в 177 261 В1 ц 150 226 96 1 178 262 В2 ч 151 227 97 1 179 263 ВЗ ш 152 230 98 4 180 264 В4 щ 153 231 99 4 181 265 В5 ъ 154 232 9А 4 182 266 В6 ы 155 233 9В 1 183 267 В7
(Приложение 1. Таблицы кодов ASCII 491 Таблица П1.3 (продолжение) Символ Код 10 Код 08 Код 16 1 184 270 В8 1 185 271 В9 1 186 272 ВА 187 273 ВВ л 188 274 ВС J 189 275 BD J 190 276 BE 1 191 277 BF L 192 300 СО Л 193 301 CI т 194 302 С2 I- 195 303 СЗ — 196 304 С4 + 197 305 С5 I- 198 306 С6 г 199 307 С7 L 200 310 С8 Г 201 311 С9 Л 202 312 СА Т 203 313 СВ 204 314 СС 205 315 CD ♦ 206 316 СЕ 4. 207 317 CF X 208 320 D0 т 209 321 D1 т 210 322 D2 L 211 323 D3 L 212 324 D4 Г 213 325 D5 Г 214 326 D6 + 215 327 D7 + 216 330 D8 J 217 331 D9 г 218 332 DA 1 219 333 DB Символ Код 10 Код08 Код 16 220 334 DC 1 221 335 DD 1 222 336 DE 223 337 DF р 224 340 E0 с 225 341 El т 226 342 E2 У 227 343 E3 ф 228 344 E4 X 229 345 E5 ц 230 346 E6 ч 231 347 E7 ш 232 350 E8 ж 233 351 E9 ъ 234 352 EA ы 235 353 EB ь 236 354 EC э 237 355 ED ю 238 356 EE я 239 357 EF = или Ё 240 360 F0 ± или в 241 361 Fl 242 362 F2 £ 243 363 F3 f 244 364 F4 J 245 365 F5 Т- 246 366 F6 яг 247 367 F7 • 248 370 F8 • 249 371 F9 250 372 FA 251 373 FB п 252 374 FC 2 253 375 FD 254 376 FE 255 377 FF
492 Язык Си++ Таблица П14 Символы с кодами 128 + 255 (Кодовая таблица 1251 - MS Windows) Символ Код 10 Код 08 Код 16 Символ Код 10 Код 08 Код 16 Ъ 128 200 80 © 169 251 А9 Г 129 201 81 е 170 252 АА f 130 202 82 « 171 253 АВ г 131 203 83 •л 172 254 АС н 132 204 84 - 173 255 AD 133 205 85 © 174 256 АЕ t 134 206 86 I 175 257 AF ♦ 135 207 87 в 176 260 ВО □ B6 210 88 ± 177 261 В1 fa 137 211 89 I 178 262 В2 Jb 138 212 8A i 179 263 ВЗ < 139 213 8B г* 180 264 В4 & 140 214 8С Р 181 265 В5 k 141 215 8D I 182 266 В6 ъ 142 216 8Е • 183 267 В7 u 143 217 8F • 184 270 В8 I) 144 220 90 » 185 271 В9 ч 145 221 91 е 186 272 ВА f 146 222 92 » 187 273 ВВ чч 147 223 93 j 188 274 ВС ff 148 224 94 S 189 275 BD • 149 225 95 в 190 276 BE — 150 226 96 х 191 277 BF — 151 227 97 А 192 300 СО □ 152 230 98 Б 193 301 С1 9C 153 231 99 В 194 302 С2 Л 154 232 9А Г 195 303 СЗ > 155 233 9В д 196 304 С4 & 156 234 9С Е 197 305 С5 К 157 235 9D Ж 198 306 С6 It 158 236 9Е 3 199 307 С7 u 159 237 9F и 200 310 С8 160 240 АО й 201 311 С9 У 161 241 А1 к 202 312 СА У 162 242 А2 л 203 313 св J 163 243 АЗ м 204 314 СС a 164 244 А4 н 205 315 CD r 165 245 А5 0 206 316 СЕ 1 166 246 Аб п 207 317 CF § 167 247 А7 р 208 320 DO Ё 168 250 А8 с 209 321 DI
риложение 1. Таблицы кодов ASCII 493 Таблица П1.4 (продолжение) Символ Код 10 Код 08 Код 16 Т 210 322 D2 У 211 323 D3 ф 212 324 D4 X 213 325 D5 ц 214 326 D6 ч 215 327 D7 ш 216 330 D8 Ж 217 331 D9 ъ 218 332 DA ы 219 333 DB ь 220 334 DC э 221 335 DD ю 222 336 DE я 223 337 DF а 224 340 E0 б 225 341 El в 226 342 E2 г 227 343 E3 Д 228 344 E4 в 229 345 E5 ж 230 346 E6 3 231 347 E7 и 232 350 E8 Символ Код 10 Код 08 Код 16 Й 233 351 Е9 IC 234 352 ЕА Л 235 353 ЕВ M 236 354 ЕС H 237 355 ED о 238 356 ЕЕ П 239 357 EF p 240 360 F0 c 241 361 F1 T 242 362 F2 У 243 363 F3 Ф 244 364 F4 X 245 365 F5 Ц 246 366 F6 4 247 367 F7 Ш 248 370 F8 249 371 F9 ъ 250 372 FA Ы 251 373 FB ь 252 374 FC Э 253 375 FD Ю 254 376 FE я 255 377 FF
ПРИЛОЖЕНИЕ 2 КОНСТАНТЫ ПРЕДЕЛЬНЫХ ЗНАЧЕНИЙ Предельные значения вводятся каждой реализацией для данных целочисленных типов и арифметических значений, представляемых в форме с плавающей точкой. Предельные значения определяются на- бором констант, названия (имена) которых стандартизированы и не зависят от реализаций. Ниже приводятся обозначения констант и их минимальные (по абсолютной величине) допустимые стандартные ANSI значения. В конкретных реализациях абсолютные значения кон- стант могут быть ббльшими, чем приведенные в таблицах. Таблица П2.1 Предельные значения для целочисленных типов - файл limits. h Имя константы Стандартное значение Смысл CHAR_BIT 8 Число битов в байте SCHAR_MIN -127 Минимальное значение для signed char SCHAR_MAX +127 Максимальное значение signed char UCHAR_MAX 255 Максимальное значение unsigned char CHAR_MIN 0 SCHAR_MIN Минимальное значение для char CHAR_MAX UCHAR_MAX SCHAR_MAX Максимальное значение для char MB«LEN_MAX 1 Минимальное число байт в многобай- товом символе SHRT_MIN -32767 Минимальное значение для short SHRT_MAX +32767 Максимальное значение для short USHRT_MAX 65535 Максимальное значение unsigned short INT_MIN -32767 Минимальное значение для int INT_MAX +32767 Максимальное значение для int UINT_MAX 65535 Максимальное значения unsigned int
риложение 2. Константы предельных значений 495 Таблица П2.1 (продолжение) Имя константы Стандартное значение Смысл LONG_MIN L0NGJ4AX ULONG_MAX -2147483647 +2147483647 4294967295 Минимальное значение для long Максимальное значение для long Максимальное значение unsigned long В табл. П2.2 префикс fli_ соответствует типу float; для типа double используется префикс dbl_. Таблица П2.2 Константы для вещественных типов - файл float. h Имя константы Стан- дартное значение Смысл FLTJRADIX 2 Основание экспоненциального представле- ния, например: 2, 16 FLT_DIG 6 Количество верных десятичных цифр FLT_EPSILON IE-5 Минимальное х, такое, что 1.0 + х * 1.0 FLT_MANT_DIG Количество цифр по основанию FLT_JRADIX в мантиссе FLT-MAX 1E+37 Максимальное число с плавающей точкой FLT_MAX_EXP Максимальное п, такое, что FLT^RADIX" - 1 представимо в виде числа типа float FLT_MAX_10_EXP 38 Максимальное целое nt такое, что 10" пред- ставимо как float FLT_MXN IE-37 Минимальное нормализованное число с плавающей точкой типа float FLT_MIN_EXP Минимальное п, такое, что 10" представимо в виде нормализованного числа FLT_MIN_10_EXP -37 Минимальное отрицательное целое л, такое, что 10" - в области определения чисел типа float
496 Язык Си++ Таблица П2.2 (продолжение) Имя константы Стан- дартное значение Смысл DBL_DIG 10 Количество верных десятичных цифр для типа double DBL_EPSILON DBL_MANT_DIG IE-9 Минимальное х, такое, что 1.0 + х * 1.0, где х принадлежит типу double Количество цифр по основанию FLT_RADIX в мантиссе дня чисел типа double DBL_MAX DBL_MAX_EXP 1E+37 Максимальное число с плавающей точкой типа double Максимальное л, такое, что FLT_RADIXn - 1 представимо в виде числа типа double DBL_MAX_1О_ЕХР +37 Максимальное целое л, такое, что 10я пред- ставимо как double DBL_MIN DBL_MIN_EXP IE-37 Минимальное нормализованное число с плавающей точкой типа double Минимальное л, такое, что 10я представимо в виде нормализованного числа типа double DBL_MIN_1O_EXP -37 Минимальное отрицательное целое л, такое, что 10я - в области определения чисел типа double
ПРИЛОЖЕНИЕ 3 СТАНДАРТНАЯ БИБЛИОТЕКА ФУНКЦИЙ ЯЗЫКА СИ Таблица ПЗ. 1 Математические функции - файл math.h Функция Прототип и краткое описание действий abs int abs(int i) ; Возвращает абсолютное значение целого аргумента i. acos double acos (double x) ; Функция арккосинуса. Значение аргумента должно находиться в диапазоне от-1 до +1. asin double asin(double х); Функция арксинуса. Значение аргумента должно находиться в диапазоне от-1 до +1. atan double atan(double х); Функция арктангенса. atanl double atan2(double у, double х); Функция арктангенса от значения у/х. cabs double cabs(struct complex znum); Вычисляет абсолютное значение комплексного числа znum. Определение структуры (типа) complex - в файле math. h. cos double cos(double x); Функция косинуса. Угол (аргумент) задается в радианах. cosh double cosh (double х) ; Возвращает значение гиперболического косинуса х. exp double exp(double х); Вычисляет значение ех (экспоненциальная функция). fabs double fabs (double х) ; Возвращает абсолютное значение вещественного аргумента х двойной точности. floor double floor(double х); Находит наибольшее целое, не превышающее значения х. Воз- вращает его в форме double. 32-382!
498 Язык Си++ Таблица ПЗ. 1 (продолжение) Функция Прототип и краткое описание действий fmod double fmod (double x, double y) ; Возвращает остаток от деления нацело ж на у. hypot double hypot(double х, double у); Вычисляет гипотенузу z прямоугольного треугольника по значе- ниям катетов х, у (z2 = х2 + у2) labs long labs(long х); Возвращает абсолютное значение целого аргумента long х. Idexp double Idexp(double v, int e); Возвращает значение выражения v * 2е. log double log(double x); Возвращает значение натурального логарифма (In х). loglO double loglO(double x); Возвращает значение десятичного логарифма (logio х). poly double poly(double x, int n, double c[ ]); Вычисляет значение полинома: c[n]x" + c[n - l]xn- 1 + ... + c[l]x + c[0] pow double pow (double x, double y) ; Возвращает значение xY, т.е. x в степени у. pow 10 double powlO(int p) ; Возвращает значение IO1*. sm double sin (double x) ; Функция синуса. Угол (аргумент) задается в радианах. sinh double sinh (double х) ; Возвращает значение гиперболического синуса для х. sqrt double sqrt (double х) ; Возвращает положительное значение квадратного корня 4* . tan double tan (double х) ; Функция тангенса. Угол (аргумент) задается в радианах. tanh double tanh (double х) ; Возвращает значение гиперболического тангенса для х.
Приложение 3. Стандартная библиотека функций языка Си 499 Таблица П3.2 Функции проверки и преобразования символов - файл с type. h Функция Прототип и краткое описание действий Isalnum int isalnum(int с); Дает значение не нуль, если с - код буквы или цифры (А-ь z, а т z, 0 9), и нуль - в противном случае. fealpha int isalpha(int с); Дает значение не нуль, если с - код буквы (А + Z, а + z), и нуль - в противном случае. isascii int isascii (int с); Дает значение не нуль, если с есть код ASCII, т.е. принимает зна - чение от 0 до 127, в противном случае - нуль. iscntrl int iscntrl (int с); Дает значение не нуль, если с - управляющий символ с кодами 0x00 + 0x01 F или 0x7Ft и нуль - в противном случае. isdigit int isdigit (int с) ; Дает значение не нуль, если с - цифра (0 + 9) в коде ASCII, и нуль - в противном случае. isgraph int isgraph(int с); Дает значение не нуль, если с - видимый (изображаемый) символ с кодом (0x21 + 0х7Е), и нуль - в противном случае. islower int islower(int с); Дает значение не нуль, если с - код символа на нижнем регистре (а + z), и нуль - в противном случае. isprint int isprint (int с) ; Дает значение не нуль,если с - печатный символ с кодом (0x20 + , 0х7Е), и нуль - в противном случае. ispunct int ispunct(int с); Дает значение не нуль, если с - символ-разделитель (соответствует iscntrl или isspase), и нуль - в противном случае. isspace int isspace(int с); Дает значение не нуль, если с - обобщенный пробел: пробел, символ табуляции, символ новой строки или новой страницы, символ возврата каретки (0x09 + 0x0D, 0x20), и нуль - в про- тивном случае. 32*
500 Язык Си++ Таблица П3.2 (продолжение) Функция Прототип и краткое описание действий isupper int isupper(int с); Дает значение не нуль, если с - код символа на верхнем регистре (А + Z), и нуль - в противном случае. isxdigit int isxdigit (int с) ; Дает значение не нуль, если с - код шестнадцатеричной цифры (0 + 9, А + F, а + f), и нуль - в противном случае. toascii int toascii(int с); Преобразует целое число с в символ кода ASCII, обнуляя все биты, кроме младших семи. Результат от 0 до 127. tolower int tolower(int с); Преобразует код буквы с к ннжнему регистру, остальные коды не изменяются. toupper int toupper(int с); Преобразует код буквы с к верхнему регистру, остальные коды не изменяются. Таблица ПЗ.З Функции ввода-вывода для стандартных файлов - файл stdio. h Функция Прототип и краткое описание действий getch int getch(void); Считывает один символ с клавиатуры без отображения на экра- не. getchar int getchar(void); Считывает очередной символ из стандартного входного файла (stdin). gets char *gets(char *s) ; Считывает строку s из стандартного входного файла (stdin). printf int printf(char *foxmat [, argument, ...]); Функция форматированного вывода в файл stdout. putchar int putchar(int c); Записывает символ с в стандартный файл вывода (stdout).
риложение 3. Стандартная библиотека функций языка Си 501 Таблица ПЗ.З (продолжение) Функция Прототип и краткое описание действий puts int puts(const char *s>; Записывает строку s в стандартный файл вывода (stdout). scant int scanf(char *format[, argument, Функция форматированного ввода из файла stdin. sprintf int sprintf(char * s,char * format[, argument, Функция форматированной записи в строку s. sscanf int sscanf(char *s,char *format [, address, ..J); Функция форматированного чтения из строки s. ungetch int ungetch (int c) ; Возвращает символ с в стандартный файл ввода stdin, заставляя его быть следующим считываемым символом. Таблица ПЗ. 4 Функции для работы со строками - файлы string. h, stdlib. h Функция Прототип и краткое описание действий atof double atof(char *str); Преобразует строку str в вещественное число типа double. atoi int atoi(char *str); Преобразует строку str в десятичное целое число. atoi long atoi(char *str); Преобразует строку str в длинное десятичное целое число. itoa char *itoa(int vt char *str, int baz); Преобразует целое v в строку str. При изображении числа ис- пользуется основание baz (2 < bas < 36). Для отрицательного числа и baz = Ю первый символ - "минус” (-). Itoa char *ltoa(long v, char *str, int baz); Преобразует длинное целое v в строку str. При изображении числа используется основание baz (2 < baz < 36). strcat char *strcat(char *sp, char *si); Приписывает строку si к строке sp (конкатенация строк).
502 Язык Си++ Таблица П3.4 (продолжение) Функция Прототип и краткое описание действий strchr char *strchr(char *str, int c); Ищет в строке str первое вхождение символа с. strcmp int strcmp(char *strl, char *str2); Сравнивает строки strl и str2. Результат отрицателен, если strl < str2; равен нулю, если strl str2 и положителен, если strl > str2 (сравнение беззнаковое). strepy char *strcpy(char *sp, char *si); Копирует байты строки si в строку sp. strespn int strespn(char *strl, char *str2): Определяет длину первого сегмента строки strl, содержащего символы, не входящие во множество символов строки str2. strdup char *strdup(const char *str); Выделяет память и переносит в нее копию строки str. strlen unsigned strlen(char *str); Вычисляет длину строки str. strlwr char *strlwr(char *str); Преобразует буквы верхнего регистра в строке в соответствую- щие буквы нижнего регистра. strncat char *strncat(char *зр, char *si, int koi); Приписывает koi символов строки si к строке sp (конкатенация). strnemp int strnemp(char *strl, char *str2, int koi); Сравнивает части строк strl и str2, причем рассматриваются первые koi символов. Результат отрицателен, если strl < str2; равен нулю, если strl « str2 и положителен, если strl > str2. stmepy char *strncpy(char *sp, char *si, int koi); Копирует koi символов строки si в строку sp ("хвост" отбрасы- вается или дополняется пробелами). strniemp char *stricmp(char *strl, char *str2, int koi); Сравнивает не более koi символов строки strl и строки str2, не делая различия регистров (см. функцию strnemp ()).
Приложение 3. Стандартная библиотека функций языка Си 503 Таблица ПЗ.4 (продолжение) Функция Прототип и краткое описание действий strnset char *strnset(char *str, int c, int koi); Заменяет первые koi символов строки str символом с. strpbrk char *strpbrk(char *strl, char *str2); Ищет в строке strl первое появление любого из множества сим- волов, входящих в строку str2. strrchr char *strrchr(char *str, int c) ; Ищет в строке str последнее вхождение символа с. strset int strset(char *str, int c); Заполняет строку str заданным символом с. strspn int strspn(char *strl, char *str2); Определяет длину первого сегмента строки strl, содержащего только символы, из множества символов строки str2. strstr char *strstr(const char *strl, const char *str2); Ищет в строке strl подстроки str2. Возвращает указатель на тот элемент в строке strl, с которого начинается подстрока str2. strtod double *strtod(const char *str, char **endptr); Преобразует символьную строку str в число двойной точности. Если endptr не равен NULL, то *endptr возвращается как указа- тель на символ, при достижении которого прекращено чтение строки str. strtok char *strtok(char *strl, const char *str2); Ищет в строке strl лексемы, выделенные символами из второй строки. strtol long *strtol (const char *str, char **endptr, int baz) ; Преобразует символьную строку str к значению "длинное чис- ло" с основанием baz (2 < baz < 36). Если endptr не равен NULL, то *endptr возвращается как указатель на символ, при дости- жении которого прекращено чтение строки str. strupr char *strupr(char *str); Преобразует буквы нижнего регистра в строке str в буквы верх- него регистра. ultoa char *ultoa(unsigned long v, char *str, int baz) ; Преобразует беззнаковое длинное целое v в строку str.
504 Язык Си++ Таблица П3.5 Функции для выделения и освобождения памяти - файлы stdlib. h, alloc. h Функция Прототип и краткое описание действий calloc void *calloc(unsigned n, unsigned a); Возвращает указатель на начало области динамически распреде- ленной памяти для размещения п элементов по m байт каждый. При неудачном завершении возвращает значение NULL. coreleft unsigned coreleft (void) ; - для моделей памяти tiny, small, medium. unsigned long coreleft (void) ; -для других моделей па- мяти. Возвращает значение объема неиспользованной памяти, функция уникальная для DOS, где приняты упомянутые модели памяти. free void free (void *Ы) ; Освобождает ранее выделенный блок динамически распределяе- мой памяти с адресом первого байта Ы. inalloc void *malloc(unsigned s); Возвращает указатель на блок динамически распределяемой па- мяти длиной s байт. При неудачном завершении возвращает зна- чение NULL. realloc void *realloc(void *Ы, unsigned ns); Изменяет размер ранее выделенной динамической памяти с адре- сом начала Ы до размера ns байт.Если Ы равен NULL, то функ- ция выполняется как malloc (). Таблица ПЗ, 6 Функции для работы с терминалом в текстовом режиме - файл con io. h Функция Прототип и краткое описание действий clreol void clreol(void); Стирает символы от позиции курсора до конца строки в текстовом окне. clrscr void clrscr(void); Очищает экран. cgets char *cgets(char *str); Выводит на экран строку str.
Приложение 3. Стандартная библиотека функций языка Си 505 Таблица П3.6 (продолжение) Функция Прототип и краткое описание действий cprintf int cprintf (char «format [, argumentt Выводит форматированную строку в текстовое окно, соз- данное функцией window (). cputs int cputs(char *str); Считывает в символьный массив str строку с клавиатуры (консоли). cscanf int cscanf (char «format [, address, Функции ввода-вывода информации, которые используют- ся при работе с терминалом в текстовом режиме deHine void deliine(void); Удаляет строку в текстовом окне (где находится курсор). gotoxy void gotoxy(int х, int у); Перемещает курсор в позицию текстового окна с коорди- натами (х, у). highvideo void highvideo(void); Повышает яркость символов, выводимых после нее на эк- ран. movetext int movetext(int xO,int yO,int xl,int y2,int x,int y); Переносит текстовое окно в область экрана, правый верх- ний угол которой имеет координаты (х, у). Координаты угловых точек окна - (хО, уО), (xl, yl). normvideo void normvideo(void); Устанавливает нормальную яркость выводимых на экран символов. textattr void textattr(int newattr); Устанавливает атрибуты (фон, цвет) символов, выводимых на экран. text background void textbackground(int с); Устанавливает цвет фона по значению параметра с. textcolor void textcolor(int с); Устанавливает цвет символов по значению параметра с. textmode void textmode(int m); Переводит экран в текстовый режим по значению парамет- ра т.
506 ЯзыкСи++ Таблица П3.6 (продолжение) Функция Прототип и краткое описание действий wherex int wherex(void); Возвращает значение горизонтальной координаты курсо- ра. wherey int wherey(void); Возвращает значение вертикальной координаты курсора. window void window(int xO,int yO,int xl,int yl); Создает текстовое окно по координатам угловых точек (хО, уО), (xl, yl). Функции из табл. П3.6 поддерживаются только на IBM PC и со- вместимых с ним компьютерах. Таблица ПЗ. 7 Специальные функции Функция Прототип и краткое описание действий Местона- хождение прототипа delay void delay(unsigned х); Приостанавливает выполнение программы на х мсек. dos .h kbhit int kbhit(void); Возвращает ненулевое целое, если в буфере кла- виатуры присутствуют коды нажатия клавиш, в противном случае - нулевое значение. conio.h memcmp int memcmp(void *sl, void *s2f unsigned n); Сравнивает посимвольно две области памяти si и s2 длиной п байт. Возвращает значение меньше нуля, если si < &2, нуль, если si ” s2 и больше нуля, если si > s2. mem.h memcpy void *memcpy (const void *p, const void *i, unsigned n); Копирует блок длиной n байт из области памяти i в область памяти р. mem.h
Приложение 3. Стандартная библиотека функций языка Си 507 Таблица П3.7 (продолжение) Функция Прототип и краткое описание действий Местона- хождение прототипа memicuip int memicnp (void *sl, void *a2, unsigned n); Подобна mamamp, за тем исключением, что игно- рируются различия между буквами верхнего и нижнего регистра. men.h memmove void *menuiiove (void *dest, const void ♦src, int n); Копирует блок длиной n байтов из src в dost. Возвращает указатель dost. теш. h memset void *memset (void *s, int c, unsigned n) ; Записывает во все байты области памяти s значе- ние с. Длина области s равна п байт. nem.h nosound void nosound(void); Прекращает подачу звукового сигнала, начатую функцией sound (). dos. h peek int peak(unsigned s, unsigned c); Возвращает целое значение (слово), записанное в сегменте s со смещением с. dos .h peekb char peekb(unsigned s, unsigned c); Возвращает один байт, записанный в сегменте s со смещением с, т.е. по адресу s: с. dos. h poke void poke(unsigned s, unsigned c, int v) ; Помещает значение v в слово сегмента s со сме- щением с, т.е. по адресу s: с. dos. h pokeb void pokeb(unsigned s, unsigned c, char v); To же, что и poke, но помещает один байт v по адресу s: с. dos .h rand int rand(void); Возвращает псевдослучайное целое число из диа- пазона 0 +215 - 1, может использовать srand (). stdlib.h
508 Язык Си++ Таблица ПЗ. 7 (продолжение) Функция Прототип и краткое описание действий Местона- хождение прототипа signal int signal(int sig); Вызывает программный сигнал с номером sig. Используется для обработки исключительных ситуаций в языке Си. signal,h sound void sound(unsigned f); Вызывает звуковой сигнал с частотой f Гц. dos.h srand void srand(unsigned seed); Функция инициализации генератора случайных чисел (rand); seed - любое беззнаковое целое число. stdlib.h
ПРИЛОЖЕНИЕ 4 ГРАФИЧЕСКАЯ БИБЛИОТЕКА ВС++ Графические средства в языке Си++ и в его предшественнике - в языке Си полностью отсутствуют. Все возможности для работы с гра- фикой конкретные реализации предоставляют в виде дополнительных библиотек графических функций. В нескольких местах книги были использованы в иллюстративных целях возможности графического представления объектов (принадлежащих классам, вводимым про- граммистом). В качестве инструмента для работы с графикой была использована графическая библиотека под MS-DOS компиляторов фирмы Borland, так как эти компиляторы наиболее доступны в нашей стране, особенно на маломощных ПЭВМ,и чаще всего используются в учебном процессе. Никаких других причин для выбора именно этой графической библиотеки и этих компиляторов не было. Пояснения по отдельным функциям графической библиотеки даны в основном текс- те соответствующих глав. Здесь приводится список библиотечных функций, которые могут быть полезными при самостоятельном реше- нии задач, связанных с графикой. Приводимой информации иногда оказывается недостаточно для безошибочного применения той или иной функции, но возможность получения справок при работе с ком- пилятором в диалоговом режиме обычно позволяет быстро устранить возникающие затруднения. Для работы с графической библиотекой необходимо включить в программу заголовочный файл graphics .h. Таблица П4.1 Функции для управления графической системой Функция Прототип и краткое описание действий closegraph void far closegraph(void); Перевод системы в текстовый режим (из графического режима) graphdefaults void far graphdefaults(void); Устанавливает по умолчанию все параметры графической системы (параметры заполнения, палитру, правила вы- равнивания текста и т.п.)
510 ЯзыкСи++ Таблица П4.1 (продолжение) Функция Прототип и краткое описание действий _graphfreemem void far _graphfreenen (void far *ptr, unsigned size); Введение этой функции в программу позволяет програм- мисту отслеживать запросы на освобождение size байт памяти функциями графической библиотеки -graphgetmem void far * far __graphgetnen(unsigned size); Введение этой функции в программу позволяет програм- мисту отслеживать запросы на выделение size байт па- мяти функциями графической библиотеки initgraph void far initgraph(int far *graphdriver, int far *graphnoder char far *pathtodriver); Перевод системы в графический режим (из текстового режима), инициализация графики mstalluserdriver int far installuserdriver(char far *nane, int huge *detect)(void)); Добавляет новый графический драйвер папе (BGI) в таб- лицу драйверов BGI (BGI - Borland Graphics Interface) installuserfont int far installuserfont(char far *nane); Устанавливает шрифты, содержащиеся в файле папе (CHR) registerbgidriver int registerbgidriver (void (*driver) (void)) ; Регистрирует драйвер driver, встроенный в текущую выполняемую программу register bgifont int registerbgifont(void (*font)(void)); Регистрирует шрифт font, встроенный в текущую выпол- няемую программу restorecrtmode void far restorecrtnode(void); Временный переход в текстовый режим, из которого была вызвана функция initgraph () setgraphbufsize unsigned far setgraphbufsize(unsigned bufsize); Устанавливает размер внутреннего буфера для графиче- ских функций
Приложение 4. Графическая библиотека ВС++ 511 Таблица П4.2 Функции для установки параметров изображения Функция Прототип и краткое описание действий setactivepage void far setactivepage(int page); Устанавливает активной для вывода графики страницу рада setallpalette void far setallpalette(struct palettetype far *palatta); Устанавливает все цвета палитры setaspectratio void far setaspectratio(int xasp, int yasp) ; Устанавливает коэффициент сжатия по координатам х и у setbkcolor void far setbkcolor(int color); Установка цвета фона setcolor void far setcolor(int color); Установка цвета точки (изображения) .setcursortype (прототип со- держится в файле conio.h) void _setcursortype (int cur^t) ; Устанавливает тип отображения курсора для текстовых режимов setfillpattern void far setfillpattern (char far *upattem, int color); Установка заданного пользователем шаблона закраски экрана или области экрана setgraphmode void far setgraphnode(int node); Переход к графическому режиму, отличному от устано- вленного функцией initgraph () setfillstyle void far setfillstyle(int pattern, int color); Установка одного из стандартных шаблонов заполнения экрана или области экрана setlinestyle void far setlinestyle(int linestyle, unsigned upattern, int thickness); Установка толщины и типа изображаемой линии setpalette void far setpalette(int colornun, int color); Устанавливает одни из цветов палитры
512 Язык Си++ Таблица П4.2 (продолжение) Функция Прототип и краткое описание действий setrgbpalette void far setrgbpalette(int colornum, int rad, int green, int blua); Устанавливает цвета для графического адаптера IBM 8514 settextjustify void far settextjustify(int horiz, int vert); Установка правил выравнивания текста при горизонталь- ном или вертикальном выводе функцией outtext () settextstyle void far settextstyle(int font, int diraction, int charsize); Установка стиля (шрифт, размеры символов) текста, вы- водимого функцией outtaxt () setusercharsize void far satusarcharsiza(int multx, int divx, int multy, int divy); Установка размеров символов, выводимых в графическом режиме setviewport void far setviewport(int laft, int top, int righ, int bottom, int clip); Установка размеров текущего окна экрана для вывода изображений или текста setvisualpage void far satvisualpaga(int рада); Делает видимой графическую страницу рада setwritemode void far setwritemode(int mode); Устанавливает режим вывода линий в графическом режиме Таблица П4.3 Функции для получения изображения на экране Функция Прототип и краткое описание действий аге void far arc(int x, int y, int stangle, int endangle, int radius); Вычерчивание дуги окружности с центром (х, у) bar void far bar(int left, int top, int right, int bottom) ; Вычерчивание закрашенного прямоугольника
Приложение 4. Графическая библиотека ВС++ 513 Таблица П4.3 (продолжение) Функция Прототип и краткое описание действий bar3d void far bar3d(int left, int top, int right, int bottom, int depth, int topflag); Вычерчивание закрашенного параллелепипеда circle void far circle(int x, int у, int radius); Вычерчивание окружности с центром (х, у) cleardevice void far cleardevice(void); Очистка экрана цветом фона clearviewport void far clearviewport(void); Очистка ранее установленного окна графического экрана drawpoly void far drawpoly(int numpoints, int far ♦polypoints); Вычерчивание контура многоугольника c numpoints вершинами ellipse void far ellipse(int x, int y, int stangle, int endangle, int xradius, int yradius); Вычерчивание дуги эллипса с центром (х, у) fillellipse void far fillellipse(int x, int y, int xradius, int yradius); Вычерчивание эллипса с центром в точке (х, у) и заполне- ние его установленным ранее шаблоном закраски fillpoly void far fillpoly(int numpoints, int far ♦polypoints); Вычерчивание закрашенного многоугольника с numpoints вершинами н заполнение его установленным ранее шаблоном закраски floodfiU void far floodfill(int x, int y, int border); Заполнение установленным ранее шаблоном закраски ограниченной области экрана, в которую попадает точка с координатами (х, у) line void far line(int xl, int yl, int x2, int y2); Вычерчивание линии от (xl, yl) до (x2, y2) 33“382!
514 Язык Си++ Таблица П4.3 (продолжение) Функция Прототип и краткое описание действий linerel void far linerel(int dx, int dy); Вычерчивание линии из текущей точки в точку, отстоя- щую от нее на величину (dx, dy) lineto void far lineto(int x, int y) ; Проведение линии из текущей точки в точку с абсолют- ными координатами х, у moverel void far moverel (int dx, int dy) ; Перемещение указателя позиции из текущей точки в точ- ку, отстоящую от нее на величину (dx, dy) moveto void far moveto(int x, int y) ; Перемещение указателя позиции нз текущей точки в точку с абсолютными координатами х, у outtext void far outtaxt(char far *textstring); Вывод текстовой строки textstring, начиная с текущей позиции outtextxy void far outtextxy(int x, int y, char far *textstring); Вывод текстовой строки textstring, начиная с точки с координатами х, у pieslice void far pieslice (int xr int y, int stangle, int endangle, int radius); Вычерчивание закрашенного сектора круга с центром в точке (х, у) putimage void far putimage(int left, int top, void far *bitmap, int op); Вывод ранее сохраненного графического изображения в окно экрана с левым верхним углом (left, top) putpixel void far putpixel (int x, int y, int color); Вычерчивание точки по координатам х, у rectangle void far rectangle(int left, int top, int right, int bottom); Вычерчивание прямоугольника с заданными вершинами
Приложение 4. Графическая библиотека ВС++ 515 Таблица П4.3 (продолжение) Функция Прототип и краткое описание действий sector void far sector(int x, int y, int stangle, int endangle, int xradius, int yradius); Вычерчивание сектора эллипса с центром в точке (х, у) и заполнение его установленным ранее шаблоном закраски Таблица П4.4 Функции для получения параметров изображения Функция Прототип и краткое описание действий detectgraph void far detectgraph(int far *graphdriver, int far *graphmode); Функция возвращает тип вашего графического адаптера graphdriver и режим graphmode с разрешением, мак- симально возможным для данного адаптера getarccoords void far getarccoords(struct arccoordstype far *arccoords); Возвращает в структуре arccoords значения координат дуги, построенной при последнем обращении к аге () getaspectratio void far getaspectratio(int far *xasp, int far *yasp); Возвращает коэффициент сжатия (yasp/xasp) по коор- динатам хну getbkcolor int far getbkcolor(void); Возвращает номер текущего цвета фона getcolor int far getcolor(void); Возвращает номер текущего цвета изображения getdefaultpalette struct palettetype *far getdefaultpalette(void); Возвращает указатель на структуру типа palettype, со- держащую информацию о палитре (наборе цветов), уста- навливаемой по умолчанию функцией initgraph () getdrivername char *far getdrivername(void); Возвращает указатель на строку, содержащую название текущего графического драйвера
516 Язык Си++ Таблица П4.4 (продолжение) Функция Прототип и краткое описание действий getfillpattern void far getfillpattern(char far *pattern); Получение кодов, применяемых пользователем для зада- ния шаблона заполнения экрана или его области getfillsettings void far getfillsettings(struct fillsettingstype far *fillinfo); Возвращает в структуре fillinfo значения параметров заполнения н цвета экрана getgraphmode int far getgraphmode (void) ; Возвращает номер графического режима getimage void far getimage(int left, int top, int right, int bottom, void far *bitmap); Получение и сохранение в Области памяти, на которую указывает bitmap, окна экрана, заданного координатами вершин getlinesettings void far getlinesettings(struct linesettingstype far *lineinfo); Возвращает в структуре lineinfo значения параметров линии getmaxcolor int far getmaxcolor (void) ; Возвращает наибольший номер цвета, который возможно установить в текущем режиме графического драйвера с помощью функции setcolor () getmaxmode int far getmaxmode (void) ; Возвращает наибольший номер режима, который возмож- но установить для текущего графического драйвера , getmodename char *far getmodename (int mode_number) ; Возвращает указатель на строку с названием графическо- го режима mode_number getmoderange void far getmoderange(int graphdriver, int far *lomode, int far *himode); Возвращает диапазон доступных графических режимов для графического драйвера graphdriver getmaxx int far getmaxx (void) ; Возвращает целое значение, равное размеру экрана по горизонтали (максимальное значение х)
Приложение 4. Графичесжая библиотека ВС++ 517 Таблица 114.4 (продолжение) Функция Прототип и краткое описание действий getmaxy int far getmaxy (void) ; Возвращает целое значение, равное размеру экрана по вертикали (максимальное значение у) getpalette void far getpalette(struct palettetype far ♦palette); Выдает указатель palette на структуру типа palette, содержащую информацию о текущей палитре (наборе цве- тов) getpalettesize int far getpalettesize(void); Возвращает количество цветов, доступных в текущем гра- фическом режиме getpixel unsigned far getpixel(int x, int y); Возвращает цвет заданной (x, у) точки gettextsettings void far gettextsettings(struct textsettingstype far *texttypeinfo); Возвращает в структуре texttypeinfo значения пара- метров текста getviewsettings void far getviewsettings (struct viewporttype far *viewport); Возвращает в структуре viewport значения параметров окна экрана getx int far getx(void); Возвращает целое значение координаты х текущей пози- ции на экране gety int far gety (void) ; Возвращает целое значение координаты у текущей пози- ции на экране graphresult int far graphresult(void); Возвращает номер ошибки графической операции (целое число от -15 до -1); значение 0 говорит об отсутствии ошибок grapherrormsg char *far grapherrormsg(int errorcode); Возвращает указатель на строку, содержащую описание ошибки номер errorcode
518 Язык Си++ Таблица П4.4 (продолжение) Функция Прототип и краткое описание действий imagesize unsigned far imagesize(int left, int top, int right, int bottom); Возвращает объем буфера, нужного для сохранения гра- фической информации в окне экрана с заданными верши- нами textheight int far textheight(char far *textstring); Возвращает целое значение высоты в пикселах символов нз строки textstring textwidth int far textwidth(char far *textstring); Возвращает в пикселах целое значение ширины строки символов textstring
ПРИЛОЖЕНИЕ 5 КЛАССЫ И КОМПОНЕНТНЫЕ ФУНКЦИИ БИБЛИОТЕКИ ПОТОКОВОГО ВВОДА-ВЫВОДА В СИ++ О библиотеке классов потокового ввода-вывода подробно гово- рилось в главе 11. В данном приложении приведены только краткие сведения о компонентных функциях и других средствах, использова- ние которых позволяет реализовать обмен с потоками. Многие из пе- речисленных ниже функций уже рассмотрены в главе 11. Там же приведены сведения об иерархии классов в библиотеке. Подробное изучение остальных средств потокового ввода-вывода потребует об- ращения к документации по конкретному компилятору. Таблица П5.1 Компонентные функции класса ios Функция Прототип и краткое описание действий bad int bad () ; При ошибке возвращает ненулевое значение. bitalloc static long bitalloc(); Возвращает установку флагов. Полученное значение может быть использовано для очистки, установки илн проверки флагов. clear void clear (int “ 0) ; Устанавливает состояние потока в нуль. eof int eof О; Возвращает ненулевое значение, если имеет место условие конца файла (EOF). fail int fail О; Возвращает ненулевое значение, если операция обмена с потоком терпит неудачу. fill char fill О; Возвращает текущее значение символа заполнения потока, char fill(char); Заменяет значением параметра символ заполнения потока; воз- вращает ранее установленное значение символа заполнения.
520 Язык Си++ Таблица П5.1 (продолжение J Функция Прототип и краткое описание действий flags long flags(); Возвращает текущее значение флагов форматирования, long flags(long); Устанавливает флаги форматирования по значению параметра; возвращает ранее установленное значение флагов. good int good () ; Возвращает ненулевое значение, если не установлен ни один флаг состояния (ошибок нет). precision int precision(); Возвращает текущее значение точности вещественных чисел, int precision(int); Устанавливает точность вещественных чисел по значению пара- метра; возвращает предыдущее значение. rdbuf streambuf* rdbuf (); Возвращает указатель на буфер (объект класса buf stream), свя- занный с потоком. rdstate int rdstate(); Возвращает текущее состояние потока. setf long setf(long); Устанавливает флаги по значению параметра; возвращает предыдущие значения флагов long self(long _setbits, long _field); Сбрасывает те биты состояния, которые отмечены в _f ield, затем устанавливает биты по значению _setbits. tie ostream* tie О; Возвращает указатель на взаимосвязанный (tied) поток. ostream* tie(ostream*); Организует поток, взаимосвязанный с потоком, на который ука- зывает ©stream*; возвращает указатель на предыдущий взаимо- связанный поток, если такой есть. unself long unself(long); Очищает биты состояния потока, отмеченные переданным пара- метром; возвращает предыдущее значение битов.
Приложение 5. Классы и функции библиотеки ввода-вывода в Си++ 521 Таблица П5.1 (продолжение) Функция Прототип и краткое описание действий width int width(); Возвращает текущее значение ширины, int width(int); Устанавливает ширину, равной значению переданного парамет- ра; возвращает предыдущее значение. Таблица П5.2 Компонентные функции класса istream Функция Прототип и краткое описание действий gcount int gcount(); Возвращает число символов, извлеченных из потока последним обращением из прикладной программы. get int get (); Передает из входного потока в прикладную программу следую- щий символ или EOF. int get(signed char *,int len,char = T\n'); int get (unsigned char *,int len, char ’\n’); Извлекает из входного потока символы и помещает их в буфер, на начало которого указывает char *. Передача символов за- вершается, если прочитано (len - 1) байтов, или встретился сим- вол-разделитель (третий параметр в функции), или достигнут конец файла EOF. Завершающий нуль-символ всегда помещается в буфер, обозначая конец принятой строки, разделитель (последний параметр) не переносится в строку, int get(unsigned charfi); int get(signed char&); Извлекает из входного потока символ и помещает его в байт, на который указывает параметр chars. int get(streambufS,char ® ’\n’); Извлекает символы нз входного потока и помещает их в буфер потока, на который ссылается streambuf. Чтение символов прекращается, если в потоке встретился символ-разделитель (второй параметр). getline istreamS getline(signed char *,int,char = '\n'); istream getline(unsigned char *,int,char == ’\n’); Совпадает c get () с тремя параметрами, но символ-разделитель также помещается в принятую строку символов.
522 Язык Си++ Таблица П5.2 (продолжение) Функция Прототип и краткое описание действий ignore iatraanl ignore(int n * l,int delim * EOF); Пропускает до n символов входного потока; останавливается, если встретился разделитель (второй параметр), по умолчанию равный EOF. peek int peek(); Извлекает следующий символ из входного потока. putback istreamfi putback(char); Помещает символ назад во входной поток. read istreamfi read(signed char *,int); is t reams read(unsigned char *,int); Извлекает из входного потока заданное вторым параметром чис- ло символов и помещает их в массив, на начало которого указы- вает первый параметр. seekg istreamS seekg(long); Устанавливает указатель чтения входного потока на абсолютную позицию, заданную параметром long. istreamS seekg(long,seek_dir); Перемещает указатель чтения входного потока на число байтов, заданное первым параметром. Второй параметр задает точку отсчета (0 - начало потока; 1 - текущая позиция потока; 2 - ко- нец потока). telig long telig(); Возвращает текущую позицию указателя чтения входного пото- ка. Таблица П5.3 Компонентные функции класса ostream Функция Прототип и краткое описание действий flush ostreamfi flush(); Флэширует внутренний буфер выходного потока. put ostreamS put(char); Помещает заданный параметром char символ в выходной поток. seekp ostreamS seekp(long); Устанавливает указатель записи выходного потока на абсолют- ную позицию, заданную параметром long.
Приложение 5. Классы и функции библиотеки ввода-вывода в Си++ 523 Таблица П5.3 (продолжение) Функция Прототип и краткое описание действий ostreamfi seekp(long,seek_dir); Перемещает указатель текущей позиции выходного потока на число байтов, заданное первым параметром. Второй параметр задает точку отсчета (0 - начало; 1 - текущая позиция; 2 - конец потока). tellp long tellp(); Возвращает текущую позицию указателя записи выходного по- тока. write ostreamfi write(const signed char * ,int n) ; ostreamfi write(const unsigned char *,int n); Помещает в выходной поток п символов из массива, на который указывает char *. Нуль-снмволы включаются в число перено- симых символов. Таблица П5.4 Флаги класса ios, управляющие форматированием ввода/вывода Константа Значение Назначение skipws 0x0001 Игнорировать пробельные символы при вводе. left 0x0002 "Прижимать" значение к левой стороне поля. right 0x0004 "Прижимать" значение к правой стороне поля. internal OxOOOS Поместить разделительные символы после знака или основания системы счисления (ОСС). dec 0x0010 Десятичная система счисления (ОСС - 10). oct 0x0020 Восьмеричная система счисления (ОСС = 8). hex 0x0040 Шестнадцатеричная система счисления (ОСС = 16) showbase OxOOSO Указывать ОСС при выводе. showpoint 0x0100 Печатать десятичную точку и следующие за ней нули при выводе вещественных чисел. uppercase 0x0200 Шестнадцатеричные цифры печатать на верхнем регистре.
524 Язык Си++ Таблица П5.4 (продолжение) Константа Значение Назначение showpos 0x0400 Добавлять ' + ’ при выводе положительных чисел. scientific OxOSOO Использовать формат 1.2345Е2 для вывода веще- ственных чисел (экспоненциальная или научная нотация). fixed 0x1000 Использовать формат 123.45 для вывода веще- ственных чисел (с фиксированной точкой). unitbuf 0x2000 Флэшировать потоки после операции «. stdio 0x4000 Флэшировать stdout, stderr после операции «. Таблица П5.5 Константы класса ios для 11 очистки" флагов форматирования Константа "Сбрасываемые " флаги Действие basefield ios : : hex, ios : : oct, ios::dec На основание системы счис- ления floatfield ios: : fixed, ios::scientific На представление веществен- ных чисел. adjustifield ios: : left, ios: : right, ios::internal На выравнивание значений в поле вывода. Таблица П5.6 Функцин-маинпуляторы Манипу- лятор Краткое описание действий dec Устанавливает десятичное основание системы счисления. hex Устанавливает шестнадцатеричное основание системы счисле- ния. oct Устанавливает восьмеричное основание системы счисления. ws При вводе позволяет извлекать из входного потока обобщенные пробельные символы.
Приложение 5. Классы и функции библиотеки ввода-вывода в Си++ 525 Таблица П5.6 (продолжение) Манипу- лятор Краткое описание действий endl При выводе помещает в поток символ новой строки и флэширует буфер потока. ends При выводе помещает в поток символ конца строки '\0' flush Флэширует буфер потока ostream. Таблица П5.7 Параметризованные функции-манипуляторы - файл iomanip. h Название Краткое описание действий setbase(int = 0) Устанавливает основание системы счисления (0 - при выводе - десятичное; при вводе - внутреннее представ- ление вводимых цифр соответствует правилам ANSI для языка Си; 8 - восьмеричное; 10 - десятичное; 16 - шест- надцатеричное). resetiosflags(long) Очищает форматные флаги, используя значение пара- метра. setiosflags(long) Устанавливает форматные флаги, используя значение параметра. setfill(int) Устанавливает символ-заполиитель. setprecision(int) Устанавливает по значению параметра точность пред- ставления вещественных чисел. setw(int) Устанавливает по значению параметра ширину поля ввода или вывода. Таблица П5.8 Компонентные функции класса f ilebuf Функция Прототип и краткое описание действий attach filebuf* attach(int fd); Связывает с объектом класса f ilebuf файл с дескриптором f d. Если файл уже открыт, возвращает NULL.
526 Язык Си++ Таблица П5.8 (продолжение) Функция Прототип и краткое описание действий close filebuf* close(); Флэширует и закрывает файл. Возвращает ссылку на буфер файла, либо 0 при ошибке. fd int fd() ; Возвращает дескриптор открытого файла, связанного с задан- ным объектом класса filebuf, либо EOF. is_rtl_open int is_rtl_open () ; Возвращает не равное нулю целое число, если с заданным объ- ектом связан открытый файл. open filebuf* open(const char*, int mode, int prot « filebuf::openprot); Открывает файл, на спецификацию которого указывает char*, в режиме mode н присоединяет к нему объект класса filebuf. Третий параметр задает защиту файла. Класс буферизации данных в потоках f ilebuf для упрощения из- ложения не показан в иерархии классов (см. рис. 11.3). Он является базовым для всех классов ввода-вывода, с которыми мы познакоми- лись, и должен использоваться при создании на их основе новых соб- ственных классов. Среди компонентных функций класса filebuf не показаны виртуальные функции. Таблица П5.9 Компонентные функции классов ifstrearn, of stream, f stream Функция Прототип и краткое описание действий open void open (const char*, int mode, int =» filebuf::openprot); Открывает файл с буфером, на который указывает первый пара- метр. Второй параметр - режим использования файла. Третий параметр - защита файла. rdbuf filebuf* rdbuf(); Возвращает указатель на буфер, связанный с потоком.
Приложение 5. Классы и функции библиотеки ввода-вывода в Си++ 527 Таблица П5.10 Режимы файла, устанавливаемые параметром node в функции open () Обозначение Значение Краткое описание действия iosr.in 0x01 Открыть только для чтения (режим по умолча- нию устанавливается для потоков класса if stream). ios::out 0x02 Открыть только для записи (режим по умолчанию устанавливается для потоков класса oifstream). ios::ate 0x04 Открыть для записи в конец файла. Если файл ие существует - создать его. ios::app 0x08 Открыть в режиме дополнения. iosr.trunc 0x10 Открыть, уничтожив содержимое файла (устанавливается по умолчанию, если установлен режим out, либо один из режимов ate или арр). ios::nocreate 0x20 Открыть только существующий файл, если файла не существует - установить состояние ошибки. ios::noreplace 0x40 Создать и открыть только ие существующий файл. Если файл существует - установить состоя- ние ошибки. ios::binary 0x80 Открыть для двоичного обмена.
ПРИЛОЖЕНИЕ 6 БИБЛИОТЕЧНЫЕ КЛАССЫ И ИХ КОМПОНЕНТНЫЕ ФУНКЦИИ Среди дополнительных средств, не входящих в собственно язык Си++, но отмечаемых в проектах стандарта и входящих в конкретные реализации, важное место занимают классы. Наиболее полезные с точки зрения пользователей и разработчиков компиляторов классы входят в библиотеки классов и поставляются вместе с компиляторами. Здесь можно отметить библиотеку классов потокового ввода-вывода, библиотеку контейнерных классов, а также некоторое количество специализированных классов. Библиотека потокового ввода-вывода подробно разобрана в главе 11, а общин список ее компонентных функций дан в Приложении 5. Контейнерные классы позволяют программисту с минимальными затратами создавать динамические массивы, списки, деревья, очереди, стеки, включающие элементы разных типов. В настоящее время кон- тейнерные классы входят в наиболее передовые реализации компиля- торов, но не стандартизованы. Контейнерные классы заслуживают отдельного тщательного изучения при подготовке профессиональных программистов. Объем настоящего пособия не позволяет рассмотреть их подробно, а бойкое перечисление имен и названий классов ничего читателю не даст. Некоторую информацию о возможностях контей- нерных классов можно найти в работе [19]. Однако за подробностями следует обращаться к технической документации по конкретным ком- пиляторам и библиотекам классов. В стандартные библиотеки компиляторов входят и менее объем- ные, но не менее полезные для частных применений классы. Остано- вимся на двух их них. Класс complex становится доступным в программе после подклю- чения заголовочного файла complex.ь. В этом файле содержатся как определения класса complex, так и все операции-функции, необходи- мые для работы с его объектами, т.е. с комплексными числами, а именно: • все арифметические операции (+, * и т.д.); • операции присваивания (=, += и т.д.); • потоковые операции записи « и чтения »; • обычные математические функции, которые вызываются только в том случае, если фактический параметр имеет тип complex.
Приложение 6. Библиотечные классы и их компонентные функции 529 В классе complex имеются две формы конструктора объектов: complex complex О; complex complex(double real, double imag ~ 0); где complex - структура, описанная в файле math.h следующим обра- зом: struct complex { double x, у; }; x и у - соответственно вещественная и мнимая части комплексного числа. При использовании первого конструктора создается комплексное число с нулевыми вещественной и мнимой частями (х=»о, у=0). Во второй форме можно задавать либо вещественную и мнимую части одновременно, либо только одну вещественную часть комплексного числа. В последнем случае мнимая часть инициализируется нулевым значением. Примеры: complex zl () ; complex z2(5.0); complex z3 (5.0,-1.0) // x==0, y=0 // X“5.0, yraO // x==5.0, y=-1.0 Таблица П6.1 Компонентные н дружественные функции класса complex Функция Прототип и краткое описание действий abs double abs(complex x); Возвращает модуль комплексного числа z acos complex acos(complex z); Возвращает значение арккосинуса комплексного числа z arg double arg(complex z); Возвращает главное значение аргумента комплексного числа z (-я < arg z я) asin complex asin{complex z); Возвращает значение арксинуса комплексного числа z atan complex atan(complex x); Возвращает значение арктангенса комплексного числа z 34-3821
530 Язык Си++ Таблица Пб. 1 (продолжение) Функция Прототип и краткое описание действий conj double conj(complex z) ; Возвращает комплексносопряженное к комплексному числу z cos complex cos(complex z); Возвращает значение косинуса комплексного числа z cosh complex cosh(complex z); Возвращает значение гиперболического косинуса комплексного числа z exp complex exp(complex z); Возвращает значение функции ez комплексного числа z imag double imag(complex z); Возвращает мнимую часть комплексного числа z log complex log(complex z); Возвращает значение натурального логарифма комплексного числа z loglO complex loglO(complex z); Возвращает значение десятичного логарифма комплексного чис- ла Z norm double norm(complex z); Возвращает квадрат модуля комплексного числа z pow complex pow(complex x, complex у); Возвращает значение ху комплексных чисел х и у polar complex polar(double mag, double angle); Возвращает комплексное число, имеющее модуль mag и значение аргумента angle real double real(complex z); Возвращает вещественную часть комплексного числа z sin complex sin(complex z); Возвращает значение синуса комплексного числа z sinh complex sinh (complex z) ; Возвращает значение гиперболического синуса комплексного числа z
Приложение 6. Библиотечные классы и их компонентные функции 531 Таблица П6.1 (продолжение) Функция Прототип и краткое описание действий sqrt complex sqrt(complex z); Возвращает одно из значений квадратного корня из комплексно- . Г t । ( ar8z - argz^ го числа z по формуле: Vz = | z | ^cos- ^— +1 • sin J J tan complex tan(complex z); Возвращает значение тангенса комплексного числа z tanh complex tanh(complex z); Возвращает значение гиперболического тангенса комплексного числа z Другой полезный класс - string - описывается в заголовочном файле cstring.h. В компиляторе ВС++ 4.5 этот класс содержит 11 форм конструкторов объектов (см. [30]): string(); Конструктор по умолчанию. Создает строку нулевой длины, string(const string^ s) ; Создает строку, совпадающую с полученной по ссылке з. string(const strings s, size_t start, size^t n = NPOS); Создает строку, содержащую n байт, начиная с позиции start исходной строки s. Тип size_t определен в заголовочном файле так: typedef unsigned size_t; значение npos опре- делено как unsigned (-1), т.е. 32767. string(const char *cp); Копирует строку, адресуемую указателем ср (до первого нуле- вого символа). string (const char *ср, size_t start, size__t n = NPOS); Создает строку, содержащую n байт, начиная с позиции start исходной строки ср. string([un]signed char с); Создает строку, состоящую из одного символа с. string([un]signed char c, size_t n = NPOS); Создает строку, состоящую из п символов с. string(const TSubString _FAR Ass); Создает строку из подстроки ss. string(HINSTANCE instance, UINT id, int len = 255); Создает строку в программах для Windows из ресурса. 34’
532 Язык Си++ Класс string замечателен тем, что в нем реализована технология, называемая "копировать при записи" ("copy-on-write"). Это означает, что при создании новых объектов класса string копируется не сама порождающая строка, а лишь указатель на нее. При дальнейших чте- ниях информации из этого объекта на самом деле выполняются опе- рации не с новой, а с порождающей строкой. Реальное же выделение памяти для объекта и копирование содержимого исходной строки происходит только в том случае, если в строку нового объекта вно- сятся изменения. Таблица П6.2 Компонентные н дружественные функции класса string Функция Прототип и краткое описание действий ansi_to_ void ansi to ош(); oem Конвертирует строку из кодировки ANSI (Windows) в кодировку OEM (MS-DOS) (доступна только при компиляции программ для Windows, в т.ч. в режиме EasyWin) append stringA append(const stringA s); stringA append(const stringA s, size_t start, size_t n = NPOS) ; stringA append (const char *cp, size_t start, size_t n = NPOS); Добавляет к исходной строке соответственно строку s или не более п символов (начиная с символа start) строки s (ср) Все три формы возвращают ссылку на результат assign stringA assign(const stringA s); stringA assign(const stringA s, size_t start, size_t n = NPOS) ; Меняет содержимое исходной строки на соответственно строку s или п символов (начиная с символа start) из строки s compare int compare(const stringA s) int compare(const stringA s, size_t orig, size t n = NPOS) Сравнивает исходную строку соответственно со строкой s или п символами строки s. Возвращает целое число, меньшее, большее или равное нулю в зависимости от результата сравнения строк contains int contains(const char *pat) const; int contains(const stringA s) const; Возвращает 1, если подстрока pat (s) найдена в исходной строке; в противном случае возвращает 0
Приложение 6. Библиотечные классы и их компонентные функции 533 Таблица П6.2 (продолжение) Функция Прототип и краткое описание действий сору size_t copy(char *cb, size_t n = NPOS); size__t copy (char *cb, size__t n, size_t pos) ; Копирует не более n символов (начиная с pos) исходной строки в строку сЬ. Возвращает число скопированных символов string сору() const throw(xalloc); Возвращает копию исходной строки c_str const char *c_str() const; Возвращает указатель (адрес) на исходную строку (или ее ко- пию) find size_t find(const stringfi s); size^t find(const string& s, siza_t pos); Возвращает позицию первого вхождения подстроки s в ис- ходную строку (начиная с позиции pos исходной строки) size_t find(const TRegexpA pat, size_t i = 0) ; size__t find (const TRegexpA pat, size_t *ext, size^t i = 0) const; Возвращает позицию первого вхождения регулярного выраже- ния pat в исходную строку Если подстрока не найдена - возвращается NPOS find_first_ of size_t find_first_of(const string^ s) const; size__t find_first__of (const stringfi s, size_t pos) const; Возвращает первую позицию исходной строки (начиная с пози- ции pos), в которой встретился некоторый символ строки s. В случае неуспеха возвращается NPOS find_first_ not_of size_t find_first_not_of (const string^ s) const; size_t find_first_not_of(const string^ s, size_t pos) const; Возвращает первую позицию исходной строки (начиная с пози- ции pos), содержащую символ, отсутствующий в строке з. В случае неуспеха возвращается NPOS find_last_ of size t find_last_of(const string^ s) const; size't find_last_of(const string^ s, size_t pos) const; Аналогична find_first_of, но возвращается последняя позиция
534 Язык Си++ Таблица П6.2 (продолжение) Функция Прототип и краткое описание действий find_last_not_of size__t f ind_last_not_of (const stringfi s) const; size_t f ind_last__not__of (const stringA s, size^t pos) const; Аналогична find_first_not_of, но возвращается по- следняя позиция get_at char get__at(size_t pos) const throw(outofrange); Возвращает символ исходной строки в позиции pos. Если pos > length () -1, порождается ис- ключение outofrange get_case_sensitive_flag static int get_case__sensitive_flag() ; Возвращает 1, если при сравнении строк различа- ются прописные и строчные буквы, 0 - если нет get_initial_capacity static unsigned get_initial_capacity(); Возвращает количество байт памяти, отведенных под исходную строку (< length () -1) get_max_waste static unsigned get_max__waste () ; Возвращает максимальное число байт, которое может быть выделено для строки get_paranoid_check static int get__paranoid__check () ; Возвращает 1, если возможна проверка коллизий хеширования; иначе возвращает 0. get_resize_increment static unsigned get_resize_increment () ; Возвращает приращение, изменяющее размер стро- ки. get_skipwhitespace_flag static int get__skipwhitespace__flag() ; Возвращает 1, если игнорируются обощеиные про- бельные символы; иначе возвращает 0. hash unsigned hash() const; Возвращает значение хеширования initiaLcapacity static size_t initial__capacity (size_t ic = 63); Устанавливает минимальное количество байт, вы- деляемых для строки по умолчанию
Приложение 6. Библиотечные классы и их компонентные функции 535 Таблица П6.2 (продолжение) Функция Прототип и краткое описание действий insert stringA insert (size_t pos, const stringA s) ; stringA insert (size_t pos, const stringA s, size_t start, size_t n = NPOS) ; С позиции pos в исходную строку вставляется содержимое строки s (не более п символов строки s, начиная с позиции start). Возвращает ссылку на результат is_nuB int is__null() const; Возвращается 1, если исходная строка пуста, и 0 - в про- тивном случае length unsigned length() const; Возвращает число символов в исходной строке MaxWaste static size_t MaxWaste (size_t mw = 63) ; Установить максимальное число байт, выделяемых для стро- ки oem__to_ansi void oem_to__ansi () ; Конвертирует строку из кодировки OEM (MS-DOS) в коди- ровку. ANSI (Windows) (доступна только при компиляции программ для Windows, в т.ч. в режиме EasyWin) prepend stringA prepend(const stringA s ); stringA prepend(const stringA s, size_t start, size_t n = NPOS) ; stringA prepend(const char *cp ); stringA prepend (const char *cp, size__t start, size_t n = NPOS) ; Вставляет в начало исходной строки строку s (ср) (п симво- лов строки s (ср), начиная с позиции start) put_at void put_at (size__t pos, char c) throw(outofrange); Заменяет символ в позиции pos исходной строки символом с. Если роз > length () -1, порождается исключение outofrange read_file istreamA read__file(istreamA is); Считать строку из входного потока is (до символа EOF или NULL) read_Iine istreamA read_line(istreamA is); Считать строку из входного потока is (до символа перевода строки или EOF)
536 Язык Си++ Таблица П6.2 (продолжение) Функция Прототип и краткое описание действий read_string istreamA read__s tring(istreamA is); Считать строку из входного потока is (до символа EOF или NULL) read_to_delim istreamA read_to_delim(istreamA is, char delim = 1\n*); Считывать строку из входного потока is до тех пор, пока не будет встречен символ delim или EOF (символ- разделитель delim из потока удаляется) read_token istreamA read__token (istreamA is); Считать строку из входного потока is до обобщенного про- бела (ведущие пробелы ие учитываются) rfind size_t rfind (const s tring & s ) ; size_t rfind(const stringA s, size_t pos) ; Аналог функции find, но возвращает позицию последнего вхождения подстроки s remove stringA remove(size_t pcs); stringA remove(size__t pos, size_t n = NPOS); Удаляет все символы (не более п символов), начиная с пози- ции pos исходной строки и до ее конца. Возвращает ссылку на строку-результат replace stringA replace(size__t pos, size_t n = NPOS, const stringA s); stringA replace (size__t pos, size_t nl, const stringA s, size_t start, size__t n2) ; Удаляет из исходной строки не более n (nl) символов и по- мещает на их место строку s (не более п2 символов из стро- ки s, начиная с позиции start) reserve size__t reserve () const; Возвращает размер отведенной памяти для размещения строки void reserve (size_t ic) ; Сообщает системе, что строка может занимать более ic байт памяти resize void resize (size__t m) ; Изменяет размер строки на m символов, отбрасывая лиш- ние либо заполняя пробелами добавленные
Приложение 6. Библиотечные классы и их компонентные функции 537 Таблица П6.2 (продолжение) Функция Прототип и краткое описание действий resizejncr ement static size__t resize_increment (size__t ri = 64); Изменяет приращение, используемое для автоматического из- менения размера строки set_case_s ensitive static int set__case_sensitive (int tf =1) ; Установка различия прописных и строчных букв: 1 - различать, 0 - не различать. Возвращает предыдущую установку set_parano id_check static int set_paranoid__check (int ck = 1); Для поиска строк используется механизм хеширования, так как возможно, что одно и то же хешированное значение получается при обработке различных строк. Вызов функции с параметром ck = 1 устанавливает заключительную проверку строк с по- мощью функции strcmp (). Если ck = 0, такая проверка не выполняется skip_white space static int skip_whitespace(int sk = 1); Устанавливается в 1, чтобы игнорировать пробелы после счи- тывания каждой лексемы, иначе устанавливается в 0 strip TSubString strip(StripType s = Trailing, char c e . -) ; В зависимости от параметра s удаляет символы с в начале, в конце либо с обеих сторон исходной строки substr string substr(size__t pos) const; string substr(size^t pos, size_t n = NPOS) const; Создает строку, содержащую копию (не более п символов), на- чиная с позиции pos, и до конца строки substring TSubString substring(const char *cp); const TSubString substring (const char *cp) const; TSubString substring(const char *cp, size_t start); TSubString substring(const char *cp, size^t start); Создает объект класса TSubString, включающий копию сим- волов, адресуемых указателем ср (начиная с символа start) tojower void to_lower(); Переводит все символы исходной строки на нижний регистр to_upper void to__upper () ; Переводит все символы исходной строки на верхний регистр
Литература 1. Страуструп Б. Язык программирования Си++: Пер. с англ.- М.: Радио н связь, 1991.- 352 с. 2. Эллис М., Строуструп Б. Справочное руководство по языку программи- рования C++ с комментариями. Проект стандарта ANSI: Пер. с англ- М.: Мир, 1992.-445 с. 3. Керниган Б., Ритчи Д. Язык программирования Си: Пер. с англ - М.: Финансы и статистика, 1992 - 272 с. 4. Turbo C++. Руководство пользователя: Пер. с англ.- М.: СП ИН- ТЕРКВАДРО, 1991.- 298 с. 5. Turbo C++. Руководство программиста: Пер. с англ- М.: СП ИН- ТЕРКВАДРО, 1991.- 394 с. 6. Turbo C++. Справочник по библиотеке: Пер. с англ.- М.: СП ИН- ТЕРКВАДРО, 1991.-568 с. 7. Дункан Р. Си++ для тех, кто знает Си // PC Magazine/USSR, 1991№3,- С. 84-106. 8. Намиот Д.Е. Язык программирования TURBO C++: Учебное пособие /Под ред. В.А.Сухомлина.-М.: МГУ, 1991.- 121 с. 9. Borland C++ 3.0. Programmer's Guide- Scotts Valley, USA: Borland International, Inc. 1991.- 467 p. 10. Borland C++ 3.0. User's Guide.- Scotts Valley, USA: Borland International, Inc. 1991,- 229 p. 11. Borland C++ 3.0. Library Reference.- Scotts Valley, USA: Borland International, Inc. 1991.- 655 p. 12. Неформальное введение в C++ и Turbo Vision: Пер. с англ.- Санкт- Петербург: Галерея "ПЕТРОПОЛЬ", 1992, 384 с. 13. Собоцинский В.В. Практический курс Turbo C++. Основы объектно- ориентированного программирования.- М.: Свет, 1993 - 236 с. 14. Романов В.Ю. Программирование на языке C++. Практический под- ход.- М.: Компьютер, 1993.- 160 с. 15. Буч Г. Объектно-ориентированное проектирование с примерами приме- нения: Пер. с англ.- М.: Конкорд, 1992.- 519 с. 16. Уинер Р. Язык Турбо Си: Пер. с англ-М.: Мир, 1991,-384 с. 17. Касаткин А.И., Вальвачев А.Н. Профессиональное программирование на языке Си. От Turbo С к Borland C++. Справочное пособие. Под общ. ред. Касаткина А.И.- Минск: Вышэйшая школа, 1992.- 240 с. 18. Рассохин Д.Н. От Си к Си++.- М.: ЭДЭЛЬ, 1993.- 128 с. 19. Цимбал А.А., Майоров А.Г., Козодоев М.А. Turbo C++: Язык и его при- менение.- М.; Джен Ай Лтд, 1993.- 512 с.
Литература 539 20. Романовская Л.М., Русс Т.В., Свитковский С.Г. Программирование в среде Си для ПЭВМ ЕС - М.: Финансы и статистика, 1991.— 352 с. 21. Borland C++. Version 4.0. Programmer’s Guide.- Scotts Valley, USA: Borland International, Inc. 1993.- 326 c. 22. Вайнер P., Пинсон Л. C++ изнутри: Пер. с англ.- Киев: ДиаСофт, 1993.- 304 с. 23. Дьюхарст С., Старк К. Программирование на Си++: Пер с англ.- Киев: ДиаСофт, 1993.- 272 с. 24. Лукас П. C++ под рукой: Пер. с англ.- Киев: ДиаСофт, 1993 - 176 с. 25. Липпман С.Б. C++ для начинающих: Пер. с англ.- М.: Унитех; Рязань: Гэлион, 1993.-Т. 1.- 304 с.; Т. 2.- 345 с. 26. Сграуструп Б. Язык программирования Си++. Второе издание : Пер. с англ.- Киев: ДиаСофт, 1993. Часть 1.- 264 с. Часть 2 - 296 с. 27. От Си к Си++ / Е.И.Козелл, Л.М.Романовская, Т.В.Русс и др - М.: Фи- нансы и статистика, 1993.- 272 с. 28. Кристиан К. Библиотека потокового ввода-вывода языка Си++ // PC Magazine/Russian Edition, 1994-№3.-С. 115-123. 29. Borland C++. Version 4.5. Programmer's Guide- Scotts Valley, USA: Borland International, Inc., 1994.- 256 p. 30. Borland C++. Version 4.5. Class Libraries Guide.- Scotts Valley, USA: Borland International, Inc., 1994.- 294 p. 31. Bortland C++. Version 4.5. Library Reference.- Scotts Valley, USA: Borland International, Inc., 1994.- 364 p. 32. Пол Ирэ. Объектно-ориентированное программирование с использова- нием Си++: Пер с англ.- Киев: НИПФ ДиаСофт Лтд, 1995.- 480 с. 33. Белкин В. Обработка исключительных ситуаций в Си++: что, когда, как. PC Magazine/Russion Edition, 1995.-№4.- С. 180-186. 34. Зуев Е., Кротов А. Новые возможности Си++ // PC Magazine/Russian Edition, 1994,-№7,-С. 176-181. 35. Бабэ Бруно. Просто и ясно о Borland C++. Пер. с англ.- М: БИНОМ, 1995.-400 с.
УКАЗАТЕЛЬ СИМВОЛОВ t операция ’’запятая” 40, 46, 50 ! операция логического отри- цания 32, 46 ! = операция неравенства 35, 46 it операция препроцессирования 17, 53 операция замещения пара- метра макроса 276 ## операция конкатенации лексем в макросе 277 % операция взятия остатка 34, 46 %= операция получения остатка, совмещенная с присваива- нием 37 & операция получения адреса 32,46 операция И (поразрядная) 34, 46 описание ссылки 215 && операция И (логическая) 36,46 &= операция поразрядного "И", совмещенная с присваива- нием 37 () операция "вызов функции” 41 описание функции 47 преобразование типов 48 * операция разыменования 32,46 операция умножения 34, 46 определение указателя 44 *= операция умножения, совме- щенная с присваиванием 37 + операция сложения 32, 34 операция "унарный плюс" 32 ++ операция увеличения (инкре- мент) 32,46 += операция сложения, совме- щенная с присваиванием 37 операция вычитания 32, 34 операция "унарный минус" 32 операция уменьшения (дек- ремент) 33, 46 “= операция вычитания, совме- щенная с присваиванием 37 -> операция доступа к компо- нентам структурированного объекта 39, 241, 284, 296 “>* операция разыменования указателей на компоненты классов 302 операция доступа к компо- ненту класса по имени объ- екта 39 . * операция разыменования указателей на компоненты классов 302 многоточие 52 / операция деления 34, 46 /* комментарий (начало) 8, 174, 266 * / комментарий (конец) 8,174,266 // комментарий (одностроч- ный) 8, 174 /= операция деления, совмещен- ная с присваиванием 37 : признак поля 258 спецификатор метки 52 : : операция указания области видимости 39, 46 операция уточнения области действия 283 : : * описание указателя на ком- понент класса 300 < операция "меньше, чем" 35, 46 « операция "сдвиг влево"34, 46
Указатель символов 541 «= операция "сдвиг влево”, со- вмещенная с присваивани- ем 37 <= операция "меньше или равно" 35, 46 == операция присваивания 37, 38,46 = 0 "чистый спецификатор" 365 == операция сравнения на ра- венство 35, 46 > операция "больше, чем" 3, 46 >= операция "больше или равно" 35, 46 » операция "сдвиг вправо" 34, 46 »= операция "сдвиг вправо", со- вмещенная с присваивани- ем 37 ? : операция условного выраже- ния (условная операция) 42, 46 [ ] операция индексации 41,46 описание массива 46, 47 \ обратный слэш 26 А операция "симметрическая разность" (исключающее ИЛИ) 35, 46 л« операция "симметрическая разность", совмещенная с присваиванием 38 _ подчеркивание (литера) 19 подчеркивание в идентифи- каторе 19, 20 {} блок (составной оператор) 48 определение класса 281 определение перечисления 24 список инициализаторов 10, 12 | побитовая включающая опе- рация ИЛИ 34, 46 (= побитовая включающая опе- рация ИЛИ, совмещенная с присваиванием 38 | | логическая операция ИЛИ 36, 46 ~ деструктор 346 операция дополнения до еди- ницы 32, 46 О нуль 32 нулевой указатель 44 \0 нулевая литера 396
ПРЕДМЕТНЫЙ указатель A-Z ASCII-код, см. Кодировка ASCII ВС++4 BGI510 EasyWin 469 ESC-последовательность 26, 263 /ar-указатель 161 Awge-указатель 164 IDE 469, 486 /л//л£-функция 190, 275, 303 /-значение 37, 59, 302, 303 MS-DOS 10 Microsoft Windows 469 леаг-указатель 160 RTTI, см. Динамическая иденти- фикация типов ТС++ 4 А Абстрактный класс 365 Абстрактный тип 281 Абстракция данных 281 Агрегатный тип 59 Аддитивная операция 34 Адрес, см. Операция взятия адреса Адреса байтов памяти 158 Адресация памяти 159 Алфавит языка Си++ 18 Аргумент функции, см. Формаль- ные параметры; Фактические па- раметры Арифметический тип, см. Скаляр- ный тип Арифметическое выражение 86 Арифметическое преобразование, см. Операция преобразования Б Базовый класс 52, 336, 339 ---абстрактный 366 — виртуальный, см. Виртуаль- ный базовый класс — непосредственный, см. Непо- средственный базовый класс — непрямой, см. Непрямой ба- зовый класс — общий 358 ---прямой, см. Непосредствен- ный базовый класс Базовый тип 57, см. также Тнп данных Бинарные операции 33 Битовое поле 258 Ближний указатель, см. пеаг- указатель Блок 48, 62 - и составной оператор 92 - контролируемый, см. Контро- лируемый блок - обработчика исключений 446 Буфер потока 380, см. также По- ток в Ввод, см. Операция ввода, см. также Поток ввода, Потоковый ввод-вывод Вещественная константа, см. Кон- станта вещественная Видимость, см. Область видимос- ти Виртуальная функция 359 — дружественная 365 - - и производный класс 363
Предметный указатель 543 — чистая 365 Виртуальный базовый класс 356 Вложение блоков 92 - комментариев 18 - контролируемых блоков 459 - переключателей 103 - составных операторов 92 - условных операторов 93 - циклов 101 Внешнее связывание, см. Связыва- ние внешнее Внутреннее связывание, см. Свя- зывание внутреннее Возвращаемое значение 170 Восьмеричная константа, см. Кон- станта восьмеричная Встраиваемая функция, см. inline- функция Вывод, см. Операция вывода, см. также Поток вывода, Потоко- вый ввод-вывод Вызов деструктора явный 347 - функции 41, 84, 173 Выражение 80, 82 - арифметическое 86 - леводопустимое, см. Леводопу- стимое выражение - первичное 81 - постфиксное 83 - праводопустимое 60 ~ префиксное 32 Вычитание, см. Операция вычита- ния г Генерация исключений 448, 453, 457 Глобальная область памяти, см. Область памяти глобальная - переменная — _newjiandler 465 — throwExceptionName 478 — „throwFileName 478 — throwLineNumber 478 д Дальний указатель, см. far- указатель Данные, абстракция, см. Абстрак- ция данных - внешние, см. Внешние данные - класса, см. Компонентные дан- ные класса - тип, см. Тип данных Декремент, см. Операция декре- мент Деление, см. Операция деления Деструктор 294, 346 - вызов 346 — явный 347 Десятичная константа, см. Кон- станта десятичная Динамическая идентификация ти- пов 481,486 Директива препроцессора 17, 53, 263 - # 264, 278 - #define 264, 265 - #elif264, 273 - #else 264, 272 - #endif 264, 272 - #error 264, 278 - #if 264, 271 - tfifdef 264, 272 - tfifndef 264, 272 - #include9, 174,264, 268 - #line 264, 278 - ttpragma 264, 278 - ttundef264, 268 Доступ к базовому классу из про- изводного класса 338, 341 - к производному классу нз базо- вого класса 341 - к элементам массива 151 Дружественная функция 315 — виртуальная, см. Виртуаль- ная дружественная функция Дружественный класс 319
544 Язык Си++ 3 Заголовок переключателя, см. Оператор switch - функции 7 - цикла, см. Цикл Заголовочный файл 175, 269 — alloc.h504 ---complex.h 283, 528 - - conio.h 309, 316, 398, 504, 511 ---constrea.h 384 ---cstring.h 466, 531 ---ctype.h499 - - dos.h 161, 256, 506 ---except.h 466, 477 - - fcntLh432 - - floath 495 — fstream.h 384, 433 ---graphics.!» 306, 345, 509 - - io.h 432 ---iomanip.h 407, 525 - - iostream.h 7, 175, 269, 384, 379 ---Iimits.h494 ---math.h497 — mem.h506 ---new.h 466 ---process.h 190 — signal.h508 ---stdarg.h 52, 181 -- stdlib.h 212, 432, 501, 507 ---stdio.h 379, 500 - - string.h 139, 214, 422, 501 — strstrea.h 11, 384, 420 — sys\stat.h 432 ---typeinfo.h481 Зарезервированное слово, см. Служебное слово Знаки операций 18, 31 Значение, возвращаемое функцией 170 - леводопустимое, см. /-значение - параметра по умолчанию 176 - праводопустимое 32 - указателя 108 ---нулевое, см. Нулевой указа- тель и Идентификатор 18, 19, 44, 54, 83 - класса 281 - массива 237 - область действия, еле. Область действия идентификатора - процессорный 265 Иерархия классов 289, 336 — библиотеки ввода-вывода 382 — виртуальных 357 --производных 349 - объектов 336 Имя 82 - массива 133 - основное 248 - переменной 59 - повторное использование 65, 66, 79 - указателя 44 - уточненное 239, 241, 252, 283 - функции 169 Индекс массива 41, 130 Индексация 83, см. также Опера- ция [] Инициализатор 75,109, 215, 292 Инициализация класса 288, см. также Конструктор - константы 19 - массива 49, 131 — динамического 155 — классов 286 — многомерного 145 - объединения 257 - объекта 70 - переменной 55, 75 - ссылки 215, 224 — на объект класса 283 - статического компонента клас- са 286 - структуры 49, 239 - указателя 109, 241 — на функцию 206 Инкапсуляция 5, 289
Предметный указатель 545 Инкремент, см. Операция инкре- мент Исключение 445, 446,453 - Bad_typeid 482 - xalloc 465 - обработка, см. Обработка ис- ключений - обработчик, см. Обработчик исключений - порождение, см. Генерация ис- ключений Исключительная ситуация, см. Исключение к Класс 59, 281 - complex 528 - constream 383 - filebuf 525 - fstream 383, 434, 526 - fstreambase 434 - ifstream 383, 434, 526 - ios 383, 519 - iostream 383 - istream 383, 417, 521 - istrstream 383, 420 - ofstream 383, 434, 526 - ostream 383, 416, 419, 522 - ostrstream 383, 424 - string 531 - strstream 383, 427 - typeinfo 481 - xalloc 466 - xmsg 466 - абстрактный 365 - базовый, см. Базовый класс - данные, см. Компонентные данные класса - дружественный, см. Друже- ственный класс - инициализация, см. Инициали- зация класса - компонент, см. Компонентные данные класса - локальный, см. Локальный класс - метод, см. Компонентная функция класса - наследник, см. Класс производ- ный - определение, см. Определение класса - памяти 61, 75 — auto 61 — register 61,115 - - static 61,73, 297 — extern 61,69,73, 169 - полиморфный, см. Полиморф- ный класс - порождающий, см. Базовый класс - порожденный, см. Производ- ный класс - потомок, см. Производный класс - производный, см. Производный класс - семейство, см. Шаблон се- мейства классов - функции, см. Функция класса Ключевое слово, см. Служебное слово Кодировка ANSI (Кодовая табли- ца MS Windows) 492 - ASCII 118, 254,488 - OEM (Кодовая таблица MS- DOS) 490 Команда препроцессора, см. Ди- ректива препроцессора Комментарий /* */ 8, 174, 266 - //7,8,174 Компонент класса, см. Компо- нентные данные класса Компонентная функция класса 282 -----дружественная, см. Друже- ственная функция -----статическая 298 Компонентные данные класса 282, 295 35“3*21
546 Язык Си++ -----защищенные 315, 339, см. также Модификатор protected -----нестатические 299 -----общедоступные 315, 339, см. также Модификатор public -----собственные 299, 339, см. также Модификатор private -----статические 285, 373 Компоновка, см. Связывание Консольный поток, см. Поток консольный Константа 18, 20 - восьмеричная 20, 21 - вещественная 20, 22 - десятичная 20 - литерная, см. Константа сим- вольная - перечислимая 20, 24 - с плавающей точкой, см. Кон- станта вещественная - символьная 20, 26, 30 - строковая 20, 28 на нескольких строках 29 - целая 20, 21 - шестнадцатеричная 20, 21 Конструктор класса 288 - - X (const Х&) 291 — абстрактного 366 — виртуального 356 — стандартный (по умолчанию) 291 Контролируемый блок 445 л Леводопустимое выражение 59, см. также /-значение Лексема 8,17, 18, 277 Лексический элемент, см. Лексема Литерал, см. Константа Литерная константа, см. Констан- та символьная Логическая операция, см. Опера- ция логическое И (ИЛИ, НЕ) Локализация 92, см. также Об- ласть видимости Локальный класс 373 м Макроопределение 48, см. также Директива препроцессора tfdefine - FP_OFF0 164 - FPJSEG0 164 -MKJPQ161 - va_arg() 52, 182 - va__end()52,183 - va_start() 52, 182 - встроенное 279 Макрос, см. Макроопределение Манипулятор 405 - без параметров 406 — dec 406 - - endl 407 - - ends 407, 428 — flush 407 - - hex 118,394,406 — oct 407 - - ws407 - с параметрами 407 — resetiosflags() 408 — setbase()407 - - setfill()408 — setiosflagsO 408 — setprecision() 408 — setw()408 Массив 130, 237 - динамический 154, 200 - доступ к элементам, см. Доступ к элементам массива - и указатель 130,195 - имя, см. Имя массива - индекс, см. Индекс массива - инициализация, см. Инициали- зация массива - многомерный 144, 196 - объединений 257
Предметный указатель 547 - объектов классов 283, 286 - описание, см. Описание масси- ва - определение, см. Определение массива - параметр 193 - символьный 30 - указателей 149 — на строки 212 — на функции 207 Метка 52, 91 - сазе в переключателе 90, 94 - default в переключателе 90, 94 Метод класса, см. Компонентная функция класса Минус, см. Операция минус унар- ный Многомерный массив, см. Массив многомерный Модификатор, см. также Слу- жебное слово - _cs 166 - __ds 166 - _es 166 - _seg 166,167 - __ss 166 - cdecl 80, 181 - const 79, 113,219 - far 80, 160 - friend 315 - huge 80, 160 - interrupt 80 - near 80, 160 - pascal 80,181 - private 289, 297, 465, см. также Компонент класса собственный - protected 289, 465, см. также Компонент класса защищенный - public 289, 297, см. также Ком- понент класса общедоступный - volatile 80 н Направленный ациклический граф (НАГ) 338, 349 Наследование 336 - доступа к компоненту класса 338 - множественное 338, 350 - неоднозначное 359 Начальные значения параметров. см. Значение параметра по умол- чанию Неоднозначность при наследова- нии 359 - при обращении к компонент- ным данным класса 311 - прн сопоставлении операторов if и else 93 Непосредственный базовый класс 349 Непрямой базовый класс 352 Нормализованный указатель, см. /wge-указатель Нулевой указатель (NULL) 44, 98, ПО, 120 о Область видимости 92 — объекта 65,68 - действия 61 --идентификатора 62, 65 — оператора 93, 107 — ссылки 219 - памяти глобальная 286 Обобщенный пробельный символ, см. Пробел обобщенный Обработка исключений 460 Обработчик исключений 446 Обращение к функции, см. Вызов функции Объединение 250, 387 - безымянное 257 Объединяющий тип 58, 252 Объект 59 - класса 296 35*
548 Язык Си+ + Объектно-ориентированное про- граммирование 5 - проектирование 5, 336 Оператор 90, 108, см. также Слу- жебное слово break 95, 104 continue 107 catch 446 do 98 else 92 for 50, 99 goto 102, 448, 462 - if 14 return 14, 103, 170,410 - switch 90, 92, 94 - throw 445,457,458,487 tru 445 - while 42, 97, 255 безусловного перехода, см. Оператор goto возврата из функции, см. Опера- тор return выбора, см. Метка case в пере- ключателе - выражение 90 - переключатель, см. Оператор switch - присваивания, см. Операция присваивания пустой 91 - составной, см. Составной оператор условный, см. Оператор if цикла 96, см. также Цикл Операционная система MS-DOS. см. MS-DOS - - MS Windows, см. Microsoft Windows Операция - #276 - ##277 О 41, 46, 47, 173 - []41,46, 47, 130,218 -{) 48 - ... 52 - : 51 -; 51, 90 - delete 45, 71, 143, 201 - new 43, 71, 138, 154, 249, 465 — и массив -sizeof 22, 33, 130, 138, 154, 218 - typeid 481 - аддитивная 34 ~ бинарная 33 - больше или равно, чем (>=) 35, 46 - больше, чем (>) 35, 46 - ввода (») 46, 385, 390 - взятия адреса (&) 32,46,109,115,121, 130, 218 - взятия остатка (%) 34, 46 - выбора компонентов структури- рованного объекта (->, .) 39, 241, 284, 296, 327 - вывода(«) 46, 385, 390 - вычитания (-) 25, 32, 34, 46, 121 - декремент (—) 33, 46, 121, 124 - деления (/) 34, 46 - запятая (,) 40, 46, 50 - индексация, см. Операция [] - инкремент (++) 32, 46, 121, 124 - логическое И (&&) 36, 46 - - ИЛИ (II) 36, 46 --НЕ(!) 32, 46 - меньше или равно, чем (<=) 35, 46 - меньше, чем (<) 35, 46 - минус унарный (-) 32 - мультипликативная 34 - над указателями 120 - не равно (!=) 35, 46 - отношения 35, 46 - плюс унарный (+) 32 - поразрядное И (?) 34, 46 -ИЛИ (1) 34, 46 ----ИСКЛЮЧАЮЩЕЕ (А) 35, 46 --НЕ (~) 32, 46 - преобразования 86 — необратимость 88 — обратимость 88
Предметный указатель 549 — операндов в арифметических выражениях 86, 87 — ссылок 86 — стандартное, см. Операция преобразования типов — типов 46, 85 -----явного, см. Операция яв- ного преобразования ти- пов — указателей 86 -----на компоненты классов 86 - префиксная 32 - приведения, см. Операция пре- образования - приоритет, см. Приоритет опе- раций - присваивания^") 37, 38, 46, 53 - разыменования (*) 32, 46, 52, 241 — указателей на компоненты классов (.*, ->*) 302 - с компонентами классов 39, 46 - сравнения на равенство («) 35, 46 - сдвига влево («) 34, 46 - сдвига вправо (») 34, 46 - сложения (+) 32, 34, 46 - указания области видимости (::)39, 46 - умножения (*) 34, 46, 52 - условная (? :) 42, 46 - уточнения области действия (::)283 - функция 322, 409 - явного преобразования типов 42, 48, 85, И1,116 Описание 50 - внешних ссылок 220. - н определение, см. Определение и описание - компонентов класса 220 - массива 131 --явное 132 - переменной 55 - функции 51,63, 90,172 — с переменным числом пара- метров 177 ---чистой виртуальной 366 - шаблона функций 235 - элементов класса 295 Определение 54 - и описание 62, 74, 92 - класса 64, 281 — базового 352 -----виртуального 356 — производного 339 - массива 131 - объединения - переменной 59 - перечисления 24 - ссылки 215, 220 — на функцию 223 - указателя 44, 108 - функции 51, 170 — виртуальной 362 -----повторное 363 — компонентной 303 — с переменным числом пара- метров 52, 178 - шаблона — классов 376 — функций 230 Основное имя, см. Имя основное Особая ситуация, см. Исключение Остаток, см. Операция взятия остатка Отношения, см. Операция отно- шения п Память, выделение автоматиче- ское, см. Класс памяти auto (register) — динамическое, см. Операция new — явное, см. Операция new - локальная, см. Класс памяти auto - ПЭВМ 157
550 Язык Си++ - регистровая, см. Класс памяти register Параграф памяти ПЭВМ 158 Параметр фактический, см. Фак- тические параметры -формальный, см. Формальные параметры - шаблона, см. Список параметров шаблона Параметр-ссылка 221 Перегрузка 32, 42, 81, 322 - дружественной функции 333 - операции ввода-вывода 388, 429 --++ 329, 332 ----332 -- []376 --« 388, 409 -->>388,413 - - new 474 - - индексации 376 - функции 227,232 - шаблона функций 236 Переключатель, см. Оператор switch 90, 92, 94 Переменная 59 - автоматическая 71, см. также Класс памяти auto - глобальная, см. Глобальная пе- ременная -индексированная 57, 147, см. также Индексация - локальная 40 -регистровая, см. Регистровые переменные - статическая 61 Переполнение 55 Перечислимая константа, см. Кон- станта перечислимая Перечислимый тип, см. Скалярный тип Плюс, см. Операция плюс унарный Побочные эффекты 81, 85, 406 Подмена функции, см. Виртуальная функция Поле битовое, см. Битовое поле Полиморфизм 5, 482 Полиморфный класс 360, 482 Поразрядные операции, см. Опе- рация поразрядное И (ИЛИ, НЕ) Порождение исключений 448, 453 Порядок вызова деструкторов класса 288, 291 — конструкторов класса 295 Поток 380 - безымянный 426 -ввода 383,387, см. также Стандартный поток ввода - включение данных 386 - входной, см. Поток ввода -вывода 383, 387, см. также Стандартный поток вывода - выходной, см. Поток вывода - запись данных, см. Поток, включение данных - извлечение данных 386 - консольный 382 - стандартный, см. Стандартный поток - строковый 382, 420 - - входной 420 - - выходной 424 - - двунаправленный 427 - файловый 382 - чтение данных, см. Поток, из- влечение данных Потоковый ввод-вывод 379, 519 Преобразование, см. Операция преобразования Препроцессор 263 - команды, см. Директивы пре- процессора Префиксная операция 32 Приведение, см . Операция преоб- разования Приоритет операций 46 Присваивание, см. Операция при- сваивания Пробел обобщенный 392 Программирование объектно- ориентированное 5
Предметный указатель 551 Продолжительность существова- ния объектов 69 ----статическая 61,70 ----локальная 71 ----динамическая 71 — участка памяти 45 Проектирование объектно-ориен- тированное 5 Производные типы 57, 116, 247, 281 Производный класс 52, 336 Пространство имен 64 Прототип, см. Описание Р Разделитель 18,47 - пробельный 17 Разыменование указателей, см. Операция разыменования Ранги операций, см. Приоритет операций Расширение действия операции, см. Перегрузка операции Регистровая переменная -_CS 166 -_DS 166 -_ES 166 -_SS 166 Регистры сегментов 159 Режим EasyWin 469 Рекурсивная функция 187 Рекурсия 81, 187 "Родовое0 программирование 119 с Связывание внешнее 61, 72 -внутреннее 61, 72 - динамическое 362 - отложенное, см. Динамическое - позднее, см. Динамическое - раннее, см. Статическое связы- вание - статическое 362 Сдвиг вправо, см. Операция сдвига вправо - влево, см. Операция сдвига влево Сегментная организация памяти 159 Сегментная часть адреса 158 Сегментные указатели, 166, 167 Сигнатура 409 - функции 169 Символ null, '\0' 396 - '\п’ 263 - подчеркивания, ’J 19 Ситуация исключительная. см. Исключение - особая, см. Исключение Скалярный тип 59 Скэн-код 254 Слово зарезервированное, см. Служебное слово - ключевое, см. Служебное слово - памяти 158 - Сложение, см. Операция сложения Служебное (ключевое) слово —_CS, см. Регистровая перемен- ная _CS - -_cs, см. Модификатор _cs —_DS, см. Регистровая перемен- ная _DS - ~__ds, см. Модификатор _ds - -_.ES, см. Регистровая переменная _ES —__es, см. Модификатор _es —_seg, см. Модификатор _seg —JSS, см. Регистровая переменная __SS - -_ss, см. Модификаторам — auto, см. Класс памяти auto - - break, см. Оператор break
552 Язык Си++ — сазе, см. Метка case в пере- ключателе — catch 445,454, 487 ---cdecl, см. Модификатор cdecl ---char, см. Тип char — class 52, 230 ---const, см. Модификатор const — continue, см. Оператор continue — default, см. Метка default в переключателе ---delete, см. Операция delete ---do, см. Оператор do — double, сМ. Тип double — else, см. Оператор else — enum 24, см. также Перечис- лимые константы — extern, см. Класс памяти extern — far, см. Модификатор far — float, см. Тип float — for, см. Оператор for — friend, см. Модификатор friend — goto, см. Оператор goto — huge, см. Модификатор huge — if, см. Оператор if — inline, см. Спецификатор inline — int, см. Тип int — interrupt, см. Модификатор interrupt — long, см. Тип long — near, см. Модификатор near — new, см. Операция new — operator 322 — pascal, см. Модификатор pascal — private, см. Модификатор private — protected, см. Модификатор protected — public, см. Модификатор public — register, см. Класс памяти register — return, см. Оператор return ----short, см. Тип short — signed 55, 87 — sizeof, см. Операция sizeof — static, см. Класс памяти static — struct 52, 282, 289, см. также Структурный тип — switch, см. Оператор switch — template 230, см. также Шаблон — this, см. Указатель this — throw, см. Оператор throw — try 445, 454 — typedef, см. Спецификатор typedef — typeid, см. Операция typeid ----union 52, 289, см. также Объединяющий тип — unsigned, см. Тип unsigned — virtual 356 — void, см. Тип void — volatile, см. Модификатор volatile — while, см. Оператор while — список 19 Смещение 158 Составной оператор 91 - и блок 92 Спецификатор - inline 190, см. также inline- функция - typedef 57,86, 208, 247 - чистый 365 Список инициализации, см. Ини- циализация - параметров шаблона 230, 233, 378 Сравнение, см. Операция сравне- ния на равенство Ссылка 53, 59, 215 - на константу 219 - на параметр 221 - на структуру 242 - иа функцию 223 - определение, см. Определение ссылки
Предметный указатель 553 Стандартный поток 385 - - ввода cin 47, 175, 385 — вывода cout 7, 8, 47, 117, 140, 385 - - ошибок cerr 385, 448 --предопределенный обмен данными 387 - - протокола clog 385 Статус доступа 338 Стек 122, 181, 186 Строка 97 Строковая константа, см. Константа строковая Строковый поток, см. Поток стро- ковый Структура 237, см. также Струк- турный тип Структурный тип 58, 238, 240, 252 Сфера действия, см Область действия т Тег, см. Структурный тип Тело - функции 8,170 - цикла 96, см. также Цикл Технология "copy-on-write" 532 Тип - char 25, 54, см также Символьные константы - double 23 float 12,23,43 -int 12, 21 - long 15, 21 - long double 23 - short 33, 55, 87 - unsigned 21 -void 8, 104, 170,218 - void* 118 - абстрактный, см. Абстрактный тип - агрегатный, см. Агрегатный тип - базовый, см. Базовый тип - беззнаковый, см. Тип unsigned - возвращаемого значения ----void, см. Тип void ----ссылка 224, 231 - данных 21, 56 — абстрактный, см. Абстрактный тип - знаковый, см. Тип signed - класс, см. Класс - объединяющий, см. Объединяющий тип - перечисляющий, см. Перечисляю- щий тип - преобразование, см. Преобразо- вание типа - производный, см. Производные типы - результата, см. Тип возвращаемого значения - скалярный, см. Скалярный тип - структурный, см. Структурный тип - указателя 44 — void*, см. Тип void* - функции 59, 169 - чисел с плавающей точкой, см. Тип float, Тип double Точность простая, см. Тип float - двойная, см. Тип double У Указание области видимости, см. Операция указания области ви- димости Указатель 147 -this 81, 309 - ближний, см. иедг-указатель - дальний, см. /дг-указатель - значение, см. Значение указателя - и массив, см. Массив и указатель - инициализация, см. Инициала пня массива
554 Язык Си** - константа 108, 133, 220, 309 - константный, см. Указатель- константа - массив, см. Массив указателей - на компонент класса 59, 300 - на константу 115 - на массив 195 - на объект 108 класса 299 - на постоянную, см. Указатель на константу - на статический компонент класса 299 - на строку 79 - на структуру 249 - на указатель 128 - на функцию 108, 204, 210 - нормализованный, см. huge- указатель - нулевой, см. Нулевой указатель - описание, см. Описание указа- теля - определение, см. Определение указателя - переменная 143 - пустой, см. Нулевой указатель - родовой, см. Тип void* - сегментный, см. Сегментные указатели Умножение, см. Операция умно- жения Условная операция, см. Операция условная Условный оператор, см. Оператор if Уточнения области действия, см. Операция уточнения области действия Уточненное имя, см. Имя уточ- ненное ф Файл заголовочный, см. Заголо- вочный файл - текстовый 17 Файловый поток, см. Поток фай- ловый Фактические параметры 84 Флаг 394, 399 - ios::ate424 - ios:app424 - ios::beg 429 - ios::cur429 - ios::dec400 - ios::end429 - ios::fixed 401 - ios::hex 400 - ios::in427 - ios::intemal 400 - ios::left 400 - ios::oct 400 - ios::out 424 - ios::right 400 - ios::sdentific 400 - ios::showbase 400 - ios::showpoint 400 - ios::showpos 400 - ios::skipws 400 - ios::stdio 400 - ios::unitbuf 400 - ios::uppercase 400 - форматирования 402 Формальные параметры 169 Форматирование данных при вво- де-выводе 398 - флаг, см. Флаг форматирова- ния Функция 41 - abortO 460, 468, 476 - bioskeyO 254 - circle()344 - dose()439 - dosegraphO 309 - concatO 184 - creat()432 - exitO 190, 468 - floodfillO 344 - FP_OFF0. см. Макроопределе- ние FP-OFF0
Предметный указатель 555 - FP„SEG(), см. Макроопределе- ние FPJSEGO - fopen() 434 - free() 71 - getch() 309, 316, 398 , - getcolor() 344 - getimageO 344 - gotoxy()316 - imagesize() 344 - initgraph() 308 - int86() 256 - main() 7, 169 - malloc() 71, 138 - MK_FP(), см. Макроопределе- ние MK„FP() - open() 434, 527 - printf() 52, 184,398 - putch()3l6 - putimageO 344 - qsort() 211 - scanf()52, 184,398 - set_new_handler() 466 - set_termmate() 476 - set_unexpected() 477 - strcmp()214 - strcpyQ 142, 422 - strlen() 139 - terminateO 457,476 - unexpected) 460, 477 - va_arg(), см. Макроопределение va_arg() - va_end(), см. Макроопределение va_end() - va„start(), см. Макроопределе- ние va_start() - виртуальная, см. Виртуальная функция - и ссылки, см. Параметр-ссылка - имя, см. Имя функции - класса, см. Компонентная функция класса - компонент класса, см. Компо- нентная функция класса - обращение к, см. Вызов функ- ции - операция, см. Операция-функ- ция - описание, см. Описание функ- ции, Прототип - определение, см. Определение функции - перегруженная, см. Перегрузка функций - подменяемая 362, см. также Функция виртуальная - подставляемая, см. inline- фуикция - прототип, см. Описание функ- ции - рекурсивная, см. Рекурсивная функция - с переменным числом пара- метров 176, см. также Опреде- ление (описание) функции с переменным числом пара- метров - самовызывающая, см. Рекур- сивная функция - семейство, см. Шаблон се- мейства функций - сигнатура, см. Сигнатура функции - ссылка, см. Ссылка на функцию - указатель, см. Указатель на функцию ц Целая константа, см. Константа целая Целочисленный тип, см. Тип int (long, short, unsigned) Цикл 96 - бесконечный 98, 99, 100 - итерационный, см. Оператор for - с постусловием, см. Оператор do - с предусловием, см. Оператор while
556 Язык Си++ ч Чисто виртуальная функция 365 ш Шаблон классов 376 - семейства функций, см. Шаб- лон функций ---классов, см. Шаблон классов - список параметров, см. Список параметров шаблона - функций 320, 375 — определение, см. Определе- ние шаблона функций Шестнадцатеричная константа, см. Константа шестнадцатерич- ная э Экземпляр класса, см. Объект класса Эскейп-последовательность, см. ESC-последовательность Эффекты побочные, см. Побочные эффекты я Явное преобразование типа, см. Операция явного преобразования типов
ОГЛАВЛЕНИЕ Предисловие к 5-му изданию.......................... 3 Предисловие к 1-му изданию.......................... 4 Глава 1. НЕФОРМАЛЬНОЕ ВВЕДЕНИЕ В СИ++............... 7 1.1. Простые программы на языке Си++............ 7 Глава 2. ЛЕКСИЧЕСКИЕ ОСНОВЫ ЯЗЫКА СИ++............. 17 2.1. Общие сведения о программах, лексемах и алфавите.... 17 2.2. Идентификаторы и служебные слова.......... 19 2.3. Константы................................. 20 2.4, Знаки операций............................ 31 2.5. Разделители............................... 47 Глава 3. СКАЛЯРНЫЕ ТИПЫ И ВЫРАЖЕНИЯ................ 54 3.1. Основные и производные типы............. 54 3.2. Объекты и их атрибуты..................... 59 3.3. Определения и описания.................... 74 3.4. Выражения и преобразования типов ......... 80 Глава 4. ОПЕРАТОРЫ ЯЗЫКА СИ++...................... 90 4.1. Последовательно выполняемые операторы..... 90 4.2. Операторы выбора.......................... 92 4.3. Операторы цикла........................... 96 4.4. Операторы передачи управления............ 101 Глава 5. АДРЕСА, УКАЗАТЕЛИ, МАССИВЫ, ПАМЯТЬ....... 108 5.1. Указатели и адреса объектов.............. 108 5.2. Адресная арифметика, типы указателей и операции над ними....................................... 116 5.3. Массивы и указатели....................... 130 5.4. Многомерные массивы, массивы указателей, динамические массивы.......................... 144 5.5. Организация памяти в процессорах 80x86 и указатели языка Си++.................................... 157
558 Язык Си++ Глава 6. ФУНКЦИИ, УКАЗАТЕЛИ, ССЫЛКИ......................169 6.1. Определения, описания и вызовы функций..........169 6.2. Функции с переменным количеством параметров.....176 6.3. Рекурсивные функции........................... 186 6.4. Подставляемые (инлайн-) функции.................190 6.5. Функции н массивы............................. 193 6.6. Указатели на функции............................204 6.7. Ссылки........................................ 215 6.8. Перегрузка функций........................ 227 6.9. Шаблоны функций............................... 230 Глава 7. СТРУКТУРЫ И ОБЪЕДИНЕНИЯ.........................237 7.1. Структура как тип и совокупность данных ........237 7.2. Объединения разнотипных данных................ 250 7.3. Битовые поля структур и объединений.............258 Глава 8. ПРЕПРОЦЕССОРНЫЕ СРЕДСТВА........................263 8.1. Стадии и команды препроцессорной обработки......263 8.2. Замены в тексте............................... 265 8.3. Включение текстов из файлов.....................268 8.4. Условная компиляция.............................271 8.5. Макроподстановки средствами препроцессора.......274 8.6. Встроенные (заранее определенные) макроимена....279 Глава 9. КЛАСС КАК АБСТРАКТНЫЙ ТИП.......................281 9.1. Класс как расширение понятия структуры..........281 9.2. Конструкторы, деструкторы и доступность компонентов класса..............................287 9.3. Компонентные данные и компонентные функции......295 9.4. Определение компонентных функций................303 9.5. Указатель this................................ 309 9.6. Друзья классов.............................. 315 9.7. Расширение действия (перегрузка) стандартных операций 322 Глm 10. НАСЛЕДОВАНИЕ И ДРУГИЕ ВОЗМОЖНОСТИ КЛАССОВ..............................................336 10.1. Наследование классов...........................336 10.2. Множественное наследование и виртуальные базовые классы............................. 349 10.3. Виртуальные функции и абстрактные классы.......359 10.4. Локальные классы........................... 373 10 5 Классы и шаблоны............................. 375
Оглавление 559 Глава 11. ВВОД-ВЫВОД В ЯЗЫКЕ СИ++.........................379 11.1. Общие сведения о библиотеке потокового ввода-вывода 379 11.2. Стандартные потоки для базовых типов........... 385 11.3. Форматирование данных при обменах с потоками.....398 11.4. Ввод-вывод для типов, определенных пользователем.409 11.5. Функции для обмена с потоками.................. 415 11.6. Строковые потоки (обмены в основной памяти)......420 11.7. Работа с файлами.................................431 Глава 12. ОБРАБОТКА ОСОБЫХ (ИСКЛЮЧИТЕЛЬНЫХ) СИТУАЦИЙ...........................................445 12.1. Общие принципы механизма обработки исключений....445 12.2. Синтаксис и семантика генерации и обработки исключений. 453 12.3. Обработка исключений при динамическом выделении памяти..........................................453 12.4. Функции, глобальные переменные и классы поддержки механизма исключений............................475 12.5. Конструкторы и деструкторы в исключениях.........479 12.6. Динамическая идентификация типов (RTTI)..........481 12.7. Опции компилятора, относящиеся к обработке исклю- чений и динамическому определению типов (RTTI)...486 Приложение 1. Таблицы кодов ASCII.........................488 Приложение 2. Константы предельных значений...............494 Приложение 3. Стандартная библиотека функций языка Си.....497 Приложение 4. Графическая библиотека ВС++.................509 Приложение 5. Классы и компонентные функции библиотеки потокового ввода-вывода в Си++.....................519 Приложение 6. Библиотечные классы и их компонентные функции . 528 ЛИТЕРАТУРА................................................538 УКАЗАТЕЛЬ СИМВОЛОВ....................................... 540 ПРЕДМЕТНЫЙ УКАЗАТЕЛЬ..................................... 542
Учебное издание Подбельский Вадим Валериевич ЯЗЫК СИ++ Редактор Л.Д. Григорьева Художественный редактор Ю.И.Артюхов Технический редактор И.В. Завгородняя Корректоры Н.П. Сперанская, Т.М. Васильева Компьютерная верстка О.В. Фортунатовой Обложка художника Ф.Г. Миллера ИБ№ 3501 Подписано в печать 05.11.2002. Формат 60x88 7[6 Гарнитура «Таймс». Печать офсетная Усл. п.л. 34,3. Уч. изд. л. 32,24. Тираж 5000 экз. Заказ № 3821. «С» 028 Издательство «Финансы и статистика» 101000, Москва, ул. Покровка, 7 Телефон (095) 925-35-02. Факс (095) 925-09-57 E-mail: mail@finstat.ru http://www.finstat.ru ГУП «Великолукская городская типография» Комитета по средствам массовой информации Псковской области, 182100, Великие Луки, ул. Полиграфистов, 78/12 Тел./факс: (811-53) 3-62-95 E-mail: VTL@MART.RU
ПОДБЕЛЬСКИЙ Вадим Валериевич Ведущий российский специалист в области математического и программного обеспечения систем автоматизации проектирования и компьютерных сетей, доктор технических наук, профессор факультета прикладной математики (ФПМ) Московского государственного института (технического университета) электроники и математики (МГИЭМ); директор центра информационных технологий и сетевых коммуникаций Республиканского исследовательского научно- консультационного центра экспертизы (РИНКЦЭ) Министерства науки итехнологий Российской Федерации, автор более 90 научных и учебно- методических трудов. Окончил Московский инженерно-физический институт по специальности «Вычислительные машины». Направления педагогической деятельности: алгоритмические языки; объектно- ориентированное программирование; автоматизация проектирования; программирование для Internet; Web-программирование. ISBN 5-279-02204-7 lllllllllllll 9 785279,,022045 L