Текст
                    
 004.42(075.4)  32.973.26  45 Arnold Willemer Einstieg in C++ © 2012 by Galileo Press Galileo Computing is an imprint of Galileo Press, Bonn (Germany), Boston (USA) German Edition first published 2012 by Galileo Press. Titel of the German Edition “Einstieg in C++” ISBN of the German Edition 978-3-8362-1385-1 All rights reserved. Neither this publication nor any part of it may be copied or reproduced in any form or by any means or translated into another language, without the prior consent of Galileo Press, Rheinwerkallee 4, 53227 Bonn, Germany. Galileo Press makes no warranties or representations with respect to the content hereof and specifically disclaims any implied warranties of merchantability or fitness for any partiand specifically disclaims any implied warranties of merchantability or fitness for any particular purpose. Galileo Press assumes no responsibility for any errors that may appear in this publication.  45  .        ++ / .    ; [.   . . .  ]. — . :  , 2013. — 528 . + CD. — (      ). ISBN 978-5-699-65451-2 , ! "# $ , !   "! %       ++    &           %  ' %$,   *  '   .   "*%  <          %$ ,   $  ,     *   , =       ,   & $ , ! % <  .    *      %     $   %  *      <    (STL).   " *  > $   %$   *" > %$       %. 004.42(075.4) 32.973.26   $  %   >  *"  %%% $          "? > &  . @   % !    %? $*  %     > F%>  #    $*         &         * , "*  '   >  ! , ! % &        $        ,   '      $< % HHH «J$*  « ».  ISBN 978-5-699-65451-2 ©  . .,    , 2013 © .  «! «"», 2013
ОГЛАВЛЕНИЕ Предисловие .................................................................................................................................................... 12 Глава 1. Введение в программирование .............................................................................................. 14 1.1. Программирование...................................................................................................................... 14 Старт программы ............................................................................................................... 14 1.1.1. Ввод, компиляция, запуск ................................................................................... 16 1.1.2. Алгоритм .................................................................................................................... 17 1.1.4. Язык C++ ................................................................................................................... 18 1.1.5. Контрольные вопросы .......................................................................................... 23 1.2. Основа программы ...................................................................................................................... 23 1.2.1. Комментарии ............................................................................................................ 24 1.2.2. Команды ..................................................................................................................... 27 1.2.3. Блоки ........................................................................................................................... 27 1.3. Переменные .................................................................................................................................... 28 1.3.1. Объявление переменных ..................................................................................... 29 1.3.2. Область действия ................................................................................................... 30 1.3.3. Правила именования и синтаксис .................................................................... 32 1.3.4. Типы переменных ................................................................................................... 34 1.3.5. Синтаксис объявления переменных................................................................ 46 1.3.6. Константы ................................................................................................................. 47 1.4. Обработка ........................................................................................................................................ 56 1.4.1. Присваивание........................................................................................................... 57 1.4.2. Мастер вычислений ............................................................................................... 58 1.4.3. Сокращения .............................................................................................................. 59 1.4.4. Понятие «функция» на примере функции случайных чисел ................ 62 1.4.5. Преобразование типа ............................................................................................ 64 1.5. Ввод и вывод .................................................................................................................................. 65 1.5.1. Потоковый вывод командой cout .................................................................... 66 1.5.2. Форматированный вывод.................................................................................... 68 1.5.3. Потоковый ввод командой cin.......................................................................... 68 1.6. Практические задания ............................................................................................................... 69 Глава 2. Циклическое программное управление ............................................................................. 70 2.1. Ветвление......................................................................................................................................... 71 2.1.1. По условию: if ........................................................................................................ 71 2.1.2. Иначе: else ............................................................................................................... 73 2.1.3. Вариант за вариантом: switch case ............................................................... 77 2.1.4. Короткая проверка с помощью символа вопросительного знака............... 82 2.2. Булевы выражения...................................................................................................................... 83 2.2.1. Переменные и константы .................................................................................... 84 2.2.2. Операторы ................................................................................................................. 84 2.2.3. Объединение булевых выражений .................................................................. 87 5
Оглавление 2.3. Постоянные повторения: циклы ........................................................................................... 94 2.3.1. Цикл с предусловием: while .............................................................................. 94 2.3.2. Цикл с постусловием: do-while ....................................................................... 98 2.3.3. Шаг за шагом: for................................................................................................... 99 2.3.4. Выходы из цикла: break и continue.............................................................102 2.3.5. Грубый скачок: goto ............................................................................................105 2.4. Примеры.........................................................................................................................................106 2.4.1. Простые числа .......................................................................................................106 2.4.2. Наибольший общий делитель..........................................................................112 2.5. Упражнения ..................................................................................................................................115 Глава 3. Типы данных и структур.........................................................................................................116 3.1. Массив.............................................................................................................................................116 3.1.1. Сортировка методом «пузырька» ...................................................................121 3.1.2. Присваивание в массивах ..................................................................................125 3.1.3. Символьные последовательности языка C .................................................127 3.1.4. Пример: определение числового значения введенного символа ........128 3.1.5. Многомерный массив .........................................................................................131 3.1.6. Пример: игра «Бермуда» ....................................................................................132 3.2. Указатель и адрес........................................................................................................................135 3.2.1. Косвенный доступ ................................................................................................139 3.2.2. Массивы и указатели ..........................................................................................140 3.2.3. Арифметика для указателей .............................................................................142 3.2.4. Константный указатель ......................................................................................144 3.2.5. Анонимный указатель.........................................................................................145 3.3. Объединение переменных: struct.....................................................................................146 3.3.1. Пример: игра «Бермуда» ....................................................................................150 3.4. Динамические структуры.......................................................................................................152 3.4.1. Выделение и освобождение памяти...............................................................152 3.4.2. Создание массивов во время выполнения программы ...........................154 3.4.3. Связанные списки ................................................................................................154 3.5. Объединение ................................................................................................................................157 3.6. Перечисляющий тип enum......................................................................................................158 3.7. Определение типов....................................................................................................................160 Глава 4. Функции ........................................................................................................................................161 4.1. Параметры .....................................................................................................................................166 4.1.1. Прототипы...............................................................................................................169 4.1.2. Указатель в качестве параметра ......................................................................170 4.1.3. Массив в качестве параметра ...........................................................................172 4.1.4. Ссылочный параметр ..........................................................................................174 4.1.5. Пример: стек ...........................................................................................................176 4.1.6. Предопределенные параметры ........................................................................178 4.1.7. Параметры функции main .................................................................................179 4.1.8. Переменное количество параметров .............................................................181 4.2. Перегрузка функций.................................................................................................................183 4.3. Коротко и быстро: встроенные функции.........................................................................183 4.4. Нисходящий метод....................................................................................................................185 4.4.1. Пример: игра «Бермуда» ....................................................................................186 6
Оглавление 4.5. Область действия переменных ............................................................................................191 4.5.1. Глобальные переменные ....................................................................................191 4.5.2. Локальные переменные ......................................................................................192 4.5.3. Статические переменные ...................................................................................193 4.6. Рекурсивные функции.............................................................................................................195 4.6.1. Область действия .................................................................................................197 4.6.2. Пример: бинарное дерево ..................................................................................198 4.6.3. Игра «Ханойская башня» ..................................................................................202 4.6.4. Пример: калькулятор ..........................................................................................204 4.7. Указатель функции....................................................................................................................211 Глава 5. Классы.............................................................................................................................................213 5.1. Классы и структуры данных .................................................................................................214 5.1.1. Союз функции и структуры данных..............................................................215 5.1.2. Доступ к элементам класса ...............................................................................219 5.2. Создание и удаление объекта ...............................................................................................220 5.2.1. Конструктор и деструктор ................................................................................220 5.2.2. Конструктор и параметры .................................................................................222 5.3. Доступная и скрытая области...............................................................................................225 5.3.1. private и public ..................................................................................................225 5.3.2. Пример: стек ...........................................................................................................229 5.3.3. Друзья .......................................................................................................................232 5.4. Конструктор копирования .....................................................................................................233 5.5. Перегрузка элементных функций ......................................................................................237 5.6. Выбор: перегрузка операторов .............................................................................................238 5.6.1. Сложение .................................................................................................................240 5.6.2. Глобальные операторные функции................................................................242 5.6.3. Инкремент и декремент .....................................................................................243 5.6.4. Оператор присваивания .....................................................................................245 5.6.5. Оператор сравнения ............................................................................................249 5.6.6. Оператор вывода...................................................................................................251 5.6.7 Оператор индекса ..................................................................................................252 5.6.8. Оператор вызова () .............................................................................................254 5.6.9. Оператор конвертирования ..............................................................................255 5.7. Атрибуты........................................................................................................................................256 5.7.1. Статические переменные и функции в классах ........................................256 5.7.2. Константы ...............................................................................................................258 5.8. Наследование ...............................................................................................................................261 5.8.1. Доступ к предкам ..................................................................................................265 5.8.2. Конструкторы и присваивание........................................................................269 5.8.3. Многократное наследование ............................................................................271 5.8.4. Полиморфизм посредством виртуальных функций ...............................272 5.9. Определение классов и синтаксический граф ..............................................................283 Глава 6. Инструменты программирования ......................................................................................285 6.1. Компилятор C++........................................................................................................................285 6.1.1. Вызов компилятора .............................................................................................285 6.1.2. Свойства компилятора .......................................................................................286 6.1.3. Сообщения об ошибках ......................................................................................287 7
Оглавление 6.2. Препроцессор...............................................................................................................................289 6.2.1. Связывание файлов: #include ........................................................................290 6.2.2. Константы и макросы: #define .........................................................................290 6.2.3. Опросы: #if ............................................................................................................293 6.2.4. Предопределенные макросы ............................................................................294 6.2.5. Другие препроцессорные команды ................................................................295 6.3. Разделение исходного кода....................................................................................................296 6.3.1. Пример: игра «Бермуда» ....................................................................................296 6.3.2. Распознавание файлов........................................................................................299 6.3.3. Объявление и определение ...............................................................................300 6.3.4. Заголовочные файлы ..........................................................................................301 6.3.5. Статические функции .........................................................................................303 6.3.6. Скрытая реализация ............................................................................................303 6.4. Компоновщик и библиотеки.................................................................................................305 6.4.1. Подключение статических библиотек ..........................................................305 6.4.2. Динамические библиотеки ...............................................................................306 6.5. Программа make ..........................................................................................................................309 6.5.1. Макросы в make-файле ......................................................................................312 6.5.2. Несколько строк....................................................................................................314 6.6. Отладка с помощью GNU Debugger ..................................................................................314 Глава 7. Другие элементы языка C++ ................................................................................................317 7.1. Обобщенное программирование .........................................................................................317 7.1.1. Шаблонные функции ..........................................................................................318 7.1.2. Шаблоны классов .................................................................................................321 7.1.3. Макропрограммирование с помощью команды #define ........................325 7.2. Пространство имен....................................................................................................................327 7.2.1. Определение пространства имен ....................................................................328 7.2.2. Доступ .......................................................................................................................329 7.2.3. Особые пространства имен ...............................................................................329 7.2.4. Анонимное пространство имен .......................................................................330 7.2.5. Граф синтаксиса ....................................................................................................331 7.3. Защита от сбоев при помощи ключевых слов try и catch .....................................332 7.3.1. Создание собственных исключительных ситуаций ................................333 7.3.2. Разработка классов ошибок ..............................................................................337 7.3.3. Исключения стандартных библиотек ...........................................................341 7.4. Низкоуровневое программирование .................................................................................347 7.4.1. Битовые операторы..............................................................................................347 7.4.2. Операторы сдвига .................................................................................................350 7.4.3. Доступ по аппаратным адресам.......................................................................351 7.4.4. Битовые структуры ..............................................................................................352 Глава 8. Библиотеки ...................................................................................................................................354 8.1. Символьные последовательности и строки ...................................................................354 8.1.1. Стандартный класс string ...............................................................................355 8.1.2. Другие библиотеки строк ..................................................................................368 8.1.3. Классические функции языка C .....................................................................369 8.2. Класс iostream для продвинутых......................................................................................376 8.2.1. Ввод командой cin ...............................................................................................376 8.2.2. Манипуляторы ......................................................................................................378 8
Оглавление 8.3. Операции с файлами ................................................................................................................381 8.3.1. Открытие и закрытие ..........................................................................................382 8.3.2. Чтение и запись .....................................................................................................384 8.3.3. Наблюдение состояния ......................................................................................390 8.3.4. Доступ к файлам в стандарте ANSI C ...........................................................392 8.3.5. Команды файловой системы ............................................................................395 8.3.6. Получение свойств файла .................................................................................398 8.4. Математические функции .....................................................................................................401 8.4.1. Стандартная математическая библиотека ..................................................401 8.4.2. Комплексные числа .............................................................................................404 8.5. Функции времени ......................................................................................................................405 8.5.1. Дата и время ...........................................................................................................405 8.5.2. Остановка времени ..............................................................................................407 Глава 9. Стандартная библиотека шаблонов (STL) .....................................................................410 9.1. Контейнер класса vector .......................................................................................................410 9.1.1. Изменение размера ..............................................................................................412 9.1.2. Итераторы ...............................................................................................................414 9.1.3. Другие функции ....................................................................................................415 9.2. Контейнер класса deque..........................................................................................................417 9.3. Контейнер класса list ............................................................................................................420 9.3.1. Добавление и удаление элементов .................................................................421 9.3.2. Перемещение элементов: splice....................................................................423 9.3.3. Добавление отсортированного списка..........................................................424 9.3.4. Сортировка и последовательность .................................................................425 9.4. Контейнер классов set и multiset ...................................................................................427 9.4.1. Добавление и удаление.......................................................................................427 9.4.2. Поиск и сортировка .............................................................................................428 9.5. Контейнер классов map и multimap ...................................................................................430 9.6. Контейнер-адаптер ....................................................................................................................432 9.6.1. Контейнер-адаптер stack..................................................................................433 9.6.2. Контейнер-адаптер queue..................................................................................434 9.6.3. Контейнер-адаптер priority_queue ............................................................435 9.7. Типы итераторов.........................................................................................................................436 9.8. Алгоритмы библиотеки STL .................................................................................................437 9.8.1. Поиск: find()...........................................................................................................437 9.8.2. Сортировка .............................................................................................................438 9.8.3. Двоичный поиск....................................................................................................440 9.8.4. Копирование: copy() ..........................................................................................440 9.8.5. Перестановка: reverse() ..................................................................................441 9.8.6. Заполнение: fill()................................................................................................441 9.8.7. Функция equal().................................................................................................442 9.8.8. Функция в качестве параметра: find_if() ..................................................442 9.8.9. Функция for_each ..............................................................................................446 9.9. Класс шаблона bitset .............................................................................................................447 Приложение А. Язык C++ для тех, кто торопится.......................................................................448 А.1. Программа ....................................................................................................................................448 А.2. Условия и циклы........................................................................................................................452 9
Оглавление А.2.1. Условия и булевы выражения .........................................................................453 А.2.2. Цикл while.............................................................................................................455 А.2.3. Цикл for .................................................................................................................456 А.3. Массивы ........................................................................................................................................457 А.4. Функции .......................................................................................................................................462 А.4.1. Разделение программ .........................................................................................462 А.4.2. Возвращаемое значение ....................................................................................465 А.4.3. Параметры ..............................................................................................................466 А.5. Классы ............................................................................................................................................470 А.5.1. Конструктор...........................................................................................................474 А.5.2. Наследование ........................................................................................................479 А.5.3. Полиморфизм .......................................................................................................481 А.6. Шаблоны.......................................................................................................................................483 Приложение Б. Устройство компилятора ........................................................................................486 Б.1. KDevelop .......................................................................................................................................486 Б.1.1. Новый проект ........................................................................................................486 Б.1.2. Компиляция и старт ...........................................................................................488 Б.2. Bloodshed Dev-C++ ..................................................................................................................489 Б.2.1. Установка ................................................................................................................489 Б.2.2. Создание проекта .................................................................................................489 Б.2.3. Компиляция и старт ...........................................................................................492 Б.3. Cygwin ............................................................................................................................................493 Приложение В. Примеры решений задач ........................................................................................495 Контрольные вопросы .....................................................................................................................495 Функция ggt() ...................................................................................................................................495 Программа НДС .................................................................................................................................496 Программа НДС с проверкой ввода ..........................................................................................497 Сложный процент..............................................................................................................................498 Угадывание чисел...............................................................................................................................498 Отсортированные числа лото .......................................................................................................499 Оптимизированная сортировка методом «пузырька» ......................................................501 Ввод дроби ............................................................................................................................................502 Функция swap .....................................................................................................................................503 Локальный стек...................................................................................................................................504 Игра «Бермуда»: поиск кораблей ...............................................................................................506 Поиск в бинарном дереве ...............................................................................................................507 Калькулятор для float......................................................................................................................508 Вопрос к знаку минус.......................................................................................................................510 Буфер: FIFO .........................................................................................................................................511 Пример стека в качестве списка ..................................................................................................513 Функция atof() с десятичной запятой...................................................................................515 Приложение Г. Глоссарий........................................................................................................................517 Приложение Д. Литература....................................................................................................................524 Предметный указатель .............................................................................................................................526
Дорогие читатели, я могу лишь настоятельно порекомендовать эту книгу, если вы собираетесь изучать язык программирования C++. Она пригодится и как техническое пособие, дополнительно к учебному курсу, и как материал для самостоятельного изучения языка программирования. Эта книга получила множество положительных отзывов от ее читателей. Так как же можно объяснить такой успех? Авторы книг для начинающих отказываются от всего, что выглядит сложным. Но основы программирования осваиваются очень быстро, и читателю хочется узнать больше — однако дальнейшей информации книги не содержат. Этим и отличается книга Арнольда Виллемера от других вводных пособий. Шаг за шагом автор сопровождает вас на пути к профессиональному программированию на языке C++: основательно, компетентно и, конечно, всегда с примерами. При этом Арнольд Виллемер всегда работает с наглядной структурой программ и алгоритмами, и находит примеры, которые сразу понятны. Тут булева алгебра будет сравниваться с поеданием мороженого и катанием на роликах, а основы программирования рассмотрены на примере игры «Бермуда». Чтобы гарантировать качество книг, мы всегда ставим высокие требования к авторам и лекторам. Если у вас появились замечания или предложения, мы будем рады их услышать. Читайте с удовольствием! Штефан Маттешек, Галилео Компьютинг
ПРЕДИСЛОВИЕ Да, я знаю, уже существует огромное количество книг по C++. Вообще такое руководство можно написать из множества побуждений. Например, есть книга «Язык программирования C++» Бьерна Страуструпа1, создателя языка C++. Это очень точное, очень обширное пособие. Оно представлено в масштабе всех компиляторов-разработчиков. Но, к сожалению, не предусмотрено для начинающих. Я же писал эту книгу так, чтобы она была легко понятна. Если ктото хочет выучить язык C++, он должен следовать наглядным примерам программ, написанных на C++. А когда начинающий уже получил базовые знания, он сможет найти здесь ответы на вопросы, которые возникают при работе с C++. Я не люблю книги, которые заканчиваются на полуслове. Предполагаю, вы знаете, что такое компьютер, умеете как-то с ним обращаться, вам интересно программирование и вы, разумеется, умеете читать. Не важно, обладаете ли вы какими-либо знаниями в области программирования или нет. Для более легкого понимания структуры языка я использовал графы, хотя они и вышли из моды в последнее время. В современной литературе их вряд ли встретишь. Тем не менее я оставил графы из-за наглядности, очень полезной для начинающих. Также я использовал диаграмму Насси-Шнейдермана, поскольку полагаю, что она хорошо передает четко структурированный процесс, описанный в главе 2. В приложении вы найдете краткий экскурс в C++. Он предназначен для торопыг, желающих быстрее получить результаты. Он может также послужить в качестве примера базовых сценариев для курса программирования, в котором не может учитываться каждая деталь. Тут некоторые 1 12 Бьерн Страуструп. Язык программирования С++. М.: Бином, 2011. Прим. ред.
Предисловие темы освещены очень кратко, однако везде указаны ссылки на соответствующие разделы книги, где тема рассматривается подробно. В подписях многих листингов в скобках указаны имена файлов. Код той или иной программы вы найдете в файле с соответствующим именем на диске, прилагающемся к книге. Для улучшения читаемости текста применено следующее форматирование: переменные и классы выделены полужирным шрифтом Courier, команды, функции (например, open()) и листинги программ выделены шрифтом Courier, функции описываются со скобками после имени, определения, имена файлов и пути выделены курсивом. Поскольку я работаю программистом C++, в этой книге везде встречаются практические примеры. Такой подход очень полезен еще и тем, что читатели зачастую указывают на непонятные моменты или даже находят ошибки. Благодарности Помимо моих читателей, я также хочу поблагодарить Штефана Маттешека. Он разрешил мне все последующие вольности. Фредерике Денеке исправил мои ужасные преступления против немецкого языка. Штефи Ерентраут сделала так, чтобы книга выглядела красиво. Кристоф Кехер позаботился о втором издании книги. Остается только упомянуть, что впервые я столкнулся с языком C++ благодаря профессору Марио Дал Син. Он помог мне набраться смелости для этой книги. Я также благодарю Штефана Энгельманна за его советы. И еще раз огромное спасибо всем вам, читатели! Я надеюсь, что эта книга будет хорошим сопровождающим пособием в первых шагах на пути изучения C++ и в дальнейшем станет удобным справочным изданием. Арнольд Виллемер, Норгардхольц
Глава 1 ВВЕДЕНИЕ В ПРОГРАММИРОВАНИЕ Программирование — вытачивание монумента без пыли и осколков. Итак, вы хотите изучать программирование. Вероятно, у вас уже даже есть четкое представление о том, что означает этот термин. Ведь, например, обработка текста также выполняется с помощью программы. Вы печатаете текст, а программа форматирует его так, как вам хочется. По окончании работы можно сохранить этот текст в файл на жестком диске компьютера или распечатать его. Все это описывает работу типичной программы. 1.1. Программирование Программа принимает вводимые пользователем данные, обрабатывает их заранее определенным способом, который закладывается программистом, и выдает результат. Действия пользователя всегда ограничены рамками, заданными программистом. Когда вы сами начинаете программировать, вы берете на себя контроль над компьютером. До этого можно было делать только то, что позволяло программное обеспечение, теперь можно самостоятельно решать, что должен выполнять компьютер. Старт программы Программа — это, прежде всего, файл, содержащий команды, которым должен следовать компьютер. Этот файл обычно находится на жестком диске или отдельном носителе. При запуске программы сначала происходит ее загрузка в оперативную память, только там она может начать выполнение. Запущенная программа называется процессом. Она будет обработана центральным процессором1. Процессор получает адрес памяти, где расположена команда, которая будет выполняться следу1 14 CPU: центральный процессорный модуль (Central Processing Unit).
Введение в программирование ющей. При старте процесса это будет адрес первой команды программы. Каждая команда указывает процессору прочитать, записать или вычислить данные. Процессор может сравнивать данные, хранящиеся в ячейках памяти, и перемещать их в зависимости от результата обработки. Команды, интерпретируемые процессором, являются системными командами. Оперативная память содержит в себе как данные, так и программы. Это память, которая стирается, если ее отключить от источника питания. Она состоит из запоминающих ячеек определенной емкости, которые хранят только целые числа. Соответственно, команды процессора состоят только из таких чисел. Пока процессоры используют подобные числовые последовательности, программировать — занятие не для каждого. В конце концов, это слишком абстрактно и, следовательно, чревато ошибками. Такие программы работают только на том процессоре, под который они были написаны, при этом разные процессоры имеют разную систему команд. Если необходимо использовать в программном обеспечении машинные команды, то на помощь приходит язык ассемблера. Этот язык с точностью один к одному компилирует команды программы в команды процессора, и более понятен для человека. Например, команда перехода для процессора 6502 — это число 76. Соответствующая команда ассемблера — JMP1. Компилятор этого языка, который тоже называется ассемблер, вырабатывает из понятных для человека команд машинные коды. На сегодняшний день языки машинных команд и ассемблера применяются все реже, так как разработка программного проекта на этих языках занимает очень много времени и программы работают только на тех компьютерах, для которых они написаны. Преимущество программы, написанной на языке ассемблера в том, что она выполняется максимально быстро. Но поскольку компьютеры постоянно совершенствуются и становятся все производительнее, имеет смысл сэкономить на высокой стоимости разработки такой программы. Сейчас язык ассемблера применяется только для написания программ управления аппаратным обеспечением, для которого невозможно применить высокоуровневые языки программирования, ведь в них отсутствует, например, необходимая в таком случае команда обработки прерывания. 1 JMP от англ. слова «jump» в переводе на русский язык «прыгать». 15
Глава 1 1.1.1. Ввод, компиляция, запуск Когда программист пишет программу, он создает текстовый файл, в котором находятся команды, содержащие описание выполнения определенных операций на соответствующем языке программирования. Современные языки состоят в первую очередь из английских слов и различных символов. Текст (код) программы вводится с помощью редактора. Редактор — это некоторый обработчик кода, который создан не для обычного форматирования, а для удобства написания программ. Исходный код программы для правильной компиляции должен содержать только чистый код, любое форматирование в таком случае может помешать. Поэтому, к примеру, нельзя использовать в качестве исходного файла программы Word-документ. Если вы все-таки непременно хотите писать программы в Word, нужно следить за тем, чтобы код был сохранен в чистом виде. Однако для программирования существуют специальные редакторы. Один такой всегда содержится в среде разработки. Минимальное требование к нему — отображение номеров строк. При обнаружении компилятором ошибки, в окне ошибок будет указан номер соответствующей строки. Компьютер не понимает исходный код программы в том виде, в котором она написана человеком. Как уже было сказано, он понимает только машинные команды. По этой причине, код программы необходимо скомпилировать — перевести на машинный язык. Для этого используют специальный переводчик, называемый компилятором. Он переводит команды исходной программы в машинные команды процессора. Из каждого текстового файла программы он создает так называемый файл объекта. В случае языков C и C++ компилятор использует еще один предварительный этап перед непосредственной компиляцией — обработку препроцессором. Препроцессор обрабатывает данные исходного кода программы перед тем, как компилятор переведет их в машинный код. Препроцессор может производить текстовые замены, связывать данные и по определенным условиям, обозначенным специальными символами в коде программы, исключать из компиляции некоторые фрагменты кода. Он распознает команды, именуемые препроцессорными, перед которыми указан символ #. На практике программный проект обычно состоит из нескольких текстовых файлов программ. Все они переводятся компилятором в файлы объектов. В итоге они будут соединены между собой с помо- 16
Введение в программирование щью компоновщика (или линкера). Отсюда следует, что программа состоит не только из исходного кода, который написан программистом, но и из стандартных функций, таких как вывод на экран, которые она снова и снова использует, однако их не требуется заново писать в проекте. Эти функции уже заложены в пакете компилятора как предварительно скомпилированные файлы и включены в библиотеки. Такая библиотека называется library. Линкер связывает файл программы с файлами библиотек. В результате получается программа для операционной системы. Рабочий день программиста обычно состоит из написания программы, запуска компилятора, который переводит код программы в код процессора, запуска самой программы и проверки ее на соответствие требованиям заказчика. Затем программист снова возвращается к исходному коду программы, чтобы исправить найденные при тестировании ошибки. Рис. 1.1. Путь разработки программы Некоторые программисты склоняются к тому, что нужно хорошо все обдумать перед тем, как писать программу. Разработчики создают блок-схемы того, что они хотят запрограммировать, полагая, что таким образом можно быстрее получить правильный результат. И в целом они правы. 1.1.2. Алгоритм После описания пути, как из исходного кода получить программу, следует ответить на вопрос, как нужно составлять программу. 17
Глава 1 Язык программирования используется для очень краткого описания последовательности действий. У компьютера нет фантазии и опыта. Поэтому он может корректно работать только тогда, когда конкретно написано, что он должен делать. Все условия должны быть четко сформулированы и соответствовать ходу выполнения программы. Такой метод описания называется алгоритмом. Алгоритм можно сравнить с рецептом приготовления какого-нибудь блюда. Аналогия также хорошо подходит, поскольку речь идет о книге рецептов по информатике, ведь читатель, возможно, не знает, что для того, чтобы что-то сварить, необходимо налить в кастрюлю воду, а для жарки нужно использовать подсолнечное масло. Тот, кто в первый раз создает эскиз алгоритма, понимает, как тяжело описать метод работы так, чтобы при его выполнении получить требуемый результат. Прервем немного изучение и выполним маленькое упражнение: рассмотрим то, как мы считаем. Для лучшего самоконтроля напишите на листе бумаги числа от 1 до 10. Готово? Прекрасно! Итак, для выполнения этого задания надо сделать следующее: 1. Написать число 1 на листе бумаги. 2. Прочитать последнее написанное число. 3. Если это число 10, остановиться. 4. Прибавить к этому числу 1. 5. Написать получившееся число под последним числом. 6. Вернуться к шагу 2. Можно практиковаться, описывая таким образом повседневные дела. Запишите, например, что вы делаете, когда одеваетесь или едете на машине на работу. Представьте себе, что некто следует вашим указаниям настолько буквально, что если вы упустите даже мельчайшую деталь — это может привести к краху программы. Проверьте такие алгоритмы с родственниками или друзьями. 1.1.4. Язык C++ Очевидно, вы выбрали для изучения язык C++, иначе не читали бы эту книгу. Независимо от того, изучаете вы C++ по собственной инициа- 18
Введение в программирование тиве или по требованию работодателя, вы более или менее познакомитесь этим языком. Для этого я позволю себе пару замечаний. Компилируемый язык C++ — компилируемый язык. Это означает, что код программы перед запуском будет переведен в команды процессора, как это описано в разделе 1.1. Некоторые другие языки, например Бейсик или сценарные языки, интерпретируются в процессе выполнения. Это означает, что код программы загружается и выполняется шаг за шагом. Если одинаковые строки повторяются несколько раз, они будут скомпилированы также несколько раз. Поскольку компиляция происходит в процессе выполнения, то очевидно, что программы написанные на таких языках, значительно медленнее. Кроме того, существуют смешанные формы. Известный представитель такого языка — Java. Здесь код программы хоть и компилируется перед началом выполнения, результат компиляции не является машинными командами процессора, а представляет собой что-то среднее. Таким образом, выполнение программы происходит гораздо быстрее, чем при построчной интерпретации исходного кода. Каждый из этих способов имеет свои плюсы и минусы. Преимущество языка-компилятора состоит прежде всего в скорости работы программы. Код компилируется один раз, и компиляция больше не влияет на время работы программы. Другое преимущество такого языка в том, что многие ошибки в коде выявляются во время компиляции. Программы-интерпретаторы хороши тем, что ошибка в коде не прерывает сразу же работу программы. Интерпретатор может остановиться, сообщить программисту о серьезной ситуации и запросить решение, если ситуацию еще можно исправить. Начинающие программисты очень ценят такой способ работы, поскольку могут проследить выполнение программы шаг за шагом и увидеть, что выполняет компьютер. Языки-компиляторы тоже это позволяют, используя специальный отладчик (см. стр. 314). Языки, работающие с виртуальной машиной, предлагают возможность запускать скомпилированную программу на различных компьютерах, и она выполняется значительно быстрее, чем программа, написанная на языкеинтерпретаторе. 19
Глава 1 Происхождение языка C Бьерн Страуструп разработал C++ на основе языка C, который, в свою очередь, был придуман в 70-х годах сотрудниками компании UNIX Брайаном Керниганом и Деннисом Ритчи. До этого для создания операционных систем использовали язык ассемблера, из-за чего они были непосредственно связаны с процессором и аппаратным обеспечением компьютера, для которого разрабатывались. Для программистов UNIX был создан язык, который в первую очередь мог бы стать «переносным ассемблером». Разработанные коды должны были быть быстрыми и компактными. Также учитывалось требование использования этого языка для написания программ работы непосредственно с аппаратным обеспечением. Именно поэтому только небольшая часть UNIX реализована на языке ассемблера, а операционную систему можно легко использовать на различных аппаратных платформах. При разработке C также учитывался аспект создания нового современного языка. Так, в C включены разработанные в то время принципы структурированного и процедурного программирования. В первоначальном варианте языка еще присутствовали некоторые недоработанные моменты, которые со временем стали обременительными. Их окончательно устранили, когда C переделали под стандарт ANSI. Вместе с тем была добавлена еще пара элементов, некоторые пункты отличались, и соответствие стандарта ANSI оказалось неполным, поэтому он получил название ANSI C или K&R (стандарт Кернигана и Ритчи). Однако он не используется уже много лет, да и K&R-программы сейчас вряд ли можно где-либо встретить. В течение 80-х и 90-х годов язык C развился до стандартного языка программирования. С того времени все больше операционных систем было написано на C. Большинство API1 также написаны на C, и с помощью команд этого языка можно получить самый быстрый доступ к их среде. Но также и в пользовательском программировании все больше используется C. Во всяком случае, в операционных системах C доказал, что он пригоден для разработки больших проектов. Объектно ориентированное программирование Большие проекты требуют, чтобы в написании программы принимало участие несколько команд разработчиков. Здесь сразу же возникают 1 Сокращение от Application Programming Interface — интерфейсы от прикладных программ до системных сред. 20
Введение в программирование проблемы разграничения областей работы. Проекты, над которыми работают несколько программистов, должны быть так разделены, чтобы отдельная группа разработчиков получила отдельное четкое задание. В идеале, по завершении проекта нужно просто сложить вместе все его отдельные части. Необходимость обсуждения должна быть сведена к минимуму, следуя старому правилу — длительность обсуждения равняется квадрату количества участников. Чтобы учесть все проблемы больших проектов, разработчики то и дело находили новые варианты решений. Те, которые показали себя с наилучшей стороны, перенимались новыми версиями программ и совершенствовались далее. Структурированное программирование объявило войну диким прыжкам между разными частями программ. Оно соединило циклы и условия в блоки так, что команды перехода не только сделались ненужными, но даже стали считаться признаком небрежного программирования. Части программ, которые то и дело повторяются, были объединены в функции. Классическими структурированными языками программирования считаются Pascal и C. Модульное программирование тематически объединило функции в модули. При этом модуль состоит из интерфейса и блока реализации. Интерфейс описывает, какие функции выполняет модуль, и как программист другого модуля может их использовать. Так, блоки реализации являются независимыми от проекта и могут быть созданы различными командами программистов, которые не мешают при этом друг другу. Модула-2 —пример типичного языка модульного программирования. В процессе создания объектно ориентированного программирования (ООП) также была применена модульная логика. Основополагающей стала идея поставить объекты данных в центр программной логики, тем самым используя их в качестве основы для разделения проекта на части. Долгое время программисты фиксировали внимание на алгоритмах и методах решения определенных задач. С объектно ориентированным программированием изменился угол зрения. Вместо метода решения опорным пунктом программы стали данные. Так, в ООП функциональная часть программы непосредственно связана со структурой данных. Посредством такой концепции связи стало возможным создавать схожие объекты классов на основе уже имеющихся классов, при этом классы-наследники перенимают определенные свойства классовпредков и только это определяет их отличие. 21
Глава 1 Поддержка в ООП классовой логики — самый важный аспект, отличающий язык C++ от C. В то же время ООП пережило на момент появления языка C++ пик своей популярности. Сейчас вряд ли можно доверять языку программирования, который не объектно ориентирован. C++ и другие языки Язык C++ хоть и не внедрил в жизнь ООП, однако, без сомнений, сделал его очень популярным. Приверженцы ООП упрекают язык C++ в гибридности. Это означает, что язык C++ допускает написание не только объектно ориентированных программ, но и позволяет программировать в классическом стиле языка C. Однако это только при желании. Язык C++ поддерживает ООП, но не принуждает к его использованию. Бьерн Страуструп видел в этом скорее преимущество. Он хотел создать такой язык программирования, который поддерживал бы программиста в следовании его стратегии. Этот прагматический подход — вероятная причина, почему именно язык C++, а не Java или C# играет существенную роль для профессиональных разработчиков. Начинающий программист не может написать программу на языке C++, не поняв перед тем принципы ООП. И именно поэтому язык C++ сделал возможным успех ООП. Поскольку так программист постепенно знакомится с преимуществами ООП и затем может их использовать. Еще одно преимущество языка C++ — это скорость. Страуструп считал очень важным сохранить скорость выполнения и компактность языка C. Поэтому язык C++ предпочтителен, если требуется, чтобы программа выполнялась быстро. Тем не менее даже если программа не должна выполняться в определенный промежуток времени, пользователь окажется рад, если она будет быстро работать. Тот, кто использует «медленные» языки программирования, должен хорошо обосновать пользователю, почему выполнение программы занимает длительное время. Конечно, можно писать на языке C++ портируемые программы, но это не догма, как например, в случае Java. Язык Java ограничивает программиста во всех областях, которые выходят за рамки действия виртуальной машины. Такая программа, вероятно, будет иначе работать на смартфоне, чем на компьютере, а с помощью языка C++ можно получить доступ непосредственно к операционной системе или даже аппаратному обеспечению. Аккуратный программист выносит функции такого доступа в отдельные классы, которые можно использовать при переносе программы на другие аппаратные платформы. 22
Введение в программирование В конце концов нужно было обеспечить языку C++ стабильную позицию на рынке. Язык Java разрабатывался для того, чтобы написанный код без повторной компиляции можно было использовать на всех аппаратных платформах. Компания Microsoft в свою очередь не хотела здесь уступать. Концерн многие годы пытался создать равноценный язык. Последним ударом была разработка отдельного языка-конкурента под названием C#. Так что руководителям проектов пришлось решать, использовать ли язык Java, который работает на всех платформах, но не поддерживается операционной системой Windows, или же язык C#, который поддерживает Windows, а другие операционные системы — нет. Язык C++ при этом можно рассматривать как золотую середину. В любом случае, необходимость в разработчиках со знанием языка C++ в последние годы только возросла. 1.1.5. Контрольные вопросы • Создайте алгоритм приготовления кофе. Попросите друга последовать этим указаниям, передав их через закрытую дверь. Какие указания приведут к ошибкам? Какие из них можно ошибочно интерпретировать? Написали бы вы алгоритм по-другому, если бы ваш друг не знал, что такое кофе и для чего он нужен? • Почему программа, которая переводится в машинный код интерпретатором построчно, медленнее той, которая переводится компилятором? Ответы на два последних вопроса вы найдете на стр. 495. 1.2. Основа программы Программой именуется набор следующих друг за другом команд, которые могут быть выполненными компьютером. Как уже говорилось ранее, программа на языке C++ пишется в специальном редакторе, а затем переводится в машинный язык с помощью компилятора1. После чего программа запускается. Программа на языке C++ всегда состоит как минимум из функции main(). Слово «main» — английское и переводится на русский язык 1 В данном случае под компиляцией подразумевается полный перевод программы, который состоит из работы компилятора и компоновщика. Если речь идет о компиляторе, всегда подразумеваются эти две составляющие. 23
Глава 1 как «главный». Что именно подразумевается в языке C++ под понятием главная функция, будет рассмотрено далее более подробно. Сейчас главное то, что в любой программе на языке C++ обязательно найдется слово «main» с двумя скобками. В некоторых случаях в скобках указана пара странных символов. Но не беспокойтесь, игнорируйте это до тех пор, пока не узнаете, зачем оно нужно. Главная функция начинается именно там, где открывается фигурная скобка, и заканчивается там, где скобка закрывается. Между фигурными скобками друг за другом следуют команды главной функции. Следующий короткий листинг1 ничего не делает, однако представляет собой самую короткую программу на языке C++. int main() { } Листинг 1.1. Самая короткая программа Эта программа вообще ничего не делает. Если вы в дальнейшем захотите добавить в нее пару действий, то следует указать команды между фигурными скобками. 1.2.1. Комментарии Если у нас уже есть программа, которая ничего не делает, нужно обязательно добавить в нее команды, которые тоже ничего не делают. Можно добавить в программу комментарии, которые компилятор будет полностью игнорировать. Цель таких комментариев состоит в том, чтобы объяснить, почему эта программа написана именно так, а не иначе. Комментарии адресованы коллегам программистам, которые в будущем захотят исправить или доработать код программы. Но чаще комментарии используются самими ее разработчиками. На практике существует большая вероятность того, что придется исправлять ошибки в собственной программе или дополнять ее, в то время как пройдет уже много времени с момента написания. Возможно, вы уже напишете много других программ, женитесь, переедете, выучите другие языки 1 24 Листингом называется исходный код программы.
Введение в программирование программирования и не сможете вспомнить каждую деталь этой программы. Вы будете благодарны, если найдете в программе указания, как и почему это работает или, по крайней мере, должно работать. Есть два способа оформить код так, чтобы компилятор не расценивал его в качестве кода программы. Самый простой — указать два слеша друг за другом. Таким образом, все, что следует за ними, будет рассматриваться в качестве комментария. int main() { //Здесь начинается комментарий //Следующая строка //Ваши собственные комментарии } Листинг 1.2. Построчные комментарии Также существует возможность комментирования большого фрагмента кода. Начало комментария обозначается комбинацией символов /*. Заканчивается комментарий аналогичными символами, но в обратном порядке */. int main() { /*Здесь начинается комментарий Следующая строка комментария не нуждается в собственных символах комментария */ } Листинг 1.3. Блочный комментарий Многие компиляторы позволяют реализовать иерархию блочных комментариев. Это означает, что можно обозначить начало комментария с помощью символов /*, где первая заключающая последовательность символов */ закроет комментарий. Последующий код компилятор просмотрит и попробует перевести. 25
Глава 1 int main() { /* Здесь начинается комментарий /* Следующие строки не требуют знака комментария */ Это компилятор попробует перевести */ } Листинг 1.4. Это некорректное написание! С помощью символов /* и */ можно комментировать не только большие блоки, но также и короткие части внутри строки. При использовании двойного слеша такая возможность отсутствует, потому что эти символы комментируют весь код до конца строки. В следующем примере закрывающая скобка main() вне комментария. int main(/* тут может быть что-нибудь написано*/) { } Во многих курсах программирования, а также правилах некоторых компаний-разработчиков, можно найти обширное описание того, где должен быть расположен комментарий и что содержать. Цель этих предписаний в том, чтобы инвестиции в программу не были потеряны. Если непонятный код в будущем придется проверить, в процессе исправления может возникнуть множество ошибок только потому, что программист неправильно понял цель некоторой части кода программы. Самое простое правило комментирования звучит так: ЗОЛОТОЕ ПРАВИЛО КОММЕНТИРОВАНИЯ Нужно писать в комментарии не то, что делается в этой строке, а почему делается именно так! Комментарий «два значения увеличиваются» бесполезен. Любой программист, который знает язык C++ хотя бы немного, может это по- 26
Введение в программирование нять прямо из кода программы. Выражение «получение суммы брутто из суммы нетто» будет более полезно, поскольку любой читатель поймет, почему здесь увеличены эти два значения. 1.2.2. Команды Программа состоит не только из скобок и комментариев, но также и из команд. Они могут служить для чтения, сохранения или вычисления определенных данных, а также для вывода их на экран. Команды — это основные компоненты программы. Команды в языке C++ могут иметь любое форматирование. Нет никакого особого порядка, в котором эти команды должны располагаться. Можно написать все команды просто одной строкой. Для компилятора важно лишь то, что все они отделены друг от друга точкой с запятой. 1.2.3. Блоки Несколько команд можно объединить в блок. Блок ограничивается фигурными скобками. Компилятор воспринимает блок в качестве единой команды. Трудно не заметить, что функция main() также имеет фигурные скобки. Фактически она является блоком, в котором объединены команды программы. Для лучшей читаемости кода программы, все команды рекомендуется объединить в блоки. Нужно следить за тем, чтобы фигурные скобки, открывающие и закрывающие блок, были указаны на одном уровне. Следующий пример демонстрирует, как должно выглядеть оформление блока. int main() { Здесь находятся команды; Это тоже относится сюда; { Новый блок, заключенный в фигурные скобки; Мы остаемся на этом уровне; 27
Глава 1 Так может продолжаться вечно; } Теперь скобка закрылась; И блок тоже закончился; } Листинг 1.5. Блок, заключенный в фигурные скобки Расстояние между отдельными блоками, заключенными в фигурные скобки, это дело вкуса. По опыту могу сказать, что расстояние в один символ не очень заметно. Двойной отступ — это минимум. Двойные и тройные отступы наиболее распространены. Но если вы отступите на восемь и более шагов, код будет слишком растянутым и сложно обозреваемым. 1.3. Переменные Раздел, касающийся переменных, содержит в себе много деталей, которые сейчас следует опустить. Начинающему программисту, который будет читать этот раздел, не нужно запоминать всю информацию. Сначала нужно получить основное представление о переменных, типах и константах. К этому разделу можно вернуться в любой момент, если захочется заполнить пробелы в знаниях. В любой программе происходит обработка информации. Эта информация хранится в памяти. Языки программирования высокого уровня не получают прямого доступа к этой информации, а используют переменные. В переменных хранятся числа и текст. Переменная имеет три основных качества: • Память Переменные всегда занимают некий объем памяти для хранения информации. О расположении и размере памяти программист в основном не должен беспокоиться. Компилятор выделяет необходимый объем в соответствии с типом переменной. • Имя Переменной в программе можно присвоить любое имя, которое однозначно ее идентифицирует. Разные имена обозначают разные переменные. Для языка C++ есть разница между строчными и про- 28
Введение в программирование писными буквами. Имя Var отличается от имени var. Правила именования переменных можно найти на стр. 32. • Тип Тип переменной определяет, какая информация может в ней храниться: символьные или числовые значения. Еще он определяет размер памяти, которая необходима для хранения переменной, а также какие операции можно с ней проводить. Две переменные, которые хранят, например, числа, могут быть перемножены. Если же они содержат текстовые значения, то операция умножения для этих переменных неприменима. 1.3.1. Объявление переменных Следующий пример демонстрирует, как объявить переменные внутри главной функции main(): int main() { int income; } Листинг 1.6. Объявление переменной Объявление переменной всегда начинается с ее типа. В примере указан тип int. Это числовой тип со знаком, но без позиций после запятой (целочисленный). Через пробел следует имя переменной, которое всегда нужно выбирать так, чтобы оно было связано с содержимым. Спокойно используйте длинные имена. Сокращение хоть и уменьшает объем работы при наборе кода, однако потом может потребоваться потратить больше времени, если придется размышлять, что в какой переменной хранится. Этот прием рекомендован, даже если вы набираете код очень долго. Еще больше времени потребуется, если нужно будет найти ошибку, которая появится, если вы перепутаете две переменные, потому что у них будут схожие имена. В конце всегда следует точка с запятой (;). Так всегда заканчивается команда и объявление переменных. Можно присвоить переменной некоторое значение сразу же после ее объявления. Для этого после имени переменной указывается знак ра- 29
Глава 1 венства. Это инициализирует переменную со следующим после знака равенства значением. int income=0; Здесь переменной income сразу после инициализации присвоено значение 0. Инициализация переменной не обязательна. Тем не менее именно из-за некорректной инициализации возникает много программных ошибок. Надежнее всего после объявления переменных присваивать им значение 0 до тех пор, пока в программе не будет установлено другое. Вместо знака равенства для инициализации переменных в языке C++ можно также использовать скобки. Это выглядит так: int income (0); Можно объявлять сразу несколько переменных одну за другой, разделяя их запятыми. int i, j=0, k; Здесь объявлены переменные i, j, k. Переменная j инициализирована со значением 0. Нижеприведенные способы объявления равнозначны: int i; int j=0; int k; Синтаксис объявления переменных вы найдете на стр. 76 1.3.2. Область действия В языке C++ можно использовать переменную до того, как она будет объявлена в коде программы1. Компилятор проверяет, можно ли использовать переменную в соответствии с ее типом. Существуют языки программирования, которые автоматически объявляют переменные после их первого появления в коде 1 Для использования достаточно описать переменную. Это означает, сообщить компилятору, что он может использовать эту переменную и какой у нее тип. Объявить ее можно в другом месте (см. стр. 191). 30
Введение в программирование программы. Такая удобная функция может быть коварна. Если назвать одну переменную DepartmentNumber, а затем написать это имя неправильно — DepartmentNumbe, то компилятор не сообщит об ошибке. Вместо этого получатся две разные переменные, причем разработчик даже не будет об этом подозревать. В языке C++ такого никогда не случится. Компилятор сразу начнет ворчать, что переменная DepartmentNumbe ему вообще не знакома, и откажется вообще что-либо с ней делать. Так что разработчик будет сразу проинформирован о своей ошибке и сможет ее исправить. В то время как в языке C объявление переменных разрешалось только в начале блока, перед первой командой, в языке C++ их можно объявлять в любой позиции кода программы. Переменная при этом будет действительна во всей области блока, в котором она инициализирована, а также во всех блоках, которые входят в этот. Если основной блок программы состоит из других блоков, то в них можно использовать переменные даже с одинаковыми именами. При этом переменная принимает то значение, которое указано внутри действующего блока, а не вне его. Следующий пример отчетливо иллюстрирует это: { int a = 5; { // здесь а равно пяти. } { int a = 3; // здесь а присваивается значение 3. { // а все еще равно 3 } } //а здесь а опять равно 5 } Листинг 1.7. Две переменные 31
Глава 1 Каждая переменная, объявленная в блоке, является локальной. Она действительна для области действия, определенной блоком. При этом значение переменной a в подблоке перекрывает значение переменной a в блоке. Переменная в общем блоке существует, но программа не может получить к ней доступ, используя то же имя переменной в подблоке. Только покинув блок, в котором объявлена локальная переменная, программа может получить доступ к внешней переменной. Все операции над локальной переменной никак не влияют на внешнюю переменную. Переменные, определенные вне всех блоков, называются глобальными. Они действительны для всех блоков, которые встречаются после объявления переменной, до тех пор, пока не встретится блок, в котором есть локальная переменная с идентичным именем. Глобальные переменные инициализируются компилятором C++ со значением 0, пока программа не присвоит переменной ее собственное значение. Напротив, содержимое локальных переменных после объявления не определено и не всегда равно 0. Тема глобальных и локальных переменных рассмотрена подробнее на стр. 160. 1.3.3. Правила именования и синтаксис В языке C++ все допустимые имена подчиняются одинаковым правилам. Эти правила действительны для имен не только переменных, но и функций и классов. Во многих книгах можно встретить обозначение этих имен словом «идентификатор». Имя должно начинаться с латинской буквы или символа подчеркивания, и может содержать много букв, знаков подчеркивания и цифр. Первый знак имени не должен быть цифрой, поскольку компилятор в таком случае перепутает имя с числом. Буквы могут быть прописными и строчными. В языках C и C++ важен регистр букв. Переменные Anton, ANTON и anton — это абсолютно разные переменные. На рис. 1.2 представлено правило именования переменных. Для создания имени нужно следовать графу по стрелкам. Если граф покинут с правой стороны, то создано допустимое имя. Запомните, имя должно состоять минимум из одного знака, который является буквой или знаком подчеркивания, и можно использовать любое количество символов в любом порядке. Также допускается добавлять цифры к буквам и символам подчеркивания. 32
Введение в программирование Рис. 1.2. Граф синтаксиса имени В прямоугольном поле расположен так называемый терминал, выделенный жирным шрифтом. Это знаки или последовательности символов, которые не разрешено вставлять иначе, чем так, как они указаны. В верхнем примере это знак подчеркивания. В овальном поле расположен нетерминал, выделенный курсивом. Это элементы, которые нуждаются в сопровождении другими символами. Они встречаются в коде или в других графах. Здесь это буквы и цифры. Описанный граф представлен на рис. 1.3. Рис. 1.3. Граф использования букв и цифр Как видно из графа, здесь берутся латинские буквы от A до Z и от a до z. Использование особых символов и букв, отличных от латиницы (в т. ч. и кириллических), в именах переменных принципиально запрещено. И напоследок, в языке C++ нельзя использовать имена команд в качестве имен переменных. Даже не зная всех команд, вы можете легко 33
Глава 1 этого избежать, если будете начинать имя переменной с прописной буквы, так как все команды в языке C++ указываются с маленькой буквы. Данный список1 содержит ключевые слова языка C++, которые нельзя использовать в качестве имен переменных. and double not template and_eq dynamic_cast not_eq this asm else operator throw auto enum or true bitand explicit or_eq try bitor export private typedef bool extern protected typeid break false public typename case float register union catch for unsigned char friend reinterpret_ cast class goto return virtual compl if short void inline signed volatile int sizeof wchar_t long static while mutable static_cast xor namespace struct xor_eq new switch const const_cast continue default delete do using 1.3.4. Типы переменных Переменные всегда имеют тип. Он указывает, какую информацию может хранить переменная, а также определяет, сколько памяти необходимо для ее хранения. Без указания типа нельзя объявлять переменную, потому что компилятор должен знать, какой объем памяти требуется для ее хранения. Он также проверяет, подходит ли этот тип под данную команду программы. Эти «придирки» компилятора созданы только для упрощения работы программиста. При компиляции обнаруживаются 1 34 Бьерн Страуструп. Язык программирования С++. М.: Бином, 2011.
Введение в программирование ошибки в коде прежде, чем они успели привести к сбою программы. Язык C++ достаточно щедрый в определенных пределах. Переменные одинаковых типов могут храниться одна в другой, пока не появляется риск потери информации. Хранение информации Информация, хранящаяся в переменных, располагается в оперативной памяти компьютера. А как работает оперативная память? Компьютер — это цифровое устройство. Он «понимает» только два состояния: включен и выключен. И самая крошечная ячейка памяти компьютера может принимать только одно из этих двух значений. Состояние «включен» обозначается 1, «выключен» — 0. Единицу информации, которая может хранить в себе 1 или 0, называют битом. Из практических соображений биты составляются в блоки. Таким образом, можно записать в них число больше 1. Этот метод аналогичен тому, который применяется в нашей системе счисления для создания чисел больше 9. Цифры указываются друг за другом и умножаются на основу системы. В десятичной системе счисления основа равна десяти, в двоичной — двум. В бите может храниться 0 или 1. В двух битах можно разместить комбинацию 00, 01, 10 и 11. Таким образом, в два бита можно записать числа 0, 1, 2 и 3. Последующий бит удвоит количество комбинаций. Возможные комбинации 000, 001, 010, 011, 100, 101, 110 и 111 и соответствуют числам от 0 до 7. По техническим причинам, байт — стандартный размер наименьшего количества битов в блоке. Байт состоит из 8 бит и может принимать значения 28 = 256, или соответствовать числам от 0 до 255 или от –128 до 127. Целые числа Один из часто используемых типов — целочисленный, хранящий целое число без знаков после запятой. В компьютерной литературе такой тип называют integer. Тип целочисленной переменной обозначается int. Сначала пишется тип int, затем следует пробел, а затем указывается имя переменной. Заканчивается объявление переменной точкой с запятой. int counter; Число, которое хранится в переменной counter, может быть положительным или отрицательным. Можно объявлять переменные, кото- 35
Глава 1 рые принимают только положительные значения, включая 0. Для этого используется ключевое слово unsigned перед типом int. unsigned int counter; Таким образом, можно не только предотвратить то, что переменная counter примет отрицательное значение, но и область значений для положительных чисел при этом удвоится. Есть два особенных случая для всех чисел: short и long. Оба атрибута могут быть расположены перед типом int. Имя атрибута указывает на максимальное число, которое может хранить переменная, и соответственно на количество памяти, которое выделяется под эту переменную. Переменные типа short или long также могут быть объявлены в качестве беззнаковых. При этом перед ними указывается ключевое слово unsigned. unsigned short int counter; Если употребляются атрибуты unsigned или short, ключевое слово int можно опустить. Таким образом объявленные переменные будут интерпретированы компилятором в качестве целочисленных: unsigned short counter; short few; unsigned positive; Листинг 1.8. Целочисленные переменные без ключевого слова int Переменная типа short занимает не менее двух байт, тип int — не менее того же размера памяти, сколько и short. Переменная типа long занимает от четырех байт и имеет минимальный размер как переменная типа int1. Количество байт, занимаемых определенными типами переменных не фиксировано и зависит от компилятора. Целочисленные значения кодируются бинарным кодом. Целочисленная переменная занимает два байта, следовательно, 16 бит. Представление числа 0 установит все биты в 0. Число 1 будет закодировано с помощью 15 нулей и одной 1. В табл. 1.1 представлены двоичные коды чисел. 1 36 Бьерн Страуструп. Язык программирования С++. М.: Бином, 2011.
Введение в программирование Таблица 1.1. Двоичные коды положительных чисел Двоичный формат Десятичный формат 0000000000000000 0000000000000001 0000000000000010 0000000000000011 0000000000000100 0000000000000101 0111111111111111 0 1 2 3 4 5 32.767 Двоичные числа компьютер может легко складывать и вычитать. Функция сложения работает так же, как этому учат в школе для десятичной системы, но только для двух цифр. Давайте посчитаем: 0+0 получается 0. 1+0 получается 1. Также как 0+1. 1+1 не может, однако, дать 2, поскольку такого числа в двоичной системе не существует, в результате получится 0 с переносом, а именно 10. Если следующий бит вычисляется, то перенос входит в вычисление, если все биты посчитаны, а перенос остается, то он теряется. Аналогично работает вычитание. 0-0 дает 0, как и 1-1. 1-0 дает 1. 0-1 дает 1 и перенос, который учитывается при расчете следующего бита, если он существует. Если кодирование положительных чисел понятно, то появляется вопрос, как выглядят отрицательные числа в двоичной системе. Кодирование должно быть совместимо с обычной системой вычисления. Если сложить –1 и 1 , должен получиться ноль. Для этого -1 вычисляется так, как будто от 0 отнимается 1. Если начать с самого правого бита, получится 0-1=1. Затем перенос в следующий бит. В следующем бите опять вычисляется 0-1, что снова приводит к переносу. ~ Перенос: Результат: 0000 0001 111 ---1111 Листинг 1.9. Бинарный расчет 0-1 Перенос проходит через все биты. Из чего следует, что число –1 в бинарном коде состоит из 1. Если это кажется нелогичным, следует 37
Глава 1 подумать, какое число даст 0, если добавить к нему 1. Здесь представлена проверка вычисления: ~ 1111 - 0001 Перенос: 111 ---- Результат: 0000 Листинг 1.10. Бинарный расчет –1+1 Числа –2, –3 и т. д. получаются с помощью декремента, как показано в табл. 1.2 Таблица 1.2. Бинарное кодирование отрицательных чисел Двоичный формат Десятичный формат 0000000000000000 1111111111111111 1111111111111110 1111111111111101 1111111111111100 1111111111111011 1000000000000000 –0 –1 –2 –3 –4 –5 –32.767 Легко заметить, что знак числа находится в первом бите. Здесь указана единица, которая обозначает минус. Такая кодировка также позволяет выяснить, насколько велико самое большое и самое маленькое число. В двух байтах можно разместить числа от –32.768 до +32.767. Если используются четыре байта, то в них можно записать числа от –2.147.483.648 до +2.147.483.647.  ВНИМАНИЕ Если первый бит бинарного кода числа единица, то это отрицательное число. Для контроля пересчитаем еще раз –3+5. В результате должно получиться 2. 38
Введение в программирование ~ 1101 - 0101 Перенос: 1 1 ---- Результат: 0010 Листинг 1.11. Бинарный расчет –3+5 При использовании беззнаковых переменных 0 является самым маленьким числом. Первый бит при этом используется для хранения не знака, а числа. Таким образом переменная типа short может принимать значения от 0 до 65.535. Если к максимальному числу прибавить 1, то снова получится 0. Таблица 1.3. Бинарные коды чисел без знака Двоичный формат Десятичный формат 0000000000000000 0000000000000001 0000000000000010 0000000000000011 0000000000000100 0000000000000101 1111111111111110 1111111111111111 0 1 2 3 4 5 65.534 65.535  ПРИМЕЧАНИЕ Есть нечто коварное в том факте, что вы не можете преодолеть предел максимального числа целого типа. Если увеличить его на 1, то снова получается самое маленькое значение. Таким удивительным образом будет работать программа. Однако может оказаться, что разработчик этого не заметит. Его задача — следить за тем, чтобы такого перехода не произошло. Символы Один из важных типов данных — символьный, его используют при обработке кода. Это касается не только тех действий, которые выполняются в редакторе, например, при написании письма. Речь идет также о вводе, сортировке и выводе таких привычных данных, как адрес или номер автомобиля. 39
Глава 1 Для хранения символов в языке C++ обычно используется тип char. Этот тип занимает один байт, и может принимать 256 значений. То есть существует 256 комбинаций нулей и единиц. Эти комбинации могут кодировать числа от 0 до 256. Латинский алфавит состоит из 26 букв, которые могут быть маленькими или большими, так что для их хранения необходимо минимум 52 комбинации. Еще нужно включить сюда 10 цифр и некоторые знаки препинания. Таким образом, первые 128 комбинаций нормированы интернациональным стандартом таблицы символов ASCII. Оставшиеся 128 используются для кодирования особых символов языков, имеющих в своей основе латинский алфавит. Пока особые символы содержатся только в языках Западной Европы, для хранения буквы достаточно одного байта. Этот набор символов нормирован как ISO 8859-1. Для кириллических или турецких букв существует другой набор символов. Но по какому праву можно исключить тут арабские, японские или древнееврейские символы? Что делать, если в программе нужны кириллические, немецкие и древнееврейские символы одновременно? Для всех этих символов будет уже недостаточно одного байта. С возрастающей интернационализацией возросла потребность в унифицированном кодировании всех национальных особых символов. Попытка закодировать все символы мира в одной системе привела к появлению стандарта UNICODE, который существует с конца 90-х годов. Сразу стало ясно, что такое количество символов не поместится в одном байте. Поэтому под каждый символ было выделено два байта. В 16 битах можно закодировать до 65.536 знаков. Для такого набора символов в языке C++ существует специальный тип wchar_t.Сколько байтов он содержит, зависит от способа реализации компилятора. Первоначальное предположение, что в двух байтах можно закодировать все символы мира, оказалось иллюзией. Сейчас для этого используются четыре байта. Однако зачем резервировать, скажем, на американском компьютере вчетверо больше памяти, только на тот случай, если в один прекрасный день внезапно понадобится обработать китайский иероглиф, который обладатель компьютера вероятно даже не сможет прочесть? Поэтому был создан еще один формат — Unicode Transformation Format (UTF). Он позволяет обслуживать UNICODE по отдельным частям. Самый известный тип такого формата UTF-8. Для каждого символа в нем выделен один байт, как для типа char. Преимущество этого формата состоит в том, что даже те программы, которые не предусматривают обработки интернациональных символов, могут их использовать. Программы, ко- 40
Введение в программирование торые резервируют для каждого символа 16 бит, используют формат UTF-16. Поэтому возможны их конфликты с операционными системами для мобильных устройств, такими, как Windows Phone. Особенно запутанным кажется для начинающих отличие между числом и цифрой. Цифра может быть числом (квазибуквой), которое служит для представления чисел (значений). В программировании в большинстве случаев цифра имеет тип char, то есть является символом, который выводится на экран. Число, напротив, является значением, которое можно вычислить. Чтобы запутаться окончательно, можно сказать, что буквы — это тоже числа. Каждая буква представлена с помощью числа, которое хранится в байте. Соответственно, это может быть число от 0 до 255. Цифры точно так же, как и буквы, кодируются с помощью чисел, но не совсем соответственно их значениям. В таблице символов ASCII они располагаются, начиная с позиции номер 48. Цифра «0» кодируется как 48, цифра «1» как 49 и т. д. Это обязательно следует учитывать при вводе числа с помощью клавиатуры. Если число будет прочитано как буква, нужно использовать 48 в качестве кодировки числа «0», то есть вводить коды чисел для соответственной интерпретации. Кстати, ASCII является не единственным стандартным набором символов. Например, для больших вычислительных машин IBM используется набор символов EBSDIC. К счастью, программисту не обязательно вдаваться в подробности кодирования цифр. Особенно запутанным кажется то, что язык C++ позволяет производить счет с помощью переменных типа char. Фактически, программу на языке C++ не будет беспокоить, если переменной типа int присвоить букву. Тип char в первую очередь свидетельствует о том, что используется один байт памяти. Содержимое может быть интерпретировано как символ или как маленькое число. Если переменная типа char фактически является переменной типа int, то у нее также есть знак. Кроме того, как переменные типа int, так и переменные типа char прежде всего имеют знак. Как было сказано выше, специальные символы языка находятся за 128-й позицией. Таким образом, в случае кодирования особого символа языка (как, например, умлаут в немецком языке) первый бит байта определен как 1. Однако для нормальной переменной типа char он будет интерпретирован в качестве знака числа. Чтобы этого избежать, нужно во всех сомнительных случаях добавлять переменной типа char атрибут unsigned. Иначе мо- 41
Глава 1 жет возникнуть ситуация, когда символ немецкого языка « », например, будет иметь меньшее числовое значение, чем «а», и будет интерпретирован в качестве отрицательного числа. Тогда сортировка укажет все специальные национальные символы в начале алфавита1. Числа с плавающей запятой В реальном мире недостаточно использовать только целые числа. При расчете веса, скорости и других физических величин у них всегда имеются значения после запятой. Также и для написания цен. Компьютер не может отрицать этот факт. Для представления числа с запятой для него существует нормальная форма, где запятая помещается перед числовыми значениями. Чтобы числовое значение не изменилось, число умножается на десять в соответствующей степени. Следующие числа являются идентичными: 823,25 = 823,25 × 1 = 823,25 × 100 823,25 = 82,325 × 10 = 82,325 × 101 823,25 = 8,2325 × 100 = 8,2325 × 102 823,25 = 0,82325 × 1000 = 0,82325 × 103 Самое нижнее число, в котором первая значимая цифра расположена после запятой, отображает стандартное представление чисел с плавающей запятой. Из этого представления следует, что число с плавающей запятой имеет два компонента. Один — это мантисса, здесь 82325. Мантисса — это числовая часть константы с плавающей запятой, без экспоненты. Другой компонент — это экспонента (значение степени) с базисом 10, здесь 3. Экспонента отделяется при письме с помощью большого или маленького Е. Следовательно, наше число будет представлено в следующем виде: 0.82325Е3. Самый простой тип чисел с плавающей запятой называется float. Эти числа также кодируются бинарным кодом, чтобы компьютер мог их правильно обрабатывать. Данный тип допускает использование целочисленных и отрицательных экспонент. Таким образом можно представлять не только очень большие, но и совсем маленькие дробные числа. Требования к размеру памяти у чисел float не очень высокие, в среднем они занимают около четырех байт. Однако можно предста- 1 При использовании атрибута unsigned все национальные особые символы будут располагаться в конце алфавита, после «z», что более привычно. 42
Введение в программирование вить и такие численные величины, как 1038. Но подобная переменная является компромиссом точности мантиссы. В случае если необходима высокая точность в представлении числа, и его размер выходит за пределы стандартной float-переменной, используется тип double. Имя означает «двойной» и касается точности. Точность зависит от размера памяти, занимаемой числом. Многие компиляторы выделяют под переменную типа double в два раза больше памяти, чем под переменную типа float. Время для расчета такой переменной также увеличивается вдвое. Если требуется особая точность, компилятор ANSI C++ располагает еще одним типом данных, более точным, чем double. Это тип long double. В зависимости от компилятора он выделяет под переменную от 10 до 16 байт. При обращении с числами с плавающей запятой часто обнаруживаются неточности. С одной стороны, это происходит из-за ограниченного количества знаков в мантиссе. С другой — причиной неточности может послужить бинарное кодирование. Это легко установить, если присвоить переменной значение –1,0 и пошагово увеличивать ее до 0,1. В двоичной системе счисления нельзя таким образом получить точное значение 0,0. Причина в том, что 0,1 в бинарном представлении также имеет период, как, например, треть в десятичном представлении. Для представления 0,1 в двоичной системе используются знаки после запятой. Их нужно представить так, чтобы 0,1 был половиной, 0,01 — четвертью, а 0,001 — восьмой частью. Чтобы представить десятую часть, восьмой части числа будет слишком много. Шестнадцатая часть это 0,0625. Остается 0,0375 до одной десятой. Одна тридцать вторая — это 0,03125. Остается 0,00625. Одна 256-я — это 0,00390625. Путем дальнейших вычислений придем к бинарному представлению из 0,000110011001111, и все еще будет не хватать определенной части. Если продолжать так считать до конца, выяснится, что будет постоянно недоставать некоторого значения. И как у калькулятора всегда возникает проблема при вычислении трети, так у компьютера с его бинарным кодированием чисел с плавающей запятой всегда возникают проблемы при вычислении одной десятой. В некоторых языках программирования, а также базах данных, существуют явные типы для десятичных чисел со знаками после запятой. Такие значения являются, кроме всего прочего, очень точными для представления валюты. Язык C++ не понимает дробных чисел, а только 43
Глава 1 числа с плавающей запятой. Это значит, что будет выделено столько памяти, сколько необходимо для представления числа. Язык C++ не устанавливает четкие требования к количеству памяти для большинства типов переменных. Такие детали реализации зависят от компилятора. Четко установлены лишь качественные различия между типами. Можно положиться на то, что short не больше long. Табл. 1.4 демонстрирует, какой размерный порядок существует на практике. Таблица 1.4. Обычные размеры типов Тип Стандартный размер Типичное использование char 1 байт Буквы, символы и цифры wchar_t 2 байта Интернациональные буквы, знаки и цифры short int 2 байта Числа для нумерации или позиций int 2 или 4 байта Стандартный размер для целых чисел long int 4 или 8 байт Обозримо большие значения без знаков после запятой float 4 байта Вычисленные значения со знаками после запятой double 8 байт Расчеты и высокие стоимости long double 12 байт Расчеты высокой точности Какой в действительности размер имеет определенный тип, зависит не от настроения разработчиков компилятора, а например, от аппаратного обеспечения или архитектуры операционной системы. Наиболее распространенные ранее компьютеры имели разрядность 32 бита, то есть 4 байта. Соответственно 4 байта обрабатывались максимально быстро. Современные модели имеют разрядность 64-бита. В этих системах тип long реализуется чаще всего 8 байтами, поскольку такой размер обрабатывается с максимальной скоростью. В то время, когда был создан язык C++, как правило, использовались 16-разрядные машины. Если бы тип int реализовали тогда двумя байтами, то в будущем у компилятора возросли бы затраты на его обработку только из-за того, что нужно было бы ограничивать 64 бита до 16. Если необходимо узнать конкретный размер типа, используется псевдофункция sizeof(). Она возвращает размер типа или переменной. Результатом работы следующего примера будет количество байт, занимаемых типом double. cout << sizeof(double) << endl; 44
Введение в программирование Точно так же можно поместить в скобки sizeof() переменную или самоопределяющийся тип. Результатом будет количество байт в памяти1. В файле limits.h хранятся размеры основных типов, например константа INT_MAX. Она содержит максимальное значение, которое может принимать переменная типа int. Аналогичная информация содержится в CHAR_MAX относительно типа char. И совсем неудивительно, что LONG_ MAX содержит максимальное значение, которое может принимать переменная типа long. Вероятно, можно удивить тем, что SHRT_MAX является максимальным значением переменной типа short, поскольку предположительно в имени должна быть еще и буква О2. Таблица 1.5. Предельные константы Константы Значение INT_MAX INT_MIN UINT_MAX CHAR_MAX CHAR_MIN WCHAR_MAX WCHAR_MIN UCHAR_MAX SHRT_MAX SHRT_MIN USHRT_MAX LONG_MAX LONG_MIN ULONG_MAX Максимальное значение переменной int Минимальное значение переменной int Максимальное значение переменной unsigned int Максимальное значение переменной char Минимальное значение переменной char Максимальное значение переменной wchar_t Минимальное значение переменной wchar_t Максимальное значение переменной unsigned char Максимальное значение переменной short Минимальное значение переменной short Максимальное значение переменной unsigned short Максимальное значение переменной long Минимальное значение переменной long Максимальное значение переменной unsigned long Для всех этих типов имеются аналогичные константы с приставкой MIN, в которых хранятся минимальные значения переменных. А если указать большую букву U перед соответствующими именами констант, можно узнать, какое максимальное значение могут принимать переменные с атрибутом unsigned. Если захочется провести короткий проверочный тест для коллег, можно спросить у них, почему не существует ULONG_MIN. 1 За счет такой гибкости sizeof() занимает особое место среди функций. Поскольку почти для всех стандартных функций передаваемыми параметрами могут служить только значения или переменные, чей тип проверен компилятором. 2 Ходит слух, что секретные службы во время холодной войны требовали такие удивительные вещи на случай захвата врагом, чтобы он не смог обращаться с компьютером. 45
Глава 1 1.3.5. Синтаксис объявления переменных Граф синтаксиса объявления переменных стандартных типов представлен на рис. 1.4. Рис. 1.4. Граф синтаксиса объявления переменной Сначала указывается тип переменной, затем имя, образование которого уже представлено графом на рис. 1.2 на стр. 33. С помощью знака равенства и константы можно инициализировать переменную. Объявление переменных завершается точкой с запятой. Можно объявить несколько переменных одной командой, разделяя их запятыми. Все вышеописанные типы представлены на графе на рис. 1.5. int short long char unsigned wchar_t float double long Рис. 1.5. Граф объявления типа Известно, что существуют типы char, wchar_t, int, float или double. Для типа int или char можно использовать атрибут unsigned. Перед 46
Введение в программирование типом int можно использовать атрибуты long и short. При использовании одного из этих атрибутов не обязательно указывать тип int. 1.3.6. Константы Константа — это неизменная величина. Все числа в программе являются константами, их значение, в отличие от значения переменной, не может изменяться. В языке C++ для констант также следует указывать тип. Целые числа Целочисленные константы самые простые для понимания. Это такие числа, как 5, –3, 17 или 5498. Однако нужно следить за тем, чтобы константа содержала только цифры, без пробелов или других особых символов. Отрицательное число обозначается знаком минус перед первой цифрой числа. Для положительных чисел допустим знак плюс, однако он необязателен.  ПРИМЕЧАНИЕ Десятичная целочисленная константа — это 0 или другое число, начинающееся с цифры, отличной от 0, за которой могут следовать любые цифры, включая 0. DecConst 0 1 2 0 3 ... ... 9 9 Рис. 1.6. Граф десятичной целочисленной константы DecConst Как уже было сказано, константа может быть обозначена символом плюс или минус перед числом. Граф на рис. 1.6 может быть использован 47
Глава 1 также для объявления констант с плавающей запятой, и будет носить имя DecConst. В следующем графе (рис. 1.7) добавлен знак числа: DecConst + – Рис. 1.7. Граф десятичной константы со знаком Следующая таблица демонстрирует примеры недопустимых констант (табл 1.6). Таблица 1.6. Недопустимые целочисленные константы Константа Причина 1 234 Указан пробел между 1 и 2 1,234 Запятая в константах не используется 1- Знак числа должен быть указан перед числом 12DM Содержимое недопустимо Наряду с десятичными константами в языке C++ существует возможность использовать восьмеричные и шестнадцатеричные системы счисления для представления констант. Такая возможность первым делом важна для программистов, работающих с микроконтроллерами. Начинающего программиста эта тема, вероятно, сейчас не заинтересует, и если это так, то можно сразу перейти к константам с плавающей запятой на стр. 51. Но и для начинающих следующее правило важно:  ПРАВИЛО Каждая целочисленная константа, которая начинается с нуля, не будет интерпретирована в качестве десятичной. Нужно избегать указывать 0 перед константами, если разработчик не знает точно, как выполняется их интерпретация. Если перед константой указан 0, а за ним буква «x», значит это шестнадцатеричная константа, то есть число с базисом 16, которое состоит из цифр от 0 до 9 и букв от «A» до «F», используемых для чисел от 10 до 15. 48
Введение в программирование В этом случае также допустимо использование маленьких букв от «a» до «f». Если за нулем следует цифра, то это восьмеричное число с базисом 8. Допустимы цифры от 0 до 7. Табл. 1.7 демонстрирует различные значения последовательности цифр 11. Таблица 1.7. Системы счисления Константа Система счисления Десятичное значение 11 Десятичная (базис 10) 1×101 + 1×100 = 1×10 + 1×1 = 11 011 Восьмеричная (базис 8) 1×81 + 1×80 = 1×8 + 1×1 = 9 0х11 Шестнадцатеричная (базис 16) 1×161 + 1×160 = 1×16 + 1×1 = 17 Расчет значений для других систем счисления так же эффективен, как и для привычной нам десятичной, в которой каждая позиция умножается на соответствующую степень десяти. Показатель степени начинается с правой позиции с 0. Самая правая цифра умножается на 100 или 1. Вторая позиция справа умножается на 101, то есть на 10. Далее следует 102, равное 100; 103, равное 1000 и т. д. Шестнадцатеричные числа имеют базис 16. Для этого не хватает цифр от 0 до 9. Поэтому для них задействованы также буквы от «A» до «F». «А» имеет значение 10, «В» — 11 и т. д., «F» имеет максимальное значение 15. Метод расчета значения шестнадцатеричной константы аналогичен методу в десятичной системе. Самая правая позиция остается неизменной, поскольку 160 = 1. Следующая позиция будет умножена на 16, следующая — на 162 =256 и т. д. Таким образом, 0×168 равно 1×256 + 6×16 + 8, что соответствует 360 в десятичной.  ПРИМЕЧАНИЕ Шестнадцатеричная целочисленная константа начинается с последовательности символов 0х или 0Х. Затем следуют любые цифры от 0 до 9 или буквы от «A» до «F» (равнозначно от «a» до «f»). Восьмеричные числа состоят из цифр от 0 до 7. Здесь позиции вычисляются с помощью степени восьмерки. Справа налево получается 1, 8, 64 и т. д. Константа 0167 будет пересчитана следующим образом: 1×64 + 6×8 + 7. Восьмеричная константа 0167 соответствует десятичному значению 119. 49
Глава 1 0 x 0 X ... 9 A ... F a ... f Рис. 1.8. Граф шестнадцатеричной константы  ПРИМЕЧАНИЕ Восьмеричная целочисленная константа всегда начинается с 0. Затем следуют любые другие цифры от 0 до 7. 0 0 1 2 ... 4 5 ... 7 Рис. 1.9. Граф восьмеричной константы 50
Введение в программирование Константы с плавающей запятой Константы с плавающей запятой — это константы, имеющие знаки после запятой. К сожалению, запятая не является интернациональным знаком, отделяющим целое число от дробной части. Например, в англоязычной среде для этого используется точка. Язык C++ не понимает значение 1,2, а предпочитает 1.2. Константа с плавающей запятой начинается с мантиссы. Мантисса — это часть константы с плавающей запятой, которая не содержит значение степени. Иногда она имеет знак, за которым следует целая часть. Затем появляется точка и числа дробной части. Если перед точкой стоит 0, его можно опустить. Экспонента используется здесь так же, как в калькуляторе. По умолчанию используется базис 10. Чтобы отделить мантиссу от экспоненты, используется прописная или строчная буква «E». Экспонента сама по себе целочисленная десятичная константа, она также может быть отрицательной. Примеры приведены в табл. 1.8 и на рис. 1.10. Таблица 1.8. Представление экспоненты Представление экспоненты Значение Представление без экспоненты 1Е3 103 1000.0 1Е0 100 1.0 1Е-1 10–1 0.1 1Е-3 100–3 0.001 Рис. 1.10. Граф константы с плавающей запятой Данный граф три раза ссылается на DecConst. Здесь речь идет о десятичной константе, как показано в графе на рис. 1.6. 51
Глава 1 При необходимости можно обозначить константу с плавающей запятой буквой «f». Распространено присваивание константе с плавающей запятой значения .0, чтобы отличать их от целочисленных констант. Символьные константы Буква, то есть константа типа char, указывается в кавычках. Такая форма объявления констант с плавающей запятой облегчает прочтение кода человеком. В компьютере для обработки букв используются числа. Рис. 1.11. Граф символьной константы Если необходимо явно указать тип константы wchar_t, перед первой кавычкой указывается большая буква «L». Какими числами кодируются символы, зависит от набора этих символов. Для большинства систем сегодня используется стандарт ASCII (American Standard Code for Information Interchange — американская стандартная кодировочная таблица для обмена информацией). Ниже представлены первые 127 символов. Для расшифровки кода в каждой строке таблицы указан номер первого символа в десятичной и шестнадцатеричной системе. Над символами указано число, которое должно быть добавлено к номеру столбца. Так, буква Z имеет десятичный номер 80 + 10 = 90. Шестнадцатеричный номер 0x50 + 0x0А = 0x5А. dec hex 00 0 16 10 32 20 48 30 64 40 80 50 96 60 112 70 52 0 0 1 1 ^A ^P ^Q ! 0 1 @ A P Q ` a p q 2 2 ^B ^R " 2 B R b r 3 3 ^C ^S # 3 C S c s 4 4 ^D ^T $ 4 D T d t 5 5 ^E ^U % 5 E U e u 6 6 ^F ^V & 6 F V f v 7 7 ^G ^W ' 7 G W g w 8 8 ^H ^X ( 8 H X h x 9 9 ^I ^Y ) 9 I Y i y 10 A ^J ^Z * : J Z j z 11 12 13 14 15 B C D E F ^K ^L ^M ^N ^O + ; K [ k { , < L \ l | = M ] m } . > N ^ n ~ / ? O _ o
Введение в программирование Первые позиции ASCII используются для контрольных символов. В таблице они обозначены как клавиши, которые необходимо нажать, чтобы ввести этот символ. Например, ^ используется в качестве символа управляющей или контрольной клавиши. При выводе этих знаков происходит управление устройством вывода. Символ конца строки обозначен номером 10, символ звукового сигнала — номером 7. Печатные символы начинаются с номера 32. Это знаки, которые выводятся на печать и не воспринимаются принтером в качестве символов управления. Забавно, что самый первый из них невидимый: пробел под номером 32 или шестнадцатеричным номером 0х20. Дальше следует восклицательный знак и другие знаки препинания. Цифры в таблице ASCII начинаются с номера 48. '0' кодируется как 48, '1' — как 49. Так продолжается до '9' — 57. Программист должен четко отличать '0' и 0. Если пользователь вводит данные, то это символьная последовательность. Чтобы вычислить из этой последовательности цифры, нужно вычесть из них значение '0'. Из '3' получится 3, если из него вычесть значение '0'. Подробное описание можно найти на стр. 128. Буквы начинаются с прописной 'A', с номера 65. Затем следуют буквы, начиная с 'a' под номером 97. Буквы следуют в алфавитном порядке от A до Z. Интернациональные особые символы, вроде немецких умлаутов, начинаются с номера 128. Где расположен тот или иной особый символ языка и существует ли он вообще, зависит от используемой таблицы символов. На западе для систем UNIX/Linux и Windows используется таблица символов ISO 8859-1 (Latin-1). Восточноевропейские языки используют Latin-2, например ISO 8859-9 — это таблица турецких символов. Не полагайтесь слепо на позицию определенного символа в таблице. Хотя формат ASCII и используется на всех компьютерах и системах UNIX, но, например, большие вычислительные машины IBM работают в основном с таблицей символов EBSDIC, которая построена совсем иначе. Лучше положиться на то, что буквы и цифры отсортированы по возрастанию и цифры следуют одна за другой непрерывно. В следующем примере, в переменной numeral_symbol хранится символ. Его числовое значение при этом должно соответствовать символу ноль. numeric_value = numeral_symbol - '0'; // правильно numeric_value = numeral_symbol - 48; // только для ASCII! 53
Глава 1 В первой строке вычитается символ '0', который представляется компилятором для машины с форматом ASCII как число 48. При компиляции этой строки на машине с другой таблицей символов, вместо него будет подставлено другое значение. В языке C++ гарантируется, что все цифры следуют одна за одной и результат будет корректен для всех цифр. Если вместо этого, как во второй строке, будет закодировано конкретное число 48, то эта строка будет работать только на машинах с таблицей символов ASCII. Отсюда следует, что первая строка значительно лучше представляет идею программиста. Чтобы быть независимым от таблицы символов, язык C++ определяет некоторые важные знаки с помощью специального символа \. За ним следует символ, который кодирует последовательность. Следующая таблица демонстрирует самые важные особые символы (табл. 1.9). Таблица 1.9. Символы управления Последовательность Значение \n \r \t \b \f \0 \\ \" \' \0nnn \0xnn Перевод строки (line feed) Возврат каретки (carriage return) Табуляция Возврат назад Перевод формата Действительный ноль (не знак '0') Вывод символа обратного слеша Вывод двойной кавычки Вывод одинарной кавычки Восьмеричное число nnn определяет символ Шестнадцатеричное число nn определяет символ Последовательности символов Последовательности символов состоят из нескольких символов, связанных друг с другом (рис. 1.12). Все, что должно принадлежать константной последовательности символов, заключается в кавычки. В одной последовательности могут использоваться все символы, которые может содержать символьная константа, а также символы управления. Например, следующая последовательность состоит из двух строк: "Это последовательность символов \n из двух строк" Иногда код может быть очень длинным и не помещаться в одну строку. Поэтому несколько символьных последовательностей, следующих друг за другом, можно разделить на несколько строк. 54
Введение в программирование Рис. 1.12. Граф константной символьной последовательности Компилятор при обработке соединит их в одну. "Эта последовательность символов настолько длинная," "что не помещается в одну строку. Но это не" "проблема. Просто перейдите на следующую строку!" Поскольку последовательности символов — это составные элементы, они не могут быть обработаны так же, как числа или одиночные символы. Поначалу в этой книге мы будем использовать их только в качестве констант для вывода на экран. А как присваивать переменным символьные последовательности, как их представлять и обрабатывать, рассмотрим далее, начиная со стр. 127, когда речь пойдет о составных типах. Символические константы Числовые константы, которые указаны в коде программы без последующего контекста, могут запутать. Если где-то в программе используется константа 8, какой-нибудь читатель сразу спросит, что это 8 на самом деле означает. Представим, что речь идет о стандартном рабочем времени в день, тогда читаемость программы будет значительно лучше, если вместо 8 использовать имя rate_of_working_time. Тогда станет сразу ясно, что это число означает. Чтобы связать число с именем, объявляется константа. В языке C++ константой называется переменная, которая не изменяется. То есть имеет тип и имя, как переменная, но ее значение остается постоянным. Поскольку константа не меняет свое значение, она должна быть проинициализирована при объявлении. Объявление константы выглядит так же, как объявление переменной с инициализацией, перед которой указано ключевое слово const. const int rate_of_working_time = 8; 55
Глава 1 Константы, в отличие от переменных, не нуждаются в памяти, поскольку они вводят имя для константного значения, которое исчезает после компиляции. Есть две основные причины объявления констант. Одна — улучшение читаемости кода программы. Вторая — удобство внесения изменений, поскольку в этом случае требуется изменить только одну строку. Если в вышеупомянутом примере норма рабочего времени будет не 8 часов, а 7, изменить код придется только в одной позиции. Если бы везде в программе стояло число, пришлось бы искать его по всему коду и при этом следить, является ли значение 8 действительно нормой рабочего времени, а не максимальным количеством рабочих в смене или минимальным тарифом уборщицы. Существует эмпирическое правило, которое гласит, что все числа кроме 0 и 1 должны иметь имена. Возможно, это преувеличение. Перед использованием числа в программе необходимо обдумать, не будет ли именованная константа бессмысленной. Так или иначе, затраты на объявление константы вначале не соизмеримы с пользой в области гибкости и читаемости. Вышеописанная форма объявления констант появилась в языке C еще с введением стандарта ANSI. До этого константы устанавливались с помощью препроцессора перед компиляцией. Препроцессор может с помощью команды #define ввести кодовую замену. Так следующая команда определяла1 бы константу rate_of_working_time из примера выше: #define rate_of_working_time 8 Выполнение этой строки укажет препроцессору, что в исходном коде программы до ее компиляции нужно использовать значение 8 везде, где программист написал слово rate_of_working_time. На первый взгляд эффект аналогичный. Но механизмы отличаются в двух важных аспектах. Способ, используемый в языке C++, требует указывать тип константы. При этом константы могут инициализироваться со значением, которое было ранее вычислено в программе. 1.4. Обработка Итак, существуют переменные и константы. В этом разделе будет описано, как константы превращаются в переменные, как можно копировать и вычислять переменные. В общем, как вдохнуть в компьютер жизнь. 1 56 Отличия понятий «объявление» и «определение» см. в глоссарии на стр. 521.
Введение в программирование 1.4.1. Присваивание Знак равенства уже был использован для инициализации переменной с определенным значением. Также его можно использовать, когда переменной необходимо присвоить новое значение. Слева от знака равенства всегда указывается переменная в качестве цели присваивания. Справа от него находится источник данных. Это может быть другая переменная, числовое значение или вычисляемое выражение. Источник данных, расположенный справа от выражения присваивания, чаще всего имеет вид выражения. Следующие примеры показывают несколько присваиваний. При этом, немного опережая изложение, здесь представлены вычислительные операции. MwStSet = 16; Netto = 200; MwStSum = Netto * MwStSet / 100; Brutto = Netto + MwStSum; Листинг 1.12. Присваивание Если слева от знака равенства вместо переменной указано нечто иное, большинство компиляторов выдадут ошибку: «L-Value expected» или аналогичную. Дословный перевод сообщения звучит так — «ожидается значение слева», то есть значение, указываемое с левой стороны присваивания. L-значение — это выражение, ссылающееся на объект. Как будет описано далее, не все типы переменных допустимы в качестве L-значения (см. стр. 125). Язык C++ допускает возможность присвоить нескольким переменным одинаковое значение одной операцией. Представьте, что присвоение значения следует справа налево. Такая особенность делает возможным использование следующей строки: a= b = c = d = 5 + 2; Операция будет проведена, начиная с вычисления данных. 5+2 в итоге позволит получить 7. Это значение (7) будет присвоено переменной d. Результат присвоения — значение 7, которое будет присвоено переменной с, — далее перейдет к переменной b и в итоге к переменной а. В результате все переменные будут иметь значение 7. 57
Глава 1 1.4.2. Мастер вычислений В листинге 1.12 было показано, как можно выполнять счет с помощью программы. Алгоритм не сильно отличается от выполнения расчета на листе бумаги. Немного необычно только то, что с левой стороны расположена цель присвоения и знак равенства. Знак умножения — *, знак деления — /. Плюс и минус выглядят привычно — + и -. Последний можно использовать также в качестве знака числа. Особый вид расчета — это модуль. Данная операция возвращает остаток от деления целых чисел. Если деление в начальных классах еще не забылось, тогда вероятно на ум придет фраза такого рода: «25 разделить на 7 будет 3 и 4 в остатке». Такой же расчет остатка есть и в языке C++. Обозначается он в качестве модуля. Знак операции — %. rest = 25 % 7; // Остаток равен 4 В языке C++ также соблюдаются законы математики. Старое правило приоритета математических операций действительно и здесь. Оно гласит, что операции умножения и деления выполняются перед операциями сложения и вычитания, если они используются в одном выражении. Бинарные операторы, символы расчета, которые соединяют между собой два выражения, указываются, в основном, с левой привязкой. Это означает, что они выполняются слева направо, если приоритет одинаков. То есть a*b/c будет интерпретировано как (a*b)/c. Единственное исключение — это присваивание. Здесь a=b=c выполняется как a=(b=c). Оператор присваивания является оператором с правой привязкой. Кроме того правую привязку имеют одноразрядные операторы. Сюда относится оператор ++, с которым еще предстоит познакомиться. В некоторых случаях нельзя однозначно сказать, в какой последовательности будут выполняться операции. Следующий пример можно интерпретировать по-разному: а = 1; b = (а*2) + (а=2); Присваивание а=2 в правых скобках даст результат 2, как это было показано при каскадном присваивании. Но будет ли операция присваивания произведена перед или после операции в левых скобках, остается неясным. Переменная b в этой строке может принять значение как 4, так и 6. 58
Введение в программирование Разработка программы и без того достаточно сложный процесс. Поэтому нужно избегать неточностей, насколько это возможно. Если результат выражения может быть неоднозначным, следует использовать скобки или производить расчет в несколько шагов. Старайтесь писать программу настолько просто и предсказуемо, насколько это возможно. Существует так называемый принцип KISS — Keep It Small and Simple (Делай проще и короче). Код программы, состоящий из собрания изощренных трюков, практически невозможно прочесть, поэтому он необслуживаемый и непрофессиональный. 1.4.3. Сокращения Человек всегда стремится сделать все максимально удобным. Так же и программисты. Они действуют из соображения никогда не делать то, что может выполнить за них компилятор, и естественно, что существуют средства и методы, позволяющие формулировать повторяющиеся задачи как можно короче. Для увеличения значения на 1, можно использовать следующий код: counter = counter + 1; Листинг 1.13. Содержимое переменной counter увеличивается на 1 Это означает, что значение переменной counter изменяется путем увеличения старого значения на 1. Сначала будет вычислено выражение, расположенное справа от знака равенства, и только потом оно будет присвоено переменной слева. Это причина, по которой переменная counter увеличивается на 1. Часто новое значение переменной происходит из старого, которое пересчитывается на основе новых данных. Всегда, если значение увеличивается, уменьшается, удваивается или делится пополам, можно использовать сокращенный вариант написания. В следующей строке значение переменной counter также увеличивается на 1: counter +=1; Листинг 1.14. Сокращенная запись инкрементирования 59
Глава 1 Здесь комбинируются плюс и знак равенства. Чтобы плюс не был интерпретирован в качестве знака числа 1, он должен быть указан перед знаком равенства. Между плюсом и знаком равенства не должно быть пробелов. Значение строки следующее: «Добавь 1 к значению переменной counter». Идея заключается в том, чтобы использовать сокращенную запись не только для сложения, но и для вычитания, умножения, деления и получения остатка от деления (табл. 1.10). Таблица 1.10. Короткая запись Короткая запись Длинная запись a += b a = a + b a –= b a = a – b a *= b a = a * b a /= b a = a / b a %= b a = a % b В особом случае, когда значение увеличивается на 1, выражение можно еще сократить. Для этого к переменной добавляются два знака плюс. counter++; Листинг 1.15. Сверхкороткая запись Как не сложно догадаться, этот двойной плюс и дал название языку C++. Его еще называют языком С, который увеличен на 1. Также существует двойной минус. Он уменьшает значение переменной на 1. Почему нет двойного астериска или двойного слеша, сложно сказать. Можно также использовать двойной плюс или минус с правой стороны присваивания. Тогда значение переменной будет увеличено уже после ее использования. Рассмотрим следующие примеры: counter = 5; Sum = 2 + counter ++; // Sum содержит 7! Листинг 1.16. Инкремент с правой стороны 60
Введение в программирование Очевидно, что переменная counter после этих команд содержит значение 6. Однако немного непонятно, какое значение имеет переменная Sum. Верно предположить, что оно будет равно 7. Как сказано выше, увеличение переменной counter на 1 произойдет только после вычисления арифметического выражения. Можно также указать двойной плюс перед переменной. Тогда ее значение сначала увеличится на 1, а затем будет использовано для вычисления выражения. counter = 5; Sum = 2 + ++ counter; // Sum содержит 8! Листинг 1.17. Другой результат В этом случае переменная Sum будет иметь другое значение. Если данное написание не кажется наглядным, то это дело вкуса. Я бы использовал такую конструкцию в своей программе. Но лучше добавить пару символов. Тогда коллега в будущем сможет быстрее понять, что написал автор. Следующие строки содержат то же самое, но значительно проще для восприятия: counter = 5; ++ counter; Sum = 2 + counter; Листинг 1.18. Аналогичная запись, но более легкая для прочтения Расположение инкремента перед переменной называется префиксом. Это значит, что переменная сначала инкрементируется, и только после этого используется. Расположение инкремента после переменной называется постфикс. Переменная сначала используется, а затем инкрементируется. Кстати, префиксный вариант несколько менее эффективен при обработке. В табл. 1.11 приведен обзор математических операторов в порядке возрастания приоритета. 61
Глава 1 Таблица 1.11. Математические операции Оператор Значение Пример + * / % ++ -- Сложение Вычитание Умножение Деление Остаток от деления Инкремент Декремент а=11+5; (16) а= 11-5; (6) а= 11*5; (55) а=11/5; (2) а= 11%5(1) ++а; или а++; --а; или а--; 1.4.4. Понятие «функция» на примере функции случайных чисел Помимо основных операторов, язык C++ содержит ряд функций, о которых будет подробно рассказано в следующих разделах. Функция внешне отличается от переменной наличием пары скобок, которые следуют за ее именем. Вызов функции ведет к тому, что программа прерывает выполнение текущей последовательности команд и обрабатывает сначала код функции, а затем возвращается обратно в основную программу. В большинстве случаев функция при этом возвращает некоторое значение. Иногда функции можно в скобках передать параметр, который она примет. Так выглядит вызов функции синуса: f = sin(45); Здесь sin — это имя функции, 45 — передаваемый параметр, а результат будет помещен в переменную f. Рассмотрим функцию случайных чисел. Для некоторых учебных программ функция случайных чисел очень полезна. Также она хорошо послужит на практике. С ее помощью можно вырабатывать данные для тестирования отдельных частей программы. Выданное компьютером случайное число не является на самом деле абсолютно случайным, оно сгенерировано специальной функцией. Хорошие случайные числа имеют две особенности: их значение сложно предугадать и они равномерно распределены. Эти числа используются не только в карточных играх или игре в кости, но также и для симуляций, которые необходимо проработать, например, миллион раз. Чтобы серия испытаний могла повторяться, имеется стартовая функция. Она получает некоторое начальное значение в качестве входного 62
Введение в программирование параметра. Если это значение уже было использовано, то в результате будет сгенерирована такая же последовательность случайных чисел. С вызовом функции srand() инициализируется генератор случайных чисел. Входным параметром функции является целое число, которое служит стартовым. После инициализации генератора случайных чисел с помощью функции srand(), можно использовать функцию rand(), которая возвращает квазислучайное число типа long. Каждый повторный вызов этой функции возвращает новую случайную величину. Обе функции находятся в стандартной библиотеке stdlib. Следующая маленькая программа начинает вычисление случайных величин с некоторой любой величины х. Затем дважды вызывается функция rand() и значение переменной chance становится случайным. // Программа, иллюстрирующая работу функции случайных чисел #include <stdlib.h> int main() { const int some_start_value=9; long chance; // здесь будет храниться результат srand(some_start_value); // Трясем кубик chance = rand(); // Бросаем один раз chance = rand(); // Бросаем второй раз } Листинг 1.19. Случайные числа Значение, которое возвращает функция rand(), как уже было сказано, является положительным целым числом, которое каждый раз иное. Минимальный результат может быть 0. Самое большое значение ограничено в компиляторе константой RAND_MAX. На практике RAND_MAX равен LONG_MAX, примерное значение которого около двух миллионов. Однако многие программы не используют настолько большие числа. Обычно такие программы симулируют бросок кубика, цифру лото или игральную карту. Чтобы сократить большие числа до 6, 49 или 52 зна- 63
Глава 1 чений, существует простой метод: использование расчета по модулю. Если нужно симулировать бросок кубика, используется модуль 6 и получаются значения между 0 и 5. Нужно только прибавить 1, и получатся привычные символы от 1 до 6. cube= rand() % 6 + 1; Если для игры необходимо действительно непредсказуемое начальное значение, я рекомендую использовать временны́е функции (см. стр. 405). Количество секунд, начиная с 1.1.1970 до текущего момента, отлично подходит в качестве начального значения. Если кажется, что эту величину можно легко рассчитать, используйте миллисекунды по модулю 1000, которые прошли в промежутке времени, в течение которого пользователь вводил данные. 1.4.5. Преобразование типа Преобразование значений одного типа в другой и обратно необходимо, если нужно присвоить переменную одного типа переменной другого типа. Так, при делении двух целых чисел получается целое число. Если 3 разделить на 4, получится 0 (и 3 в остатке). Но если нужно вместо этого получить 0,75, следует один из операндов обозначить в качестве числа с плавающей запятой, чтобы язык C++ использовал при обработке деление для чисел с плавающей запятой. Такое преобразование называют «кастинг»1. Чтобы выражение одного типа преобразовать в другой, имеются два определения. Первое унаследовано от языка C. Перед выражением в скобках указывается желаемый тип результата. В языке C++ ввели определение, при котором за именем типа следует в скобках выражение, которое будет преобразовано в желаемый тип: int value; value = (int)something; // Классический C-кастинг value = int(something); // C++ Некоторые преобразования в языке C++ проводятся без появления запросов. Это происходит в тех случаях, когда преобразование не ведет к потере информации, хранящейся в переменной. Например, переменная типа short или константа будет просто преобразована в тип long. Здесь нет никаких проблем. Любое значение типа short может быть 1 Имя происходит от названия кастинг-шоу, популярных телепередач, во время которых происходит отбор конкурсантов. 64
Введение в программирование присвоено переменной типа long. В обратном случае все усложняется. Число 200 000 не поместится в переменной типа short, поскольку она занимает только 2 байта. Здесь компилятор, скорее всего, выдаст предупреждение о том, что информация может быть потеряна. Может произойти так, что компилятор вместо чисел с плавающей запятой использует целые числа. Например, должно быть использовано классическое тройное правило: 3 помидора стоят 4 рубля. Сколько стоят 5 помидоров? В программе это будет рассчитано следующим образом: float price = (4/3)*5; Содержимое переменной price может удивить — оно равно 5. Причина в том, что компилятор производит вычисление 3/4 как для целых чисел и обрезает значения после запятой. Так что результат деления будет равен 1. Умножение его на 5 даст вышеуказанное значение. Однако скорее следует ожидать, что на кассе придется заплатить 6,67 рубля, и продавец отклонит все доводы в пользу программы. В таких случаях можно применить преобразование типов. Нужно как минимум один из операндов выражения конвертировать в тип float, чтобы произвести расчет для чисел с плавающей запятой. После согласования расчет даст ожидаемые 6,66667. float price = (float(4)/3)*5; Наконец стоит сказать, что скобки вокруг 4/3 с точки зрения языка C++ совсем не лишние, ведь выражение содержит операции одинакового приоритета, и следовательно, будет вычисляться слева направо. Поэтому скобки имеют далеко не формальное значение. Тот, кто впоследствии станет искать ошибку в программе, сразу увидит, как автор хотел расставить приоритеты. Если в каком-то выражении возникают сомнения в приоритете вычислений, лучше указать больше скобок. Программа от этого не станет выполняться медленнее. Компилятор все равно уберет такие скобки при оптимизации. 1.5. Ввод и вывод Вы с радостью убедитесь в том, как удивительно компьютер умеет считать. Но даже если любопытство не относится к вашим порокам, так или иначе однажды вам захочется узнать, что же рассчитал компьютер. Рассмотрим функции вывода. 65
Глава 1 1.5.1. Потоковый вывод командой cout Язык C++ использует для ввода и вывода потоковую модель. Переменные при этом следуют за объектом вывода, который носит имя cout. Поток вывода и поток ввода находятся в одной библиотеке. Перед первым использованием этих функций в программе нужно написать следующие строки: #include <iostream> using namespace std; В первой строке командой #include к программе подключается файл iostream. Имя файла здесь заключено в угловые скобки. Это сообщает компилятору, что файл находится в директории, указанной по умолчанию, а не в директории компилируемой программы. Вторая строка означает, что будет использовано пространство имен std. Пространства имен позволяют разграничить одинаковые имена переменных. Чтобы имя из стандартной библиотеки можно было использовать в собственной программе или в других библиотеках, оно расположено в пространстве имен std. Для каждого имени можно установить, будет ли оно использовано в пространстве имен std или в другом. Чтобы не возникало конфликтов, следует использовать команду using namespace, которая установит, что пространство имен std распространяется на все элементы программы. Пространства имен будут подробно рассмотрены на стр. 327. Если используется старый компилятор C++, то он может еще не учитывать пространства имен. Тогда он не сможет найти и файл iostream. В таком случае придется использовать старые варианты подключения файла iostream.h. Новый компилятор GNU Compiler при этом будет выдавать предупредительные сообщения, что использование файла iostream.h совсем устарело. #include <iostream.h> Для вывода данных используется команда cout1. Затем следует комбинация символов <<, которые указывают направление потока, затем выводимые данные. 1 «Out» в команде cout переводится с английского языка как «наружу». Это направленный объект вывода. Буква «с» стоит впереди потому, что это любимая буква Бьерна Страуструпа. Поэтому он и назвал свой язык C++. 66
Введение в программирование cout << result; cout << endl; Листинг 1.20. Вывод переменной В примере выводится содержимое переменной result. В строке ниже в поток вывода отправляется значение endl. Это не переменная, определенная программистом, а предопределенная константа, обозначающая конец строки. Использование endl способствует тому, чтобы вывод был с новой строки и все выводимые данные сразу отображались на экране. Чтобы улучшить читаемость кода и сократить работу при его наборе, можно указывать несколько выводимых объектов друг за другом, разделяя их комбинацией символов <<. cout << result << endl; Листинг 1.21. Последовательность вывода Так можно выводить не только переменные, но и константы. Это особенно удобно при использовании символьных последовательностей, с помощью которых можно добавить к результатам программы пояснительный код. cout << "Результат: " << result << endl; Листинг 1.22. Выходная последовательность Этот пример демонстрирует, что значение, которое сейчас появилось на экране, является результатом работы программы. Такая информация повышает комфорт при работе с программой. Для вывода ошибок предусмотрен объект cerr. Обычно вывод происходит так же, как при использовании команды cout. Если же пользователь прерывает стандартный ход программы, ошибки выдаются не в потоке вывода, а возникают на экране. Поэтому следует выводить данные с помощью команды cout, а сообщения об ошибках с помощью команды cerr. 67
Глава 1 1.5.2. Форматированный вывод Если выводить командой cout последовательно несколько строк, они будут отображены друг за другом без каких-либо разделяющих знаков. Вывод значения 8 и сразу же за ним следующий вывод числа 16 будут отображены на экране в виде 816. Соответственно, следует указать между ними как минимум пару пробелов или поясняющих слов. cout << "Было введено число: " << input << "После увеличения его вдвое получилось " << output << endl; При выводе таблицы могут возникнуть определенные сложности. Чтобы узнать, сколько пробелов нужно выводить после каждого значения, нужно сначала выяснить, сколько позиций занимает каждое число. Чтобы это упростить, существует, так называемый, манипулятор setw()1. Он указывается перед командой cout и форматирует последующую выводимую информацию. В скобках setw() указывается количество позиций, которые резервируются для последующего вывода. Все позиции, которые не занимает выводимая информация, заполняются пробелами, данные выравниваются по правому краю. Прежде чем работать с манипуляторами, нужно подключить файл iomanip с помощью команды include. Эти строки располагаются в самом начале программы: #include <iostream> #include <iomanip> using namespace std; В следующем примере символы обеих выводимых строк расположены рядом друг с другом и выровнены по правому краю. cout << setw(7) << 3233 << setw(6) << 128 << endl; cout << setw(7) << 3 << setw(6) << 1 << endl; Дополнительную информацию о манипуляторах можно найти на стр. 387. 1.5.3. Потоковый ввод командой cin Чтобы считать данные, вводимые с клавиатуры, используется команда cin 2. 1 2 Слово «setw» состоит из «set width», что переводится как «установить ширину». «in» в «cin» означает «внутрь». Это объект ввода. Происхождение буквы «с» я уже объяснял. 68
Введение в программирование Оператор ввода состоит из двух символов >>. Они указывают на переменную, в которую будут записаны вводимые данные. // Демонстрация ввода и вывода #include <iostream> using namespace std; int main() { int input_number; int increase; cout << "Пожалуйста, введите число!" << endl; cin >> input_number; increase = input_number * 2; cout << "Число, увеличенное вдвое " << increase << "." << endl; } Листинг 1.23. Простой диалог между пользователем и программой (inout.cpp) 1.6. Практические задания • Перепишите листинг 1.23. Скомпилируйте и запустите. Введите разные значения. В главе 6 вы найдете описание первых шагов при работе с компилятором. • Измените программу, удаляя некоторые части. Подумайте, к чему приведут эти изменения. Если вы получите от компилятора сообщение об ошибке, прочтите его. Попробуйте установить связь между изменениями, внесенными в программу, и сообщением от компилятора. • Напишите программу, которая выводит на экран два слова «Привет, пользователь». • Напишите программу netto.cpp, которая в соответствии с вводимой ценой за массу нетто товара вычисляет и выдает налог на добавочную стоимость. Будьте министром финансов и сами установите закон о добавочной стоимости для вашей программы. • Дополните программу netto.cpp так, чтобы она выводила цену за массу брутто товара. Решение вы найдете на стр. 496. 69
Глава 2 ЦИКЛИЧЕСКОЕ ПРОГРАММНОЕ УПРАВЛЕНИЕ Игра пришла в движенье. До сих пор программы состояли только из следующих друг за другом команд. Даже указания по приготовлению кофе можно было описать последовательностью действий: Поставить новый одноразовый бумажный фильтр в кофеварку Насыпать 6 ложек молотого кофе в фильтр Налить в кофеварку 1 литр воды Включить кофеварку Подождать Выключить кофеварку Взять кофейник с напитком На самом деле это далеко не полное описание процесса. На практике действия выполняются в зависимости от состояния переменных и отдельные блоки повторяются. Подробное описание процесса приготовления кофе выглядит следующим образом: Если в кофеварке находиться уже использованный одноразовый бумажный фильтр { Вынуть его из кофеварки Выкинуть в мусорный бак } Вынуть новый фильтр из пачки фильтров Вставить фильтр в кофеварку Повторить 6 раз: { Насыпать ложку молотого кофе в фильтр 70
Циклическое программное управление } Налить в кофеварку 1 литр воды Включить кофеварку Повторить: { Подождать минуту } до тех пор, пока кофе не перестанет капать из фильтра Выключить кофеварку Забрать напиток В этой главе будет описано, как управлять ходом программы в зависимости от значения ее переменных, и как могут повторяться отдельные части кода. 2.1. Ветвление Есть различные причины, из-за которых программа, в зависимости от значения определенных переменных, не должна выполнять те или иные операции. Приведу несколько типичных примеров: • Совершенно очевидно, что программа не должна производить деление, когда знаменатель равен 0. • Перед тем, как извлечь корень, стоит проверить, не является ли число отрицательным. Программе не следует стрелять в инопланетян, если последняя ракета уже выпущена из космического корабля. В таких случаях программа должна менять ход своего выполнения в зависимости от значения определенных переменных. Для этого необходима команда, которая при определенном условии пропустит одну или несколько строк кода. Чтобы сформировать такое условие, необходимо использовать операторы, с помощью которых можно сравнивать различные величины. 2.1.1. По условию: if С помощью команды if1 можно исключить выполнение отдельных частей кода. Синтаксис команды if описан на рис. 2.1. 1 В пер. с англ. «если» или «в случае». Прим. ред. 71
Глава 2 Рис. 2.1. Синтаксис команды if После ключевого слова if следуют скобки, в которых формулируется условие, при выполнении которого будет обработан блок кода. Этот блок может содержать одну или несколько команд, заключенных в фигурные скобки (рис. 2.2). Рис. 2.2. Граф блока команд Надежнее всего команды и блоки команд, следующие после условия или объявления цикла, отделять от основного кода. Некоторые редакторы делают это автоматически. Для отделения можно использовать табуляцию или пробелы. Если используются несколько различных редакторов или систем, лучше производить разделение с помощью знаков пробела, поскольку табуляцию различные редакторы интерпретируют по-разному. В первом примере показано деление двух переменных. Если бы делитель был равен 0, это привело бы к сбою программы. Поэтому осторожный программист перед расчетом организует проверку, равен ли делитель 0. Знак неравенства состоит из символов !=. if (denominator != 0) result = numerator / denominator; Листинг 2.1. Проверка Деление будет произведено только в том случае, если условие выполняется, то есть делитель не равен нулю. После команды if может стоять еще одно условие. Оно будет проверено только в том случае, если выполняется первое. Такие последова- 72
Циклическое программное управление тельности называются вложением. В следующем примере представлены вложения операторов if. Здесь переменные проверяются на равенство. Знак равенства состоит из символов ==, это сделано для того, чтобы можно было легко отличать проверку от операции присваивания. c=2; if (a==0) if (b==0) c=0; cout << c << endl; Листинг 2.2. Вложения оператора if Проверка, содержит ли переменная b ноль, будет выполнена только в том случае, если переменная a содержит 0. И только если значения обеих переменных равны 0, переменной c тоже будет присвоен 0. Иначе ее значение будет и в дальнейшем равняться 2. 2.1.2. Иначе: else Вернемся к примеру с делением. Было бы здорово, если бы программа не только проверяла, равен ли делитель 0, но и информировала бы пользователя о том, что деление в таком случае не было произведено. Это сообщение должно появляться только тогда, когда условие не выполняется. Такой результат можно получить при использовании двух конструкций if. if (denominator != 0) result = numerator / denominator; if (denominator == 0) cout << "Делитель равен 0! Расчет невозможен!"; Листинг 2.3. Проверка двух противоположных условий Когда то и дело требуются такие конструкции, язык C++ предлагает особую команду для случаев, когда условие в скобках оператора if не выполняется. Команда обозначается ключевым словом else. 73
Глава 2 if (denominator != 0) result = numerator / denominator; else cout << "Делитель равен 0! Расчет невозможен!"; Листинг 2.4. Упрощенная проверка противоположного условия На рис. 2.3 показан полный граф синтаксиса для if - else. Рис. 2.3. Граф синтаксиса if - else Структурные диаграммы Код программ, в которых часто используются вложенные условия и циклы, быстро становится плохо читаемым. Чтобы процесс выполнения программы представить наглядно, используются различные диаграммы. В этой книге используется диаграмма Насси-Шнейдермана. Такие диаграммы являются чем-то вроде дизайнерского инструмента для разработки четко структурированных программ. В диаграмме Насси-Шнейдермана программа представлена в виде большого прямоугольника. Каждая часть программы начинается сверху и заканчивается внизу. Никаких других выходов из программы больше нет. Программа, в которой нет проверок какого-либо условия и структур контроля, выглядит как кирпичная стена (рис. 2.4). Рис. 2.4. Простая структурная диаграмма При выполнении проверки блок вертикально делится пополам. Левая часть будет содержать команды, которые следуют в случае, когда 74
Циклическое программное управление условие выполняется, правая — команды, которые следуют, если оно не выполняется. Условие пишется в верхней половине разделенного блока команд, то есть в перевернутом треугольнике. На рис. 2.5 представлено вложенное условие, которое описывает последовательность действий для случая, когда человеку нужно сходить в туалет. Рис. 2.5. Вложенное условие if в виде структурной диаграммы Рассмотрим структурную диаграмму на примере программы для отдела банка. Обычно клиент получает 3% от вклада. Если деньги лежат на счете больше трех лет, клиент получает 4%, а если больше шести лет — 5%. Рис. 2.6. Начисление процентов в виде структурной диаграммы Эта диаграмма может сразу же быть описана программно: 75
Глава 2 if (year<3) { percent=3; } else { if (year<6) { percent=4; } else { percent=5; } } Листинг 2.5. Вложенное условие if «Висящий» else Особая проблема с вложенными условиями может возникнуть в том случае, когда при нескольких операторах if существует только один оператор else. Эта проблема получила название «проблема висящего else». if (a == 0) if (b == 0) c=5; else cout << "К чему я отношусь?"; Листинг 2.6. «Висящий» else При беглом рассмотрении можно подумать, что команда else относится к проверке переменной а. Но это только на первый взгляд. Из-за того, что else располагается сразу же после проверки переменной а, кажется, что else к ней и принадлежит. Но это не так. Язык C++ в таком 76
Циклическое программное управление случае относит ключевое слово else к последней команде if, таким образом, оно принадлежит к проверке переменной b. Такая ситуация будет однозначной, если команды после if написаны в фигурных скобках. Оба возможных варианта расположены рядом друг с другом для лучшей наглядности. Результат будет одинаковым в обоих случаях. В левом примере else относится к проверке переменной a, в правом — к внутренней проверке переменной b. if (a == 0) if (a == 0) { { if (b == 0) if (b == 0) { { c=5; c=5; } } } else else cout << "Куда?"; cout << "Куда?"; } Листинг 2.7. Фигурные скобки однозначно указывают принадлежность При установке границ блоков ситуация сразу становится понятной. Пока не нарушаются правила формирования блоков, всегда будет понятна структура кода и куда относится команда else. В этом примере отлично видно, что использование фигурных скобок улучшает читаемость кода и наглядно демонстрирует, к чему относятся те или иные команды. Уже по этой причине рекомендуется не экономить на них при использовании оператора if. 2.1.3. Вариант за вариантом: switch case Команда if позволяет реализовать проверку только двух вариантов: выполняется условие или нет. Если переменная в ходе выполнения программы может принимать несколько различных значений, и при каждом из них должна выполняться определенная последовательность команд, можно использовать несколько вложенных операторов if или специальную команду проверки. Отличным примером служит супермаркет, 77
Глава 2 который включает в себя несколько этажей, и на каждом этаже представлен отдельный вид товаров. Сначала задача будет решена посредством каскадного использования команды if. if (level == 1) { cout << "Сладости, книги" << endl; } else if (level == 2) { cout << "Одежда" << endl; } else if (level == 3) { cout << "Одежда" << endl; } else if (level == 4) { cout << "Игрушки" << endl; } else if (level == 5) { cout << "Электроника " << endl; } else { cout << "Гараж" << endl; } Листинг 2.8. Каскадная проверка оператором if Если посмотреть внимательно, то я здесь немного смошенничал. Вообще, после каждой команды if и else нужно делать отступы. Но в дан- 78
Циклическое программное управление ном конкретном случае листинг не стал запутаннее от того, что эти отступы я опустил. Такую форму можно часто встретить в листингах, если команда if следует сразу же за командой else. Эта конструкция будет значительно проще, если использовать оператор выбора. switch (level) { case 1: cout << "Сладости, книги" << endl; break; case 2: case 3: cout << "Одежда" << endl; break; case 4: cout << "Игрушки" << endl; break; case 5: cout << "Электроника" << endl; break; default: cout << "Гараж" << endl; break; } Листинг 2.9. Оператор case Оператор выбора начинается с ключевого слова switch. В следующих за ним скобках указано целочисленное выражение, результат которого управляет разветвлением программы. Это выражение может быть переменной, как например переменная level, или вычислением, которое в результате даст целое число. Можно также использовать буквы, поскольку в языке C++ буква является числом. В примере используется переменная level, очевидно принимающая только целочисленное значение. 79
Глава 2 В следующем за ключевым словом switch блоке находится строка проверки, начинающаяся ключевым словом case. Затем следует константа, имеющая значение, с которым будет сравнена проверяемая переменная, после чего стоят две точки. Если целочисленное выражение равно константе, программа будет выполнять указанные далее команды до тех пор, пока не встретит оператор break. После этой команды программа покинет текущий блок. Последующие операторы case не остановят программу. Если оператор break отсутствует, то программа будет выполнять последовательно все команды блока switch - case. Если проверяемая величина не равна ни одному из определенных значений case, программа будет выполнять команды, указанные после ключевого слова default. Рекомендуется всегда указывать defaultвариант. Однако это необязательно. На рис. 2.7 представлен граф синтаксиса оператора выбора Рис. 2.7. Граф синтаксиса команды switch Блок case представлен на отдельном графе на рис. 2.8. Рис. 2.8. Граф синтаксиса блока case В некотором отношении этот граф не совсем корректный. Если следовать ему, можно предположить, что разрешено многократное использование оператора default в одном блоке case. Однако разрешается наличие только одной default-ветви в одном блоке case, поскольку иначе это потеряет всякий смысл. Мы еще рассмотрим такой случай на другом, несколько более сложном графе. В конце концов, правило, что каждая константа может быть только однократно использована, также здесь не отображено. Но для наглядности я все-таки оставлю этот упрощенный граф. 80
Циклическое программное управление Пример с этажами может создать впечатление, что все возможные значения проверяемой величины должны быть представлены непрерывной последовательностью. Это бессмысленно. Не нужно сортировать значения в порядке возрастания. Также не обязательно в конце использовать оператор default. На практике оператор выбора используется в случаях, когда необходимо проверить реакцию на команду или результат работы. В программировании графических сред часто можно встретить такой оператор выбора. Программы ожидают результата ввода данных с клавиатуры, мыши или другого источника, распознают его, основываясь на сообщении от системы, и реагируют заранее предписанным образом. Системные сообщения обычно являются целыми числами. В следующем примере показано, что будет, если в переменной symbol хранится символ. Оператор выбора интересуется символами от '3' до '6' и символом '9'. Самый простой случай — если значение переменной symbol будет равно '6'. Тогда переменной е будет присвоено значение 1. Также и с цифрой '5' все достаточно просто. В этом случае, переменная d получит значение 2. switch (symbol) { case '3': case '4': case '9': a = 4; case '5': d = 2; break; case '6': e = 1; break; } Листинг 2.10. Оператор выбора Если значение переменной symbol равно '3', '4' или '9', тогда сначала переменной a будет присвоено значение 4. Поскольку затем нет коман- 81
Глава 2 ды break, переменной d будет присвоено 2. Затем команда break прервет текущий ход программы. Поскольку допустимо опускать оператор break, сообщений об ошибке компилятор не выдаст, если оператор не будет указан. Такие ошибки сложно найти, поскольку часто следующую команду case понимают как окончание предыдущего блока case. В этом примере также показано, что оператор default необязателен. Остальные значения проверяемой переменной просто не будут обработаны. 2.1.4. Короткая проверка с помощью символа вопросительного знака С помощью вопросительного знака можно сократить проверки таких условий, где вычисление влияет только на одну величину. В общем, если требуется выражение, которое меняется в зависимости от вычисления одной величины, его можно коротко сформулировать с помощью оператора вопросительного знака. Типично использование вопросительного знака в функциях вычисления абсолютной величины. Они всегда возвращают положительное значение. Если передаваемое в функцию значение отрицательное, то оно должно быть умножено на –1. С помощью команды if это будет сформулировано следующим образом: if (value >= 0) { Abs = value; } else { Abs = - value; } Листинг 2.11. Расчет абсолютной величины с помощью команды if С помощью вопросительного знака можно определить это значительно короче. Сначала нужно сформулировать условие (здесь: value >= 0). Затем следует знак вопроса. После этого следует указать выражение, 82
Циклическое программное управление которое должно использоваться, если условие выполняется (здесь: Abs = value ). Через двоеточие должно располагаться выражение, которое будет использоваться в случае, если условие не выполняется (здесь: Abs = - value). Abs = value >=0 ? value : - value; Листинг 2.12. Расчет абсолютной величины с помощью оператора вопросительного знака Знак вопроса можно указать в любой позиции, где ожидается выражение, то есть проверка (рис. 2.9). Выражение с вопросительным знаком строится следующим образом: Рис. 2.9. Граф синтаксиса выражения с вопросительным знаком Многих программистов этот оператор сбивает с толку, особенно тех, кто привык к другим языкам программирования, которые не происходят от языка C, поскольку там нет ничего похожего. Следующий пример рассчитывает минимум между переменными a и b и демонстрирует, как вопросительный знак может сократить программный код. Minimum = a < b ? a : b; Перед знаком вопроса проверяется, меньше ли переменная a переменной b. Если это так, тогда переменная a будет минимальной, и поэтому она находится между знаком вопроса и двоеточием. В обратном случае, переменная b будет минимальной, и поэтому она расположена после двоеточия. 2.2. Булевы выражения «Что есть истина?», — спрашивает Понтий Пилат в Ин. 18, 38. В языке C++ поиск истины осуществляется с помощью булевых выражений. Они могут использоваться в операторах if и ?, а также в циклах. Так что пришло время рассмотреть их поближе. 83
Глава 2 Результат условия может принимать два состояния: истина или ложь. Для истинного состояния в языке C++ используется ключевое слово true, для ложного — false. Язык C не знал этих двух ключевых слов, однако интерпретировал число 0 в качестве лжи и любое другое число в качестве истины. Язык C++ унаследовал эти свойства, так что false можно задавать с помощью 0. 2.2.1. Переменные и константы В языке C++ можно задавать логические константы. Они имеют тип bool и могут принимать значения true или false. Хотя для таких переменных теоретически используется только 1 бит, на практике они занимают минимум 1 байт. bool little = true; little = a < b; if (little) { ... Листинг 2.13. Булевы переменные В языке C также можно найти булевы типы и константы. В среде Windows распространены, в основном, BOOL, TRUE и FALSE. Они не являются элементами языка, а определяются с помощью макросов (стр. 325), и подразумевают под FALSE — 0, под TRUE — 1, а под BOOL — тип char или int. 2.2.2. Операторы Самый простой способ сформулировать выражение — это сравнить два численных значения. Чтобы узнать, больше или меньше первое число второго, между операндами указывается знак <. Для быстрого запоминания можно представить, что знак < сделан из знака =, в котором слева (со стороны прочтения) расстояние между двумя линиями уменьшилось. if (a < b) 84
Циклическое программное управление Последующие за этой строкой команды будут выполнены только в том случае, если содержимое переменной a будет меньше содержимого переменной b. Если добавить к знаку < знак =, можно получить условие «меньше или равно», которое проверит, меньше или равно левое значение правому. if (a <= b) Для проверки условия «больше или равно» используется аналогичная комбинация из символов >=. Между этими символами нельзя указывать пробел. Чтобы проверить равенство двух значений, в языке C++ используются два стоящих друг за другом знака равенства. Таким написанием эта операция отличается от операции присваивания. if (a == b) Следующие за этой строкой команды будут выполняться только если значения переменных a и b равны друг другу. К сожалению, здесь может подстерегать ловушка. Посмотрите на следующий код: if (a=1) // Осторожно! Присваивание, а не проверка! { a=5; } Листинг 2.14. Коварная западня! На первый взгляд кажется, что переменной a будет сразу присвоено значение 5, если значение переменной a равно 1. Но на самом деле речь идет о присваивании а=1. После выполнения команды if переменной a будет присвоено значение 1. Вероятно, не это планировал программист. Отсюда следует, что результатом выражения в скобках всегда будет 1, потому что выполнение происходит справа налево. Однако любое число, отличное от 0, всегда интерпретируется как истина, и присваивание а=5 будет выполнено в любом случае. Правильная запись этого условия выглядит так: 85
Глава 2 if (a==1) { a=5; } Листинг 2.15. Исправлено! Вследствие особой коварности такого кода, если в условии будет указано присваивание, многие компиляторы выдадут предупреждение. Это не ошибка, поскольку в связи со своим происхождением от языка C, язык C++ позволяет использовать числовые значения в качестве булевых. Некоторые программисты приобретают привычку в проверочном выражении ставить на первое место константу, так как а==1 равноценно 1==а. Если вдруг по какой-то причине программист забудет указать второй знак равенства, при выражении 1=а компилятор тут же выдаст ошибку. С другой стороны, можно исходить из того, что программист, использующий такую форму при написании условия, вместе с мыслями о надежности программы, думает также и о вероятности того, что можно просто-напросто забыть указать второй знак равенства. Знак неравенства состоит из последовательности символов !=. Восклицательный знак представляет собой отрицание. Таблица 2.1. Сравнение числовых значений Оператор Значение a a a a a a a равно b ? a неравно b? a больше b? a больше или равно b? a меньше b? a меньше или равно b? == b != b > b >= b < b <= b В одном условии можно сравнить только два операнда друг с другом. Если нужно проверить, например, лежит ли значение переменной a в диапазоне между 5 и 10, нужно использовать две операции сравнения. Следующая проверка вряд ли будет одобрена компилятором. 86
Циклическое программное управление if (5<=a<=10) // не работает! { // сделай что-нибудь; } Листинг 2.16. Еще один коварный случай! Сначала компилятор вычислит выражение 5<=a1. Оно может быть истинным или ложным, и в результате получится 1 или 0. Далее полученное значение будет проверено на соответствие условию <=10. Поскольку 0 или 1 меньше 10, выражение в примере будет всегда иметь значение true. В действительности автор хотел здесь проверить, будет ли значение переменной a больше или равно 5, и меньше ли 10 значение переменной a. if (5<=a) { if (a<=10) { // сделай что-нибудь; } } Листинг 2.17. Решение с помощью вложения Эти выражения связаны таким образом, что должны выполняться оба условия, чтобы выполнился внутренний блок. 2.2.3. Объединение булевых выражений Если необходимо выполнение двух условий, результатом проверки которых будет true, можно использовать объединение с помощью операции «И». Пример: если некоторое значение должно лежать в определенном интервале, нужно составить два условия: значение должно быть равно хотя бы минимуму интервала и не должно превышать максимум. Оба условия должны быть определены, чтобы общее условие 1 Для экспертов: операция сравнения является операцией с левой привязкой. 87
Глава 2 выполнялось. Такое объединение называется «И» и обозначается символами &&. if (5<=a && a<=10) { // сделай что-нибудь; } Листинг 2.18. Решение с помощью «И» объединения Другой пример: фирма выплачивает дополнительный денежный бонус только в том случае, если сотрудник состоит в штате и работает в фирме минимум полгода. Здесь также должны выполняться оба условия и речь идет о необходимости использования «И» объединения. Следующая таблица истинности (табл. 2.2) демонстрирует все возможные комбинации обоих операндов и какой результат даст их «И» объединение: Таблица 2.2. Таблица истинности для операции «И» a b a && b true true false false true false true false true false false false Другое дело, когда необходимо проверить утверждение, что величина не входит в определенный интервал. Такая величина может быть либо больше максимума, либо меньше минимума. Здесь объединение с помощью операции «И» бессмысленно, поскольку в таком случае вполне достаточно, чтобы выполнялось одно из условий. Если значение меньше, чем минимум, значит, оно уже лежит вне интервала. Объединение, которое приводит к результату true, если одно из объединенных условий истинно, называется «ИЛИ»-объединением и обозначается с помощью символов ||1. 1 В некоторых таблицах символов этот символ изображается не непрерывной чертой, а с зазором посередине. 88
Циклическое программное управление if (5>a || a>10) { // сделай что-нибудь; } Листинг 2.19. Объединение «ИЛИ» Вернемся к одному из примеров: одна довольно щедрая фирма могла бы сделать так, что сотрудники, недавно устроившиеся в компанию, внештатные сотрудники, наравне с сотрудниками, которые работают в фирме уже больше двух лет, получали бы дополнительные бонусные вознаграждения. В этом случае было бы достаточно выполнения такого частичного условия как «ИЛИ», которое формирует полное условие. В примере будет использовано «ИЛИ»-объединение. Таблица 2.3. Таблица истинности для операции «ИЛИ» a b a||b true true false false true false true false true true true false Третий логический оператор отрицает булево значение. Оператор «НЕ» влияет на один операнд и обозначается в языке C++ с помощью знака !. Если выражение, перед которым указан восклицательный знак, равно true, то результат операции будет false. Таблица 2.4. Таблица истинности для операции «НЕ» a !a true false false true  Результатом операции «И» будет значение «истина» только в том случае, если все операнды имеют значение «истина». Результатом операции «ИЛИ» будет значение «истина» в том случае, если как минимум один из операторов имеет значение «истина». Обратите внимание на оба определения операций «И» и «ИЛИ». Языковое использование очень часто бывает неряшливым. Если, напри- 89
Глава 2 мер, в музее висит предупреждение, что запрещено кататься на роликах и есть мороженое, то в понимании булевой алгебры там можно спокойно разъезжать на роликах. Также можно есть мороженое. Нельзя только выполнять оба этих действия одновременно. Однако вряд ли получится убедить в этом смотрителя музея. Лучше полагайтесь на то, что он боится за музейные экспонаты, и вряд ли заботится о математических формулах. В повседневной речи «ИЛИ»-объединение наоборот уточняется до «или-или». Разница заключается в том, что операция «ИЛИ» возвращает значение «истина», если хотя бы одна из частей выражения имеет значение «истина». Выражение «или-или» означает, что только одна из частей выражения истинна. В языке C++ нет специального выражения для выполнения операции «или-или». Однако такая операция часто называется «сложение по модулю два» (в английском языке обозначается как XOR). Конечно, можно реализовать ее на основе «И» и «ИЛИ». Для этого рассмотрим таблицу истинности, где обозначено, когда выражение имеет значение true, а когда false. Таблица 2.5. Таблица истинности для операции «сложение по модулю два» a b a XOR b true true false false true false true false false true true false Существуют два варианта решения. Первый заключается в том, чтобы все строки, которые истинны, скопировать и объединить с помощью «ИЛИ». Пример такой копии — две средние строки таблицы. Первая строка звучит в качестве «a И НЕ b», где a истинно или true, а b — ложно или false. Вторая строка — «НЕ a И b». В конце концов, оба выражения связаны с помощью «ИЛИ». Для контроля можно построить следующую таблицу (табл. 2.6). Таблица 2.6. Таблица истинности (а И НЕ b) ИЛИ (НЕ a И b) a b a && !b !a && b (a && !b)|| (!a && b) true true false false true false true false false true false false false false true false false true true false 90
Циклическое программное управление Второй вариант представляет собой попытку связать воедино все строки, значение которые истинно. Если при этом появляется пара строк со значением «ложь», их надо исключить. Например, выражение «а ИЛИ b» включало бы три верхние строки. Только первая строка не подходит к выражению «a XOR b», так что она исключается с «И НЕ». Выражение имеет вид: «(a ИЛИ b) И НЕ (a И b)». Рассмотрим таблицу истинности (табл. 2.7) также и для этого выражения, чтобы проверить, правильно оно или нет. Таблица 2.7. Таблица истинности для выражения «(a ИЛИ b) И НЕ (a И b)» a b a || b a && b (a || b) && !(a && b) true true false false true false true false true true true false true false false false false true true false Закон Де Моргана При инверсии (отрицании) логических выражений можно столкнуться с некоторыми сюрпризами. Здесь действует закон Де Моргана: !(a && b) эквивалентно !а || !b !(а || b) эквивалентно !а && !b Для примера инвертируем условие, которое проверяет, лежит ли некоторое значение в определенном интервале. В результате инверсии условие должно было бы проверять, лежит ли значение вне интервала. Поскольку выражения таких условий уже были ранее определены, можно легко проверить результат их инверсии. if (a>=2 && a<=5) Самая простая форма построения инверсии — заключить выражение условия в скобки, а перед ними указать восклицательный знак. if (!(a>=2 %% a<=5)) Это выражение работает без нареканий, поэтому лучше использовать его, чем более элегантное, но, возможно, неверное. Красота нередко несет в себе много ошибок. Неправильно было бы, например, просто перевернуть операторы сравнения. if (a<2 && a>5) // быстро, но неверно! 91
Глава 2 Здесь проверялось бы, является ли переменная a меньше, чем 2, и в то же время больше, чем 5. Само собой разумеется, что не существует значения, которое удовлетворяло бы этому условию. Если взамен использовать закон Де Моргана, инвертировать условие по частям и заменить операции «И» на «ИЛИ», то результат будет корректный: if (!(a>=2) || (a<=5)) Теперь части целого выражения можно упростить: (!a>=2) легче преобразовать в a<2. Точно так же выражение !(a<=5) эквивалентно a>5. Таким образом, правильно преобразованное условие имеет вид: if (a<2 || a>5)) // правильная инверсия! Переменная a лежит вне интервала, если она меньше 2 или больше 5. Если одно из выражений истинно, то результатом проверки всего условия будет «истина». Если переменная a лежит в интервале, то оба выражения будут ложными, и результат проверки всего условия будет «ложь». Отсюда следует, что логическая операция «И» будет инвертирована в том случае, если каждая часть выражения будет инвертирована отдельно и оператор «И» будет заменен на оператор «ИЛИ». То же самое действительно и в обратном порядке: чтобы инвертировать логическую операцию «ИЛИ», каждая часть выражения должна быть инвертирована и операция «ИЛИ» заменена на «И». Короткое замыкание Программа при обработке логического условия не обязана проверять все части этого условия. В том случае, например, если в объединенном условии «И» при проверке первой его части получен результат «ложь», не требуется проверять остальные части условия, поскольку при любом их значении результатом всего условия будет «ложь». Точно так же, при объединении с помощью операции «ИЛИ» нескольких условий, если первое из них равно «истина», то все выражение будет иметь в результате значение «истина». Этот факт используется в языке C++ для увеличения коэффициента полезного действия. Программа в такой ситуации прервет дальнейший анализ условия и сделает выводы. В большинстве случаев подобное по- 92
Циклическое программное управление ведение едва ли можно почувствовать, так как выигрыш в скорости незначителен. Однако сложнее, если второе выражение помимо поиска истины выполняет что-либо еще. if ((salary<20000) && (personal_nr++>10)) { ... Листинг 2.20. Короткое замыкание При нормальном выполнении программой такой операции, значение переменной personal_nr будет увеличено на 1. Однако фактически это произойдет только в том случае, если зарплата не меньше 20000. Если же переменная personal_nr должна инкрементироваться даже для сотрудников с маленькой зарплатой, следует поменять последовательность условий: if ((personal_nr++>10) && (salary<20000)) { ... Листинг 2.21. Последовательность написания условий также важна! Но лучше вообще избегать подобных ситуаций. Внутри условия должны быть только проверки или логические операции. Арифметические операции надежнее располагать до или после условия. personal_nr++; if ((personal_nr>10) && (salary<20000)) { ... Листинг 2.22. Конец головоломкам Но если вы хотите сами представить логику, которой пользуется компилятор, советую определить ее подробно. Так более понятно, что объединенное условие состоит из двух простых условий. 93
Глава 2 if (salary<20000) { if (personal_nr++>10)) { ... Листинг 2.23. Идентично, но более наглядно Здесь более четко представлено то, что вы намереваетесь сделать. Так можно предотвратить ситуацию, когда кто-то впоследствии добавит ошибку при модернизации программы только из-за предположения, что программист-разработчик вероятно не знал, как компилятор будет обрабатывать такую конструкцию. 2.3. Постоянные повторения: циклы Существует много задач, в которых нужно повторять команды, пока не будет получен рассчитываемый результат. Такое повторение в программировании называется циклом. Любая форма перечисления будет реализована в программе с помощью цикла. При расчете нулевых условий по правилу ложного положения (или метод regula falsi) в каждом цикле вычисляется новое приближение, пока не будет найдено значение функции при нулевых условиях с указанной точностью. В компьютерной игре можно стрелять до тех пор, пока не будет уничтожен последний космический корабль. Цикл повторяет определенные команды, но всегда имеет условие, по которому он будет прерван. Или, если сформулировать позитивно: цикл будет повторяться до тех пор, пока действительно его условие. Необходимо внимательно следить за правильностью условия цикла. Случается, что тот не заканчивается, поскольку его условие всегда выполняется. Такие циклы называются бесконечными. Еще одной причиной их возникновения может быть то, что, хоть условие и сформулировано верно, программист забыл, что данные, которые опрашиваются в условии цикла, в теле цикла необходимо изменять. 2.3.1. Цикл с предусловием: while Самый простой цикл начинается с ключевого слова while. В переводе на русский язык оно означает «пока». И это уже дословно описывает 94
Циклическое программное управление то, как работает данный цикл. Он повторяет команды, пока некоторое условие выполняется. Такие циклы также используются в повседневной жизни. Пока суп недостаточно соленый, мы сыпем в него соль. И, как вначале проверяется содержание соли, так и в цикле while сначала проверяется условие, а затем происходит выполнение команд. Синтаксис цикла очень напоминает команду if. Фактически различие состоит только в том, что в цикле эта команда повторяется. Если условие не выполняется, цикл ведет себя так же, как оператор if: команды попросту не будут выполняться. Цикл начинается с ключевого слова while. В скобках указывается условие, в случае выполнения которого, будут выполняться следующие за ним команды. Здесь также можно объединить несколько команд в один блок, если заключить их в фигурные скобки. На рис. 2.10 показан граф описанного синтаксиса. Рис. 2.10. Граф синтаксиса while Подумайте, что вы делаете, когда считаете. Вы начинаете с определенного числа, обычно с 1. Потом следует повторение, при котором вы увеличиваете число на 1. Когда вам захочется заняться чем-то другим, помимо счета, вы прервете повторение на определенном значении. В программе используется переменная, которая устанавливается в качестве начальной. Ее значение должно быть определено до начала цикла, иначе при каждом повторении переменной будет присваиваться ее начальное значение, и цикл станет бесконечным. Предположим, в процессе выполнения цикла программа каждый раз увеличивает переменную на 1. Цикл начинается с ключевого слова while. Сразу же за ним в скобках формулируется условие. До тех пор, пока условие действительно, программа остается в цикле. Если нужно посчитать до 10, следует проверять, меньше или равна переменная 10. Сразу за условием можно определить команду, которая будет повторяться в цикле. Если требуется увеличивать переменную и выводить ее значение на экран, то нужно использовать две команды. Это можно организовать, используя блок, то есть структуру с фигурными скобками. В блоке будет расположен вывод переменной и последующее ее увеличение. 95
Глава 2 #include <iostream> using namespace std; int main() { i = 1; // Установлено начальное значение переменной while (i <= 10) // Условие цикла { cout << i << endl; // Вывод i++; // Без этой строки цикл будет выполняться //бесконечно } } Листинг 2.24. Счет При определении границ цикла легко допустить ошибки. Если вместо <= будет использован только символ <, программа будет считать только до 9. Причина в следующем: как только значение переменной i достигнет 10, условие больше не будет выполняться и цикл прервется. Также можно построить цикл, выполняющийся 10 раз, в котором счет начнется с 0 и в условии станет проверяться, меньше ли значение переменной, чем число 10. Тогда переменная i будет принимать значение от 0 до 9. Если увеличивать значение i перед выводом его на экран, можно получить значения от 1 до 10. #include <iostream> using namespace std; int main() { i = 0; while (i < 10) { i++; cout << i << endl; } } Листинг 2.25. Счет, вариант №2 96
Циклическое программное управление while проверяет условие перед началом цикла. Это означает, что команды в нем не будут выполняться, если не выполняется условие цикла. Структурная диаграмма цикла while представлена на рис. 2.11. Проверка, будет ли цикл выполняться дальше, указана в самом начале и охватывает команду или блок команд, который выполняется в цикле. Рис. 2.11. Структурная диаграмма цикла while В следующем примере посчитаем в обратном порядке. i = 10; while (i > 0) { i--; } Листинг 2.26. Обратный счет Здесь переменная i имеет начальное значение 10. В условии цикла проверяется, больше ли нуля значение переменной i. Это верно, так что программа входит в цикл и начинает выполнять его тело. Тут происходит только уменьшение значения переменной i на 1, поэтому переменной i после первого прохода цикла будет присвоено значение 9. Это без сомнений больше, чем 0. Цикл будет повторяться до тех пор, пока значение переменной i вследствие декрементирования не уменьшится до 0. Тогда при следующей проверке условие цикла не будет выполняться, и программа сразу же его покинет. 97
Глава 2 В листинге 2.26 отсутствует вывод. В качестве маленького упражнения можно поразмыслить, что будет выводиться на экран, если расположить команду вывода перед или после декрементирования переменной i в цикле. Если вы уверены в том, что произойдет — проверьте! 2.3.2. Цикл с постусловием: do-while В некоторых ситуациях проверка в начале цикла неудобна. Иногда нужно произвести некоторое действие, и только по окончании его выполнения можно узнать, необходимо ли совершить это действие еще раз. Например, ребенок выпрашивает у деда сладости до тех пор, пока тот не отдаст ему их все1. Ребенок в любом случае попросит сладости, потому что никак иначе он не может проверить, выполняется ли условие. В этом случае условие будет проверяться только после выполнения тела цикла. Типичное использование цикла с постусловием в программировании — это проверка вводимых данных. Внутри цикла программа требует от пользователя ввод данных. Сначала происходит проверка их правильности. Если ввод не удовлетворяет условию, цикл повторяется еще раз. Еще один пример — игра «Сапер». Пользователь должен нажать на выбранную клетку. До тех пор, пока на выбираемых пользователем клетках нет мин, он может продолжать игру. Только по окончании цикла можно узнать, на какую клетку нажал пользователь. На рис. 2.12 представлен граф синтаксиса цикла do-while. Рис. 2.12. Граф синтаксиса цикла do-while Здесь также можно использовать блок с фигурными скобками, чтобы в цикле выполнялось несколько команд. Структурная диаграмма отличается от диаграммы while тем, что условие указано в конце цикла (рис. 2.13). 1 Наученные опытом родители твердо уверены, что конечное условие системы «дедушка» не выполняется. 98
Циклическое программное управление Рис. 2.13. Структурная диаграмма цикла do-while В следующем примере показан обратный счет. Рассмотрите листинг и подумайте, будут ли выведены цифры 10, 1 и 0! Затем проверьте, соответствуют ли действительности ваши предположения. #include <iostream> using namespace std; int main() { int i = 10; do { cout << i << endl; i--; } while(i>0); } Листинг 2.27. Цикл do-while 2.3.3. Шаг за шагом: for Оба рассмотренных ранее цикла требуют три элемента: • Инициализации перед началом цикла. • Условия, которое должно выполняться, чтобы цикл работал. • Некоторого действия, вследствие которого результат условия может измениться. 99
Глава 2 Если не хватает одного из этих элементов или он некорректно определен, есть риск получить бесконечный цикл. Цикл for «помнит» все эти три элемента. Так как они собраны в одном месте, их легко контролировать. Цикл, как правило, используется при счете; и в этом случае особенно проявляется предусмотрительность цикла for. В следующем примере показан счет от 0 до 9. for (i=0; i<10; i++) { cout << i << endl; } Листинг 2.28. Счет с помощью цикла for В скобках, которые следуют за ключевым словом for, вначале указывается инициализация стартового значения, где значение переменной i устанавливается равным 0. При использовании цикла while инициализация проводится перед началом цикла. На втором месте в скобках располагается условие цикла. Третья команда — инкрементирование переменной счетчика, которая обычно указывается в конце цикла. Эти три элемента разделяются с помощью точек с запятой. Цикл for структурирован следующим образом: Рис. 2.14. Граф синтаксиса цикла for Цикл for очень похож на цикл while. Только первый элегантнее объединяет все особенности второго и не позволяет программисту так легко забыть важные элементы. Верхнюю схему легко представить в качестве цикла while.  100 Стартовая команда; while(условие) { Тело цикла; Завершающая команда; }
Циклическое программное управление Как и было обещано, данная структурная диаграмма не отличается от диаграммы цикла while. Только часть, расположенная для while в начале цикла, определяется в цикле for в области условия (рис. 2.15). Рис. 2.15. Структурная диаграмма цикла for Некоторые программисты в структурной диаграмме помещают заключительную команду в поле команд for вместе со стартовой командой и условием. Если требуется несколько команд в стартовом или заключительном блоке, то не так просто объединить их в блок, заключив в фигурные скобки, и записать этот блок в скобках команды for. Тем не менее возможно поместить несколько команд в стартовой или заключительной части, просто разделяя их запятыми. Следующий пример иллюстрирует такой вариант: #include <iostream> using namespace std; int main() { int i, a; for (i=0, a=10; i<5; i++, a--) { cout << i << "-" << a << endl; } } Листинг 2.29. Несколько команд в управляющей части цикла for 101
Глава 2 Причина в том, что запятая — это оператор, который разделяет два выражения. И при этом объединяет их в одну команду. Соответственно с точки зрения компилятора i=0, a=10 — одна команда, которая состоит из двух выражений, разделенных оператором запятой. Три элемента в скобках цикла for могут отсутствовать. Если начальная инициализация не требуется, можно просто ее опустить и указать только точку с запятой. Так же и с заключительной командой. Тогда за вторым символом точки с запятой сразу следует закрывающая скобка. Особый вариант цикла for то и дело встречается в примерах программ. Если программист специально хочет написать бесконечный цикл, то для этого часто используется цикл for, в скобках которого нет ничего, кроме двух знаков точки с запятой. Две пустые позиции язык C++ расценивает, как истину. Очевидно, он всегда настроен оптимистично. Там, где условия цикла каждый раз являются истиной, программа никогда не выходит из цикла. Такой тип бесконечного цикла имеет, однако, другой выход, кроме как по главному условию, определенному вначале. Сюда относится, например, команда break, которая будет описана в следующей главе. for (;;) // будет работать до следующего отключения электричества { ... } 2.3.4. Выходы из цикла: break и continue Обычно выход четко определен условием цикла. Иной случай возможен при вызове команды break. При этом цикл будет покинут сразу же по выполнению команды. Обычно этот оператор располагается в середине тела цикла на случай возникновения особой ситуации, например ошибки. error=0; while (a>0) { ... c=4; 102
Циклическое программное управление if (error==9) { break; } a++; ... } Листинг 2.30. Цикл прерывается командой break Такой механизм, конечно, неслыханно практичен. Можно сразу же среагировать на определенное событие, которое требует покинуть цикл. Но такая конструкция имеет две ошибки, если рассматривать ее с точки зрения красоты стиля. Во-первых, уже не однозначно ясно, на основании какого условия был покинут цикл, во-вторых, команды до и после оператора if выглядят расположенными на одном уровне. Но все же команда a++ выполняется не по тому же условию, что с=4. Слишком частое использование оператора break свидетельствует о недостатке планирования. Вместо тщательно сформулированного условия и управления ходом программы с помощью проверки оператором if, цикл просто спонтанно обрывается. Следующий программный код эквивалентен предыдущему, но имеет преимущество в том, что процесс выполнения программы и конец цикла определены более четко. error=0; while (error!=9 && a>0) { ... c=4; if (error!=9) { a++; ... } } Листинг 2.31. Альтернатива оператору break 103
Глава 2 Перед тем, как использовать команду break для выхода из цикла, следует подумать над тем, можно ли написать код более элегантно. Если все-таки имеются причины работать с командой break, следует, как минимум, в начале цикла в комментарии указать на это отклонение от нормы. Единственное действительно осмысленное использование команды break — в операторе выбора switch-case, поскольку здесь дело заключается в том, что после обработки одного из случаев программа должна покинуть конструкцию switch-case. К той же категории относится команда continue. После ее выполнения программа не покидает цикл, а переходит сразу же к его концу. Таким образом, часть тела цикла, которая следует после команды continue, не будет выполняться. while (a>0) { ... c=4; if (error==9) { continue; } a++; ... } Листинг 2.32. Команда continue пропускает оставшуюся часть тела цикла Использование команды continue еще более простое, чем команды break. Ее единственное достоинство при использовании в проверочном блоке if состоит в том, что команда а++ не будет выполнена. Но, в то же время, именно это и является недостатком: можно не заметить, что эта команда будет выполняться только тогда, когда значение переменной error не будет равно 9. while (a>0) { ... c=4; 104
Циклическое программное управление if (error!=9) { a++; ... } } Листинг 2.33. Альтернатива команде continue 2.3.5. Грубый скачок: goto С помощью команды goto можно перейти в любую желаемую позицию программного кода в пределах выполняющейся функции (рассмотрены в главе 4). Цель перехода обозначается так называемой меткой. Метка состоит из имени и двоеточия. За обозначением метки всегда указывается как минимум одна команда. if (a==0) { a++; goto middle; } b+=2; if (b>6) { goto end; middle: // здесь b>6 или a==1 b=4; } else { c=2; } end: a++; Листинг 2.34. Дикие скачки 105
Глава 2 Прыжок в позицию с меткой middle: демонстрирует, какое безобразие возможно с помощью команды goto. Несмотря на то, что в таком переходе компилятор не увидит никаких изъянов, сложно предположить, что произойдет в результате выполнения такого кода. Если используется команда goto, то нельзя допускать переходов с ее помощью из одного блока в другой или покидать цикл. Если вы будете постоянно использовать в своих программах оператор goto, то в глазах ваших коллег это будет смотреться точно так же, как если бы вы в столовой, вместо того, чтобы использовать нож, резали бы шницель боевым топором. В старых (действительно старых) языках программирования был возможен только прямой прыжок, если требовалось перейти в другую позицию программного кода. Поскольку код таких программ был сложен для восприятия, в 70-х годах объявили крестовый поход против использования команды goto. Современные языки имеют все инструменты для организации условий и циклов без использования команды goto. Прыжок усложняет исполнимость программы. Поэтому следует его избегать. 2.4. Примеры Циклы и условия — это основные блоки каждой программы. Нужно потренироваться, чтобы в постановке задачи распознать условия и циклы и научиться тому, как можно их комбинировать. С этой целью в данной главе будут рассмотрены некоторые примеры. Они подобраны таким образом, чтобы продемонстрировать, как циклы и условия можно компоновать друг с другом. Диаграмма Насси-Шнейдермана используется здесь не только для наглядности, но и в качестве дизайнерского инструмента. 2.4.1. Простые числа Простое число — это число, которое можно разделить без остатка только на его собственное значение и на 1. Для расчета таких чисел не существует специальной формулы. Нужно для каждого конкретного числа проверять, можно ли разделить его без остатка на другое число или нет. 106
Циклическое программное управление Программа должна выводить все простые числа между 3 и 100. Таким образом, имеется большой перечисляющий цикл для каждого кандидата. #include <iostream> using namespace std; int main() { const int max_prime_number =100; // Конец расчета int prime_number; // Тестовый кандидат for (prime_number =3; prime_number <= max_prime_number; prime_number ++) // Проработать все числа { // Проверить, является ли число действительно //простым числом // Тестовый вывод: cout << prime_number << " "; } cout << endl; } Листинг 2.35. Простые числа с первым циклом В цикле организован тестовый вывод. Таким образом можно видеть, что простые числа действительно следуют от 3 до 100. Если вы не уверены, выполняют ли определенные части кода программы то, что вы ожидаете, спокойно используйте такой вывод. Потом его можно удалить. Если складывается впечатление, что выводимое количество чисел слишком большое, следует просто уменьшить значение переменной max_prime_number до 10. Следующим шагом каждый кандидат в простые числа должен быть проверен в цикле. Тут он будет разделен на числа от 2 до своего собственного значения минус 1. Если он делится без остатка, то эти цифры делимы между собой и кандидат не является простым числом. Значит, необходим следующий цикл, расположенный в уже существующем. 107
Глава 2 В этом цикле будет происходить деление. Он начинается с 2 и заканчивается на значении prime_number - 1. #include <iostream> using namespace std; int main() { const int max_prime_number =100; int prime_number, divisor; for (prime_number=3; prime_number<=max_prime_number; prime_number++) { // Проверка, является ли простое число действительно //простым for (divisor=2; divisor<=prime_number-1; divisor++) { // Тестовый вывод cout << divisor << " "; } cout << endl; } cout << endl; } Листинг 2.36. Простые числа с внутренним циклом Теперь надо проверить, делится ли prime_number на divisor. Для этого уже можно использовать команду %, которая вычисляет остаток от числа. Она возвратит 0, если при делении нет остатка. Таким образом, нужно составить условие, которое будет проверять, есть ли остаток от деления переменной prime_number на переменную divisor. #include <iostream> using namespace std; int main() { const int max_prime_number=100; 108
Циклическое программное управление int prime_number, divisor; for (prime_number=3; prime_number<=max_prime_number; prime_number++) { // Проверка, является ли простое число действительно //простым for (divisor=2; divisor<prime_number; divisor++) { // деление без остатка? if (0==prime_number % divisor) { // Число можно разделить, значит оно не //простое! // Тестовый вывод: cout << prime_number << "-" << divisor << endl; } } cout << endl; } cout << endl; } Листинг 2.37. Простые числа с проверкой на делимость Решение уже близко. Если запустить программу, будут выведены кандидаты в простые числа и их делители. Эти числа не являются простыми, поскольку у последних не может быть делителя. Если программа заметит это при выполнении данного фрагмента кода, она будет знать, что кандидат не является простым числом. Чтобы запомнить состояние, лучше всего использовать переменную типа bool. #include <iostream> using namespace std; int main() { const int max_prime_number=100; int prime_number, divisor; bool the_prime_number; cout << "2"; 109
Глава 2 for (prime_number=3; prime_number<=max_prime_number; prime_number++) { the_prime_number = true; // Проверка, является ли простое число действительно //простым for (divisor=2; divisor<prime_number; divisor++) { // Есть ли остаток от деления? if (0== prime_number % divisor) { // Число можно разделить, значит оно не //простое! the_prime_number = false; } } // Проверка окончена. // Если это простое число, выводим его! if (the_prime_number) { cout << ", " << prime_number; } } cout << endl; } Листинг 2.38. Расчет простых чисел завершен (prime_number.cpp) Эта программа выполняет требования, которые были установлены вначале. Прежде объявляется переменная the_prime_number. Для каждого нового кандидата значение этой переменной устанавливается равным true. Если появляется остаток от деления, значение переменной устанавливается равным false. После того как все делители будут проверены, если при этом значение переменной the_prime_number все еще останется true, то кандидат будет выведен на экран. Число 2 будет просто выведено в начале, без всяких расчетов. Вообще-то такое решение не является оптимальным, так как внутренний цикл продолжит выполняться даже в том случае, если при делении кандидата на 2 не будет остатка. Это можно легко остановить, если добавить в цикл еще одно 110
Циклическое программное управление условие. Оно станет проверять, было ли проведено деление числа для всех делителей, а также был ли при делении получен остаток. #include <iostream> using namespace std; int main() { const int max_prime_number=100; int prime_number, divisor; bool the_prime_number; cout << "2"; for (prime_number=3; prime_number<=max_prime_number; prime_number++) { the_prime_number = true; // Проверка, является ли простое число действительно //простым for (divisor=2; the_prime_number && divisor < prime_number; divisor++) { // Есть ли остаток от деления? if (0==prime_number % divisor) { // Число можно разделить, значит оно не //простое! the_prime_number = false; } } // Проверка окончена. // Если это простое число, выводим его! if (the_prime_number) { cout << ", " << prime_number; } } cout << endl; } Листинг 2.39. Оптимизированный расчет простых чисел 111
Глава 2 Эта программа возвращает точно такой же результат, как и предыдущая, однако выполняется более эффективно. Вероятно, это будет заметнее, если увеличить значение переменной max_prime_number до 10000. В завершении рассмотрим структурную диаграмму программы расчета простых чисел, представленную на рис. 2.16. Рис. 2.16. Структурная диаграмма программы расчета простых чисел 2.4.2. Наибольший общий делитель Наибольший общий делитель (НОД) двух чисел — это самое большое число, на которое без остатка делятся два числа. Расчет НОД используется в математике для дробей. Если числитель и знаменатель разделить на наибольший общий делитель, то дробь будет максимально сокращена. Алгоритм вычисления НОД может базироваться на алгоритме вычисления простых чисел. Для этого оба числа раскладываются на простые множители и все общие простые множители для этих чисел между собой перемножаются. Но можно сделать еще проще. Эвклид уже около 2000 лет назад описал очень легкий способ преобразования для вычисления НОД. Оба числа, для которых нужно найти НОД, назовем a и b. Идея основывается на том утверждении, что НОД — делитель не только для a и b, но 112
Циклическое программное управление также для b и (a-b), пока b больше, чем a1. Значит, можно рассматривать вместо a и b равнозначно b и (a-b) и уже не нужно искать, какое из этих чисел больше. Так же можно поступить с двумя другими числами, не только с a и b. Снова вместо большего числа будет использована разница между ними. При дальнейшем повторении этого способа, когда самое маленькое число станет равно 0, второе число при этом будет НОД (рис. 2.17). Рис. 2.17. Структурная диаграмма для поиска НОД В данном примере разработчик программы должен начинать со структурной диаграммы. Следом записываются оба кандидата. Затем цикл повторяется, пока меньшее число остается больше 0. Внутри цикла должно проверяться, какое из двух чисел больше. В данном случае значения могут меняться местами, перед тем как одно из них будет вычтено из другого. Так формализуется общее описание алгоритма и вставляется в структурную диаграмму. Почти все можно сразу описать кодом на языке C++. Только обмен содержимого в переменных нужно раскрыть подробнее, перед тем как вставить этот код в программу. Может возникнуть идея написать сначала a=b и потом b=a. Но это, однако, не станет работать, потому что содержимое переменной a при первом присваивании будет изменено и, следовательно, потеряно. Поэтому следует использовать вспомогательную переменную, которая сохранит содержимое переменной а перед тем, как оно изменится. help = a; a = b; b = help; Листинг 2.40. Обмен значений переменных 1 Я предполагаю, что вам не интересно доказательство данного утверждения. 113
Глава 2 Процесс обмена представлен еще раз в виде структурной диаграммы. Одновременно здесь использован синтаксис языка C++ (рис. 2.18). Рис. 2.18. Более подробная структурная диаграмма для НОД Теперь это легкая задача — перенести структурную диаграмму в соответствующую ей программу на языке C++. Эта диаграмма может служить подспорьем при разработке программы, прежде всего, если имеются отдельные сложные участки. #include <iostream> using namespace std; int main() { int a, b, help; cin >> a; cin >> b; while (b>0) { if (b>a) { // обмен a и b help = a; a = b; b = help; } 114
Циклическое программное управление a = a - b; } cout << a << endl; } Листинг 2.41. Программа на языке C++ для нахождения НОД 2.5. Упражнения • Дополните программу netto.cpp (см. стр. 497) из последней главы, чтобы она проверяла, больше ли 0 цена за массу нетто товара, и только тогда проводила расчет. • Добавьте в программу netto.cpp сообщение об ошибке, которое будет выводиться, если пользователь ввел некорректные данные. Пример решения приведен на стр. 497. • Сложные проценты: вы ежегодно вносите на счет под 5% годовых по 5000 Евро. Создайте таблицу с годовым взносом слева и состоянием счета справа. Пример решения на стр. 498. • Угадывание чисел: пусть компьютер выберет случайное число в диапазоне от 1 до 1000. Пользователь вводит число и компьютер сообщает, больше, меньше или равно это число тому, которое он выбрал. Это будет повторяться до тех пор, пока пользователь не угадает число. Пример решения приведен на стр. 498.
Глава 3 ТИПЫ ДАННЫХ И СТРУКТУР Час управления типами данных: в этой главе будут собраны воедино все базовые типы, а из них созданы новые. Для отображения реальных данных недостаточно только базовых типов языка C++. Однако их разрешается компоновать. С помощью компоновки стандартных типов в новые структуры данных программист может попытаться отобразить информацию такой, какой она является в действительности. 3.1. Массив Массив1 — это комбинация нескольких переменных одного типа. К элементам массива обращаются по их позиционному номеру. Массивом можно описать ряд элементов одного типа. Примером могут быть числа лото, где всегда требуется выбрать шесть целых чисел. Если требуется реализовать выбор шести чисел лото с помощью программы, можно использовать массив из шести элементов целого типа. 1 5 13 [0] [1] [2] 27 [3] 43 [4] 44 [5] Рис. 3.1. Числа лото в массиве На рис. 3.1 показано, как расположены числа лото в массиве. Под каждой ячейкой находятся квадратные скобки, в которых указана позиция числа. Вопреки человеческой привычке, нумерация элементов в массиве начинается с 0. Если в массиве хранится шесть чисел, последняя позиция будет иметь номер 5. 1 Понятие «массив» часто переводится как «поле». Однако этот термин не очень точен, и многие программисты предпочитают использовать слово «массив». Я также принадлежу к их числу, поэтому использую его в данной книге. 116
Типы данных и структур Чтобы объявить в программе массив для чисел лото, сначала нужно указать тип одного-единственного элемента. Затем следует имя массива. В квадратных скобках следует количество элементов в массиве, которые он должен содержать. int lotto[6]; То, что максимальный индекс массива 5, а элементов в нем 6, выглядит немного странно. Однако нужно привыкнуть, на этом еще неоднократно будет акцентироваться внимание. Также важно, что объявление массива требует выделить память под шесть переменных целого типа. Когда программа покидает блок, в котором находится объявление массива, память автоматически отчищается. Если требуется получить доступ к одному из элементов массива, сначала нужно указать имя всего массива. Затем в квадратных скобках следует номер элемента, доступ к которому требуется получить. При этом всегда важно помнить, что нумерация элементов в массиве начинается с нуля. lotto[2] = rand() % 49 + 1; cout << lotto[0]; Первая строка демонстрирует, как третьему элементу массива — не второму — можно присвоить случайное число от 1 до 49. Вторая строка выводит значение первого элемента на экран. Синтаксический опознавательный знак массива — это квадратные скобки. Они используются при объявлении массива для указания количества элементов, принадлежащих ему. Квадратные скобки также используются, когда необходимо получить доступ к элементу. Поэтому они называются индексным оператором. В примере показано, как заполнить массив чисел лото случайными значениями. int lotto[6]; srand(0); for(i=0; i<6; i++) { lotto[i] = rand() % 49 + 1; } Листинг 3.1. Числа лото в виде массива 117
Глава 3 Переменная lotto — массив чисел целого типа. Квадратные скобки с числом 6 внутри говорят о том, что здесь будет храниться 6 переменных. Как получить доступ к этим значениям, показано в цикле for. Каждой из шести переменных по порядку присваиваются случайные значения в диапазоне от 1 до 49. Которая из переменных используется — показано в квадратных скобках. Номер позиции называется индексом. В языке C++ он всегда начинается с 0. Поскольку в данном массиве 6 элементов, индекс приобретает значения от 0 до 5. Там, где элементы приобретают значение случайным образом, не исключена вероятность того, что два элемента будут одинаковыми. На следующем шаге разработки в случае одинаковых значений нужно выбрать их заново. Можно просмотреть в цикле все полученные при заполнении массива числа и проверить, нужно ли какое-то из них заменить. Следующая программа гарантирует, что все выбранные числа не повторяются. int lotto[6]; int i, j; bool new_number; srand(0); for(i=0; i<6; i++) // вытащим одно за другим шесть чисел { do // повторим вытаскивание числа до нового значения { // число отличается от всех предыдущих lotto[i] = rand() % 49 + 1; new_number = true; // удовлетворительный набор for (j=0; j<i; j++) { // просмотреть все, ранее вытащенные шарики if (lotto[j]==lotto[i]) { // здесь обнаружено дублирование значения new_number = false; } } } while (!new_number); } Листинг 3.2. Числа лото в виде массива (lotto.cpp) 118
Типы данных и структур Внешний цикл с переменной i в качестве индекса перебирает все выбранные числа лото по порядку. Сама процедура выбора числа происходит в цикле do-while, который должен повторяться, пока не будет выбрано такое число, которого еще не было ранее. Решение может быть принято только по окончании выполнения тела цикла. Поэтому в данном случае удобнее всего использовать цикл с постусловием. После выбора производится проверка. Во внутреннем цикле for просматриваются все ранее полученные числа. Это означает, что индекс j начинается с 0 и остается меньше i, поэтому он всегда меньше, чем индекс только что выбранного числа. В случае появления идентичного значения, переменная new_number типа bool будет установлена в false. Это приведет к повторению операции выбора числа. После каждой такой операции эта переменная должна устанавливаться в true, иначе цикл никогда не будет покинут, если однажды встретится повторяющееся значение. Как уже упоминалось, каждый массив начинается с индекса 0. Это часто сбивает с толку. Массив из шести элементов имеет максимальный индекс 5. Чтобы запутать вас окончательно, следует сказать, что при объявления массива из шести элементов, в квадратных скобках следует указывать 6. Для доступа к элементу максимальным числом в квадратных скобках будет 5. Однако чтобы достигнуть полного понимания этой конструкции, сначала нужно ее прочувствовать. int lotto[6]; lotto[0] = 1; // первый элемент! ... lotto[5] = 3; // шестой элемент, ok! lotto[6] = 4; // седьмой элемент: ошибка!!! Листинг 3.3. Переход границы Такой переход за границу существующего массива не будет ни отмечен компилятором, ни заметен при работе программы. По этой причине очень важно контролировать, соблюдает ли программа указанные границы массива. Прежде чем жаловаться на недостатки, следует учесть, что контроль границ не бесплатный. Система обработки текущей программы при каждом доступе к элементу массива должна была бы проверять, не перейдены ли границы. Ресурсы компьютеров при этом задействовались бы только потому, что некоторые программисты не удосужились проследить, переходит ли программа границы массива. 119
Глава 3 Язык C++ передает им ответственность за это и предлагает в качестве компенсации максимально высокую скорость доступа. Уже в начале главы цикл for был использован для работы с массивом. Цикл for и массивы «принадлежат» друг другу. Чтобы в таком цикле перебрать все элементы массива, нужно контролировать только некоторые моменты. Прежде всего, переменная индекса должна начинаться с 0. Условие следует формулировать как «значение индексной переменной меньше размера массива». В заключительной команде значение индексной переменной просто увеличивается. Особенно четко это видно, если размер массива задан некоторой константой, в данном случае MAXLOTTO. const int MAXLOTTO=6; int lotto[MAXLOTTO]; for (int i=0; i<MAXLOTTO; i++) { lotto[i] = ... } Листинг 3.4. Цикл for и массив Массив также можно инициализировать. При этом для каждого его элемента последовательно указываются числовые значения, разделяющиеся запятыми, и помещаются в фигурные скобки. const int MAXLOTTO=6; int lotto[MAXLOTTO] = { 12, 7, 45, 2, 21, 9 }; Листинг 3.5. Инициализация массива Здесь нужно указать, что такая запись функционирует только для инициализации, но не для присваивания. Нельзя в другой позиции кода программы таким же образом присвоить иные значения — присваивание возможно только поэлементно. Особый случай инициализации массива со значением null. В языке C++ любую структуру данных можно инициализировать со значением null, если при инициализации указать {0}. При этом выделяемое количество памяти под такую переменную будет установлено в 0. 120
Типы данных и структур const int MAXLOTTO=6; int lotto[MAXLOTTO] = {0}; Листинг 3.6. Инициализация со значением null Чтобы установить размер памяти, занимаемой массивом, можно также использовать псевдофункцию sizeof(). В следующем примере показано, как получить размер массива, отдельного его элемента и общее количество элементов. #include <iostream> using namespace std; int main() { double c[13]; cout << "Количество памяти, занимаемое массивом double c[13]: "; cout << sizeof(c) << endl; cout << " Количество памяти, занимаемое элементом: "; cout << sizeof(c[0]) << endl; cout << "Количество элементов: "; cout << sizeof(c) / sizeof(c[0]) << endl; } Листинг 3.7. Функция sizeof() в массиве (sizeof.cpp) 3.1.1. Сортировка методом «пузырька» Однотипные данные больших объемов хотелось бы выводить на экран в отсортированном виде,вследствие чего сортировка часто используется в программировании. По этой причине стандартная библиотека языка C++ включает в себя также методы сортировки (см. стр. 438), использовать которые гораздо удобнее, чем писать собственные. Тем не менее в качестве упражнения работы с массивами стоит разок заглянуть за кулисы и самим запрограммировать какой-нибудь простой алгоритм сортировки. Чтобы представить, как компьютер выполняет сортировку, разложите перед собой на столе пять или десять игральных карт. Вообразите, как 121
Глава 3 протекает процесс, который приведет к тому, что в конце концов все карты будут лежать по порядку. При этом нужно учитывать, что компьютер может сравнивать между собой одновременно только две переменные. Один из практикуемых методов — просмотреть все карты и сравнить каждую с ее соседкой справа. Если правая карта меньше левой, следует поменять их местами. На рис. 3.2 показано, как четыре карты могут сравниваться друг с другом за одно прохождение цикла. Те, что сравниваются друг с другом, подчеркнуты, а стрелки указывают на позиции, которые карты будут занимать после сравнения. 4 9 4 9 3 2 3 9 4 3 2 9 3 4 3 2 4 9 2 3 4 9 Рис. 3.2. Сортировка методом «пузырька» Видно, что числа поменяли свое расположение уже после первого прохода программы, и что максимальное число стало крайним справа. Остальные числа еще не отсортированы, поэтому цикл должен повториться. Теперь нужно обработать только три первые карты, поскольку самая большая уже находится в правильной позиции. В следующем проходе программы максимальная из трех оставшихся карт также будет перемещена вправо. В завершение нужно сравнить только две последние карты. Таким образом, для сортировки методом пузырька требуются два вложенных цикла. Внешний — задает количество сравнений. Первый проход этого цикла начинается с трех сравнений. Там, где карта должна всегда сравниваться с соседней справа, нельзя начинать с четырех. Во втором проходе требуется два сравнения, а в последнем — только одно. Следовательно, внешний цикл считает в обратном порядке. Внутренний 122
Типы данных и структур цикл всегда проходит с левого края до границы внешнего цикла, то есть его индекса. В листинге показано, как это описывается кодом: for (i=MAX-1; i>0; i--) { for (j=0; j<i; j++) { ... } } Листинг 3.8. Цикл для сортировки методом «пузырька» Вместо того чтобы изобретать решение в виде листинга, наверное, разумнее использовать диаграмму Насси — Шнейдермана. Во внутреннем цикле каждый элемент будет сравниваться с его соседом справа. Если элемент слева больше элемента справа, они должны поменяться местами (рис. 3.3). Рис. 3.3. Структурная диаграмма сортировки методом «пузырька» Теперь можно структурную диаграмму описать на языке C++. Следующий листинг представляет собой программу, включая заполнение массива случайными тестовыми значениями и вывод результатов на экран. #include <iostream> using namespace std; #include <stdlib.h> const int MAX=5; 123
Глава 3 int main() { int field[MAX], dop, help; int i, j, k; srand(56); // Генератор случайных чисел готов for (i=0; i<MAX; i++) { // Заполнение и вывод массива field[i] = rand() % 100 + 1; cout << field[i] << " "; } cout << endl; for(i=MAX-1; i>0; i--) { for (j=0; j<i; j++) { cout << "(" << j << "-" << j+1 << "): " ; if (field[j]>field[j+1]) { // Требуется перестановка help = field[j]; field[j] = field[j+1]; field[j+1] = help; } cout << field[j] << " - " << field[j+1] << " "; } // Здесь выводится массив cout << endl << MAX-i << "-й проход цикла завершен: "; for (k=0; k<MAX; k++) { cout << field[k] << " "; } cout << endl; } } Листинг 3.9. Сортировка методом «пузырька» (bubble.cpp) 124
Типы данных и структур Следующие строки демонстрируют пробный запуск программы. В первой находятся случайные значения, которыми заполнен массив. В следующей видно, как сравниваются значения. В скобках указаны участвующие в каждом случае перестановки индексы, за ними — элементы после перестановки. Вслед за каждым проходом цикла выводится весь массив и видно, что максимальный для этого прохода элемент перемещен вправо. 80 91 14 78 17 (0-1): 80 - 91 (1-2): 14 - 91 (2-3): 78 - 91 (3-4): 17 - 91 1-й проход цикла завершен: 80 14 78 17 91 (0-1): 14 - 80 (1-2): 78 - 80 (2-3): 17 - 80 2-й проход цикла завершен: 14 78 17 80 91 (0-1): 14 - 78 (1-2): 17 - 78 3-й проход цикла завершен: 14 17 78 80 91 (0-1): 14 - 17 4-й проход цикла завершен: 14 17 78 80 91 Упражнения • Дополните программу лото сортировкой методом пузырька так, чтобы числа лото всегда выводились по возрастанию. Пример приведен на стр. 499. • Программа сортировки методом «пузырька» может быть оптимизирована. Если после одного из проходов цикла слева направо ни одно число не было перемещено, тогда очевидно, что числа уже расположены в правильном порядке, и нет необходимости в их дальнейшей сортировке. Пример на стр. 501. 3.1.2. Присваивание в массивах Массив принадлежит к немногочисленным структурам данных, которые не подходят под стандарт L- значений. Следовательно, массив не может располагаться с левой стороны оператора присваивания1. В некоторых компиляторах, таких как GNU Compiler, все же возможно копировать массивы посредством присваивания. Вообще-то, это 1 Массив так же не может быть возвращаемым значением для функции (см. стр. 128). 125
Глава 3 работает только для массивов одинакового размера и типа. При таком присваивании квадратные скобки опускаются, поскольку иначе будут скопированы только отдельные элементы. int source[MAX]; int target[MAX]; target = source; // нестандартно! Другие компиляторы, такие как Visual C++ корпорации Microsoft, напротив, выдадут ошибку, что переменная target не является переменной L-значения. Если необходимо копировать массив, то нужно использовать следующий цикл: int source[MAX]; int target[MAX]; int i; for (i=0; i<MAX; i++) { target[i] = source[i]; } Если нужно быстро скопировать один массив в другой, который имеет такой же тип и размер, можно использовать функцию memcpy(). Функция копирует побайтно выбранную область памяти. Первый параметр функции — цель, второй — источник, третий — размер. Преимущество функции memcpy() заключается в высокой скорости. Недостаток — в том, что компилятор не контролирует, соответствуют ли тип и размер источника цели. Для примера показан вызов функции: #include <string.h> int source[MAX]; int target[MAX]; memcpy(target, source, sizeof(source)); Поскольку такой вариант чреват ошибками, лучше использовать его только в случае, когда высокую скорость требуется обеспечить любой ценой. Чтобы использовать функцию memcpy(), нужно подключить файл string.h. 126
Типы данных и структур 3.1.3. Символьные последовательности языка C Важная область применения массивов — последовательности символов. В них можно хранить слова, предложения и разный код. Такая последовательность символов называется строкой. Обычно строки реализуются в виде массивов элементов типа char. По этой причине уже в первой версии языка C имела место подобная организация. Но у нее есть свои недостатки. Так, строке не может быть присвоено другое кодовое значение, поскольку массив не является L- значением. Поэтому в стандартную библиотеку языка C++ был добавлен новый тип string. Мы рассмотрим его подробнее на стр. 354. Последовательности символов, базирующиеся на массиве типа char, сокращенно называются С-строками. Они иногда используются и в языке C++. Так константные последовательности символов определяются как С-строки. Множество интерфейсов реализованы в виде массива типа char. Новый тип string предлагает возможность объединить обе формы представления символьных последовательностей, которые также могут существовать по отдельности. Тип string не заменяет классическую С-строку, а дополняет ее. Но все же в новых программах рациональнее использовать тип string. Поскольку С-строка — это обычный массив, нельзя изменить ее размер во время выполнения программы. Поэтому программист должен задавать настолько большой размер массива, чтобы код поместился в него даже в случае, если он превышает предполагаемый объем в несколько раз. Чтобы в одном массиве отделять друг от друга символьные строки, как самые короткие, так и самые длинные, конец строки должен быть маркирован. Разделение осуществляется с помощью 0. Здесь речь идет не о символе '0', а о нулевом бите, который представлен в виде '\0' или как числовая константа 0. Каждая строковая константа всегда включает в себя этот заключающий символ. Поэтому пустая строка (вида "") на самом деле не пустая. Она содержит в себе один элемент, то есть 0. В примере показаны объявление и инициализация С-строки: char last_name[6] = "Kai"; В данном случае для массива резервируется шесть знаков. В первых трех элементах хранятся символы 'K', 'a' и 'i'. В четвертом элементе с индексом 3 хранится 0. Состояние остальных двух элементов неопределен- 127
Глава 3 но. Там могут находиться еще старые данные, обозначенные на рис. 3.4 символами 'z' и 'M'. ’K’ ’a’ ’i’ 0 [0] [1] [2] [3] ’z’ [4] ’M’ [5] Рис. 3.4. Заполненный массив типа char Размер массива можно также указать при инициализации. В данном случае значение в квадратных скобках будет отсутствовать, а массив — иметь такой размер, какой требует символьная последовательность. При этом заключительный символ также считается. В следующем примере создается массив из четырех элементов. Изменение размера массива в данном случае невозможно. char last_name[] = "Kai"; Для работы с С-строками существует большое количество различных вспомогательных функций (см. стр. 369), которые так или иначе довольно часто используются. В новых программах, тем не менее, преимущественно используется тип string (см. стр. 355). 3.1.4. Пример: определение числового значения введенного символа Преобразование символьных последовательностей в числовые значения — актуальная тема. Вводимые пользователем данные часто представляются в качестве символьной последовательности. Это касается как передачи параметров вызова (см. стр. 179), так и полей для ввода данных в графических интерфейсах, которые представляют свое содержимое в виде символьных последовательностей. Конечно, еще в библиотеках языка C, имелось множество функций конвертирования, например, функция atoi() (см. стр. 372). Но в целом такие функции не всегда удобны в использовании. Подобное конвертирование относительно просто написать самостоятельно и затем позже расширить его. Следующий пример позволяет считывать символьную последовательность с консоли и конвертировать ее содержимое в переменную long. 128
Типы данных и структур #include <iostream> using namespace std; const int MAX=256; int main() { char input[MAX]; int i = 0; long value = 0; cin.getline(input, MAX); while (input[i]>='0' && input[i]<='9') { value *= 10; value += input[i] — '0'; i++; } cout << value << endl; } Листинг 3.10. Считывание целых значений Рассмотрим по отдельности самые важные строки программы: cin.getline(input, MAX); Вызов функции cin.getline(input, MAX) позволяет пользователю начать ввод данных и записывает введенную им строку в массив input типа char. Ввод ограничен символом MAX. while (input[i]>='0' && input[i]<='9') Цикл while просматривает всю введенную строку, пока текущий символ находится в указанных пределах. Это устанавливается условием. Программа покинет цикл, как только встретит первый символ, не являющийся цифрой. То же условие предохраняет от выхода за пределы символьной последовательности, потому как нулевой завершающий символ также лежит в указанной условием области. value *= 10; value += input[i] — '0'; 129
Глава 3 Результат будет записан в качестве значения переменной value. Перед этим важно инициализировать эту переменную со значением 0. В цикле при каждом его повторении предыдущее значение переменной умножается на 10 и таким образом происходит сдвиг позиции влево. Новая цифра должна лежать в диапазоне между 0 и 9. Посредством вычитания символа '0' достигается, к примеру, то, что из символа '2' можно получить число 2. Это значение затем будет добавлено к уже имеющемуся значению переменной. Рассмотрим работу программы на примере числа «735» (табл. 3.1). Таблица 3.1. Пошаговая таблица i value input[i] После выполнения команды 0 0 1 1 1 2 2 2 3 0 7 7 70 73 73 730 735 735 '7' '7' '3' '3' '3' '5' '5' '5' '\0' value value i++; value value i++; value value i++; *= 10; += input[i] — '0'; *= 10; += input[i] — '0'; *= 10; += input[i] — '0'; Базируясь на описанном выше алгоритме, очень просто считывать также числа с плавающей запятой, в которых дробная часть отделена символом запятой. Обычно язык C++ использует англо-американскую систему представления таких чисел и отделяет дробную часть с помощью точки. При проходе цикла while для позиций перед запятой проверяется, является ли следующий символ запятой или нет. Затем выполняется почти такой же цикл. В переменной NK устанавливается десятичная экспонента, на которую будет разделена следующая цифра до того, как она будет добавлена к значению переменной value. #include <iostream> using namespace std; const int MAX=256; int main() { char input[MAX]; int i=0; double value = 0; 130
Типы данных и структур cin.getline(input, MAX); while (input[i]>='0' && input[i]<='9') { value *= 10; value += input[i] — '0'; i++; } if (input[i]==',') { double NK = 1; i++; while (input[i]>= '0' && input[i]<= '9') { NK *= 10; value += (input[i]- '0')/NK; i++; } } cout << input << value << endl; } Листинг 3.11. Считывание чисел с запятой (number_input.cpp) Упражнение • Напишите программу, которая позволяла бы вводить дроби и конвертировала их в переменные типа long. Числитель и знаменатель должны разделяться слешем. При вводе 3/4 переменная должна содержать 0.75. Пример приведен на стр. 502. 3.1.5. Многомерный массив Язык C++ позволяет организовывать массивы различных размерностей. При объявлении многомерного массива для каждой размерности используются отдельные квадратные скобки. Подобная конструкция представляет собой массив массивов. double matrix[MAXCOLUMN][MAXROW]; 131
Глава 3 Здесь объявляется массив, количество строк в котором равно MAXROW, а столбцов — MAXCOLUMN. Занимаемая таким массивом память определяется следующим образом: sizeof(double)* MAXROW * MAXCOLUMN Доступ к элементам двумерного массива осуществляется также с помощью двух пар квадратных скобок. Если необходимо использовать элемент из четвертого столбца и третьей строки, доступ к нему будет получен через определение matrix[3][2]; 3.1.6. Пример: игра «Бермуда» В качестве примеров двумерных массивов можно рассматривать множество различных игр, в которых используется двумерное поле. Представленная здесь компьютерная игра «Бермуда» аналогична классической игре «Морской бой», хоть и с некоторой претензией. Если при игре в Морской бой вы можете рассчитывать только на собственное везение, попадете вы в корабль или нет, то в «Бермуде» можно по результатам запросов сделать вывод, где спрятан корабль. Эта игра будет встречаться в книге в разных местах. Для начала здесь реализуется игровое поле в качестве примера двумерного массива. В игровом поле размерностью девять на семь клеток спрятаны четыре корабля. Игрок может начинать с любой позиции. Он получает либо сообщение, что на этой позиции найден корабль, либо указание, в каких направлениях от этой позиции видны корабли. При этом игра станет пеленговать влево, вправо, вверх, вниз или во все четыре стороны до тех пор, пока не будет найден корабль или обнаружен конец игрового поля. Два корабля, стоящих один за другим в одном направлении, будут распознаны как один корабль, поскольку пеленгатор не может видеть, что находиться позади первого корабля. Первый шаг состоит в том, чтобы определить игровое поле. В двумерном массиве будут храниться результаты попыток пользователя отгадать, где спрятан корабль. Для хранения этой информации лучше всего подходит тип char. Следующий листинг демонстрирует, как поле сначала инициализируется с точками. Затем программа переходит в цикл, где создается игровое поле и пользователь может вводить координаты. Для этого вводится номер строки, потом следует буква столбца, например 2C. На данном этапе разработки поля, указанные во введенных координатах, будут сначала маркированы знаком х в игровом поле. 132
Типы данных и структур На этом основании программа выдаст сообщение, были ли введены действительно существующие координаты. #include <iostream> using namespace std; const int X=9; const int Y=7; int main() { char game_space[X][Y]; int x, y; char cx, cy; for (x=0; x<X; x++) { for (y=0; y<Y; y++) { game_space[x][y] = '.'; } } // Создание игрового поля и ввод координат bool loop_end=false; int xin, yin; do { cout << " 1 2 3 4 5 6 7 8 9" << endl; for (y=0; y<Y; y++) { cout << (char)('A'+y) << " "; for (x=0; x<X; x++) { cout << " " << game_space[x][y]; } cout << endl; } 133
Глава 3 cin >> cx >> cy; xin = cx — '1'; yin = cy — 'A'; if (xin>=0 && xin<9 && yin>=0 && yin<7) { game_space[xin][yin] = 'x'; } else { loop_end = true; } } while (!loop_end); } Листинг 3.12. Игровое поле «Бермуда» (bermuda1.cpp) После запуска программы на экран будет выведено игровое поле с координатами. Программа останавливается, ожидая ввода данных. После ввода 2С — возврата в программу — на экране появляется символ х в соответствии с введенными координатами. После ввода значения 6F символ x будет расположен в двух местах — с координатами 2С и 6F. Если ввести координаты, которых нет в поле, программа завершит работу. 1 2 3 4 5 6 7 8 9 A . . . . . . . . . B . . . . . . . . . C . . . . . . . . . D . . . . . . . . . E . . . . . . . . . F . . . . . . . . . G . . . . . . . . . 2C 1 2 3 4 5 6 7 8 9 A . . . . . . . . . B . . . . . . . . . 134
Типы данных и структур C . x . . . . . . . D . . . . . . . . . E . . . . . . . . . F . . . . . . . . . G . . . . . . . . . 6F 1 2 3 4 5 6 7 8 9 A . . . . . . . . . B . . . . . . . . . C . x . . . . . . . D . . . . . . . . . E . . . . . . . . . F . . . . . x . . . G . . . . . . . . . Xx 3.2. Указатель и адрес Указатель — это особенная переменная. Ее содержимое в целом несущественно. Гораздо интереснее то, что она может указывать на другую переменную. Таким образом, посредством переменной-указателя рассматривается не сама переменная, а содержимое другой. Эту косвенность поначалу сложно понять, и позднее она еще неоднократно станет поводом для путаницы. Представьте, что каждый день вы ездите на работу на машине и паркуетесь на современной многоярусной стоянке. Для этого вы заезжаете на машине в специальный лифт, выходите и нажимаете на кнопку. Двери лифта закрываются, и ваш автомобиль автоматически отправляется на свободное парковочное место. После того как процесс завершен, вы получаете магнитную карту. Когда вечером вы придете забирать машину, следует вставить эту карту в специальное отверстие в лифте. Через некоторое время его двери откроются и появится ваш автомобиль. Вы можете сесть в него и поехать домой. Магнитная карта соответствует указателю. Посредством нее вы получаете доступ к вашему автомобилю, который стоит где-то в большом парковочном комплексе. Где именно находится автомобиль, и что записано на 135
Глава 3 магнитной карте, вам неизвестно. И в целом вас это не интересует, пока вы получаете машину обратно. Но если вы вдруг потеряете карту, это будет равноценно потере автомобиля. Он занимает парковочное место и физически существует. Однако вы не можете больше до него добраться. Поскольку есть люди, которые не настолько доверительно относятся к технике, как вы, на каждом предприятии установлен информационный терминал. В него можно вставить карту и получить информацию о вашем автомобиле, которая была собрана при его парковке. Автомобиль взвешивается в парковочном лифте, чтобы кто-нибудь не подменил его пустым каркасом. С помощью датчиков устанавливается высота, ширина и длинна машины для оптимального размещения на парковке. И наконец, с помощью сканера считывается номерной знак. Если вставить магнитную карту в терминал, вы получите всю эту информацию в следующем порядке: вес, высота, ширина, длина и номерной знак. Эти данные принадлежат не карте, а автомобилю, на который она указывает. Администрация парковки при этом предлагает новый сервис. Если владелец авто потерял карту, то он может получить свой автомобиль обратно после того как введет его номерной знак. Переменные ведут себя как автомобили. Они располагаются в памяти, которая сравнима с парковкой. Обычно мало кого интересует, где переменная фактически находится. В основном речь идет об имени переменной, как, например, о номерном знаке, с помощью которого в примере вы получили свой автомобиль. Можно также определить переменную-указатель, которая соответствует магнитной карте. Если присвоить указателю адрес переменной, можно получить доступ к ней через этот указатель. Такой процесс аналогичен маркировке магнитной карты. Компьютер на парковке кодирует на магнитной карте, где он припарковал авто. Что на самом деле записано на карте, интересует нас так же мало, как содержимое переменной указателя. Вместо того, чтобы следить за непосредственным содержимым указателя, его используют для просмотра переменной, на которою он указывает. Если потерять содержимое указателя, то содержимое переменной, на которую он указывал, можно будет получить только в случае использования ее имени.  Указатель — это переменная, которая хранит адрес в памяти, по которому размещена другая переменная. Указатель служит для того, чтобы косвенно указывать на содержимое некоторого участка памяти и для получения доступа к нему. Для объявления указателя сначала определяется тип переменной, на которую он указывает. Затем следует символ * и имя указателя. 136
Типы данных и структур char *char_pointer; Переменная char_pointer — это указатель на некоторую переменную типа char. Выражаясь иначе, в переменной char_pointer хранится адрес в памяти, по которому располагается некоторая переменная типа char. Особая позиция в памяти называется адресом. Позиции памяти в компьютере пронумерованы, за адресом скрыто некоторое число. Чтобы выяснить адрес переменной, используется символ &. Этот оператор в английском языке носит имя «амперсанд» и в программировании часто так и называется. В следующем примере показано, как указателю char_pointer присвоить адрес переменной letter. char letter = 'A'; char_pointer = &letter; Как уже было сказано, нас интересует содержимое не указателя, а переменной, на которую он указывает. После того, как указателю был присвоен адрес переменной, можно получить к ней доступ, если установить перед именем указателя символ *. cout << *char_pointer; Эта команда выведет на экран символ A, который хранится в переменной letter. Также можно присвоить переменной letter новое значение, используя указатель. *char_pointer = 'B'; cout << letter; С помощью символа * перед именем указателя четко обозначается, что обращение идет не к содержимому указателя, а к позиции в памяти, на которую ссылается указатель. Переменная char_pointer все еще содержит адрес переменной letter, но ее содержимое изменено. Если вывести переменную letter, на экране появиться символ В. На рис. 3.5 показана ситуация после присваивания символа В через указатель char_pointer. Вверху справа находится переменная letter. Здесь мы исходим из того, что она располагается в памяти по адресу 17543. Именно этот номер содержит переменная char_pointer, которая в свою очередь хранится по адресу 23164. Соотношения размеров также подходят. Пока переменная типа char занимает один бит, указатель, обычно, занимает на компьютере от четырех до восьми байт. 137
Глава 3 Рис. 3.5. Указатель Можно с уверенностью представить, что с помощью указателя крайне удобно получать доступ к переменным. И, конечно же, указатель применяется очень часто. Здесь перечислены примеры типичного использования указателей, которые мы рассмотрим в книге немного позже. 1. Можно написать часть программы, которая будет получать доступ к переменной через указатель. Если присвоить указателю другой адрес, та же часть программы станет работать уже с другой переменной. Далее в книге это продемонстрировано вместе с параметрами функции. 2. В процессе работы программы может потребоваться выделить новую часть памяти. Как будет показано позже, для этого используется команда new. Чтобы программа могла получить доступ к новому участку памяти, new предоставляет указатель. 3. С помощью указателей можно построить сложные конструкции данных. Для этого из данных и указателей строятся объединения переменных (см. стр. 146). Если указатель такого объединения ссылается на следующее объединение, можно построить цепь в виде линейного списка. Если в объединении данных интегрировать несколько указателей, можно создать конструкцию, подобную дереву. Для надежности указатель, у которого еще нет конкретного целевого адреса, следует устанавливать равным 0. По адресу 0 в памяти не могут храниться переменные, и по этой причине в случае ошибочного запроса доступа по нулевому указателю программа сразу же прервет свое выполнение. Кошмарный конец всегда лучше, чем кошмар без конца. Последний происходит тогда, когда указатель указывает на какое-то случайное значение и программа продолжает выполняться без сообщения об ошибке. char *char_pointer; // char_pointer объявлен char_pointer=0; // защищен посредством обнуления адреса 138
Типы данных и структур Если нужно переменную-указатель инициализировать со значением 0, можно просто написать =0 как для переменной целого типа. В программировании на языке C в связи с этим часто встречается константа NULL. Здесь речь идет о 0, который явным образом указывает на использование его для указателя. В языке C++ константа NULL и не требуется, и не имеет смысла. char *char_pointer=0; 3.2.1. Косвенный доступ Чтобы с помощью указателя получить доступ к другой переменной, перед именем указателя ставится символ *. Таким образом устанавливается доступ не к содержимому указателя, а к содержимому переменной, на которую он указывает и адрес которой хранит. Для повторения здесь приведены команды из прошлой главы: char *char_pointer; // Объявление указателя char_pointer = 0; // Инициализация со значением 0 char letter = 'A'; // Переменная с содержимым char_pointer = &letter; // Получение адреса переменной *char_pointer = 'B'; // Переменная поменяла значение 'B' Символ * идентичен символу умножения. Компилятор, тем не менее, поймет, что именно означает этот символ, по написанию его в программе. В случае доступа через указатель он расположен в позиции, где в другом случае указан знак числа. Поскольку знак числа может быть либо плюс, либо минус, компилятор распознает символ * в этой позиции как оператор косвенного доступа. Поэтому данный символ еще называют косвенным оператором. Приведенный ниже код может вызвать чувство, что с помощью указателя можно делать что угодно. В комментариях описано, как работают соответствующие команды. int main() { int *int_pointer = 0; int intVar = 5; int_pointer = &intVar; // указатель получает адрес переменной intVar // теперь int_pointer указывает на intVar 139
Глава 3 *int_pointer = 1; // переменной, на которую указывает int_pointer, будет // присвоено значение 1. При этом только содержимое // intVar равно 1. intVar = *int_pointer + 1; // значение intVar будет изменено на сумму 1 и значение, на // которое указывает int_pointer. Но только сама переменная. // Поэтому значение intVar в итоге будет равно 2. } В примере указатели, как уже было рекомендовано, устанавливаются равными 0. Если переменная-указатель — локальная, без инициализации она может содержать любое значение. То есть она указывает на совершенно случайную позицию в памяти. Если по ошибке доступ по этому указателю произойдет раньше, чем его значение будет корректно определено, программа обратится к совершенно случайному адресу памяти. Так будут получены полностью бессмысленные или неверные данные. Программа станет их обрабатывать и только позже выдаст ошибку, причину которой уже будет сложно выяснить. Если указатель установить равным 0, его преждевременное использование сразу же приведет к сбою программы. Затем с помощью отладчика можно будет установить, в каком месте программы произошел сбой. Сразу станет очевидно, что указатель установлен равным 0, и где вы забыли корректно указать адрес. 3.2.2. Массивы и указатели В языках C и C++ указатели и массивы удивительным образом сроднились. Указателем можно ссылаться на массив. При этом он будет указывать на первый элемент массива. int values [4]; int *values_pointer = 0; values_pointer = values; Особенно интересно то, что в процессе присваивания указателю массива квадратные скобки можно опустить. При этом переменнаяуказатель будет вести себя так, словно она изначально была массивом. В следующем примере указатель используется в качестве переменной массива: 140
Типы данных и структур values_pointer = values; values_pointer[3] = 4; Эта ситуация представлена на рис. 3.6. Не без причины переменнаямассив values представлена в качестве указателя. Рис. 3.6. Указатель совместим с массивом Следующие конструкции, вероятно, вызовут у вас холодный пот; однако для компилятора они совсем не покажутся странными. Тут ситуация сначала выглядит аналогично: int values[4]; int *values_pointer = 0; values_pointer = values; values_pointer[3] = 5; values_pointer = & values[2]; values_pointer[1] = 3; // записывается в values[3]! Листинг 3.13. Массив и указатель Строка, в которой указателю присваивается адрес третьего элемента массива, особенно оригинальна: values_pointer = &values[2]; Здесь values[2] не что иное, как переменная целого типа, и, разумеется, указателю можно было бы присвоить и другой адрес. Если использовать этот указатель с квадратными скобками, он будет вести себя как массив values, однако сдвинутый на два элемента вправо, как представлено на рис. 3.7. 141
Глава 3 Рис. 3.7. Массив со сдвинутым указателем Из-за сдвига значение элемента values_pointer[0] будет идентично значению элемента values[2]. В результате, values_pointer[1] будет последним элементом массива values. Если бы при этом обрабатывался элемент values_pointer[2], был бы получен доступ к памяти, которая находится вне резервируемой массивом области. Результат при этом сложно предсказать.  Основное отличие: при объявлении массива память резервируется в зависимости от количества элементов, а при объявлении указателя — только для хранения переменной-указателя. 3.2.3. Арифметика для указателей Указатель можно инкрементировать и декрементировать. При этом к нему будет добавляться столько байт, сколько требует тип переменной, на которую он указывает. Указатель на начало массива при инкрементировании будет указывать на следующий элемент. Поэтому указатели широко используются для перебора элементов массивов. Если необходимо присвоить элементам массива 0, можно сделать это с помощью оператора индекса. int values[MAX]; for(i=0; i<MAX; i++) { values[i] = 0; } Альтернативой может служить использование указателя и просмотр массива с его помощью: 142
Типы данных и структур int values[MAX]; int *pointer = values; for(i=0; i<MAX; i++) { *pointer = 0; pointer++; } Из этих двух циклов быстрее работает второй. Это совершенно ясно, так как при использовании указателя его значение увеличивается при каждом проходе цикла. Для этого процессор добавляет к значению указателя размер типа элементов массива. В первом цикле, напротив, размер типа должен быть сначала умножен на индекс и затем добавлен к адресу массива, чтобы получить доступ к элементу. После этого нужно увеличить индекс на 1. Рис. 3.8. Блуждающий указатель Символьные последовательности очень элегантно обрабатываются в цикле for. В скобках после ключевого слова for указано начальное значение, условие, по которому работает цикл, и затем команда, выполняющаяся в самом конце тела цикла. Следующий пример исходит из того, что переменная source содержит последовательность символов, а затем происходит вывод букв одна за другой на экран. char source[MAX]; for (char *p = source; *p; p++) { cout << *p ; } Листинг 3.14. Цикл for для строк 143
Глава 3 В начальной команде объявляется указатель р и инициализируется вместе с массивом source. Следующая команда — условие, по которому выполняется цикл. То, что оно такое короткое, несколько смущает. Условие *р содержит символ, на который указатель р ссылается в настоящий момент. В данном случае это не булево значение, но не трудно вспомнить, что язык C++ интерпретирует нулевое значение в качестве ложного, а все остальные — как истинные. Здесь это означает, что цикл будет продолжаться, пока не наткнется на завершающий строку символ 0. Только тогда выражение *р примет значение ложь. Заключительная команда в итоге заботится о том, чтобы после каждого прохода цикла переменная р указывала на следующий символ строки. Сложение и вычитание Указатель можно не только инкрементировать и декрементировать, но и прибавлять к нему значения. Это сложение в том же отношении согласуется с инкрементированием, то есть прибавляемое значение рассматривается в качестве единицы размера типа. Следующее выражение указывает на два элемента позади того, на который ссылается указатель. *(pointer+2) = 4; pointer[2] = 4; Листинг 3.15. Однозначные команды Во второй команде выполняется абсолютно то же самое, что и в первой. При этом будет получен доступ ко второму элементу, расположенному после начального. Скобки в первом случае необходимы, поскольку косвенный оператор имеет более высокий приоритет, чем плюс. 3.2.4. Константный указатель Ключевое слово const вам уже знакомо по объявлению констант. Его допустимо также использовать и для указателей. При этом оно может располагаться в двух разных позициях и в зависимости от этого иметь различное значение. const int *const_target = &var_target; Этот указатель объявлен так, что ключевое слово const находится прямо перед типом цели. Через него целевую переменную нельзя изме- 144
Типы данных и структур нить. Указатель способен при этом инкрементироваться. Можно таким образом просматривать и выводить содержимое массива. int * const const_pointer = &var_target; Указатель const_pointer не может быть изменен относительно его цели. Ключевое слово const находится прямо перед именем указателя и обозначает, что его нельзя менять. В целом переменная, на которую он ссылается, может изменяться. Поэтому она уже при объявлении указателя должна быть однозначно инициализированной. const int * const Constant = &var_target; Для указателя Constant нельзя менять ни значение переменной, ни значение указателя. Константный указатель используется чаще всего при определении параметров функции. 3.2.5. Анонимный указатель Указатель всегда имеет одинаковый размер, не важно, на данные какого типа он ссылается. В конце концов, он содержит всего лишь адрес памяти, который имеет одинаковый размер для любого типа. Он зависит только от архитектуры процессора компьютера. Для 32-разрядной архитектуры указатель занимает 32 бита, то есть 4 байта. На какой тип ссылается указатель, с точки зрения компьютера совершенно безразлично. Компилятор, правда, следит за тем, чтобы указатель на переменную типа char не стал внезапно использоваться так, словно он указывает на переменную типа float. В определенных ситуациях может быть обоснованным создание указателя, цель которого еще неизвестна. При этом указатель объявляется с типом void. void *ptr; Переменных типа void не существует, так что объявление переменной такого типа приведет к ошибке. Однако разрешается объявлять указатель с этим типом. Такому указателю можно присвоить значение другого указателя, при этом компилятор не станет возмущаться. Наоборот, в языке C++ используется присваивание указателю различных типов. Чаще всего указатель на void является связующим звеном для указателя, чей целевой тип определится в процессе выполнения программы. 145
Глава 3 Как уже было сказано, каждому void-указателю можно присвоить другой. Но обратная ситуация допустима только в том случае, если перед void-указателем в скобках указать целевой тип (см. стр. 64). void *voidPtr; int *intPtr; voidPtr = intPtr; // работает без проблем intPtr = (int *)voidPtr; // необходимы скобки 3.3. Объединение переменных: struct С помощью массивов можно объединить переменные одного типа. В реальном мире, однако, требуется объединять между собой данные различных типов. Так, автомобиль имеет марку и тип, которые можно определить с помощью последовательности символов. Сюда же относится пробег и производительность, информацию о которых следует хранить в целочисленных переменных. Для цены используется тип с плавающей запятой. Для некоторых компаний-дистрибьюторов для цены можно было бы использовать тип double. Все вместе эти данные описывают автомобиль. Вероятно, вы станете утверждать, что автомобиль имеет гораздо больше составных характеристик. Есть тормозной диск, турбокомпрессор и стеклоочистители. В реальности это, конечно, верно. Однако программист всегда интересуется только определенными особенностями, которые он вместе с заказчиком утверждает перед разработкой. Нашего примера будет, пожалуй, достаточно для маленького автосалона. Фирма по прокату автомобилей, возможно, вообще не интересуется их стоимостью, но ей необходимо знать, допустим, что машиной будет пользоваться некурящий. Автомастерская станет интересоваться абсолютно всеми составляющими частями авто. Программа, управляющая логистикой корпоративных транспортных средств, интересуется только номерным знаком. Создается модель автомобиля, которая содержит основные составные части и пренебрегает другой информацией, кроме той, что необходима программе. Еще в языке C для подобных целей использовались структуры, которые объединяли в себе несколько переменных. Ключевое слово для определения такой составной переменной звучит как struct. За ключевым словом следует имя нового типа. В следующих фигурных скобках 146
Типы данных и структур перечисляются составные части новой структуры. Такое определение не сильно отличается от обычного объявления переменных. Для моделирования автомобиля создается новый тип, назовем его TAutoTyp, который состоит из нескольких элементов. struct TAutoTyp // Объявляем новый тип { char brand[MaxMarka]; char model[MaxModel]; long km; int kW; float price; }; // Здесь легко забыть про точку с запятой! Листинг 3.16. Структура для автомобиля Ключевое слово struct начинает объявление типа. Затем следует имя создаваемого типа, здесь TAutoTyp. В следующих фигурных скобках располагаются составные части структуры. В конце указана точка с запятой, о которой периодически забывают даже опытные программисты. Новый тип TAutoTyp создан. Он может так же широко использоваться, как и тип int. Можно, например, создать новую переменную такого типа, или новый массив, или объявить указатель с таким типом. TAutoTyp collection; // Объявлена новая переменная TAutoTyp auto_parking[100]; // Массив автомобилей TAutoTyp *parking_map; // Указатель на автомобиль Переменная collection содержит только ту информацию, которая определена в структуре TAutoTyp при ее создании. Чтобы добраться до отдельных частей структуры, после ее имени указывается точка и затем располагается имя требуемой составляющей части. // Для доступа к отдельным элементам структуры collection.km = 128000; collection.kW = 25; collection.price = 25000.00; 147
Глава 3 Если необходимо получить доступ к элементам структуры через указатель, следует обращаться через символ * и затем через точку указывать имя элемента. Символ * и имя указателя следует заключать в скобки перед точкой. TAutoTyp *parking_map = 0; // Никакого адреса в памяти parking_map = &collection; // Указывает на структуру collection (*parking_map).price = 12500; // Элемент price в collection Листинг 3.17. Указатель на структуру Хоть это решение и может казаться логичным, но оно ни элегантно, ни просто. К счастью, существует более красивый вариант получить доступ к элементу через указатель. Для этого используется комбинация символов ->, напоминающая стрелку. parking_map->price = 12500; Структура представляет собой L-значение. Ее можно размещать с левой стороны присваивания. Одной структуре можно присвоить другую, если она такого же типа. При этом из переменной-источника в новую переменную будет бит за битом скопирована вся информация. TAutoTyp my_next_auto, my_dream_auto; my_next_auto = my_dream_auto; Несмотря на то, что совершенно очевидно, что обе эти переменные структур идентичны, нельзя сравнить их с помощью двойного знака равенства. В структурах можно объединять декларацию типов и объявление переменных, где имя переменной указывается прямо после фигурных скобок. struct // Здесь не указывается тип { char brand[MaxMark]; char model[MaxModel]; long km; int kW; 148
Типы данных и структур float price; } my_first_auto, my_dream_auto; Листинг 3.18. Структура для автомобиля Здесь переменные my_first_auto и my_dream_auto объявляются прямо после объявления структуры. Если в таком случае переменные определяются сразу после объявления структуры, имя типа указывать не обязательно. Однако при этом в дальнейшем, конечно, невозможно объявление новых переменных такого типа. Структуры также можно инициализировать. Для этого, как и при работе с массивами, используются фигурные скобки. Здесь значения также отделяются запятой. TAutoTyp JB = {"Aston Martin", "DB5", 12000, 90, 12.95}; TAutoTyp GWB = {0}; Структуры использовались еще в языке C и были там единственной возможностью объединения данных. В языке C++ для этой цели обычно используются классы (см. стр. 213 и далее), которые могут значительно больше, чем С-структуры. Однако по причине совместимости ключевое слово struct используется в языке C++ и, вероятно, будет и далее перениматься. Тем не менее, структура в языке C++ позволяет значительно больше, чем в языке C. В целом она соответствует классу, в котором все элементы доступны извне (рис. 3.9). Рис. 3.9. Граф синтаксиса структуры Граф VarDef можно найти на стр. 46. В этой позиции объявляются переменные. При этом разрешаются также массивы, указатели и структуры. Если в позиции Vars указывается имя переменной, то в этом месте происходит объявление переменных данной структуры. Здесь также можно разместить массивы и указатели структуры. При объявлении нескольких переменных их следует разделять запятыми. 149
Глава 3 3.3.1. Пример: игра «Бермуда» После моделирования игрового поля (см. стр. 131) следует разместить на нем корабли. Каждый корабль имеет свои координаты x и y. В дальнейшем каждый корабль должен быть маркирован, если он был найден. Поэтому потребуется еще одна переменная типа bool, назовем ее detected. Чтобы объединить корабли в этой форме, будет использована структура. По правилам игры всего должно существовать четыре корабля, поэтому нужно использовать массив структур. const int max_ship=4; struct tShip { int x; int y; bool detected; }; tShip Ship[max_ship]; Листинг 3.19. Объявление кораблей Следующим шагом следует спрятать корабли. Естественно, как и для цифр лото, два корабля не могут находиться на одной позиции. Следующее определение, как и в программе лото, должно предотвращать ситуацию, когда новый корабль помещается на уже занятое место. Здесь показана полная программа: // Бермуда: добавление структуры #include <iostream> using namespace std; #include <stdlib.h> const int X=9; // Ширина игрового поля по горизонтали const int Y=7; // Высота игрового поля по вертикали const int max_ship=4; // Количество кораблей struct tShip { // Копия корабля int x; // Позиция по горизонтали 150
Типы данных и структур int y; // Позиция по вертикали bool detected; }; tShip Ship[max_ship]; // Наш флот int main() { int i, j; // Переменные счета bool new_number; // Позиция обнаружена? srand(0); for(i=0; i<max_ship; i++) // Все корабли { Ship[i].detected = false; // Определение позиции do { // Выбрать случайную позицию Ship[i].x = rand() % X; Ship[i].y = rand() % Y; new_number = true; // Проверить, не занимает ли данную позицию // один из предыдущих кораблей for (j=0; j<i; j++) { if (Ship[j].x== Ship[i].x && Ship[j].y== Ship[i].y) { // Уже один расположен! new_number = false; } } // теперь найдена новая позиция } while (!new_number); } // для контроля выведем все корабли for (i=0; i<max_ship; i++) 151
Глава 3 { cout << Ship[i].x << "," << Ship[i].y << " "; } cout << endl; } Листинг 3.20. Прячем корабли (bermuda2.cpp) Теперь созданы структуры данных для программы игры «Бермуда». Следующим шагом все части обретут функциональность. 3.4. Динамические структуры Обычно переменные, которые будут использоваться в программе, определяются при ее создании. Для этого уже перед началом написания кода необходимо определить, какие данные требуются. Предположим, что в программе необходимо заполнить лист покупок. Можно использовать для этого массив, в котором будут храниться записи о необходимых покупках. Но сколько записей может быть максимально? Неважно, какое значение вы выберете: оно будет слишком маленьким, если вы намерены закупить продукты на полгода вперед, и слишком большим, если вам нужно просто купить яйца и бекон для импровизированного обеда. Для таких случаев язык C++ предлагает возможность выделять память по мере выполнения программы, организуя доступ к ней указателем. 3.4.1. Выделение и освобождение памяти Команда new запрашивает новый участок памяти. Чтобы можно было к ней обратиться, она возвращает указатель. Чтобы команда new «знала», сколько необходимо запросить памяти, за ней следует желаемый тип данных. float *float_pointer = new float; Эта память выделяется из рабочей памяти программы, так называемой «кучи». Единственная связь программы с памятью — это указатель. Программа несет за него ответственность. Это означает, что память должна быть доступна с помощью данного указателя, пока это необходимо, а затем следует ее освободить командой delete. 152
Типы данных и структур Только что выделенная память может быть сразу же инициализирована. Для этого в скобках после типа указывается значение инициализации. int *int_pointer = new int(2); // Инициализация Переменной, на которую указывает int_pointer, сразу после создания будет присвоено значение 2. Выделенная память в определенный момент должна быть освобождена. Особенно это важно, когда через указатель выделяется следующий участок памяти, прежде чем предыдущий был освобожден, тогда остатки блуждают по основной памяти, и к ним уже нельзя получить никакого доступа. Это можно сравнить с автомобилем, который теряет масло. В подобных случаях говорят о так называемой «утечке памяти». Как и при утечке масла, утечка памяти в небольших количествах не трагична и проявляется, вероятно, минимальным снижением производительности. Опасна она только в том случае, когда оставшиеся участки памяти превышают определенный уровень. Тогда возможна критическая потеря производительности или сбой программы. По этой причине необходимо следить за тем, чтобы каждый занятый участок памяти был освобожден после его использования. Для этого служит команда delete. За ней следует указатель, который ссылается на данный участок памяти: delete float_pointer; Указатель не должен быть тем, с помощью которого запрашивалась память. Он должен только лишь указывать на верный участок памяти и принадлежать такому же типу. После освобождения памяти командой delete рекомендуется присвоить переменной-указателю значение 0. Это предотвратит ситуацию, когда в другом месте по ошибке будет освобожден тот же участок памяти. Команда delete распознает, когда значение указателя равно 0 и не пытается освободить память по этому адресу. Почти так же важен факт, что дальнейшее использование этого указателя для считывания или записи участка памяти сразу приведет к ошибке, которую легко будет обнаружить. После освобождения памяти указатель ссылается в конце концов только на тот участок, который больше недействителен. Возможно, что в дальнейшем эту память снова потребуется выделить. Если значение указателя на нее не будет установлено равным 0, и программа станет и дальше работать с этим недействительным указателем, тогда начнет использоваться участок памяти, который, вероятно, относиться к другой части программы. При этом программа продолжит работать словно в нормальном режиме, и данная ошибка никогда не будет обнаружена. 153
Глава 3 3.4.2. Создание массивов во время выполнения программы С помощью команды new можно также создать динамический массив. Особенность его состоит в том, что размер массива задается как параметр. В некоторых случаях программа может узнать, насколько большой требуется массив, только после старта. При динамическом создании используется ровно столько памяти, сколько необходимо. Для создания массива во время выполнения программы, для оператора new после указания типа данных в прямоугольных скобках помещается количество требуемых элементов массива. Указатель, который ссылается на новый участок памяти, может обрабатываться точно так же, как и массив. Если с помощью команды new был создан массив, этот участок памяти освобождается специальной для массива записью команды delete[]. Хотя при нормальном вызове команды delete большинство компиляторов не найдут изъянов, результат будет непредсказуем. int *Lotto = 0; // Указатель объявлен и инициализирован Lotto = new int [6]; // Запрошен участок памяти под массив // из 6 элементов for (i=0; i<6; i++) // Просмотр массива { Lotto[i] = rand() % 49 + 1; // Запись чисел лото } delete[] Lotto; // Освобождение памяти Lotto = 0; // Предохранение указателя Листинг 3.21. Запрос создания массива во время работы программы 3.4.3. Связанные списки Если требуется объединить несколько элементов одного типа, то первой мыслью будет использовать массивы. Если же сложно перед первым запросом на выделение памяти определить максимальное количество элементов в массиве, то хорошим решением будут связанные списки. Когда требуется новый элемент данных, он создается и добавляется в список. Если какой-то элемент не нужен, его можно удалить. 154
Типы данных и структур Сколько элементов может находиться в списке, ограничено только свободной памятью. Доступ к элементу определенной позиции, правда, сложнее, чем в массиве. База связанного списка — структура, которая содержит, с одной стороны, данные, с другой стороны — указатель на следующий элемент списка. struct TList { int data; TList *next; }; Есть нечто озадачивающее в использовании типа TList при его объявлении. Компилятор должен увидеть на этом месте незнакомый ему еще тип TList, где указатель объявлен также этим типом. Однако указатель всегда имеет одинаковый размер, и неважно, на что он указывает. При беглом просмотре кажется странным, что в структуре есть указатель, который, казалось бы, указывает на самого себя. Однако, как уже намекает имя next, указатель ссылается не на собственную структуру, а на следующую, которая имеет такой же тип. Связанный список выглядит так, как представлено схематично на рис. 3.10. Рис. 3.10. Связанный список Переменная anchor — это указатель на тип TList, который образует базис для доступа программы к связанному списку. Через якорь можно добраться до первого элемента списка. Там уже содержится указатель next, ссылающийся на следующий элемент списка. Так программа может обрабатывать все элементы, пока указатель next не примет значение 0. Этим обозначается конец списка. Если тот пуст, переменная anchor должна содержать 0. Новый элемент списка создается с помощью команды new. При этом надо внимательно следить за тем, чтобы указатель next был корректно определен. Если вы не можете сразу же объявить следующий элемент списка, установите значение указателя равным 0. 155
Глава 3 #include <iostream> using namespace std; struct TList { int data; // симулирует данные TList *next; // Связь со следующим элементом }; TList *anchor = 0; // Начало списка int main() { int content; TList *knot, *old; // Заполнение списка числами до тех пор, пока не встретится //число 0 do { cout << "Введите число(0 для завершения ввода)" << endl; cin >> content; if (content) { // Новые элементы списка: TList *knot = new TList; knot->data = content; // Запись данных knot->next = anchor; // Прикрепляем предыдущий //элемент списка anchor = knot; // Передаем дальше начальное //значение } } while (content); // Выводим список в обратном порядке // при этом удаляем выведенные элементы while (anchor) // Не равен 0! Список не пустой! { cout << anchor ->data << endl; 156
Типы данных и структур old = anchor; // Перестраховываемся при дальнейшем //удалении anchor = anchor->next; // Следующий элемент вперед delete old; // Удаляем выведенный элемент } } Листинг 3.22. Программа демонстрации связанного списка Используя связанные списки, можно создать гибкое хранилище данных. Допустимо добавлять новые данные в один конец списка и удалять с другого конца. Именно так работает буфер. Если обозначить последний элемент списка в качестве первого, получится замкнутая структура. 3.5. Объединение Объединение — union — представляет собой особый тип структуры. Его можно описать в качестве связи или-или. В одном объединении хранится несколько переменных, из которых можно использовать только одну. То есть обрабатывать можно только один из представленных в объединении элементов. Объединение занимает столько памяти, сколько требуется для хранения самого большого из его элементов. В объединении хранится взаимозаменяемая информация. Так банк может использовать данные кредитной карты с номером и месяцем, а когда ее действие прекращается — счет с личным номером и банковским индексом. union tBank { tCredit_card card; tNumber number; }; Структура union содержит только непосредственные данные. Она не сохраняет информацию о том, какой из вариантов был использован последним. Она не сможет предотвратить ситуацию, когда одна версия была записана, а другая прочитана. Компилятор не выдаст предупреждения, если запись была произведена в переменную card, а данные 157
Глава 3 прочитаны из переменной number. То, что результат бессмысленный, может определить только пользователь. В некоторых программах посредством объединения происходит обмен последовательности байтов между разными устройствами. Слово из 16 бит занимает 2 байта. Но последовательность этих байт в различных машинах отличается. В компьютерах, например, младший байт считывается первым; в большинстве процессоров структуры RISC — наоборот. union tTransform { struct { unsigned char high; unsigned char low; } byte; unsigned short word; } Transform; int main() { Transform.word = 7656; cout << (int) Transform.byte.high << endl; cout << (int) Transform.byte.low << endl; } Листинг 3.23. Программа union.cpp В результате программа выдаст на компьютере с процессором Intel значения 232 и 29. Использование объединения может легко привести к непредвиденным результатам. Поэтому рекомендуется применять его только в том случае, когда нет никакой альтернативы. 3.6. Перечисляющий тип enum С помощью типа enum можно организовать перечисления,такие как дни недели или цвета́. Элементы перечисления сортируются по имени. 158
Типы данных и структур После ключевого слова enum следует имя перечисления. В следующих за ним фигурных скобках перечисляются имена, которые относятся к этому типу. Они разделяются запятыми (рис. 3.11). Рис. 3.11. Граф синтаксиса enum В следующем примере представлено перечисление цветов. enum color {red, yellow, green, blue}; При дальнейшем выполнении программы переменные можно объявить с типом color и присвоить им элементы перечисления. color traffic_lights; … traffic_lights = red; Внутри перечисления элементы нумеруются по порядку. Таким образом red будет иметь индекс 0, yellow — индекс 1 и т. д. По этой причине константы перечисления можно присваивать целочисленным переменным. Если попытаться сделать наоборот — это приведет к выводу компилятором предупреждения. color traffic_lights; int i; … i = red; // никаких проблем traffic_lights = 3; // появится предупреждение Значения по умолчанию можно также задать при объявлении, как показано в следующем примере: enum color{red=2, yellow, green=7, blue}; Это приведет к тому, что red будет равен 2, yellow — 3, green — 7, а blue — 8. 159
Глава 3 3.7. Определение типов Командой typedef можно присваивать типам имена. Часто такое употребление используется для сокращения написания длинных названий типов. Можно типу unsigned char присвоить имя uchar. Это выглядит следующим образом: typedef unsigned char uchar; Команда typedef способна также пояснять, как используется тип. Если тип uchar будет использоваться для хранения байта, можно четко обозначить в программе эту цель: typedef uchar byte; Можно объявить тип с именем order_number, который является не чем иным, как типом long. Однако при определении его таким образом указывается, что эта переменная не будет участвовать в расчетах. На этапе тестирования допустимо также обозначить какой-нибудь глупый тип, если окончательный еще неизвестен. Вместо этого можно предварительно установить обычное целочисленное значение. typedef int tData; Этот тип tData можно использовать, например, в линейном списке при тестировании. Если модуль функционирует, можно удалить строку с командой typedef и позже использовать правильный тип tData.
Глава 4 ФУНКЦИИ Функции позволяют объединять команды. Так начинается творческий процесс создания программного обеспечения, который выходит за переделы просто технически верного нанизывания команд одна на другую. Вы наверняка помните функции из математики. Вызов функции в программировании очень похож на математическое представление. Например, вызов функции синуса в программе формулируется точно так же, как вы помните это из курса математики. a = sin(alpha); Здесь sin — имя функции, alpha — параметр, передаваемый в функцию при вызове. Переменной a присваивается значение, которое возвращает функция. Глядя на вызов функции, нельзя понять, как вычисляется синус угла. Детали скрыты в теле функции. Да и для программиста в данном случае не важно, как рассчитывается синус. Он знает, что это такое, и хочет получить результат. Чтобы написать функцию самостоятельно, следует в одном блоке с конкретным именем объединить различные команды. Из-за этого функции иногда называют подпрограммами. Функция должна, по возможности, строиться так, чтобы выполнять только одно конкретное задание. Ее можно вызывать по имени из любой позиции программы и так часто, как это необходимо. После выполнения функции программа возвращается в ту позицию кода, откуда была вызвана функция, и продолжает работу с выполнения следующей команды. Одна функция уже встречалась нам ранее — это функция main(). Она вызывается операционной системой для запуска программы. Можно написать множество дополнительных функций и вызывать их из main(), поскольку внутри одной функции разрешено вызывать другие. Обычно функция возвращает некоторое значение. Оно с помощью ключевого слова return после окончания функции передается в про- 161
Глава 4 грамму, вызвавшую эту функцию. Тип возвращаемого значения определяется при описании функции. Возвращаемое значение можно присвоить переменной. В качестве первого примера напишем функцию next(). Она после каждого вызова возвращает число на единицу больше предыдущего. Для счета вводится глобальная переменная next_number, которая инкрементируется внутри функции. В конце это значение возвращается с помощью команды return. Для тестирования функция вызывается несколько раз, и возвращаемое значение выводится на экран. #include <iostream> using namespace std; int next_number=0; // В этой переменной хранится состояние счета int next() // изменение глобальной переменной next_number { next_number++; // увеличение глобальной переменной return next_number; } int main() { int n; n = next(); cout << n << endl; next(); // здесь n не изменяется! cout << n << endl; n = next(); cout << n << endl; } Листинг 4.1. Функция next (next.cpp) Объявление функции next() начинается с определения типа возвращаемого значения, в данном случае — int. Тип возвращаемого значения может быть любым. Единственное, чем не может быть возвращае- 162
Функции мое значение, — это массивом, поскольку он не является L-значением. В то время как в языке C можно было опустить возвращаемое значение, и тип функции автоматически определялся в качестве int, в стандарте ANSI C++ это не допускается, хотя некоторые компиляторы могут отнестись к такому определению толерантно. Компилятор GNU Compiler выдаст при этом ошибку — тип возвращаемого значения неверный, тогда как компилятор Visual C++ просто выдаст предупреждение. Если функция не должна возвращать значение, ее тип определяется ключевым словом void. За типом возвращаемого значения следует имя функции. Оно подчиняется тем же правилам, что и имя переменных (см. стр. 32). Имя начинается с латинской буквы или знака подчеркивания. Затем следует любое количество латинских букв, цифр и знаков подчеркивания. Регистр букв учитывается. Есть смысл потратить время на поиск подходящего имени, поскольку оно должно облегчать читаемость кода программы и в то же время быть заметным, чтобы не приходилось при каждом вызове функции возвращаться к ее определению. За именем функции следуют скобки, в которых могут содержаться параметры функции. Если параметры отсутствуют, скобки остаются пустыми или содержат ключевое слово void. Что такое параметры и как они должны быть построены, будет рассмотрено позже. В фигурных скобках следует тело функции — блок команд, который содержит выполняемый функцией код и завершается командой return. Пока функция имеет возвращаемое значение, после return должна быть указана переменная. На рис. 4.1 представлен граф синтаксиса для объявления функции. Рис. 4.1. Граф синтаксиса для объявления функции • Тип возвращаемого значения Здесь можно использовать любой тип, кроме массива. Функции без возвращаемого значения определяются ключевым словом void. В отличие от языка С, в языке C++ тип должен быть указан явно. 163
Глава 4 • Имя функции Имя начинается с латинской буквы или знака подчеркивания. Затем следуют латинские буквы, цифры и знаки подчеркивания в любом порядке. Имя соответствует графу синтаксиса, показанному на рис. 1.2 на стр. 33. • Параметры Функция может как вообще не иметь параметров, так и иметь любое их количество, при этом они разделяются запятой1. Определение параметра соответствует определению переменной. Подробнее параметры рассмотрены в следующей главе. • Тело Это последовательность команд, которые составляют содержимое функции и выполняются после ее вызова. Функция вызывается посредством указания ее имени. После него располагается пара скобок с параметрами или без них, это обязательно. Параметры вызова функции должны соответствовать определенным требованиям, описанным ниже. Если функция возвращает некоторое значение, ее вызов можно использовать в выражении. Например, вызов функции может располагаться справа от оператора присваивания. Следующий пример демонстрирует функцию без возвращаемого значения. Она выводит на экран разделяющую линию. Тип функции не может быть просто так опущен, вместо этого нужно указать ключевое слово void2. void separator() { cout << "-------------------------" << endl; return; } int main() { 1 Некоторые компиляторы могут ограничивать число возможных параметров. Кроме того, большое количество параметров указывает на плохое построение программы. 2 Void —пустой, лишенный чего-либо (англ.). Прим. ред. 164
Функции separator(); cout << "Программа для определения..." << endl; separator(); } Листинг 4.2. Простая функция separator() В функции, которая не возвращает значение, ключевое слово return используется без аргумента. Эта команда завершает работу функции. В примере ключевое слово return указано в самом конце. Там, где функция так или иначе завершит работу, команду return можно опустить. Объявление функции separator() происходит перед ее первым вызовом. Это имеет смысл, поскольку так компилятор будет знаком с данной функцией заранее и сможет проверить, в порядке ли ее параметры и имя. Если определить функцию в начале затруднительно, то предварительно следует объявить ее прототип. Как это сделать, описано далее. В функции main() функция separator() вызывается два раза. При вызове происходит сначала ее выполнение, а затем возврат в программу на следующую за вызовом функции строку. За кадром Как получается, что программа после вызова функции всегда находит правильный адрес возврата? Для этого используется так называемый стек1 — особая структура памяти, куда могут записываться данные, которые будут считываться в обратном порядке. Такую структуру можно представить в виде стопки книг. Если складывать книги одна на другую, прочесть можно только верхнюю, и получить книги из стопки можно только в порядке, обратном тому, как их складывали. Таким же образом при вызове функции адрес команды записывается в стек. После того как функция выполнена, программа возвращается по последнему адресу в стеке. Неважно, сколько переходов из одной функции в другую содержит программа, адрес возврата надежно защищен. По информации, которая хранится в стеке, в случае сбоя программы отладчик сделает вывод, после вызова какой функции программа дала сбой. Возможность помещать часто используемые блоки кода в функцию и вызывать ее в тех местах программы, где они используются, позволяет исключить ошибки при копировании части кода из одного места 1 От англ. stack — стопка. Прим. ред. 165
Глава 4 в другое. При корректировке функции остаются в выигрыше все части программы, где вызывается эта функция. Имеет смысл использовать функции даже в том случае, если они вызываются в программе только единожды. Функции служат для соблюдения порядка, чтобы разделить большую программу на отдельные части кода, которые решают только одну задачу. Отсюда следует, что законченность функции улучшает читаемость кода и уже этим уменьшает количество ошибок. 4.1. Параметры Функция может иметь параметры, которые служат для передачи в нее значений. Это делает работу с ней значительно более гибкой. Чтобы из функции добраться до значения переменной в основной программе, можно, конечно, использовать и глобальные переменные. Однако в параметрах открыто указывается, какую информацию функция получает извне. При этом можно говорить об интерфейсе функции. Объявление параметров функции аналогично объявлению переменных. Тип параметров важен для проверки соответствия передаваемого значения внутреннему параметру функции. Через имена параметров функция получает их значения извне. Для наглядности функция separator() будет расширена так, чтобы можно было задать количество символов "-" в разделяющей линии. void separator(int number) { int i; for (i=0; i<number; i++) { cout << "-"; } cout << endl; } ... int main() 166
Функции { int width = 45; separator(width); cout << "Программа для определения..." << endl; separator(45); } Листинг 4.3. Функция separator() с параметром Параметр number с точки зрения функции — самая обычная локальная переменная, в которую при вызове функции копируется значение передаваемого параметра. Поскольку речь идет о копии, функция не имеет доступа к передаваемым переменным. Ей доступна только локальная переменная number. Она может быть прочитана внутри функции. Поскольку эта переменная локальна, она также может быть изменена. Такие изменения переменной number внутри функции не затрагивают передаваемый в нее параметр width. Программист не замечает того, что происходит с переменными внутри функции. Значение, которое при вызове передается в функцию, копируется. Поэтому возможно передавать также константы (здесь 45). Передача данных между программой и функцией всегда должна осуществляться с помощью параметров. Даже если кажется, что удобнее использовать глобальную переменную, это следует делать только в хорошо обоснованных исключительных ситуациях. Доступ из функции к глобальной переменной называется эффект страниц. Он ведет к непредсказуемости связей. Поэтому такое поведение функции всегда должно комментироваться. Параметр передается в функцию в качестве значения. То есть содержимое переменных, которые передаются программой в функцию, нельзя изменить в ее теле. В примере значение переменой number изменялось бы в функции separator(),и это было бы заметно. Значение 45, с которым вызывалась функция, не изменится. Также не изменится переменная width, которая передавалась в функцию при первом вызове.  Переменные параметров являются локальными переменными функции, которые при ее вызове инициализируются в функции со значением, скопированным из передаваемых в нее параметров. 167
Глава 4 Параметры копируются в стек перед адресом возврата. Так можно получить к ним доступ из функции, как и к любым локальным переменным. После того как функция закончит свое выполнение, память, занимаемая ее переменными, будет освобождена. Это касается локальных переменных, переменных параметров и пространства памяти, которое было занято перед адресом возврата. На рис. 4.2 повторно представлен граф с рис. 4.1. На рис. 4.3 он будет дополнен параметрами функции. Рис. 4.2. Граф синтаксиса функции Рис. 4.3. Граф синтаксиса параметров Удивительно, что при объявлении параметров можно написать тип без соответствующего ему имени переменной. Естественно, внутри функции нельзя получить доступ к этому параметру. Однако есть случаи, в которых задан интерфейс функции и нужно написать его техническую реализацию. В связи с этим может оказаться, что какой-то параметр вообще не будет использоваться. Поскольку изменение интерфейса может повлиять на работу других частей программы, лишний параметр оставляют. Это может привести к предупреждению от компилятора, который полагает, что все параметры функции должны использоваться. Чтобы предупреждение исчезло, можно опустить имя параметра. Этим вы сообщите компилятору, что данный параметр намеренно не используется. Функция вызывается с помощью ее имени. Программа, вызывающая функцию, передает свои данные в нее в качестве параметров. При этом данные программы копируются в переменные параметров функции. Если функция возвращает данные в программу, то для этого используется команда return. Это схематически представлено на рис. 4.4. 168
Функции Рис. 4.4. Интерфейс функции 4.1.1. Прототипы Если вызов функции располагается в коде программы до ее непосредственного определения, компилятор не может проверить, корректен ли он, и сразу выведет сообщение об ошибке. Если невозможно или затруднительно определять функции в порядке вызова, можно объявлять их перед первым вызовом. Такое объявление называется прототипом. Объявление аналогично определению функции, однако вместо тела функции в фигурных скобках, после скобок с параметрами указывается только точка с запятой. В следующем примере функция separator() будет сначала определена, затем вызвана в main(), и только в конце она будет реализована. void separator(int sights_number); int main() { int width = 45; separator(45); cout << " Программа для определения..." << endl; separator(width); } void separator(int number) { ... } Листинг 4.4. Прототип 169
Глава 4 В прототипах можно опустить имена переменных или, как показано в листинге, для лучшей читаемости присваивать им иные имена. Для компилятора важны имя функции, тип возвращаемого значения и типы параметров. 4.1.2. Указатель в качестве параметра При вызове функции передаваемые параметры копируются в их переменные. Поэтому они словно находятся на улице с односторонним движением между программой, которая вызывает функцию, и самой функцией. В некоторых случаях требуется, чтобы функция изменяла переменные, которые в нее передаются. Это можно получить очень простым способом. Вместо непосредственно переменной в функцию передается ее адрес. Затем он копируется в переменную указателя, которая определена как параметр функции. Этот указатель является ссылкой, предоставляющей доступ к переменной, адрес которой передан в функцию, ипозволяющей изменить ее содержимое. В следующем примере функция increment() будет увеличивать на единицу переменную из программы, которая вызывает функцию. Для этого программа передает адрес переменной, который определяется с помощью стоящего перед ним символа амперсанда (&). Если программа передает адрес переменной целого типа, тип параметра должен быть целочисленным указателем. void increment(int *target) { *target += 1; } int main() { int my_value = 5; increment(&my_value); // после этого my_value равно 6 } Листинг 4.5. Изменяющийся параметр После вызова функции increment() содержимое переменной my_value будет равно 6. При вызове с помощью символа амперсанда 170
Функции четко указывается, что функция получает доступ к этой переменной. Параметром является указатель.  Чтобы функция могла изменить содержимое переменной, ее адрес передается в качестве аргумента функции. Соответствующий параметр будет определен в качестве указателя на тип переменной. С помощью этого указателя можно изменить содержимое переменной программы, которая вызвала функцию (рис. 4.5). Рис. 4.5. Указатель в качестве параметра График демонстрирует, как программа обслуживает параметр target с помощью адреса переменной my_value, и как функция использует переданный указатель *target, чтобы получить доступ к переменной my_value. Константный указатель Вместе с возможностью изменять внешние переменные использование указателя имеет преимущество при работе с большими структурами данных, поскольку не требуется копировать большое количество информации. Указатель на современном компьютере занимает четыре или восемь байт. Также он иногда используется в случаях, когда доступ к данным основной программы не предусматривается. Чтобы определить это корректно, указатель объявляется константным. На стр. 14 уже 171
Глава 4 было показано, как можно двумя способами объявить переменные указателей константными. Эти варианты действительны и для параметров функций. const int *const_target; Если ключевое слово const указано перед типом, это означает, что переменная основной программы не может изменяться с помощью такого указателя. Указатель сам по себе может инкрементироваться в функции, например, для перебора элементов массива. int * const const_pointer; Если ключевое слово const стоит перед именем указателя, это означает, что указатель const_pointer не изменяется. Например, он не может быть инкрементирован. При этом разрешено изменение переменной, на которую он ссылается. const int * const Constant; Последний пример сочетает в себе оба варианта и запрещает как инкрементирование указателя, так и доступ к переменной. Использование const имеет обоснованную пользу. Оно говорит программе, что может делать функция с указателем, а что нет. За выполнением этих условий следит компилятор. 4.1.3. Массив в качестве параметра Массив также можно передать в функцию в качестве параметра. Для этого допустимо определить его множеством разных способов. Например, если в функцию должен быть передан массив целых чисел из пяти элементов, можно использовать следующие определения. void fa(int a[5]); void fb(int a[]); void fc(int *a); Параметр функции fa() указывает, что она ожидает массив из пяти целых чисел. Предполагается, что эти значения будут скопированы в функции. Однако дело происходит иначе. Массив не копируется, передается только его адрес. Изменения, которые произойдут при выполнении функции, будут производиться непосредственно с массивом, а не 172
Функции с его копией. Компилятор не проверяет, содержит ли массив действительно пять элементов. Параметр fb() ожидает массив. Поскольку в квадратных скобках ничего не указано, можно догадаться, что здесь размер массива не контролируется. Изменения массива в функции отражаются непосредственно на оригинальном массиве. Параметром fс() является указатель на целочисленную переменную. Как и ожидается, здесь также передается массив. Все три параметра совместимы между собой. Несмотря на то, что все три параметра технически работают одинаково, рекомендуется определять массив с помощью квадратных скобок. При беглом чтении кода сразу будет понятно, что в этом месте ожидается массив, а не требуется получить доступ к переменной с помощью указателя. Если требуется запретить изменение данных массива в функции, следует указать ключевое слово const перед определением параметра. Тогда при каждой попытке изменения данных в массиве компилятор будет выдавать ошибку. void fa(const int a[5]) { a[2] = 3; // компилятор выдаст ошибку! } Многомерный массив Для многомерных массивов только первую размерность можно определять тремя вышеприведенными способами. Вторая размерность должна быть четко определена. Допустимыми являются следующие прототипы: void fa(int a[5][4]); void fb(int a[][4]); void fc(int (*a)[4]); Сходство в передаче массивов для первой размерности очевидно. Вторая размерность указана непосредственно. Величина второй размерности должна быть указана, поскольку внутри функции невозможно будет корректно выделить память. Например, 173
Глава 4 элемент a[1][1] находится в нашем примере (выше) на седьмой позиции. Если бы вторая размерность равнялась 10, тогда элемент a[1][1] был бы на двенадцатой позиции. В случае двумерного массива вторая размерность проверяется компилятором. Вызов функции с массивом, объявленным в качестве b[5][3], будет обнаружен и запрещен компилятором. Скобки вокруг *а в функции fc()необходимы, иначе параметр будет определен не как двумерный массив, а в качестве одномерного массива с указателем на целый тип. 4.1.4. Ссылочный параметр В языке C++ существует еще один способ, как из функции получить доступ к внешней переменной. Для этого вместо указателя в качестве параметра используется ссылка. Ссылка при определении параметров синтаксически отличается тем, что вместо символа астериска указывается амперсанд. Содержимое указателя и ссылки различается тем, что в ссылку не копируется адрес, она непосредственно указывает на переменную. Таким образом, ссылочная переменная не содержит адрес в памяти, а является в некоторой степени другим именем объекта, который передается в качестве аргумента. Внутри функции доступ к переменной осуществляется не с помощью указателя, переменная здесь обрабатывается непосредственно. Следовательно, при вызове функции передается не адрес, а переменная. При вызове функции с переменной в качестве параметра нельзя отличить ссылку на переменную от обычного параметра. Такой вызов функции в случае использования константного параметра недопустим: #include <iostream> using namespace std; int func(int &parameter) { parameter = parameter + 4; } int main() { int number = 5; 174
Функции func(number); cout << number << endl; // результат: 9! } Листинг 4.6. Передача параметров по ссылке (linkpar.cpp) Разница между указателем и ссылкой, используемыми в качестве параметров, совсем незначительная. На первый взгляд, она состоит только в синтаксисе определения. И на самом деле, на практике можно одно заменять другим. Разница заключается только в том, что указателю в любой момент можно присвоить другое значение, и указать тем самым на иную переменную. Ссылка, напротив, является представителем передаваемой переменной и не может использоваться для какой-либо иной. В больших структурах данных ссылка имеет значительное преимущество при передаче данных в функцию, поскольку при этом не требуется копировать всю структуру. По этой причине ссылочная передача данных широко используется, даже когда переменные в функции не должны изменятся. Если необходимо защитить данные от изменения в вызываемой функции, используется ключевое слово const. int func(const bigtype &link_par) { ... По причине того, что ссылка является представителем переменной, нельзя ни устанавливать ссылки в 0, ни присваивать им иного значения. Ссылка получает свое значение с помощью инициализации или при вызове функции. Присваивание всегда ведет к изменению переменной, на которую указывает ссылка. Ссылка может использоваться в качестве возвращаемого значения. Следует следить за тем, чтобы она не указывала на локальную переменную функции, так как после выполнения функции локальная переменная удаляется (см. стр. 166 и далее). В таком случае ссылка будет указывать на недействительную переменную, что рано или поздно приведет к проблемной ситуации. Упражнение • Напишите функцию с именем swap(), которая получает два ссылочных параметра и обменивает их содержимое между собой. Решение приведено на стр. 503. 175
Глава 4 4.1.5. Пример: стек Следующий пример реализует стек. Данная структура помещает данные в память и выдает их в порядке, обратном записи. Две ключевые операции стека — это push()1 для записи данных, и pop()2 для их получения. В качестве данных используются целые числа. Есть только один стек, доступ к которому можно получить с помощью переменной Stack. #include <iostream> using namespace std; struct TListStack { int data; // симуляция данных TListStack *next; // связь со следующим элементом }; TListStack *Anchor = 0; // глобальный начальный пункт стека void push(int data) // добавление нового элемента { // создание нового элемента: TListStack *node = new TListStack; node->data = data; // заполнение данными node->next = Anchor; // прикрепление к предыдущему элементу Anchor = node; // установка нового начального пункта } int pop() // получение последнего записанного в стек элемента { int content=0; // промежуточное сохранение элемента if (Anchor) // не равен 0! Стек не пустой! { // сохранение указателя чтобы в будущем удалить //элемент: TListStack *old = Anchor; 1 2 176 В пер. с англ. «толкать». Прим. ред. В пер. с англ. «вытягивать». Прим. ред.
Функции Anchor = Anchor ->next; // следующий элемент вперед content = old->data; // сохранение данных delete old; // удаление элемента } return content; // возвращение содержимого } int main() // тест функции стека { push(2); // записать данные в стек push(5); // записать данные в стек push(18); // записать данные в стек cout << pop() << endl; // вывод должен быть 18 cout << pop() << endl; // вывод должен быть 5 cout << pop() << endl; // вывод должен быть 2 } Листинг 4.7. Стек в качестве связанного списка (stackstruct.cpp) Можно, конечно, вместо целочисленной переменной использовать любые другие структуры данных. В случае если в программе стек используется для различных типов данных, допустимо вместо данных использовать указатель на них. Используя указатель на тип void, можно создать гибкие структуры со всеми возможными типами. В конце концов, смысл функций push() и pop() при изменении типа данных не меняется. Но прежде чем потратить силы на эксперименты в этой области, рекомендуется рассмотреть примеры. Возможно, проблема уже имеет значительно более элегантное решение (см. стр. 317). Начиная со стр. 229, тема стека будет рассмотрена еще раз на базе классов. С помощью классов можно значительно легче и надежнее реализовать связанные списки. Упражнение • Расширьте программу стека так, чтобы в ней можно было использовать не один , а сколько угодно различных стеков, в том числе и локальных. Подсказка: вместо функций push() и pop() в глобальном списке используйте переменную Anchor, список следует передавать в качестве параметра. Решение приведено на стр. 504. 177
Глава 4 4.1.6. Предопределенные параметры Язык C++ позволяет указывать некоторые параметры функции с какими-то значениями уже при определении функции. Тогда при вызове функции можно опустить любые из этих параметров. При этом они получат значение, которое было им присвоено при определении функции. void PreVar(int a, int b, int c=4, char d='A', float e=0.0) { ... } int main() { PreVar(5, 2, 5); } Листинг 4.8. Предопределенные параметры После вызова функции PreVar() ее параметры предопределены следующим образом: • а==5 Значение установлено при вызове функции • b==2 Значение установлено при вызове функции • с==5 Значение установлено до вызова функции. Хотя параметр предопределен со значением с=4, это, однако, будет действительно только в том случае, если при вызове функции будет задано не более двух параметров. • d=='A' При вызове функции были заданы только три параметра, поэтому используется предопределенное значение. • е==0.0 При вызове функции были заданы только три параметра, поэтому используется предопределенное значение. 178
Функции 4.1.7. Параметры функции main Пришло время рассмотреть поближе функцию main(). Она так же имеет параметры и тип возвращаемого значения. Правда, большинство компиляторов разрешают опускать и то и другое. Стандарт ANSI (см. стр. 495) предписывает, чтобы функция main() определялась с типом возвращаемого значения. Тип возвращаемого значения функции main() является целочисленным, это значение функция возвращает операционной системе. Значение 0 говорит о том, что программа завершила работу без сбоев. Все остальные значения интерпретируются системой в виде номеров ошибок. Что конкретно означает каждое число, четко не предопределено и задается программистом. Возвращаемые значения могут обрабатываться с помощью вызывающих программ или скриптов. Поэтому для каждой возможной причины сбоя программы следует определить свой номер ошибки и описать его. Функция main() имеет два параметра вызова. По ним программа может определить, какой командой и с какими аргументами вызвана функция. Первый параметр традиционно называется argc1. Это параметр целочисленного типа, который определяет количество параметров запущенной программы. Имя программы также считается параметром, поэтому минимальное значение argc равно 1. Второй параметр называется argv2 и представляет собой массив указателей на тип char. Здесь располагается имя программы и ее параметры, задаваемые операционной системой. Полное определение main() имеет следующий вид3: int main (int argc, char* argv[]) { ... } Имена argc и argv являются чистой условностью. Эти параметры можно назвать иначе. Однако, поскольку они известны именно под та1 argc состоит из английского словосочетания «argument count», что означает «количество аргументов». 2 argv от английского словосочетания «argument vector», что в переводе означает «вектор аргументов». 3 В некоторых случаях можно встретить третий параметр, которым является указатель на переменную режима работы. Однако этот параметр не поддерживается стандартом POSIX. Причина в том, что доступ к этой переменной посредством функций getenv() и setenv() гораздо проще. 179
Глава 4 кими именами, их изменение вряд ли приведет к лучшему пониманию кода программы. Рассмотрим сначала второй параметр argv. Это массив указателей на С-строки. Графически его можно представить следующим образом (рис. 4.6). Рис. 4.6. Параметр argv Параметр argv является массивом, содержащим указатели. Каждый из указателей ссылается на определенную С-строку, то есть на массив элементов типа char, конец которого обозначен нулевым байтом. Первый указатель, то есть элемент argv[0], всегда указывает на имя программы, с которым она была запущена. Соответственно, значение параметра argc всегда равно минимум 1. Эта особенность прежде всего используется в UNIX, чтобы с помощью имени программы управлять также ее функциональностью. Яркий пример — программа архивации gzip, которая распаковывает архив, если ее вызвать под именем gunzip. Если имя файла задано в качестве параметра, оно будет располагаться в argv[1]. В таком случае переменная argc будет иметь значение 2. Следующая программа демонстрирует имя, под которым она была вызвана. В случае, если были заданы дополнительные параметры, они будут выведены в строке. #include <iostream> using namespace std; int main(int argc, char **argv) { 180
Функции for (int i=0; i<argc; i++) { cout << argv[i] << endl; } return 0; } Листинг 4.9. Отображает параметры вызова (main.cpp) 4.1.8. Переменное количество параметров Существуют функции, к примеру printf() (см. стр. 373), которые допускают переменное количество параметров. В таких функциях в списке параметров после последних специфицированных указаны три точки. Они сообщают, что далее может располагаться любое количество дополнительных параметров любого типа. При вызове таких функций к уже определенным предписанным параметрам можно добавить сколько угодно дополнительных. К этим дополнительным параметрам нельзя добраться внутри функции так же просто, как к обычным, поскольку они не имеют имен, которые можно использовать для доступа к ним. С помощью макрокоманды va_start() можно получить доступ к дополнительным параметрам. Первый — это указатель на тип va_list. По его содержимому функция va_arg() определяет, какой размер имеют переменные. Этот указатель определяется макрофункцией va_start(). Второй параметр va_start() — последний стандартный параметр функции. Доступ к параметрам прекращается после вызова макроса va_end(), единственный параметр которого — указатель. Между тем доступ к отдельному параметру осуществляется с помощью макроса va_arg(). Первый параметр — указатель. Второй — тип данных, который ожидается. Возвращаемое значение макроса является входным параметром функции с переменными параметрами. #include <iostream> using namespace std; #include <stdarg.h> void func(int num, ... ) 181
Глава 4 { va_list param; // указатель на параметр int intpar; // содержимое параметра va_start(param, num); // подготовка доступа // просмотр всех параметров //(зависит от переменной num) for (int i=0; i<num; i++) { intpar = va_arg(param, int); // достать параметр cout << intpar << endl; // вывести его } cout << "---" << endl; va_end(param); // закрыть доступ } int main() { // проверка функции с различными параметрами func(2, 5, 2); func(0); func(4, 5, 2, 0, 3); } Листинг 4.10. Доступ к дополнительным параметрам (vararg.cpp) Количество параметров передается в функцию из программы, которая ее вызывает. Макрос не несет эту информацию. В примере выше количество параметров ожидается в качестве первого параметра функции. В функцию func() можно передавать только целочисленные параметры. Для функции printf() в качестве типа и количества параметров используется формат строк. Как мы видим, применение этой техники легко приводит к ошибкам. Компилятор не может проверить правильность параметров. Если при вызове функции передано неверное их количество, она получит безобразие вместо данных, однако ошибка будет заметна не сразу. По этой причине переменные параметры используются только там, где это неизбежно. 182
Функции 4.2. Перегрузка функций В классических языках программирования, таких как C, функция имеет свое оригинальное имя. В таких языках две функции не могут носить одинаковое имя. Язык C++ предлагает роскошную возможность не называть функцию другим именем только потому, что она имеет другое количество параметров. Компилятор при вызове функции связывает ее имя с соответствующим количеством параметров. Следующий пример демонстрирует две функции, которые имеют одинаковые имена, но разные параметры. Компилятор на основании параметров распознает, какая функция с каким вызовом связана. #include <iostream> using namespace std; void show(int i) { cout << "int: " << i << endl; } void show(double f) { cout << "double: " << f << endl; } int main() { show(12); // вызывает первую функцию (12 тип int) show(2.5); // вызывает вторую функцию (2.5 тип float) } Листинг 4.11. Перегруженные функции (overload.cpp) Функции отличаются параметрами, а не типом возвращаемого значения. 4.3. Коротко и быстро: встроенные функции Вызов функции требует определенного времени. Адрес возврата, а также параметры записываются в стек. Функция запускается. После ее выполнения параметры удаляются из памяти, и программа возвра- 183
Глава 4 щается обратно к пункту, из которого она ушла в функцию. Несмотря на то, что кажется, будто это очень долго, для вызова функции тратится незначительная часть времени выполнения программы, и зачастую это время не принимается в расчет. Однако в программах, выполнение которых должно быть максимально быстрым, время на вызов функции может приобретать бо́льшую значимость. Для его уменьшения перед функцией можно использовать атрибут inline. Тогда компилятор не будет вызывать команды функции, а начнет сразу же копировать ее код в позицию вызова. Если компилятор определит, что такая подстановка не приносит выигрыша во времени, то он будет компилировать встроенные функции так же, как и обычные. #include <iostream> using namespace std; inline int min(int a, int b) { return a<b ? a : b; } int main() { cout << min(4, 3) << endl; cout << min(3, 4) << endl; } Листинг 4.12. Встроенная функция (inline.cpp) В примере код функции используется не обычным способом — подставлен непосредственно в позицию вызова. Этим экономится время на переход к функции, копирование параметров в стек и переход обратно в программу. Скомпилированный код будет выглядеть так, словно он был написан в позиции вызова. #include <iostream> using namespace std; int main() 184
Функции { cout << 4<3 ? 4 : 3 << endl; cout << 3<4 ? 3 : 4 << endl; } Листинг 4.13. Преобразование Встроенная функция имеет те же преимущества, что и обычная, предоставляя при этом выигрыш во времени. Механизм подстановки, конечно, увеличивает код программы. Однако, если функция не особенно большая, это не принимается во внимание. 4.4. Нисходящий метод Функции позволяют не только многократно использовать единожды написанный код. Они являются, прежде всего, возможностью разделять большие программы на отдельные блоки. Удачное разделение состоит из функций, которые сами включают в себя вызовы функций, и при этом решение задачи описывается чередой маленьких шагов. Нисходящий метод — это техника разработки, при которой комплексная проблема разделяется на шаги. Например, основная программа начинается с вызова функции инициализации. Затем следует цикл ввода данных и их обработки до тех пор, пока не встретится некое событие, которое завершит программу. Даже это условие может быть сформулировано в виде функции, тип возвращаемого значения которой bool. Таким образом, программа разделена уже на первом этапе. Затем функция инициализации также делится на части. Здесь можно было бы для каждой важной структуры данных программы вызывать отдельную функцию. Таким же образом другие функции делятся на меньшие подпрограммы. При этом функции становятся все проще и читаемей, пока они не будут состоять только из нескольких строк кода. Такая манера разработки реализуется с помощью процедурных языков и позволяет подразделять программу на логически связанные функции. Даже если видно, что объектно ориентированное программирование имеет иную стратегию разработки, нисходящий способ проектирования функциональных модулей или проектов, которые менее ориентированы на данные, чем на методы, может оказаться очень полезным. 185
Глава 4 4.4.1. Пример: игра «Бермуда» Лучше всего рассмотреть разделение задачи на части и упрощение ее с помощью нисходящего проектирования на примере. Для этого рассмотрим игру «Бермуда», правила которой описаны на стр. 131. Сначала игра будет описана несколькими общими шагами. В фазе инициализации сперва создается игровое поле. Затем оно выводится на экран, пользователь получает возможность ввода, и программа определяет, сколько кораблей видно из выбранной пользователем точки. Последние шаги повторяются, пока игра не выиграна или пользователь отказывается играть дальше и прерывает программу. На рис. 4.7 в виде структурной диаграммы показан главный цикл игры. Рис. 4.7. Структурная диаграмма игры «Бермуда» Структурная диаграмма описывается следующим кодом. int main() { init_game_field(): do { output_game_field(); input_data(); search_ships(); write(); } while (!end_game()); } 186
Функции Отельные части задачи выделены в функции. Они могут раскладываться дальше и дальше. Требуемые параметры еще не были рассмотрены. При этом поток данных является важным аспектом разделения программы на отдельные функции. Качество разложения на отдельные функции зависит еще и от того, сколько параметров следует передавать. Если их слишком много, это признак того, что функция составлена не оптимальным образом. Для игры «Бермуда» требуется игровое поле. К нему прилагается также массив из четырех кораблей. Между вводом данных пользователем и поиском кораблей требуются также координаты. И в завершение необходимо количество направлений в игровом поле. Для этого используется следующий код: int main() { char game_field[X][Y]; tShip ship[max_ship]; int x, y; int number; init_game_field(game_field, ship); do { output_game_field(game_field); input_data(); number=search_ships(ship,x,y); write(game_field, x, y, number); } while (!end_game(ship)); } Листинг 4.14. Главная программа Рассмотрим функцию init_game_field(). Она выполняет две операции. Сначала должен быть подготовлен двумерный массив. Затем корабли следует расставить на свои позиции. Оба задания уже были выполнены на стр. 131 и 150. Единственное, что осталось сделать, — упа- 187
Глава 4 ковать эти фрагменты программы в функции и определить их входные параметры. void init_output(char game_field[X][Y]) { ... } void init_ship(tShip ship[]) { ... } void init_ game_field(char game_field[X][Y], tShip ship[]) { init_output(game_field); init_ship(ship); } Листинг 4.15. Инициализация Вывод игрового поля уже был запрограммирован, а ввод данных легко реализуется одной строкой. Запись в игровое поле также не несет в себе ничего сложного. Логическую функцию end_game() тоже легко написать. Она состоит из простого цикла, который проверяет, найдены ли все корабли. Самое интересное задание — это реализовать ответ на запрос пользователя. Тут решение не сразу бросается в глаза, поэтому разложим функцию search_ships() на отдельные части. Поиск кораблей начинается с позиции, указанной пользователем. Сначала должно быть установлено, не найден ли корабль на координатах, непосредственно введенных пользователем. В таком случае, корабль будет помечен как найденный, из константы max_ship будет вычтена единица, причем данная константа не должна ни в коем случае быть спутана с количеством найденных кораблей. В случае если по координатам пользователя корабль отсутствует, следует производить поиск влево, вправо, вниз, вверх и по всем диагоналям. 188
Функции int search_ships(tShip ship[], int x, int y) { int number=0; if (ship_exist(ship, x, y, true)) { return max_ship; } else { number += search_left(ship, x, y); number += search_left_up(ship, x, y); number += search_up(ship, x, y); number += search_right_up(ship, x, y); number += search_right(ship, x, y); number += search_right_down(ship, x, y); number += search_down(ship, x, y); number += search_left_down(ship, x, y); } return number; } Листинг 4.16. Поиск кораблей Восемь функций поиска похожи между собой. Например, рассмотрим функцию search_right_up(). Она достаточно сложная, и если будет реализована, то все остальные легко написать по ее подобию. Данная функция увеличивает координаты x и y, начиная с позиции, введенной пользователем, пока не встретится корабль или край игрового поля. int search_right_up(tShip ship[], int x, int y) { x++; y--; while(x<X && y>=0) { if (ship_exist(ship, x, y)) { 189
Глава 4 return 1; } x++; y--; } return 0; } Листинг 4.17. Диагональный поиск Под конец еще следует реализовать функцию ship_exist(). Это относительно просто осуществить с помощью цикла for, если проверять каждый корабль, не занимает ли он текущую позицию. Функция возвращает значение false, если корабль не найден, а также помечает найденный корабль, однако последнее происходит не всякий раз. В задачи функции входит еще и просмотр направлений. При этом подсчитывается количество видимых кораблей. Таким образом, требуется следующий параметр, который сообщает, должна ли функция маркировать корабль или нет. При этом он будет использоваться только тогда, когда корабль найден, предварительно его значение устанавливается в false. bool ship_exist(tShip ship[], int x, int y bool mark=false) { for (int i=0; i<max_ship; i++) { if (ship[i].x==x && ship[i].y==y) { if (mark) { ship[i].finded = true; } return true; } } return false; } Листинг 4.18. Вопрос попадания 190
Функции Видно, что изначально казавшаяся сложной задача, легко решается при разделении ее на отдельные части. Полный листинг программы можно найти на диске, прилагающемся к книге, в файле под именем bermuda3.cpp. В нем уже реализовано решение поставленной ниже задачи. Упражнение • Это конечно не очень хорошо, что поиск в различных направлениях осуществлен разными, но, тем не менее, похожими друг на друга функциями. Вместо этого значительно более элегантно было бы, если бы использовалась только одна функция, которая выполняла бы поиск в различных направлениях, в зависимости от того, какие параметры в нее передаются. Для реализации этой задачи требуется поразмыслить. Однако не сдавайтесь слишком быстро. Решение приведено на стр. 506. 4.5. Область действия переменных Область действия переменных уже упоминалась в связи с блочной конструкцией языка. В связи с функциями область действия переменных имеет особое значение. Функции должны быть написаны так, чтобы они были как можно более закрытыми от доступа извне. Особенно это касается переменных. 4.5.1. Глобальные переменные Переменные, объявленные вне функций, действительны во всех функциях программы. При старте программы они создаются и инициализируются. При завершении программы они удаляются. Хотя значения глобальных переменных и устанавливаются равными 0 при создании, тем не менее рекомендуется инициализировать их в коде самостоятельно. Для глобальных переменных можно также использовать прототипы. Такое объявление отличается от обычного определения переменной тем, что под нее не выделяется память. Определение «знакомит» переменную с компилятором. При этом также указывается ее имя и тип. Поэтому определение является также и объявлением. Переменная может быть объявлена единожды, но определять ее можно сколько угодно раз, пока определение соответствует объявленным параметрам. 191
Глава 4 Переменная объявляется без ее определения, если перед ней указано ключевое слово extern. При этом extern не означает, что определение переменной должно находиться в другом файле. Это говорит только о том, что это не определение переменной, а ее объявление. enum tMarker = ... ; extern tMarker actual_marker; tError read_number(char *source) { ... actual_marker = NUMBER; ... } ... tMarker actual_marker = 0; Листинг 4.19. Объявление переменной В большинстве случаев рекомендуется избегать глобальных переменных. Многие глобальные переменные могут легко привести к тому, что код программы потеряет читаемость и гибкость. Передача данных между функциями должна реализовываться с помощью параметров. Если данные о длительности вызова функции требуется получить во время ее выполнения, используется локальная переменная. Хотя в некоторых редких случаях, когда необходимо использование незначительного количества глобальных переменных, требующихся почти для всех функций, их использование значительно упрощает интерфейс функции. 4.5.2. Локальные переменные Если внутри блока объявлена переменная, она называется локальной. Вне функции нельзя получить доступ к этой переменной. Она создается и инициализируется при ее определении, и удаляется, когда программа покидает блок, в котором переменная была объявлена. Локальные переменные хранятся в стеке (см. стр. 523). Там же располагаются адрес возврата вызванной функции и ее параметры. Эта 192
Функции память легко освобождается после того, как программа покидает функцию. В противовес глобальным переменным, область памяти стека не установлена в 0 до ее использования. Область памяти переменной после выхода из функции должна быть освобождена, однако ее значение после этого не исчезает. По этой причине не инициализированные локальные переменные имеют неопределенное значение и с большой вероятностью не равны 0. Везде, где это возможно, следует использовать локальные переменные. Таким образом функции не будут иметь непредсказуемого эффекта страниц, как это может случиться при использовании глобальных переменных. Память станет оптимально использоваться, поскольку она задействована только во время выполнения функции. Даже если вы еще не почувствовали этой проблемы, имейте в виду: функции, которые используют глобальные или статические переменные, с большой вероятностью не могут работать параллельно. Переменные параметров также являются локальными. Они помещаются в стек и удаляются из него при завершении функции, а инициализируются с параметрами вызова функции. Только ссылочные переменные не могут быть локальными, поскольку они являются ссылкой на переменную программы, вызывающей функцию. 4.5.3. Статические переменные Локальные переменные могут быть объявлены в качестве статических1. Такие переменные создаются и инициализируются, когда их определение встречается в программе первый раз. Переменная и ее содержимое при этом сохраняется до конца выполнения программы. Для определения переменной в качестве статической используется ключевое слово static. Такие переменные видимы только в блоке, в котором они определены. В отличие от глобальных переменных к ним нельзя получить доступ извне. Статическая переменная — средство выбора, если функция требует некоторого подобия памяти для информации о ее предыдущем вызове. 1 Можно описывать глобальные переменные как статические. Однако это будет иметь совершенно иное значение. 193
Глава 4 В следующем примере сравниваются три переменные, и демонстрируется различное их поведение. #include <iostream> using namespace std; int globvar = 1; void func() { static int statvar = 1; int locvar = 1; cout << "статическая:" << statvar; cout << "глобальная:" << globvar; cout << "локальная:" << locvar << endl; statvar++; globvar++; locvar++; } int main() { func(); func(); func(); locvar = 8; // компилятор выдаст ошибку statvar = 8; // компилятор выдаст ошибку globvar = 8; // это работает func(); func(); } Листинг 4.20. Статическая переменная Функция func() работает с тремя переменными. Все три выводятся и декрементируются. Функция вызывается в главной программе три раза. Затем программа пытается присвоить переменным значение 8. Компилятор сразу же запретит это для переменных locvar и statvar, 194
Функции поскольку они доступны только внутри функции func(). Она вызывается еще два раза. Когда обе ошибки будут устранены, программу можно запустить. статическая: 1 глобальная: 1 локальная: 1 статическая: 2 глобальная: 2 локальная: 1 статическая: 3 глобальная: 3 локальная: 1 статическая: 4 глобальная: 8 локальная: 1 статическая: 5 глобальная: 9 локальная: 1 В примере показано поведение различных переменных. Поведение локальной переменной locvar пояснить проще всего. Она создается поновому при каждом вызове функции и разрушается, когда программа покидает функцию. Каждый раз переменная инициализируется заново и принимает значение 1. После инкрементирования внутри функции переменная имеет значение 2. Но это ей не поможет, поскольку, так или иначе, при завершении функции она будет удалена. Глобальная переменная globvar создается и инициализируется при старте программы. Внутри функции она инкрементируется и выводится. Поскольку эта переменная является глобальной, она доступна во всех функциях и в главной программе. Это продемонстрировано с помощью того, что в main() после третьего вызова функции func() устанавливается значение 8. Во время следующего вызова она обрабатывает эту переменную уже с новым значением. Статическая переменная statvar ведет себя частично как локальная, частично как глобальная переменная. Как локальная переменная она защищена от неконтролируемого доступа извне. С другой стороны, она не теряет своего значения после завершения работы функции. Инициализация проводится только один раз, при первом старте. 4.6. Рекурсивные функции Для решения определенных задач требуются функции, которые вызывают сами себя. Это называется рекурсией, что подразумевает повторение. Чтобы цикл не стал бесконечным, вызов функцией самой себя должен подчиняться определенному условию, которое однажды приведет к завершению. Так, многие циклы запрограммированы в качестве ре- 195
Глава 4 курсии. Первый пример демонстрирует преобразование цикла по принципу рекурсии. Второй — тоже самое, но без рекурсии. Обе функции подсчитывают сумму чисел от 1 до max. long sum_recursion(long max) { if (0 < max) { return sum_recursion(max - 1) + max; } return 0; } long sum_iterat(long max) { long Sum = 0; while (0 < max) { Sum+=max; max--; } return Sum; } int main(int argc, char** argv) { long max = atol(argv[1]); cout << sum_recursion(max) << endl; cout << sum_recursion(max) << endl; } Листинг 4.21. Сумма от 1 до max (recsum.cpp) Экономия двух строк, вероятно, не впечатляет. Но в примере речь идет не об элегантности или эффективности, а о предоставлении альтернативного решения. Этот простой пример упрощает понимание работы рекурсии. 196
Функции Тело такой функции соответствует телу цикла. Он не содержит решения всей задачи, а реализует один шаг. Так же, как задача решается путем повторения цикла, рекурсия позволяет получить решение с помощью вызова самой себя. За кадром Рекурсия использует стек, в который при каждом повторном вызове функции помещаются адрес возврата, локальные переменные и параметры. Если функция запрограммирована без эффекта страниц, стек хранит в себе полный набор всех предыдущих ее состояний. Если функция вызывает сама себя с другими параметрами, то предыдущее состояние остается в стеке, словно «замораживается». В верхушке стека выделяется память для новых значений. При завершении повторения функции пространство памяти стека, выделенное под нее, очищается, и функция возвращается к «замороженным» значениям. Следующая схема должна точнее описать то, как работает рекурсия. sum_recursion(4) [=6]+4 sum_recursion(3) sum_recursion(2) [=3]+3 [=1]+2 sum_recursion(1) [=0]+1 sum_recursion(0) Сначала функция sum_recursion() вызывается с параметром 4. Пока его значение не станет равно 0, она вызывает саму себя с параметром, уменьшенным на 1. Здесь получилось бы 3. Вызов повторяется за вызовом, пока значение параметра не станет равным 0. После этого функция не вызывает себя, а просто выполняет все следующие далее в ее теле команды. Она возвращает 0. Затем функция переходит в предыдущий свой вызов, где значение параметра max было равно 1. Оно добавляется к результату, который вернула функция, и передается далее в качестве результата. Затем возврат происходит на тот уровень, где значение параметра max было равно 2. Так продолжается до тех пор, пока функция sum_recursion(4) не вернет 10. 4.6.1. Область действия Определенные задания, которые с помощью рекурсии можно решить в несколько строк, иначе можно реализовать только с основательными 197
Глава 4 затратами. Сюда относится, к примеру, задача, при которой программа ветвится на манер дерева. Представьте, например, функцию, которая должна выводить код на языке C++ так, что все файлы, которые подключаются до #include, должны также выводиться на печать. При этом следует учитывать, что каждый подключенный файл может содержать несколько команд #include. С помощью цикла можно реализовать сложный трюк, когда программа после печати одного файла должна возвращаться обратно, где она только что была. Следующий каркас функции печати демонстрирует, как можно было бы решить эту задачу с помощью рекурсии. print_recursion(char * FileName) { open_file(FileName) while (!end_file()) { read_line(); if (command_include(IncludeFileName)) { print_recursion(IncludeFileName); } print_line(); } } Листинг 4.22. Рекурсия печати Эта функция справляется с любым количеством вложенных одна в другую команд #include и в целом достаточно легко читается. Можно исходить из того, что препроцессор C++ (см. стр. 521) считывает файлы также рекурсивно. 4.6.2. Пример: бинарное дерево Бинарное дерево проще всего реализовать с помощью рекурсии (рис. 4.8). Оно состоит из узлов, которые имеют две ветви, указывающие на следующие узлы. Каждый узел содержит некоторую информацию. 198
Функции 27 16 7 36 19 43 Рис. 4.8. Бинарное дерево Еще раз следует отметить то, что с точки зрения биологии кажется странным: корни такого дерева находятся вверху. В примере выше в корне находится число 27. Узлы слева содержат значения меньше 27, справа — больше. Это верно для всех узлов этого дерева. Если информация отсортирована подобным образом, ее можно найти очень быстро. Чтобы построить такое дерево в программе, нужно определить кажый узел, и связать их все в форме дерева. Структура данных узла имеет следующий вид: struct tTree { int content; tTree *left; tTree *right; }; Листинг 4.23. Узел бинарного дерева Дерево создается, когда несколько узлов связываются друг с другом с помощью указателей left и right, не создавая при этом замкнутый круг. Напомню: узел создается с помощью команды new (см. стр. 152). Возвращаемое значение — это указатель на новый узел. Для доступа к его элементам следует после указателя ввести комбинацию символов -> и затем сообщить имя элемента, к которому требуется доступ (см. стр. 146). 199
Глава 4 tTree *NewKnot; NewKnot = new tTree; NewKnot -> content = 5; Листинг 4.24. Создание и заполнение узлов бинарного дерева Следующий листинг демонстрирует функцию, которая перебирает элементы бинарного дерева и выводит их на экран. До этого вы можете, вооружившись карандашом, попробовать перебрать по порядку элементы дерева, расположенного на рисунке. При этом следует постоянно придерживаться левой стороны. Если дальше влево двигаться нельзя, следует отобразить содержимое узлов и попробовать сделать шаг вправо, а после, если это возможно, снова двигаться влево. Рассматривая рекурсивную функцию output_tree(), можно установить, что она работает именно таким образом. Сначала проверяет, существует ли вообще текущий лист. Затем вызывает саму себя для левой ветви. Функция будет вызывать себя до тех пор, пока ветви слева существуют. Когда она больше не находит левой ветви, содержимое текущего узла выводится на экран. Затем функция вызывается для правой ветви. Отсюда она снова просматривает все левые части, пока они не закончатся. Очевидно, что рекурсивное решение соответствует интуитивному решению задачи. Его преимущества сразу станут понятны, если попробовать вывести данные дерева с помощью цикла. struct tTree { int content; tTree *left; tTree *right; }; void output_tree(tTree *leaf) { if (leaf==0) return; output_tree(leaf->left); cout << leaf->content << endl; output_tree(leaf->right); } Листинг 4.25. Вывод отсортированного бинарного дерева (bintree.cpp) 200
Функции Соответственно, добавление элемента в дерево проще всего реализовать с помощью рекурсии. При этом сначала осуществляется поиск листа, к которому будет добавлен элемент. Там должен находиться нулевой указатель. В эту позицию прикрепляется новый лист, что в то же время прекращает рекурсию. Здесь возвращается адрес, по которому новый лист располагается в дереве. tTree * insert(tTree *leaf, int content) { if (leaf==0) // найдена свободная позиция { // добавление нового листа leaf = new tTree; leaf->left = leaf->right = 0; leaf->content = content; return leaf; // возвращение указателя на новый лист } if (content < leaf->content) { // присваивание сохраняет старый лист или новый leaf->left = insert(leaf->left, content); } else if (content > leaf->content) { // присваивание сохраняет старый лист или новый leaf->right = insert(leaf->right, content); } return leaf; // возвращает текущий лист } Листинг 4.26. Добавление элемента в отсортированное дерево (bintree.cpp) Добавление нового листа в дерево не будет работать в переборе, в котором вызывается команда new, но будет работать в вызванной до этого функции в следующей позиции: leaf->left = insert(leaf->left, content); 201
Глава 4 В большинстве случаев это присваивание не изменит лист, поскольку в качестве первого параметра передается именно тот лист, который располагался в этой позиции. Только в случае, если leaf->left равно 0, будет создан новый лист. Указатель на него возвращается, и на это место помещается новый элемент. Упражнение • Напишите функцию рекурсии, которая ищет числа в бинарном дереве. Решение приведено на стр. 507. 4.6.3. Игра «Ханойская башня» В качестве следующего примера на тему рекурсии мне бы хотелось представить игру «Ханойская башня». Вам, вероятно, знакома игра с тремя колышками. На левом друг поверх друга расположено несколько упорядоченных по размеру колец разного диаметра. При этом самое большое лежит снизу. Суть игры состоит в том, чтобы переместить кольца на правый колышек. При этом можно брать только одно кольцо, и нельзя класть большее кольцо на меньшее. Рисунок 4.9 иллюстрирует начальную позицию игры. Рис. 4.9. «Ханойская башня» Есть доказательство, что данную задачу можно решить при любом количестве колец. Для этого используется полная индукция, которая подводит базис под рекурсивный метод для решения задачи с помощью программы. Метод доказательства полной индукции состоит из двух фаз: первая — это утверждение, в котором доказывается, что решение верно для одного элемента. Вторая — индуктивный шаг. Здесь принимается, что решение верно также для n элементов. Исходя из этого должно быть доказано, что утверждение верно и для n+1 элементов. Если это удастся, то доказательство будет верным для дюбого числа элементов. Доказательство 1. Утверждение Если башня состоит из одного кольца, можно просто переложить его с левого колышка на правый. 202
Функции 2. Индуктивный шаг Докажем, что башню из n+1 колец можно правильно переместить, если можно это сделать с башней из n колец. Будем исходить из того, что слева находится n+1 колец. Верхние n колец следует переместить на средний колышек — мы предположили, что это возможно. На левом колышке осталось одно кольцо, которое перекладывается на правый. Тогда n колец с центрального колышка перекладываются на него же. В итоге на правом колышке располагаются все n+1 колец. Доказано. Рекурсивная функция carry() в точности реализует индуктивный шаг. Сначала она проверяет условие выхода из рекурсии. Оно заключается в том, что все кольца перемещены. Пока условие не выполняется, она вызывает себя с параметром n-1 колец, чтобы переместить стопку с исходного колышка на свободный. Затем перемещается самое нижнее кольцо. Это реализуется с помощью вывода на экран. Отображается, с какого колышка на какой нужно переместить кольца. Для этого функция снова вызывает саму себя. Свободное место устанавливается с помощью небольшого трюка. Параметры передают, какие колышки заняты. Третий колышек должен быть свободен. На него перемещаются оставшиеся кольца. Сумма номеров колышков всегда равна 1+2+3=6. Если два из них известны, нужно просто сложить их номера, и номер третьего будет разницей между 6 и полученной при сложении суммой. #include <iostream> using namespace std; void carry(int from, int to, int rings_number) { int free; if (rings_number==0) return; free = 6 — from - to; // определение свободного //места 203
Глава 4 carry(from, to, rings_number-1); cout << from << " - " << to << endl; carry(free, to, rings_number-1); } int main() { carry(1, 3, 3); // с колышка 1 на колышек 3 перемещается 3 // кольца } Листинг 4.27. «Ханойская башня» на основе рекурсии (hanoi.cpp) Первый вызов отображает, что кольца сначала находятся на колышке 1, затем они должны быть перемещены на колышек 3, и количество перемещаемых колец равно 3. Программа выводит следующее решение. 1 - 3 1 - 2 3 - 2 1 - 3 2 - 1 2 - 3 1 - 3 Чтобы решить задачу для трех колец, следует переложить кольцо сначала с колышка 1 на колышек 3. Затем переложить следующее кольцо с колышка 1 на колышек 2. Если следовать этой инструкции, то задача будет решена. 4.6.4. Пример: калькулятор Даже в построении компилятора часто используются рекурсии. В качестве примера далее представлена программа, которая может выполнять простые подсчеты. Она использует строку символов. Если внедрить эту функцию в программу для ввода данных, пользователь сможет также вводить простые выражения аналогично табличным расчетам. Первая ступень — так называемый лексический анализ. Здесь идентифицируются арифметические операторы и числовые константы, и связываются между собой в лексемы. 204
Функции Тип перечисления tToken демонстрирует, какой символ был распознан. Помимо четырех основных арифметических операций (PLUS, MINUS, MUL, DIV) здесь находятся скобки (LPAR и RPAR) и числа (NUMBER). Символ END указывает на конец ввода, а символ ERROR требуется для обозначения ошибки ввода. Функция searchToken() распознает символы и может определить целочисленное значение. enum tToken { PLUS, MINUS, MUL, DIV, LPAR, RPAR, NUMBER, END, ERROR }; // глобальные переменные: tToken aktToken; // последняя распознанная лексема double TokenNumber; // значение числовой константы char *srcPos; // программная позиция tToken searchToken() // поиск с актуальной позиции следующей лексемы. // здесь также устанавливаются числовые константы // и записываются в глобальные переменные TokenNumber. { aktToken = ERROR; if (*srcPos==0) { aktToken = END; } else { switch (*srcPos) { case '(': aktToken=LPAR; break; case ')': aktToken=RPAR; break; case '*': aktToken=MUL; break; case '/': aktToken=DIV; break; 205
Глава 4 case '+': aktToken=PLUS; break; case '-': aktToken=MINUS; break; } if (*srcPos>='0'&& *srcPos<'9') { aktToken=NUMBER; TokenNumber = 0.0; } while (*srcPos>='0' && *srcPos<'9') { TokenNumber *= 10; TokenNumber += *srcPos-'0'; srcPos++; } if (aktToken != NUMBER) { srcPos++; } } return aktToken; } tToken Error(char *s) // вывод сообщения и возврат ошибки { cerr << s << endl; return ERROR; } Листинг 4.28. Лексический анализ (calc.cpp) Рекурсия используется в начале программы для интерпретации лексем в команды, при этом учитываются скобки и приоритеты. Сначала функция вызывается для обработки операций с самым низким приоритетом. В примере это функция PlusMinus(). Она сразу же вызывает обработку умножения и деления в функции MulDiv(), но сначала обраща- 206
Функции ется к функции Brackets(), поскольку скобки определяют приоритет операций. Функция Brackets() проверяет, какой приоритет имеет лексема. Сюда относятся открытые скобки, число или знак числа. Если лексема не является ни одним из этих элементов, функция завершается и возвращается к вызванной функции MulDiv(). Эта функция ищет символ умножения (*) или деления (/). Если лексема не является ни одним из этих символов, функция возвращается в PlusMinus() и ищет сложение или вычитание. Если же функция Brackets() нашла левую скобку, то она вызывает функцию PlusMinus(), чтобы проанализировать выражение в скобках. Когда функция Brackets() снова вызывается функцией PlusMinus(), речь идет о непрямой рекурсии. double PlusMinus(); // Прототип double Brackets() // определение скобок, чисел и знака числа // рекурсивный вызов Brackets() и PlusMinus()! { double Number; switch(aktToken) { case NUMBER: searchToken(); return TokenNumber; case MINUS: searchToken(); return -Brackets(); case LPAR: searchToken(); Number = PlusMinus(); if (aktToken != RPAR) { return Error(") expected"); } 207
Глава 4 searchToken(); return Number; case END: return 1; } return Error("primary expected"); } double MulDiv() // определение деления и умножения // рекурсивный вызов Brackets()! { double Number; // вызов операции с наивысшим приоритетом Number = Brackets(); while (aktToken==MUL || aktToken==DIV) { if (aktToken==MUL) { searchToken(); Number *= Brackets(); } else if (aktToken==DIV) { searchToken(); Number /= Brackets(); } } return Number; } double PlusMinus() // определяет сложение и вычитание // рекурсивный вызов MulDiv() { double Number; // вызов операции с наивысшим приоритетом 208
Функции Number = MulDiv(); while (aktToken==PLUS || aktToken==MINUS) { if (aktToken==PLUS) { searchToken(); Number += MulDiv(); } else if (aktToken==MINUS) { searchToken(); Number -= MulDiv(); } } return Number; } double analysis(char *s) { srcPos = s; searchToken(); // предварительное определение первой //лексемы return PlusMinus(); } int main(int argc, char* argv[]) { double Number = analysis(argv[1]); cout << Number << endl; return 0; } Листинг 4.29. Синтаксический анализатор (calc.cpp) При вызове функции analysis()в качестве параметра необходимо указывать символьную последовательность, в которой находится анализируемое выражение. Вводимые данные не должны содержать пробел или числа с дробной частью, поскольку функция searchToken() не мо- 209
Глава 4 жет проанализировать числа с плавающей запятой. На стр. 372 и далее приведены примеры программ, в которых представлено решение и для дробных чисел. Программа получает проанализированную строку в качестве параметра. В UNIX нужно обращать внимание на то, чтобы строка для анализа помещалась в кавычки, если необходимо провести умножение1. Здесь представлено несколько тестовых запусков: > calc "12*(5+2)" 84 > calc "4*(5+2)" 28 > calc "4*5+2" 22 > calc "(5-4)*5*6+(7-3)*((4+2))" 54 > calc "(5-4)*5*6+(7-3)*((4+5)/3)" 42 > Калькулятор справляется с правилом приоритетов арифметических операций, не имеет проблем со скобками и может посчитать также сложные выражения. Упражнения • На стр. 372 приведен пример, как можно прочесть числа с плавающей запятой из символьной последовательности. Интегрируйте это решение в код программы калькулятора, чтобы оно могла работать также с числами типа double. Решение приведено на стр. 508. • Дополните программу калькулятора так, чтобы между лексемами мог располагаться пробел. При тестировании строку с пробелом следует вводить в кавычках, поскольку она будет интерпретирована в качестве нескольких параметров. • Почему программа не путается со знаком минус, хотя иногда он может быть знаком числа, а иногда обозначать арифметическую операцию? Ответ на стр. 510. 1 Причина в том, что оболочка UNIX расценивает астериск как выделение места под файл и можно получить забавные результаты. 210
Функции 4.7. Указатель функции Функция занимает место в памяти и имеет в ней адрес, который можно сохранить в переменной и затем использовать для вызова функции. Такой указатель может потребоваться, к примеру, чтобы сообщить операционной системе, какие функции пользовательской программы были вызваны, если происходит определенное событие. Так, например, в каждой приличной UNIX программе происходит обработка сигналов. При этом пользовательская программа сообщает операционной системе функции, которые должны быть вызваны при ее окончании. В них приложение еще может быстро защитить данные, прежде чем подчиниться операционной системе и завершить работу. Чтобы вызвать функцию через указатель, должен быть точно указан ее интерфейс, который также охватывает тип возвращаемого значения и параметры. Структура указателя представлена графом на рис. 4.10. Рис. 4.10. Граф синтаксиса указателя функции Скобки вокруг имени указателя и астериск очень важны. Они не позволяют компилятору присвоить высший приоритет параметрам в скобках и тем самым ошибочно интерпретировать команду в качестве прототипа. Чтобы установить указатель на функцию для добавления целого числа в структуру дерева, лучше всего ориентироваться на прототип функции. tTree * fit_in(tTree * leaf, int content); // прототип После этого необходимо указать астериск перед именем функции и заключить то и другое в скобки. Затем нужно убрать имена параметров, и указатель на функцию готов. tTree * (* fit_in)(tTree * , int); // указатель на функцию Указатель может получать адрес функции с помощью прямого присваивания имени. Допустимо перед именем функции указать знак адре- 211
Глава 4 са. Это не навредит, а для читателя будет понятно, что здесь речь идет об адресе функции. fit_inPointer = fit_in; // первый вариант fit_inPointer = &fit_in; // второй вариант Вызов функции с помощью указателя не отличается от Anchor = fit_inPointer(Anchor, 22); // вызов через указатель
Глава 5 КЛАССЫ Язык C++ поддерживает объектно ориентированное программирование посредством классов. Эта парадигма позволяет создавать надежные и хорошо обозримые программы. Но язык C++ не навязывает написание объектно ориентированных программ. Он позволяет прийти к этому самостоятельно. Бьерн Страуструп не изобрел объектно ориентированное программирование при разработке языка C++. Но можно с уверенностью сказать, что именно этот язык сделал такой метод программирования популярным. На первый взгляд кажется удивительным, что язык C++ не принуждает к объектно ориентированному программированию. Страуструп пишет: «Моей целью не было заставить всех разработчиков писать в одинаковом стиле». Я даже рискну предположить, что успех языка C++ и объектно ориентированного программирования объясняется именно тем, что программист имеет возможность сохранять свой собственный стиль написания программы и шаг за шагом попробовать все преимущества объектно ориентированного программирования, изучая и оценивая. Все-таки один убеждающий фактор стоит десятка принуждающих. Поэтому мне хотелось бы пригласить вас открыть для себя классы языка C++. Если для вас станут полезны их очевидные преимущества и возможности, вы быстро поймете, почему объектно ориентированная парадигма приобрела настолько большую популярность, что в других языках программирования также потребовалось внедрение классов. Разработка программного обеспечения занимает много времени и чревата ошибками. По этой причине все время создаются новые эффективные средства программирования. Так структурированное программирование с помощью циклов и условий добилось того, что код может быть написан без скачков, то есть без использования команды goto,которая приводила к тому, что уже после нескольких ее повторений программа становилась тяжело обозримой. Такие случаи называют спагетти-кодом, поскольку отдельные нити программы очень тяжело 213
Глава 5 отслеживаются. Процедуры и функции позволили реализовать нисходящий метод программирования, стратегию, при которой большие проекты разделяются на маленькие части и позволяют сохранить очевидными сложные алгоритмы. Модульность с принципом секретности позволила разделить работу над программой между командами программистов. Все эти подходы были очень точно алгоритмизированы. Объектное ориентирование ставит объекты и структуры данных на центральное место и рассматривает алгоритмы в качестве их действий. Такой подход значительно лучше моделирует реальность. Мы ничего не делаем ради действия, однако мы используем объекты. С разными типами объектов мы обращаемся по-разному. Если модель программирования приближена к реальности, в плане разработки это будет проще. В конце концов, программа является маленькой моделью действительности. И для программиста проще применять в работе привычный образ мышления. 5.1. Классы и структуры данных В одном классе можно объединить несколько различных типов данных для моделирования своего собственного типа. Этот принцип уже известен из структур (стр. 146). Согласно ему можно объединять массивы и другие, самостоятельно созданные типы — фантазия не ограничивается. Речь идет о том, чтобы данные о некотором реальном объекте объединить в класс и создать пригодную модель реальности в программе. Рассмотрим простой пример даты. Она состоит из трех целых чисел, которые удобно подходят целочисленным переменным. Для определения класса используется ключевое слово class. class tDate { public: int Day; int Month; int Year; }; Листинг 5.1. Класс даты — первый шаг 214
Классы Слово public с двумя точками означает, что все следующие элементы общедоступные. Не следует забывать это слово, поскольку иначе вся информация в классе будет скрытой, и к ней нельзя будет получить доступ извне. Почему закрытый доступ хорош, мы увидим потом. Команда public: просто сообщает о том, что можно получить доступ к элементам Day, Month и Year так же, как это делается в структурах (см. стр. 146). tDate today; today.Day = 13; Здесь создан объект today типа tDay. Объект содержит три целочисленные переменные — Day, Month, Year, как это видно из объявления класса. Чтобы получить доступ к переменной, следует сначала указать имя объекта, затем точку, а после нее — имя элемента, к которому требуется получить доступ. В примере дню присваивается значение 13. Чтобы добраться до элемента класса через указатель, его следует обозначить астериском и через точку указать имя элемента. Так как приоритет точки выше приоритета астериска, указатель следует заключить в скобки. Поскольку данное написание доступа неочевидно, чаще всего используется комбинация символов ->. tDate *once; (*once).Day = 13; once->Day = 13; Переменная once является указателем на объект класса tDate. Здесь представлены две альтернативы доступа к элементу Day через указатель.  Класс — это абстрактное определение типа данных. Объект — это переменная, тип которой — класс. Можно сказать, что объект — это экземпляр класса. 5.1.1. Союз функции и структуры данных Теперь можно поместить дату в созданный тип данных, создав такую переменную, то есть объект. Но зачем нужна красивая структура данных, когда ее не могут оживить функции? Вам захочется вводить и выводить дату. Возможно, вы также захотите узнать день недели некоторой даты или даже рассчитать, когда будет Пасха. Захочется посмотреть сегод- 215
Глава 5 няшнее число или дату, которая будет через две недели. Короче говоря, объединение данных не имеет особой ценности без функций, которые их обрабатывают. Но и функции бесполезны без связи с данными. Так, расчет сегодняшней даты имеет смысл только в связи с датой, а расчет дней недели не может быть внедрен кроме как вместе с датой. По этой причине функции так же интегрируются в классы, как и элементы данных. Функция, которая принадлежит к некоторому классу, называется элементной функцией или от английского варианта — функцией-членом. В других объектно ориентированных языках такие функции еще называются методами или операциями. Так же как к элементам данных можно получить доступ только через реально существующий объект, то есть переменную класса, так и функция может быть вызвана только через объект. Объект и имя функции будут разделены точкой. Функция работает с данными объекта, через который она была вызвана. С процедурной точки зрения можно представить это так, что элементная функция всегда несет с собой в качестве параметра объект, через который она была вызвана. Предположим, нам нужно в класс даты добавить функцию расчета дня нового года. Она будет так же встроена в структуру класса, как и три целочисленные составляющие. class tDate { public: int Day; int Month; int Year; void NewYear(int Year); }; today.NewYear(2000); Функция NewYear() имеет в качестве параметра число года. Она должна установить объект, через который была вызвана, в день нового года, переданный ей параметром. Поскольку это элементная функция, она вызывается объектом типа tDate. В примере — это объект today. Элементная функция может получить доступ прямо к объектам класса. Все изменения, которые она внесет в эти данные, повлияют на объект today. Это значит, что после вызова функции элементы объекта today 216
Классы будут выглядеть следующим образом: Day и Month получат значение 1, а Year — 2000. Для вызова элементной функции ее имя прикрепляется к имени объекта точно так же, как и для доступа к элементам. Поскольку функция непосредственно связана с объектом, ему не требуется передавать в нее никакие параметры. Следующий листинг демонстрирует, как функция NewYear() объявляется в классе, и как выглядит определение функции. class tDate { public: int Day; int Month; int Year; void NewYear(int Year); }; void tDate:: NewYear(int pYear) { Day = 1; Month = 1; Year = pYear; } Листинг 5.2. Класс с элементной функцией Чтобы отличать элементную функцию NewYear() от глобальной, перед ее именем сначала указывается имя класса и отделяется от него парой двоеточия. Также можно определить функцию прямо в классе. Но тогда код очень быстро станет нечитаемым. Поэтому так можно делать только с очень короткими функциями. class tDate { public: 217
Глава 5 int Day; int Month; int Year; void NewYear(int Year); { Day = 1; Month = 1; Year = pYear; } }; Листинг 5.3. Класс с элементной функцией Функция, которая в определении класса не только объявлена, но и определена, автоматически компилируется в качестве встроенной (см. стр. 183). В обоих случаях при вызове элементная функция получает прямой доступ к переменным объекта, которым она вызвана. В общем, элементные функции класса могут получить доступ ко всем другим элементам объекта без дополнительного указания имени класса. Номенклатура Объектно ориентированное программирование вводит новые понятия. Объект — обозначает область памяти, которая определена с помощью класса. Принципиально можно представить себе объект как особую переменную, а класс — в качестве определения ее типа. В книге объект то и дело будет обозначаться переменной, особенно там, где он ведет себя как обычная переменная. Данные класса часто обозначаются в книге как элементные переменные. В литературе по объектно ориентированному программированию нередко встречается название атрибут. Этим указывается, что элементные переменные определяют свойства объекта. Функции класса, которые здесь называются элементными, можно найти в литературе по объектно ориентированному программированию под именами методы или операции. Это определение обосновано тем, что функция доступна только через объект и является его действием. 218
Классы Я отказался от этих терминов по нескольким причинам. Прежде всего, Бьерн Страуструп, создатель языка C++, использует такие же обозначения, что и вэтой книге. На мой взгляд, для начинающего проще, когда одинаковые вещи называются одинаково. 5.1.2. Доступ к элементам класса Внутри элементной функции можно получить прямой доступ к элементам объекта. Но можно также использовать предопределенный указатель самовызова this. Он выражает то, что требуется явно обратиться к элементу этого объекта. void tDate:: NewYear(int pYear) { this->Day = 1; this->Month = 1; this->Year = Year; } Листинг 5.4. Доступ через указатель this Локальные переменные, к которым также относятся параметры, перекрывают элементы класса с такими же именами. Через указатель this можно явно получить доступ к элементу класса. Это особенно заметно при присвоении числа года. Цель — элемент класса, источник — параметр. Также можно также получить доступ через имя класса. Для этого указывается двойное двоеточие между ним и именем элемента. void tDate:: NewYear(int pYear) { tDate::Day = 1; tDate::Month = 1; tDate::Year = Year; } Листинг 5.5. Доступ по имени класса 219
Глава 5 5.2. Создание и удаление объекта Преимущество классов, которое ценит едва ли не каждый программист, — это возможность определять функции, которые при создании объекта вызываются автоматически и тем самым гарантируют, что он всегда будет корректно инициализирован. В качестве аналога можно написать функцию, которая вызывается каждый раз при разрушении объекта и освобождает занимаемые им ресурсы. Поскольку эти задачи решаются только единожды при определении класса, они предотвращают многие ошибки, которые в классическом программировании возникают вследствие упущенной инициализации. 5.2.1. Конструктор и деструктор Элементные функции, которые вызываются при создании объекта, называются конструктором. В нем можно позаботиться о том, чтобы все элементы объекта были корректно инициализированы. Конструктор всегда носит имя класса и не имеет возвращаемого значения, даже такого, как void. Стандартный конструктор не имеет параметров. Может быть несколько конструкторов, которые отличаются друг от друга параметрами. Как противоположность конструктора существует деструктор. Он вступает в работу, когда объект удаляется. Деструктор важен прежде всего тогда, когда объект в течение своего существования задействовал ресурсы системы. С помощью деструктора гарантируется их высвобождение. Имя деструктора состоит из знака тильды (~) и имени класса. Как и конструктор, он не имеет типа возвращаемого значения и типа void. Деструктор никогда не имеет параметров. В случае класса даты есть смысл устанавливать конструктором все элементы в 0. По этому признаку элементная функция может определить, что дата еще не была установлена. При этом можно выяснить текущую дату и установить ее. В примере определен деструктор, хотя в случае даты он ничего не выполняет. class tDate { public: 220
Классы tDate(); ~tDate(); ... }; tDate::tDate() { Day=0; Month=0; Year=0; } tDate::~tDate() { } Листинг 5.6. Конструктор и деструктор Время вызова конструктора и деструктора зависит от того, когда создается и разрушается объект. Глобальные объекты создаются при запуске программы и разрушаются при ее завершении. Локальные — вызывают конструктор на том шаге программы, где находится их определение, и удаляются, когда программа покидает область действия объекта. Создание и удаление объекта может быть явно указано с помощью операторов new и delete. Если массив создается командой new, то для создания каждого из его элементов вызывается конструктор. Соответственно, при удалении для каждого элемента вызывается деструктор delete[]. { tDate today; tDate *tomorrow; // конструктор не вызывается! tDate *holiday; // конструктор не вызывается tomorrow = new tDate; // конструктор вызывается holiday = new tDate[14]; // конструктор не вызывается 14 раз delete tomorrow; // вызывается деструктор ... delete[] holiday; // деструктор вызывается 14 раз } // вызывается деструктор объекта today Листинг 5.7. Вызов конструктора и деструктора 221
Глава 5 Особая форма инициализации В большинстве случаев конструктор состоит из нескольких присваиваний, которые инициализируют переменные объекта. Вместо того чтобы задавать переменным объекта начальные значения посредством присваивания в теле конструктора, их можно инициализировать следующим способом: tDate::tDate() : Day(0),Month(0),Year(0) { } Листинг 5.8. Альтернативная реализация Для этого между заголовком и телом конструктора перечисляются одна или несколько инициализаций. Они отделяются от заголовка с помощью двоеточия. Инициализация состоит из имени переменной или константы и скобок, в которых находится значение инициализации. В этом случае значение переменных Day, Month, Year устанавливаются равным 0. Тело конструктора пустое. Инициализация происходит еще до выполнения тела функции. Существует решающее отличие от присваивания значения переменной: в теле конструктора может находиться только одно присваивание переменной, поскольку оно является формой инициализации. В большинстве случаев разница незначительная, но если класс содержит ссылочные переменные или константы, задать им первоначальное значение можно только с помощью инициализации. Любые попытки сделать это с помощью присваивания потерпят поражение1. 5.2.2. Конструктор и параметры Конструкторы также могут принимать параметры. Передаваемое конструктору значение обычно используется для инициализации переменных. Конструкторы можно перегружать, как и обычные функции. Помимо стандартного конструктора может существовать множество других, 1 При вызове конструкторов базовых классов данная форма инициализации очень важна. Но к этому мы еще вернемся. 222
Классы отличающихся параметрами. Компилятор выберет вызываемый конструктор в зависимости от передаваемых ему параметров. Следующий пример демонстрирует класс tDate с конструктором с тремя параметрами. class tDate { public: tDate(int Day, int Month, int Year=-1); ... }; tDate::tDate(int Day, int Month, int Year) { this->Day=Day; this->Month=Month; this->Year=Year; if (Year<0) { // установить текущий год ... } } tDate Start(1,1,1970); tDate NewYear(31,12); tDate *Christmas = new tDate(24,12); Листинг 5.9. Конструктор с параметрами Объект Start с помощью конструктора устанавливается в 1.1.1970. Объект NewYear в качестве параметра получает значение 31.12 без ввода года. Третьим параметром, который будет передан переменной, в данном случае будет –1. При этом отрицательное число года внутри конструктора будет интерпретировано в качестве текущего года. Поскольку единственный существующий в классе tDate конструктор требует входной параметр, объект такого класса не может быть создан без инициализации переменной, соответствующей данному параметру. 223
Глава 5 Конвертирующий конструктор Если присваивать переменной типа float переменную типа int, она будет автоматически конвертироваться. При создании класса можно указать, какой тип в подобном случае должен подвергаться такой процедуре. Для этого создается конструктор с одним параметром, который должен иметь желаемый конвертируемый тип. Конструктор с одним параметром приведет к тому, что компилятор будет использовать его для конвертирования типа параметра. class tFraction { public: tFraction(char *); Add(tFraction&); }; ... char input[MAXSTR]; getline(cin, input, MAXSTR); tFraction b1(input); getline(cin, input, MAXSTR); b1.Add(input); В классе tFraction находится конструктор, который принимает в качестве параметра указатель на тип char, то есть С-строку. Функция Add() принимает только тип tFraction. Компилятор позволяет сделать вызов функции Add() с параметром С-строка, поскольку с помощью этого конструктора он может скомпилировать данный параметр в тип tFraction. Конструктор конвертирования будет автоматически вызываться в случае, когда необходимо преобразование типа. Однако если в программе это не требуется, допустимо перед именем конструктора указать ключевое слово explicit. Тогда преобразования можно будет добиться, вызывая конструктор на манер вызова функции. class tFraction { public: 224
Классы explicit tFraction(long); ... }; tFraction Fraction=12; // не будет работать через компилятор tFraction Fraction(12); // так работает Стандартный конструктор Стандартным конструктором считается тот, вызов которого возможен без параметров. Это не означает, что ему нельзя их иметь. Конструктор с параметрами, которые полностью инициализируются с начальными значениями, также является стандартным, поскольку он может быть вызван и без параметров. Если класс вообще не имеет собственных конструкторов, то компилятор создает пустой стандартный конструктор. Как только появляется собственный конструктор, автоматически созданный удаляется. Это также случай, когда ни один из самостоятельно написанных конструкторов не обходится без параметров. В данном случае создание объекта без параметров вызовет ошибку. В вышеописанном примере создание объекта типа tFraction или создание массива привело бы к ошибке компилятора, поскольку в данном примере нет конструктора, который вызывался бы без параметров. 5.3. Доступная и скрытая области Так же как и в функциях, локальные переменные в классах недоступны извне. Вы можете защитить данные и элементные функции, которые предназначены только для внутреннего использования. Такие элементы являются скрытыми. К ним могут получать доступ только элементные функции. При этом любой доступ контролируется. Открытые элементные функции определяют, как можно изменять закрытые элементы данных, и какие процессы для этого требуются. 5.3.1. private и public По умолчанию все элементы класса являются закрытыми. Если в его определении указать метку public:, все элементы, следующие после нее, будут общедоступными. Если за этим блоком следуют закрытые элементы, нужно указывать их после метки private:. Можно чередо- 225
Глава 5 вать такие метки сколько угодно. Однако рекомендуется скомпоновать открытые и закрытые элементы в два блока. Более практично первыми определять открытые элементы. Хорошая идея обозначать элементы данных закрытыми. За счет этого доступ к ним будет контролироваться с помощью элементных функций. Так улучшается устойчивость программы и можно проверить зависимость. Также следует это делать в том случае, когда при создании класса еще не обозначились причины, по которым следует ограничивать доступ. Тогда для него будут использоваться все остальные модули функций. Если затем возникнет зависимость, потребуется только соответствующая реализация элементной функции. Классы могут точно так же оставаться неизменными, как и все части программы, которые их используют. Следующий пример — класс дроби. Составные части данных — numerator и denominator — скрыты от внешнего доступа параметром private. К общедоступным функциям относятся те, которые позволяют записать и прочесть эти значения. class tFraction { public: tFraction() {numerator=0; denominator=1;} long get_denominator() {return denominator;} long get_numerator() {return numerator;} void set_denominator(long p) {if (p!=0) denominator =p;} void set_numerator(long p) {numerator=p;} void Show(); private: long numerator; long denominator; }; void tFraction::Show() { cout << numerator << "/" << denominator << endl; 226
Классы } int main() { tFraction fraction; long in_denominator, in_numerator; cout << "Введите через пробел числитель и знаменатель!" << endl; cin >> in_numerator >> in_denominator; fraction.set_denominator(in_denominator); fraction.set_numerator(in_numerator); fraction.Show(); } Листинг 5.10. Расчет дроби с приватными данными В примере класса tFraction элементы данных numerator и denominator являются закрытыми. Доступ осуществляется с помощью функций, которые начинаются со слова get или set. Это предотвращает ситуацию, когда кто-нибудь присвоит знаменателю значение 0. Также можно при изменении или выводе на экран автоматически сократить дробь. Имеет смысл ограничить доступ к данным класса. Это предотвратит установку значения месяца в 13 или 0. Однако, возможно, день недели тоже должен быть составной частью даты. Для этого чтобы не пересчитывать его каждый раз по-новому, день недели можно записать в закрытую переменную. Но при этом нужно обеспечить, чтобы дату нельзя было изменять непосредственно. Если изменение даты происходит через определенную функцию, можно обозначить день недели как недействительный. Если потребуется получить его значение, функция должна будет проверить, следует его пересчитать или нет. class tDate { private: int day; int month; 227
Глава 5 int year; int weekday; public: tDate(); ~tDate(); void set_day(int value); void set_month(int value); void set_year(int value); int get_day() return day; int get_month() return month; int get_year() return year; int get_weekday(); }; tDate::tDate() { weekday = -1; } void tDate::set_month(int value) { if (value>0 && value<13 && month!=value) { month = value; weekday = -1; } } Листинг 5.11. Класс с приватными элементами При этом каждый, кто использует этот класс, учитывает, что при изменениях следует устанавливать значение переменной weekday равным –1, а переменные day, month и year определять в качестве закрытых, тем самым, защищая их от доступа извне. Их можно изменить только с помощью элементных функций. Для этого используется функция set_month(), которая, с одной стороны, не может присвоить месяцу в объекте неверное значение. С другой стороны она заботится о том, чтобы день недели обозначался как недействительный, как только из- 228
Классы менилось значение месяца. Этим достигается то, что элементная функция get_weekday() может возвращать день недели без пересчета, пока он не установлен другой функцией в –1. Только тогда он должен быть рассчитан заново. Общедоступные элементы класса называются интерфейсом. Они являются доступом к элементам для пользователя класса. С одной стороны, интерфейс должен обеспечивать как можно более простое использование программы. С другой — он должен оставаться ограниченным и хорошо просматриваемым. Прежде всего интерфейс не должен зависеть от внутренней реализации класса. 5.3.2. Пример: стек На стр. 154 уже был представлен связанный список. Там мы рассматривали первую реализацию стека с помощью структур. Поскольку теперь появилась возможность использования классов, можно создать более надежные и просматриваемые списки. Функции push() и pop() принадлежат только классу и уже не являются независимыми. Поскольку так комбинация объединения данных и функциональности стека определяется лучше, можно значительно обширнее смоделировать стек с помощью классов. // программа демонстрации стека на основе класса #include <iostream> using namespace std; class tNode // узел связанного списка { public: int d; // данные, здесь целые числа tNode *next; // скрепление }; // стек как объединение из якоря и операций class tStack { public: tStack(); 229
Глава 5 ~tStack(); void push(int); // добавление информации int pop(); // получение информации private: tNode * Anchor; // каждый стек имеет свой собственный // якорь }; tStack::tStack() { Anchor = 0; // подготовка пустого списка } // деструктор удаляет все лишние элементы tStack::~tStack() { tNode *last = Anchor; // вспомогательный указатель для //защиты якоря while (Anchor) // пока в списке есть элементы { last = Anchor; // первый элемент защищен Anchor = Anchor->next; // установлен якорь на //следующий элемент delete last; // первый элемент освобожден } } // добавление нового элемента в начало списка void tStack::push(int d) { tNode *node = new tNode; // создание элемента списка node->d = d; // заполнения поля данных node->next = Anchor; // прикрепление к списку Anchor = node; // присвоение этого элемента якорю } // прочесть верхний элемент и удалить его из списка int tStack::pop() { 230
Классы int content=0; // вспомогательная переменная типа данных //элемента if (Anchor) // пока список не пуст { tNode *old = Anchor; // защитить первый элемент Anchor = Anchor->next; // установить якорь на второй //элемент content = old->d; // скопировать содержимое узла delete old; // освободить прочитанный узел } return content; // вывести содержимое узла } int main() // главная программа для тестирования стека { tStack stack; // объявить стек stack.push(2); // записать тестовые данные stack.push(5); stack.push(18); cout << stack.pop() << endl; // прочесть стек cout << stack.pop() << endl; cout << stack.pop() << endl; } Листинг 5.12. Реализация стека (stack.cpp) Упражнения • Создайте класс tFIFO, который также создает динамическую структуру данных. В отличие от стека, принцип FIFO1 аналогичен обслуживанию по очереди. Пакет, помещенный первым, также первым и считывается. Подсказка для решения: не рекомендуется использовать якорь, лучше заменить его на два указателя. Первый будет ссылаться на начало очереди, а второй — на конец. Решение приведено на стр. 511. 1 Акроним от фразы First In, First Out — «первым пришел — первым ушел». Прим. ред. 231
Глава 5 • Дополните класс tFraction приватной функцией ggT-function(), которая вызывается при выводе функцией Show() для сокращения дроби. 5.3.3. Друзья Друзья — это люди, которым мы доверяем и с которыми делимся секретами. Точно так же классы способны искать себе друзей, которые могут получать доступ к их закрытым элементам. Рассмотрим вопрос дружбы между функциями или классами. Друг класса определяется внутри него. При этом следует ключевое слово friend и определение функции или класса, который принимается другом. class pal; class police { public: void help(pal&); }; // Чтобы класс pal мог использовать класс в интерфейсе, класс // colleague должен быть определен здесь class colleague; class pal { private: int secret; void spion(); friend void help(pal); friend class colleague; friend void police::help(pal&); }; class colleague { 232
Классы public: colleague() { JB.secret=007; } pal JB; }; void help(pal Egon) { Egon.spion(); } Листинг 5.13. Дружественные классы (friend.cpp) Класс pal имеет три «дружественных» определения: первое разрешает функции help() доступ ко всем элементам. Во втором определении класс colleague обозначается дружественным. Тем самым каждой элементной функции класса colleague разрешен доступ к любому элементу класса pal. Третье определение позволяет функции help() из класса police получать доступ ко всем элементам класса pal. Все остальные функции класса police не имеют разрешения на доступ. 5.4. Конструктор копирования При копировании объекта содержимое области памяти, которую он занимает, побитно копируется в целевую переменную. Иногда это ведет к нежелательным результатам. Такое происходит всегда, когда класс содержит указатель, который ссылается на данные вне объекта. При копировании указатель также копируется. Но данные, на которые он ссылается, не копируются. Поэтому при копировании указатель оригинала и указатель копии ссылаются на одну и ту же область памяти. Это означает, что копия объекта не является настоящей копией, а работает с данными, на которые ссылается указатель оригинала. На рис. 5.1 отражена данная ситуация. Еще сложнее случай, когда копия должна быть удалена. Переменные параметры в конце функции тоже будут удалены, поскольку они имеют характер локальных переменных. Тогда правильный деструктор 233
Глава 5 класса освободит память внешней переменной, на которую ссылается указатель. Рис. 5.1. Копирование объекта Поскольку он ссылается на область памяти оригинала, она также будет освобождена. Это значит, что каждый объект, который передается функции в качестве параметра, потеряет свой внешний участок памяти при завершении функции. КОГДА ДОЛЖЕН СОЗДАВАТЬСЯ КОНСТРУКТОР КОПИРОВАНИЯ? Если класс имеет ссылочные элементы на внешнюю память, нужно создавать конструктор копирования. Подобными элементами являются указатели или ссылки. Однако и чужие классы могут содержать такие элементы, например класс Strings (см. стр. 338). Копирование данных внешней памяти нельзя сделать автоматически, это задача программиста. Если создается класс, содержащий указатель на внешние данные, можно написать конструктор копирования, который будет вызываться тогда, когда объект класса требуется скопировать. Конструктор копирования вызывается внутренними процессами копирования для параметров инициализации, передаваемых параметров и возвращаемого значения. При передаче параметров через указатель или ссылку он не вызывается, поскольку в таком случае объект не копируется. Присваивание явно запрещено, потому что конструктор копирования будет перекрываться оператором присваивания (см. стр. 238). Конструктор копирования должен сначала скопировать элементы данных и для всех указателей, которые не равны 0, выделить область в памяти и поместить туда оригинальные данные из внешней памяти. 234
Классы Его главной задачей является копирование данных, на которые ссылаются указатели. Если класс имеет указатель на внешние данные, для него следует писать конструктор копирования. Синтаксис Конструктор копирования не возвращает значения и носит имя класса. В качестве единственного параметра он получает ссылку на собственный класс. Параметр должен быть ссылкой, чтобы конструктор копирования не вызывал себя сам. Если в области данных класса tStack вместо целочисленных переменных использовать указатель, то для этого класса потребуется конструктор копирования. Чтобы продемонстрировать, как он должен работать, создадим простой класс с указателем на внешнюю область памяти. // программа демонстрации конструктора копирования #include <iostream> using namespace std; class tClass { public: tClass() // конструктор: создание внешних данных { pointer = new int; *pointer = 5; } ~tClass() // деструктор: освобождение внешней памяти { delete pointer; pointer = 0; } void SetData(int a) { *pointer = a; } int GetData() { return *pointer; } tClass(tClass& k) // конструктор копировани { 235
Глава 5 // для демонстрации он сообщает о себе cout << "Конструктор копирования" << endl; // внешние данные создаются и копируются pointer = new int; *pointer = k.GetData(); // обычные элементы данных также копируются rest = k.rest; } int rest; // предназначается для обычных элементов // данных private: int *pointer; // указатель, т.е. необходим конструктор //копирования }; // Функция служит только для демонстрации.Поскольку в параметрах // передается значение, при вызове также вызывается конструктор // копирования void func(tClass para) { cout << "Функция:" << para.GetData() << endl; } // Главная программа для тестирования int main() { tClass Object; Object.SetData(7); func(Objekt); // здесь активизируется конструктор // копирования cout << Object.GetData() << endl; } Листинг 5.14. Внешняя память и конструктор копирования (copycons.cpp) 236
Классы Для того чтобы показать, где запускается конструктор копирования, программа выдает короткое сообщение на экран. Если закомментировать конструктор копирования, то после вызова функции, область памяти, на которую ссылается указатель объекта, будет свободна. Если захотите это проверить, измените деструктор так, чтобы он устанавливал внешнее значение равным 9 вместо того, чтобы его удалять. ~tClass() // деструктор: используется здесь в преступных целях { *pointer = 9; // только для тестирования } При закомментированном конструкторе копирования, последняя строка программы выдаст 9 в качестве четкого указания на то, что деструктор работал в той области, на которую ссылается указатель. Если снова активировать конструктор копирования, опять появится 7. 5.5. Перегрузка элементных функций Перегрузка уже была рассмотрена в разделе 3.7 в разделе «Функции» (см. стр. 138). Здесь эта тема всплывет еще раз — для рассмотрения перегрузки элементных функций. Для примера будем использовать класс tFraction, который позволяет считать дроби. В нем заложены две элементные функции с именем Add(). Первая добавляет целые числа к дроби, вторая использует дробь в качестве параметра. class tFraction { public: tFraction() {numerator=0; denominator=1;} long get_denominator() {return denominator;} long get_numerator() {return numerator;} void set_denominator(long p) {if (p!=0) denominator =p;} void set_numerator(long p) {numerator=p;} void Add(long); 237
Глава 5 void Add(tFraction); void Show(); private: long numerator; long denominator; }; void tFraction::Add(long summ) { numerator+=summ*denominator; } void tFraction::Add(tFraction summ) { numerator = numerator*summ.get_denominator() + summ.get_numerator()*denominator; denominator = denominator * summ.get_denominator(); } Листинг 5.15. Расчет дробей с перегруженными функциями (fraction.cpp) Какую из функций сложения следует вызвать, решает компилятор на основании передаваемых в нее параметров. 5.6. Выбор: перегрузка операторов Конечно, код выглядит гораздо более элегантно, если складывать два объекта типа tFraction, используя непосредственно знак плюс. В языке C++ есть возможность реализации операторов. Для этого создается функция, перед именем которой стоит ключевое слово operator. Затем добавляется оператор, которому будет подражать функция. Можно использовать следующие: new + % ~ > /= |= <<= >= -- () delete - ^ ! += %= << == && , [] new[] * & = -= ^= >> != || ->* delete[] / | < *= &= >>= <= ++ -> 238
Классы То, что операторы были самостоятельно реализованы, не изменяет ничего в контексте оператора. Сохраняется приоритет, количество операндов и сочетаемость (левая или правая привязка). Также нельзя изменять операторы, относящиеся к определенным типам данных. Минимум один из двух операндов должен иметь тип, определенный пользователем. Операторная функция не обязательно должна принадлежать классу. Она может быть также глобальной. Однако некоторые операторы не могут быть перегружены без принадлежности к определенному классу. Это оператор присваивания =, вызов функции (), индекс [] и оператор указателя ->. Если операторная функция принадлежит классу, объект класса является первым операндом функции. В следующем примере перегружается оператор плюс. Поскольку операторная функция принадлежит к классу tFraction, она представляет сложение, при котором первый операнд имеет тип tFraction. // элементная функция tFraction tFraction::operator+(long summ); Fraction = Fraction.operator+(Long); // вызов Fraction = Fraction + Long; // вызов Если оператор требует второй операнд, он передается параметром. Пример иллюстрирует сложение. Здесь имеется два операнда. Второй передается в качестве параметра функции operator+(), здесь он имеет тип long. Пользователь, работающий с функцией сложения, предполагает, что параметры можно менять местами. Тогда первый операнд имел бы тип long, то есть стандартный тип, для которого отсутствует класс. Только в таком случае может быть написана глобальная операторная функция, и она будет иметь два параметра. // глобальная функция tFraction operator+(long, const tFraction& o2); Fraction = operator+(Long, Fraction); // вызов Fraction = Long + Fraction; // вызов Привычные связи не работают автоматически, когда оператор перегружается. Так, из перегрузки оператора плюс не следует, что комбинация += будет прибавлять второй операнд. Если требуется, чтобы это правило выполнялось для вашего типа данных, следует дополнительно реализовать оператор +=. Точно так же для оператора ++. 239
Глава 5 Перегрузка операторов имеет свои подводные камни, когда сложно интуитивно понять связь между функциональностью и оператором. Если смысл оператора не совсем точно подходит к перегруженному оператору, лучше вместо него написать обычную функцию, иначе оператор будет скорее запутывающим, чем полезным1. Если, например, определен оператор равенства, а оператор неравенства — нет, или они не противоположны, тогда их использование будет несоответствующим. Как только операторы класса начнут вести себя иначе, чем операторы базовых типов, пользователь класса быстро запутается. Ему, вероятно, потребуется какое-то время, пока он не обнаружит, что ваша функция не имеет отношения к оператору. Только тогда у него появится шанс понять, что вы хотели ею реализовать. 5.6.1. Сложение Как и для функции Add(), которая рассматривалась при перегрузке функций, следует определить операторную функцию сложения — одну для дробей и одну для целых значений. В качестве примера класса для перегруженных операторов, будем использовать класс tFraction. Для сложения в классе уже была построена одна функция. Теперь оно должно быть реализовано оператором +. Для этого сначала будет построена обычная функция, которая имеет одинаковое имя с функцией operator+(). #include <iostream> using namespace std; class tFraction { public: tFraction() { numerator=0; denominator=1;} long GetDenominator() {return denominator;} long GetNumerator() {return numerator;} void SetDenominator(long p) {if (p!=0) denominator=p;} void SetNumerator(long p) {numerator=p;} tFraction operator+(long); 1 Такой печальный опыт привел к тому, что в языке Java перегрузка операторов вообще отсутствует. 240
Классы tFraction operator+(tFraction); void Show(); private: long numerator; long denominator; }; tFraction tFraction::operator+(long summand) { numerator+=summand*denominator; return *this; } tFraction tFraction::operator+(tFraction summand) { numerator = numerator*summand.GetDenominator () + summand.GetNumerator()*denominator; denominator = denominator * summand.GetDenominator (); return *this; } Листинг 5.16. Класс дробей с перегруженным оператором Такую функцию можно вызывать непосредственно. Для этого требуются: объект типа tFraction, который примет результат, объект типа tFraction, через который функция будет вызвана, и объект типа tFraction или long, который будет служить вторым операндом. tFraction Summ, Summand1, Summand2; Summ = Summand1.operator+(Summand2); Summ = Summand1 + Summand2; Две последние строки идентичны. Первый операнд определяется классом, чья элементная функция operator+(). Второй определяется типом параметра. Тип возвращаемого значения функции operator+() устанавливается в зависимости от типа переменной, которой присваивается результат. В следующем примере для класса tDate определен знак плюс. Нет смысла складывать две даты, поэтому он не предназначается для них. Но 241
Глава 5 можно к некоторой дате прибавить определенное количество дней для получения другого числа. Объявление и определение выглядит так: class tDate { public: tDate operator+(int Days); }; tDate tDate::operator+(int Days) { // расчет даты return *this; } Листинг 5.17. Перегрузка операторов Сложение возвращает новую дату. В качестве правого операнда допустимо использовать целое число. Так дата с помощью простого сложения может сместиться на 14 дней. tDate today; today = today + 14; Листинг 5.18. Использование оператора Возможно, вас посетила мысль, что строку today + 14 можно написать иначе. Однако оператор += не определен. Было бы логично потребовать его создания, когда уже существует оператор +. Точно так же дело обстоит и с оператором ++. 5.6.2. Глобальные операторные функции Для класса дробей реализовано сложение, позволяющее использовать переменные типа long. При этом можно с левой стороны знака плюс располагать дробь, а с правой стороны — целое число. Если требуется реализовать возможность менять операнды местами, возникнет проблема, поскольку левый операнд является классом. Тогда придется 242
Классы операторную функцию класса обозначить типом long. Но это, конечно, недопустимо, поскольку long является базовым типом, а не классом. Тем не менее можно добавить вместо этого глобальную операторную функцию. У нее будет два параметра, и не составит никаких проблем определить первый в качестве типа long. tFraction operator+(long o1, const tFraction& o2) { tFraction summ; summ.SetNumerator(o2.GetNumerator()+o1*o2.GetDenominator ()); summ.SetDenominator (o2.GetDenominator ()); return summ; } В таком случае для прямого доступа к элементам предлагается определять глобальную функцию в качестве «друга». Это показано в следующем листинге. class tFraction { ... // глобальная функция operator+ может получить доступ к // элементам friend tFraction operator+(long one, const tFraction& two); ... }; tFraction operator+(long one, const tFraction& two) { tFraction summ; summ.numerator = two.numerator +one*two.denominator; summ.denominator = two.denominator; return summ; } 5.6.3. Инкремент и декремент Инкремент и декремент имеют две формы представления. В первой оператор указан слева, а во второй — справа от переменной. Если оператор указан слева, его называют префиксом. В таком случае сначала срабатывает оператор, а затем используется переменная. В форме пост- 243
Глава 5 фикса оператор указан справа. Здесь сначала используется переменная, а потом срабатывает оператор. a = 5; b = ++a; // b==6 a = 5; b = a++; // b==5 Чтобы в операторе operator++ различать префиксную и постфиксную формы, постфиксный вариант получает дополнительно целочисленный параметр, который, конечно, вообще не используется при вычислениях. Операция постфикса требует всегда локальную переменную для сохранения предыдущего значения, которое после увеличения переменной должно быть возвращено. Отсюда можно получить немного улучшенное решение для варианта префикса. class tFraction { tFraction& operator++(); // префикс tFraction operator++(int); // постфикс ... }; tFraction& tFraction::operator++() // префикс-инкремент { // расчет новой дроби // также можно использовать: *this = *this + 1; numerator += denominator; return *this; } tFraction tFraction::operator++(int) // постфикс-инкремент { tFraction oldFraction =*this; // сохранено старое значение numerator+=denominator; // увеличение переменной return oldFraction; // возврат старого значения } Листинг 5.19. Инкрементирование с префиксом и постфиксом (fraction.cpp) 244
Классы Возвращаемое значение функции operator++ является результатом вычисления. Постфикс-оператор не может возвращать ссылку, поскольку тогда это будет ссылка на локальную переменную, которая разрушится после завершения функции. 5.6.4. Оператор присваивания Оператор присваивания вызывается тогда, когда с левой стороны присваивания указан объект класса, в котором он реализован. Если в классе не реализован оператор присваивания, объект при таком написании будет скопирован побитно. Это прекрасно работает до тех пор, пока класс не содержит указателей на внешнюю память. В таком случае будет скопирован только указатель. Это приведет к тому, что оба объекта после присваивания будут ссылаться на одну и ту же область внешней памяти. Такая ситуация уже знакома вам по конструктору копирования (см. стр. 233). Рис. 5.2. Копирование объекта Разница между конструктором копирования и оператором присваивания состоит в том, что при присваивании целевой объект уже существует. Это значит, что целевой объект до присваивания уже имел некоторые данные во внешней памяти, доступ к которым осуществлялся через его указатель. Этот указатель при побитном копировании в момент присваивания будет переписан и в итоге станет указывать на внешние данные объекта-источника. Рисунок 5.2 отчетливо демонстрирует данную проблему. При копировании указатель также будет скопирован. Там, где указатель оригинала содержит адрес на данные Data1, указатель целевого объекта после присваивания также будет указывать на данные Data1. Предыдущая 245
Глава 5 область с Data2 останется в памяти без возможности доступа к ней. Поскольку оба объекта указывают на одну и ту же область памяти, объект, вызванный первым, отнимет у второго данные. Оператор присваивания имеет следующие функции: • Внешняя память целевого объекта должна быть освобождена. • Для целевого объекта необходимо выделить область памяти, которая подходит для данных объекта-источника. • Внешние данные объекта-источника должны быть скопированы во внешнюю память целевого объекта. • Все элементные переменные, которые не являются указателями, следует скопировать. Если объем внешней памяти источника и целевого объекта одинаков, процедуру освобождения и выделения памяти можно пропустить. Тогда будет достаточно скопировать данные из памяти источника во внешнюю память целевого объекта. Чтобы знать, когда оператор присваивания должен быть вызван, его нужно четко отличать от инициализации. Последняя всегда связана с объявлением. В таком случае используется конструктор копирования (см. стр. 233). Также при передаче параметров и возвращении значения функции вызывается конструктор. Если возвращаемое значение в итоге будет присвоено некоторому объекту, вызывается оператор присваивания. Следующий пример рассматривает маленький класс tAuto, который содержит знак автомобиля. Это реализуется с помощью указателя. Пример содержит конструктор копирования и оператор присваивания. Главная программа проводит несколько тестов. #include <iostream> using namespace std; class tAuto { public: tAuto() { sign=0; Reader=4; } ~tAuto() { delete sign; sign = 0; } 246
Классы void SetSign(char *a); char *GetSign () const { return sign; } tAuto(const tAuto& k); // конструктор копирования tAuto &operator=(const tAuto &k); // оператор //присваивания private: // две приватные функции для работы со строками int StringLength(char *s) { int len=0; while (*s++) len++; return len+1; } void StringCopy(char *source) { char *goal = sign; while (*source) *goal++ = *source++; *goal=0; // завершающий ноль } char *sign; // указатель на внешнюю память int Reader; // нормальная переменная }; // обычная запись номерного знака void tAuto::SetSign(char *sn) { if (sign) delete sign; // память освобождена int len = StringLength(sn); // определен размер sign = new char[len]; // выделена новая область памяти StringCopy(sn); // данные переданы } // конструктор копирования tAuto::tAuto(const tAuto& s) { cout << " конструктор копирования " << endl; int len = StringLength(s.GetSign ()); // размер? 247
Глава 5 sign = new char[len]; // выделение новой области памяти StringCopy(s.GetSign ()); // передача данных Reader = s.Reader; // копирование остальных данных } // оператор присваивания tAuto &tAuto::operator=(const tAuto &s) { if (this != &s) // нет присваивания самому себе? { cout << "Присваивание:" << s.GetSign() << endl; delete sign; // удалить старые данные int len = StringLength(s.GetSign()); // размер? sign = new char[len]; // выделить память StringCopy(s.GetSign()); // передать данные Reader = s.Reader; // скопировать остальные данные } return *this; // назад к текущему объекту } // для тестирования конструктора копирования и оператора // присваивания tAuto transfer(tAuto para) { cout << "Функция:" << para.GetSign() << endl; return para; } int main() { tAuto Object; tAuto goal; Object.SetSign("HG-AW 409"); goal = transfer(Object); cout << Object.GetSign() << endl; cout << goal.GetSign() << endl; } Листинг 5.20. Определение оператора присваивания (assignment.cpp) 248
Классы В самом начале оператор присваивания проверяет, не происходит ли присваивания значению самого себя. В таком случае выполнение операции будет фатально. Оператор присваивания прежде всего удаляет внешние данные целевого объекта. Если цель и источник идентичны, внешние данные будут потеряны. После этой проверки оператор присваивания выполняет следующие шаги: освобождает старую память, запрашивает новую, копирует внешние данные и все переменные объекта. Если установлено, что старая память имеет такой же объем, как и новая, можно просто провести прямое копирование. В примере показано, что возврат осуществляется с помощью ссылки. Это необходимо для того, что бы при этом не был дополнительно вызван конструктор копирования. Оператор присваивания всегда имеет один параметр. Можно определить несколько операторов присваивания в одном классе. При этом они будут различаться типом параметра. Оператор присваивания можно реализовать только элементной функцией, но не глобальной. 5.6.5. Оператор сравнения Оператор сравнения возвращает значение типа bool. Для него, как и для всех операторов, которые реализованы в виде элементной функции, левым операндом выступает тип класса. Тип правого операнда устанавливается с помощью параметра функции. Даже без дополнительных мер два объекта можно сравнить между собой оператором равенства. Код на языке C++ при этом побитно проверяет их равенство. Но такой метод сравнения иногда приводит к неверным результатам. Например, дроби 1/2 и 2/4 равны, однако вышеописанный метод этого не определит. При сравнении двух объектов типа tDate день недели не учитывается. В итоге два дня отличаются между собой только элементами Day, Month и Year, а не тем, рассчитан ли для этой даты день недели. В тех случаях, когда класс содержит указатель, ссылающийся на внешние данные, два неидентичных, но одинаковых по содержанию объекта, никогда не будут равны между собой, поскольку указатели ссылаются на различные участки памяти. Функция, написанная самостоятельно, проверяла бы не равенство указателей, а равенство данных, на которые они ссылаются. Следующий пример демонстрирует оператор сравнения для класса tFraction. Он заботится о том, чтобы дроби при сравнении сокращались. 249
Глава 5 bool tFraction::operator==(tFraction vgl) { short(); vgl.short(); return(numerator == vgl.numerator && denominator == vgl.denominator); } Листинг 5.21. Оператор сравнения для класса tFraction Для класса tDate будет просто выполняться сравнение дня, месяца и года. При сравнении классов с указателями будет сравниваться содержимое памяти, на которую они указывают. Операторы сравнения < и > выполняются по описанному образцу. Особое значение имеет оператор «меньше». Он используется, к примеру, в методе сортировки библиотеки STL и контейнере Map, который записывает отсортированные объекты в файл для определения их последовательности. В классе, который должен для этого использоваться, следует определить знак сравнения «меньше». Оператор равенства всегда должен быть реализован в паре с оператором неравенства. Это не расточительство, ведь последнее легко определить через проверку первого. class tFraction { ... bool operator==(tFraction op2); bool operator!=(tFraction op2) { return !(*this == op2); } ... }; Листинг 5.22. Неравенство Таким же образом через оператор >= можно определить оператор <. 250
Классы 5.6.6. Оператор вывода Оператор вывода обычно реализуется не элементной, а глобальной операторной функцией. Причина в том, что с левой стороны указан не объект класса, а объект потока. Следующий пример демонстрирует оператор вывода для класса tFraction. Сначала выводится числитель, затем слеш, затем знаменатель. ostream& operator<<(ostream& Stream, const tFraction& B) { return Stream<<B.GetNumerator()<< "/" <<B.GetDenominator(); } Листинг 5.23. Вывод дроби (fraction.cpp) Поскольку параметр В объявлен в качестве константы, внутри функции нельзя изменять значения элементов данных. Поэтому отсюда можно вызывать только функции, определенные в качестве константных. Только так компилятор, не заглядывая в реализацию класса tFraction, может гарантировать программе, вызывающей функцию, что передаваемое значение внутри функции не будет изменено. Функции GetNumerator() и GetDenominator() определены далее в качестве константных: class tFraction { ... long GetDenominator() const {return denominator;} long GetNumerator() const {return numerator;} ... }; Листинг 5.24. Константные функции (fraction.cpp) Также можно определить операторные функции в качестве «друзей» класса. Тогда им будет разрешен доступ ко всем закрытым элементам. Это определяет, что данные операторы также относятся к классу. 251
Глава 5 class tFraction { ... friend ostream& operator<<(ostream&, const tFraction&); friend istream& operator>>(istream&, tFraction&); ... }; ostream& operator<<(ostream& Stream, const tFraction& B) { return Stream << B.numerator << '/' << B.denominator; } Листинг 5.25. Константные функции (fraction.cpp) 5.6.7 Оператор индекса Оператор индекса представлен в виде квадратных скобок, которые используются для доступа к элементу массива. Он также может быть перегружен. Операторная функция получает индекс в качества параметра, который пользователь размещает в квадратных скобках. В функции определяется значение элемента по индексу и передается обратно в качестве возвращаемого значения. Оператор индекса можно реализовать только элементной функцией. Следующий пример демонстрирует его определение в классе, который представляет последовательность символов и называется SafeString. Оператор индекса должен гарантировать, что границы буфера не нарушаются. #include <iostream> using namespace std; class tSafeString // класс строк с защищенным индексом { public: tSafeString(int len) // максимальная длина строки //должна быть установлена { maxLen = len; 252
Классы safestr = new char[len]; // запрос на выделение //внешней памяти mist = 0; // шаблон } ~tSafeString() { delete[] safestr; safestr=0; } char& operator[](int i); // в настоящем классе должен быть реализован //конструктор копирования и оператор присваивания private: char *safestr; // внешняя строка! char mist; // шаблон для ошибочного доступа int maxLen; }; char& tSafeString::operator[](int i) { if (i<maxLen && i>=0) { return *(safestr+i); // буква найдена! } return mist; // возвращение 0 поскольку вернуть ссылку //нельзя! } int main() { tSafeString str(6); // 6 знаков, от 0 до 5 str[5] = 'A'; // записать char c = str[5]; // прочесть cout << c << endl; // тест, является ли символ действительно //буквой A } Листинг 5.26. Хранитель строк (safestr.cpp) 253
Глава 5 Пока индекс имеет корректное значение, операторная функция возвращает ссылку на соответствующий элемент внутренней строки safestr. Он может быть прочитан или записан, в зависимости от того, с какой стороны оператора присваивания указано выражение. В случае если индекс расположен вне допустимой области, будет использована переменная mist. Функция не может просто вернуть 0, поскольку возвращаемая ссылка в любом случае должна указывать на некоторое значение. Возвращаемое значение операторной функции должно быть ссылкой, потому что только в этом случае оператор может располагаться с левой стороны присваивания. Присваиванием можно изменить значение элемента массива, поскольку ссылка является его заместителем. Если возвращение ссылки невозможно, а возможно только возвращение элемента, будет использоваться исключительно локальная его копия, а значение в массиве останется нетронутым. Параметр operator[]() не обязательно должен быть целым числом. Допустимо, например, использовать последовательность символов. При этом можно предоставить доступ к списку адресов. Можно также принимать последовательность символов как входной параметр, чтобы получить доступ к содержимому через короткую запись. Так функционируют хеш-переменные в языке Perl. Подобный доступ очень нагляден, и это демонстрирует следующий пример. AutoSign["HH"] = "Hansestadt Hamburg"; По такому же принципу работает контейнер STL map (см. стр. 430). 5.6.8. Оператор вызова () Оператор вызова позволяет обратиться к объекту класса в качестве функции. Сначала сложно понять, что должно произойти, чтобы объект, такой как дата или дробь, был вызван подобным образом. Функциональные объекты подробно описаны в книге Страуструпа1. Они создаются, когда одной функции недостаточно. Например, в библиотеке STL функция find_if() (см. стр. 441) принимает параметр, который вызывает функцию, возвращающую логическое значение. Так очень удобно производить поиск объекта. Условие 1 254 Бьерн Страуструп. Язык программирования С++. М.: Бином, 2011.
Классы устанавливается в функции. Но если она должна иметь память, например, помнить последнее искомое значение, тогда она наткнется на собственные ограничения. С функциональными объектами класс может вести себя в качестве функции и заполнить этот пробел. Следующий класс tCallMe представляет собой такой объект. class tCallMe { public: int operator() (int) { /* сделать что-нибудь */ } ... }; Листинг 5.27. Оператор вызова Использование этого оператора выглядит совершенно так же, как и вызов функции для объекта. Если функция не имеет возвращаемого значения, ее вызов можно спутать с вызовом конструктора. Поэтому оператор вызова отличается прежде всего тем, что его нельзя использовать при инициализации объекта. Следующий пример демонстрирует вызов конструктора и оператор вызова. tCallMe Call(23); // вызов конструктора ... Call(23); // оператор вызова Листинг 5.28. Оператор вызова 5.6.9. Оператор конвертирования Чтобы чужой тип преобразовать в объект собственного класса используется конструктор конвертирования, который уже описан на стр. 222. Функция преобразования не имеет объявленного возвращаемого значения, а начинается с ключевого слова operator. Через пробел следует тип, в который должно быть конвертировано значение, затем рас- 255
Глава 5 полагается пара пустых скобок, поскольку функция преобразования не имеет параметров. Она возвращает выражение целевого типа. Было бы логично дополнить класс tFraction функцией преобразования, которая из дроби создает числа с плавающей запятой: class tFraction { public: operator double() { return double(numerator)/double(denominator); } ... 5.7. Атрибуты Атрибуты static и const уже были рассмотрены в связи с переменными. Применительно к классам они имеют несколько иное значение. 5.7.1. Статические переменные и функции в классах Статические элементные переменные в классах имеют мало общего со статическими переменными в функциях. В то время как элементная переменная имеет один экземпляр на один объект, статические элементные переменные существуют в одном экземпляре на класс, независимо от того, сколько в нем объектов. Они создаются перед тем, как объявляется первый объект класса. В такой переменной можно хранить количество активных объектов. Для этого статическая переменная инкрементируется в конструкторе и декрементируется в деструкторе. С нее таким способом может быть считано, сколько активных объектов данного класса существует на данный момент. Статическая элементная переменная объявляется точно так же, как и любая другая элементная переменная класса, только перед ней указывается ключевое слово static. Она может быть объявлена только в классе. Однако определять ее следует отдельно от класса, поскольку иначе компоновщик будет недоволен. 256
Классы class StatClass { ... static int Numerator; }; Статическая переменная класса не может быть инициализирована в конструкторе, поскольку тогда при создании нового объекта ее значение будет установлено заново. Вместо этого она определяется в качестве глобальной переменной, которая указывается после имени класса. Таким образом, она может также сразу быть инициализирована: int StatClass::Numerator = 0; На первый взгляд кажется бессмысленным определять статическую функцию, поскольку каждый ее элемент существует только в одном экземпляре в памяти. Но статические функции имеют свое особое предназначение. Они не могут получить доступ к данным объекта, ооскольку не принадлежат к нему, и предназначены в основном для использования в качестве интерфейса для статических переменных. Так, например, можно защитить статический счетчик от внешних манипуляций: статическая переменная объявляется закрытой и изменяет свое состояние только с помощью статической функции. Общим между статическими функциями и статическими переменными является то, что они принадлежат к классу, однако могут быть использованы независимо от существования или количества объектов. Для вызова статических функций следует указать имя класса, два двоеточия и имя функции. Наличие объекта класса в данном случае не требуется. Следующий пример содержит статическую переменную Numerator, которая подсчитывает, сколько объектов класса существует одновременно. Она является закрытой, но через статическую функцию HowMuch() приобретает актуальное значение. Для тестирования создан массив StatClass, состояние счета выводится на экран. #include <iostream> using namespace std; class StatClass { 257
Глава 5 public: StatClass() {Numerator++;} ~StatClass() {Numerator--;} static int HowMuch() {return Numerator;} private: static int Numerator; }; int StatClass::Numerator=0; int main() { cout << StatClass::HowMuch() << endl; StatClass field[4]; cout << StatClass::HowMuch() << endl; { StatClass field[5]; cout << StatClass::HowMuch() << endl; } cout << StatClass::HowMuch() << endl; } Листинг 5.29. Статические переменные и функции в классах (statclass.cpp) После старта программы счетчик инициализируется в 0. При создании массива из четырех элементов класса StatClass его конструктор будет вызван четыре раза. Массив field будет определен еще раз во внутреннем блоке и тем самым перекроет внешний массив field, который хотя и недоступен в этот момент, тем не менее продолжает существовать. Затем в блоке создается девять элементов. После того, как программа покинет блок, внутренний массив освободится. Для каждого элемента будет вызван деструктор. Программа возвратит соответственно числа 0, 4, 9 и 4. 5.7.2. Константы Указатели и ссылки особенно часто определяются в качестве констант для оповещения вызывающей программы, что они не могут быть изменены внутри функции. Если это параметр объекта класса, нельзя 258
Классы предоставить ни прямой, ни косвенный доступ к элементам данных внутри области действия этого определения. Следующий пример взят из части оператора вывода. Здесь дробь передается ссылкой. Для оповещения, что дробь не изменяется при выводе на экран, второй параметр определяется константным. ostream& operator<<(ostream& Stream, const tFraction& B) { return Stream << B.GetNumerator() << "/" << B.GetDenominator(); } Листинг 5.30. Вывод дроби (fraction.cpp) Внутри функции для константных параметров вызывается элементная функция. Для компилятора не так просто определить, изменяет ли функция GetNumerator() значения объекта В. Для этого он должен проверить, изменяет эта функция данные, прямо или косвенно, через другие элементные функции, или нет. При этом компилятор предполагает худший вариант и исходит из того, что элементная функция может вносить изменения. Поэтому он будет недоволен вызовом функцииGetNumerator(). Константные функции Чтобы компилятор был уверен, что ни одна из вызываемых функций не изменяет данные, их можно определить в качестве константных. Это достигается ключевым словом const после скобок с параметрами функции, в определении класса. class tFraction { ... long GetDenominator () const {return denominator;} long GetNumerator () const {return numerator;} ... }; Листинг 5.31. Константные функции (fraction.cpp) 259
Глава 5 Только когда вызываются константные функции и элементные переменные не изменяются, компилятор может быть уверен, что объект останется нетронутым. Если внутри константной элементной функции запрашивается доступ к переменной для ее изменения, то компилятор не будет преобразовывать эту функцию. К рассмотрению вопроса о том, остается ли объект класса константным, привлекаются все переменные. В некоторых случаях класс имеет элементные переменные, для которых не важно, изменяются ли они в объекте. Так, например, элемент данных WeekDay класса tDate не влияет на дату. Эта переменная для одинаковой даты может иметь как значение дня недели, так и значение –1, если день недели еще не рассчитан. При этом дата объекта не изменится. Для обозначения таких переменных используется ключевое слово mutable. Этот элемент можно изменять с помощью константных функций. Инициализация констант Константы могут объявляться внутри классов даже в том случае, если кажется, что компилятор не позволит провести такую инициализацию. Фактически обычная инициализация со знаком равенства и следующим за ним значением в определении класса невозможна. Присваивание в конструкторе также не работает, поскольку происходит присваивание константе. Но константу можно предопределить через инициализатор в конструкторе. class tConstant { public: const int MaxBuffer; tConstant() : MaxBuffer(500) {} tConstant(long size); }; tConstant::tConstant(long size): MaxBuffer(size) { Инициализатор всегда устанавливается при определении функции конструктора, но не при его объявлении. 260
Классы 5.8. Наследование Информатика полна аналогий. Так, в объектно ориентированном программировании используется понятие «наследование», когда класс является производным от другого класса и перенимает его свойства. Один класс может служить базисом для создания другого без изменения кода. Определяется новый класс, и указывается, что он является производным от базового. Тогда все открытые элементы базового класса будут принадлежать новому без повторного их определения. При этом говорят, что новый класс наследует свойства базового класса. Поскольку базовый класс не изменяется, и код без дальнейшей примерки вставляется в производный класс, такая форма повторного использования кода не несет никакого риска. За счет элементов, которые определяются в производном классе, он становится отдельным случаем базового класса, перенимает все его свойства и оставляет возможность добавлять новые элементы. В следующем примере отчетливо представлено, почему добавление свойств является специализацией. С точки зрения компьютерной программы все личности имеют имена, адреса и телефонные номера. Партнер по бизнесу имеет еще и банковский счет. Некоторые деловые партнеры могут быть клиентами. Клиенты, помимо качеств делового партнера, имеют также адрес доставки. Поставщики, не будучи клиентами, являются все же деловыми партнерами. У них имеются открытые счета. Сотрудники тоже деловые партнеры, поскольку у них есть банковские счета. Но у них также имеется страховочный счет. Разъездные агенты имеют все качества сотрудников и еще рабочий участок. Для определения этой структуры создается исходный обобщенный класс Person, а каждый производный класс имеет специализацию. Преимущество такой техники в том, что составной код базового класса не нужно писать заново для каждого вновь создаваемого. Например, функция проверки адреса, которая написана для Person, автоматически используется и для других классов, которые прямо или косвенно являются производными от него, не требуя при этом изменения ни одной строки кода. Изменение в классе Employee влияли бы на все производные от него классы. Но это не подействовало бы на класс с деловыми партнерами. 261
Глава 5 В данном случае эти отношения типа «является». Клиент «является» деловым партнером и имеет из дополнительных свойств только адрес доставки. Помимо элементов данных наследуются также функции, например, обозначенная ранее функция проверки адреса. Только конструкторы, деструкторы и операторы присваивания не могут быть унаследованы (рис. 5.3). Рис. 5.3. Классы личностей От какого класса наследуется новый, в языке C++ определяется следующим образом: при определении класса после ключевого слова class и его имени указывается двоеточие, затем следует ключевое слово public и имя базового класса. Чтобы не разрабатывать общие свойства для каждой группы личностей заново, сначала создается базовый класс Person. Каждая группа личностей может быть производной от класса Person и наследовать все его свойства. Что общего есть у классов, определено в классе Person. Страховка требуется только для сотрудников. Только клиент имеет адрес доставки. Если требуется управлять поставщиками, можно также позаимствовать тип Person. Поставщик наследует все от личности и имеет еще некоторые отдельные свойства, как, например, список открытых счетов. 262
Классы class Person { public: string Name, Address, Telephone; }; class Partner : public Person { public: string Kto, BLZ; }; class employee : public Partner { public: string health_insurance; }; class customer: public Partner { public: string deliver_address; }; class supplier : public Partner { public: tOpenPost *bills; }; class out_duty : public employee { public: tBlock Block; }; Листинг 5.32. Личности Открытые производные классы, которые полностью перенимают интерфейс базового, совместимы с присваиванием. Объект типа employee может быть присвоен объекту типа Person. Однако после присваива- 263
Глава 5 ния информация о страховке и банковском счете будет потеряна. Скопированный объект не имеет расширенной информации производного класса. Указателю на базовый класс можно присвоить адрес объекта производного класса. В этом случае объект не потеряет информацию, поскольку он не был изменен. В обратном порядке это не работает — объекту производного класса нельзя присвоить объект базового. Person person; employee Employee; person = Employee; // ok Employee = person; // это не понравится компилятору Если написан класс tDate, который нужно вставить в календарь, то требуется определить праздничные дни.С точки зрения календаря они отличаются тем, что каждый праздник имеет название. Можно, конечно, присвоить название каждой дате. Однако большинство дней не являются праздниками. Лучше создать производный класс tHoliday и добавить ему название. class tHoliday : public tDate { public: char Name[40]; }; ... tHoliday Easter; Ostern.Day = 25; Ostern.Month = 4; Листинг 5.33. Праздник С объектом Easter можно работать точно так же, как и с объектом tDate. Класс tHoliday наследует все свойства класса tDate и добавляет только одну отличительную характеристику — «праздник». Важное преимущество состоит в том, что не нужно менять класс tDate. Каждое изменение сегмента кода может привести к ошибкам. По этой причине в больших проектах может быть вообще запрещено изменять классы, от которых требуется создать производный. Именно по- 264
Классы тому, что пользователь базового класса не обязан использовать его код, библиотеки классов гибче, чем библиотеки функций. Изменения проводятся непосредственно в производном классе. 5.8.1. Доступ к предкам К элементу класса обычно могут получить доступ только элементы того же класса. Элементы, которые должны быть доступны другим классам, определяются как public. Это действует и при наследовании. Производный класс не может получить доступ к приватным элементам базового, но ко всем открытым — легко, как к своим собственным. Через наследование возможен еще один вид доступа, помимо private и public, — это protected., Доступ к таким элементам могут получить только производные классы. В следующей главе мы разберемся, что же означает открытое наследование. protected Помимо прав доступа private и public, наследование предлагает еще один вариант. С помощью него элементы класса можно определить так, что доступ к ним извне будет разрешен только для производных классов. Для этого используется ключевое слово protected. class Basis { private: int private; protected: int protect; public: int public; }; class successor : public Basis { public: void access() 265
Глава 5 { a = private; // это приведет к ошибке! a = protect; // это работает a = public; // это работает в любом случае } }; int main() { Basis myVar; a = myVar.private; // это, конечно же, не работает a = myVar.protect; // это тоже a = myVar.public; // это работает } Листинг 5.34. Ключевое слово protected Производный класс может переопределить тип доступа к открытым ему элементам базового класса, как это показано в следующем примере: class Basis { private: int private; protected: int protect; public: int public; }; class successor : public Basis { protected: using Basis::public; public: using Basis::protect; }; 266
Классы int main() { successor a; b = a.protect; // о чудо, это работает! } Листинг 5.35. Изменение типа доступа Подобный подход редко можно встретить на практике уже потому, что он использует выражение, которое не очень хорошо обрабатывается при модуляции. Оно применяется только как крайняя мера для исправления через производную неумелой передачи прав доступа. Атрибуты доступа при наследовании До этого мы рассматривали только производные, перед именем которых в базовом классе стояло ключевое слово public. Естественно, там может также стоять protected или private. Следует предусмотрительно относиться к определению производного класса, поскольку, при отсутствии указания атрибутов наследования, производный класс будет private.  Открытые элементы базового класса в производном классе с атрибутом private становятся закрытыми. class Basis { private: int private; protected: int protect; public: int public; }; class successor : Basis // осторожно: это private! { public: 267
Глава 5 int f1() { return private; } // так не работает! int f2() { return protect; } // так работает int f3() { return public; } // так работает }; int main() { successor a; Basis b; i = a.public; // это неверно! i = b.public; // это функционирует безупречно b = a; // это снова не работает } Листинг 5.36. Закрытое наследование Класс-наследник с атрибутом private, как приведенный в данном примере класс successor, имеет точно такие же возможности доступа внутри элементной функции, как и для класса-наследника с атрибутом public. Это видно из трех примеров элементных функций. Разница будет значительной только в отношении внешнего доступа. Нельзя получить доступ к открытым элементам базового класса из класса-наследника. Также попытка присвоить объект производного класса объекту базового класса будет сразу же прервана компилятором. protected Класс-наследник с атрибутом protected не отличается в правах доступа извне от класса-наследника с атрибутом private. Преобразование еще возможно только внутри элементной функции. На практике наследование почти всегда происходит с параметром public. Наследники, обозначенные как protected или private, не приносят практической пользы. Если они вам понадобятся, следует развернуто это прокомментировать. Элементы базовых классов При создании производного класса элементы базового класса наследуются. Когда элементы определены как public или private, к ним можно получить доступ внутри производного класса точно так же, как к собствен- 268
Классы ным. Если определяется элемент в классе-наследнике, и это имя уже используется в базовом классе, то новый элемент перекроет старый с таким же именем. В некоторых случаях в производном классе требуется использовать функции базового, но необходимо добавить к ним пару строк. Тогда функции в производном классе должны быть реализованы по-новому, а функции базового класса просто вызываются в заново созданных функциях на требуемом месте. Чтобы можно было вызвать функцию базового класса, следует указать их имена, разделив двойным двоеточием. class tBasis { public: int doSmth(int a); }; class tSpecialCase : public tBasis { public: int doSmth(int a); }; int tSpecialCase:: doSmth(int a) { int oldValue = tBasis:: doSmth(a); ... return oldValue; } Листинг 5.37. Вызов функции базового класса (kasfunc.cpp) В примере функция doSmth() в классе tSpecialCase сначала вызывает функцию doSmth() класса tBasis, а затем обрабатывает особые случаи для производного класса. 5.8.2. Конструкторы и присваивание Прежде чем будет выполнен конструктор в производном классе, запускается конструктор базового класса. Для деструктора все наоборот. Деструктор базового класса выполняется последним. Такое поведение 269
Глава 5 логично, поскольку производные классы надстраивают собственные элементы над свойствами базового. Естественно, базовый объект должен быть создан перед тем, как будет запущен конструктор производного класса. Для деструктора все выполняется в обратном порядке, поскольку он должен вызываться последним, чтобы базовый объект не был уже удален в тот момент, когда производный класс пытается освободить память, занятую его собственными надстройками. Если классы наследуются от некоторого базового класса, у которого нет конструктора, аналогичного конструктору производного класса, требуется явно вызывать конструктор базового класса. Это можно сделать, используя его как инициализатор. В следующем примере базовый класс имеет только конструктор, который ожидает целочисленную переменную. То есть стандартный конструктор отсутствует. Производный класс, однако, имеет стандартный конструктор. Компилятор будет возмущаться, если не найдет эквивалента в базовом классе. Чтобы этого не случилось, конструктор базового класса будет явно вызван в качестве инициализатора. При вызове стандартного конструктора производного класса, базовый конструктор будет вызван с параметром 5. class tBasis { public: tBasis(int i); // нет стандартного конструктора }; class tSpecialCase : public tBasis { public: tSpecialCase() : tBasis(5) // вызван базовый //конструктор { ... } }; Листинг 5.38. Вызов конструктора (kaskonstrukt.cpp) Создание объекта типа tSpecialCase вызывает стандартный конструктор. Без инициализатора компилятор вызывал бы стандартный 270
Классы конструктор базового класса tBasis. Но его не существует. С помощью инициализатора конструктор явно вызывается с целочисленным параметром перед тем, как начинается инициализация tSpecialCase. Конструктор копирования Если базовый класс имеет конструктор копирования, он не будет автоматически унаследован, но может быть вызван инициализатором, как показано в следующем примере. tSpecialCase(const tSpecialCase& object) :tBasis(object) { ... } Оператор присваивания Оператор присваивания также не наследуется автоматически. Обычно присваивание базового класса вызывается непосредственно функцией operator=, поскольку оператор присваивания базового класса может обозначаться только так. tSpecialCase& operator=(const tSpecialCase& object) { if (this!=&object) { // не допускать собственного копирования! tBasis::operator=(object); // присваивание некоторых элементов } return *this; } 5.8.3. Многократное наследование В языке C++ возможно наследование от нескольких базовых классов. При этом они после двоеточия перечисляются через запятую со всеми своими атребутами. 271
Глава 5 class Auto : public Motor, public Carriage { ... }; Класс Auto наследует все свойства и функции классов Motor и Carriage. При этом могут возникнуть конфликты имен, поскольку программист класса Motor не согласовал все имена переменных с программистом класса Carriage. Если понятие Camp будет происходить от этих двух классов, для обозначения происхождения следует, отделяя двойным двоеточием, располагать имена базовых классов перед именем Camp. class Auto : public Motor, public Carriage { ... a = Motor::Camp; ... b = Carriage::Camp; ... }; Существуют разногласия в вопросе, имеет ли смысл множественное наследование. Все согласны, что код программ, которые требуют множественных связей, быстро становится сложно обозримым и в нем легко допустить ошибку. Это причина, по которой некоторые языки программирования вообще не допускают множественного наследования. 5.8.4. Полиморфизм посредством виртуальных функций В химии полиморфизм означает разнообразие кристаллов, а в биологии — разнообразие видов (как, например, муравьи) при разделении их на категории по роду занятий. В языке программирования, где наследование является важной концепцией, это понятие, как нетрудно догадаться, близко значению, из области биологии. Вместо полиморфизма можно говорить о «самостоятельности объекта», но для обозначения этого выражения нет красивого греческого слова. Производные классы наследуют функции базовых. Если функция класса не подходит своей функциональностью классу-наследнику, ее 272
Классы можно переписать, и она будет перекрыта для всех объектов производного класса. Объекты базового класса по тому же имени будут вызывать совсем другую функцию, нежели объекты класса-наследника. В следующем листинге это показано на примере музыкальных инструментов. #include <iostream> using namespace std; class Bass { public: void boom() { cout << "Bass" << endl; } }; class Tuba : public Bass { public: void boom() { cout << "Tuba" << endl; } }; int main() { Tuba tuba.boom(); Bass bass.boom(); } Листинг 5.39. Пример — музыка Нет ничего удивительного в том, что туба звучит иначе, чем бас. И тем не менее, функция принадлежит этому объекту. Каждый объект знает, какая из функций ему принадлежит. Но что делать, если нужно собрать оркестр из этих инструментов и получить звук каждого из них? Первая проблема состоит в том, что басы и тубы относятся к одной категории. Но к нам на помощь приходит тот факт, что при открытом наследовании производные классы совместимы с базовыми. Так объект tuba может быть передан в качестве аргумента функции, которая ожидает в качестве параметра объект типа Bass, поскольку туба в целом является басом. Речь здесь не может идти о копировании или присваива- 273
Глава 5 нии, ведь тогда туба потеряет все свои особенности, которые отличают ее от баса. Но если параметр является указателем, туба остается тубой и может быть присвоена указателю типа бас. Если при передаваемом указателе вызывается элементная функция boom(), нужно помнить, что каждый объект вызывает принадлежащую ему функцию. Расширим задачу функцией DoThis(). В качестве параметра ей будет передаваться указатель на Bass. Тогда она будет вызывать функцию boom() через объект. #include <iostream> using namespace std; class Bass { public: virtual void boom() { cout << "Бас" << endl; } }; class Tuba : public Bass { public: void boom() { cout << "Туба" << endl; } }; void DoThis(Bass *tute) { tute->boom(); } int main() { Tuba tuba; Bass bass; DoThis(&bass); DoThis(&tuba); } Листинг 5.40. Пример — музыка Если запустить программу, на экране сначала появится слово «Бас», а затем слово «Туба». Рассматривая листинг подробнее, станет понятно, 274
Классы что перед элементной функцией boom() у класса Bass указано ключевое слово virtual. Если убрать слово virtual, можно установить, что на экране дважды будет выведено слово «Бас». Очевидно, что компилятор при переводе кода уже связывает функцию звучания баса с передаваемой тубой. Здесь речь идет о «предварительном связывании». Компилятор узнает внутри функции только объект указателя на тип Bass. Соответственно, связь с элементной функцией Bass будет установлена при компилировании. После объявления элементной функции boom() в базовом классе с атрибутом virtual, каждый передаваемый в нее объект будет сам проверять, какая из функций звучания ему принадлежит. Однако какой объект скрывается за каким указателем, устанавливается во время выполнения программы. В итоге объект должен сам вызывать функцию, которая будет выполняться. Такое явление в процессе работы программы обозначается как «позднее связывание». В языке C++ вы оповещаете компилятор, что функции должны быть связаны позднее, для этого в базовом классе перед функцией boom() указывается ключевое слово virtual. Ключевое слово virtual оповещает компилятор, что объект несет ответственность за то, какая функция будет вызвана. Если в DoThis() вызывается функция звучания, передаваемый объект берет ее вызов на себя. При выполнении программы так сообщается об объекте Tuba. Чтобы сделать возможным позднее связывание, во время выполнения программы объект должен быть в состоянии определить, к какому классу он принадлежит. Эта информация хранится в виде указателя. При этом объект класса с виртуальной функцией занимает больше места в памяти, чем сумма элементных переменных. Пример: фирма Вернемся еще раз к классу Person и его производным, которые были созданы на стр. 261 в теме «Наследование». Используется базовый класс Person. Его производными являются customer, employee и supplier. Также здесь присутствует функция, которая разным способом обрабатывает классы. Так, платежи всегда одинаковы, — должен быть произведен банковский перевод. Для этого требуются специфические процессы. Например, платеж сотрудни- 275
Глава 5 ку, ожидающему в этом месяце определенную сумму зарплаты, должен учитывать налог и социальные отчисления. Если же платеж получает поставщик, сумма перевода не должна подвергаться такой обработке. Платеж клиенту может, например, иметь скидку или рекламацию. Поскольку все эти операции должны по-разному обрабатываться программой, каждый производный класс использует собственную функцию payment(). class Person { public: virtual void payment(float money); }; class employee : public Person { public: virtual void payment(float money); }; class customer : public Person { public: virtual void payment(float money); }; class supplier : public Person { public: virtual void payment(float money); }; void Payment(Person &Man, float Summ) { Man.payment(Summ); } int main() { supplier Ivanov, Stepanov; 276
Классы customer Kuznecov; employee Vladimirov; Payment(Ivanov, 100); Payment(Stepanov, 100); Payment(Kuznecov, 100); Payment(Vladimirov, 100); } Листинг 5.41. Платеж Функция payment() объявляется с ключевым словом virtual как в базовом классе, так и в производных, поскольку в последних она реализуется иначе. Достаточно определить только функцию базового класса с атрибутом virtual. Однако чтобы было возможно дальнейшее наследование, необходимо определить эту функцию и в производных классах таким же образом. Персона, являющаяся поставщиком, получит платеж в качестве поставщика. Объект использует способ оплаты, который лучше всего подходит этому типу. Только в случае, когда производный класс не имеет собственной функции payment(), будет отправлен запрос к функции payment() того класса, который в наследственной цепи числится следующим, предлагающим данную функцию. Так платеж временному сотруднику будет обработан функцией payment() класса employee. В главной функции main() определяются только четыре персоны: два поставщика, клиент и сотрудник. Поскольку они являются производными объектами класса Person, в качестве ссылочного параметра можно передавать функцию Payment(). Внутри функции объекты обрабатываются в качестве персон, но там нельзя определить, является персона поставщиком или клиентом. Функция вызывает элементную функцию payment(). Поскольку в классе Person она описана как виртуальная, этот объект использует ту функцию, которая лучше всего ему подходит. Поскольку каждый объект знает свою собственную принадлежность к определенному классу, он вызывает свою собственную функцию payment(). Важно, чтобы передача персоны осуществлялась ссылкой. Если бы персона передавалась значением, при вызове функции создавалась бы копия в переменной Man. Эта копия была бы настоящей персоной и не имела информации о своем прошлом. Таким образом, функция должна 277
Глава 5 работать с оригиналом, поскольку только оригинал «знает», что он за человек. Поэтому параметр должен передаваться либо ссылкой, либо указателем. Полиморфизм позволяет оставлять неизменными такие функции как Payment() даже в тех случаях, когда появляются другие типы персон. В процедурных языках программирования функция, которая обрабатывает несколько похожих типов, реализовывалась бы через случайное распознавание, критерием для которого был бы тип персоны. Каждый раз при появлении нового типа эта функция должна была бы дополняться. Но посредством введения полиморфизма не нужно изменять ни код функции Payment(), ни до этого реализованные функции payment(). Такое расширение при использовании полиморфизма приводит к тому, что классы можно по-особенному обрабатывать, без изменения кода функции Payment() или кода элементных функций payment(). Не требуется, чтобы программист вообще занимался источником. Любое изменение функции несет в себе риск, что составные части программы будут дестабилизированы. Если возможно расширение с помощью полиморфизма, то это значительный взнос в надежность программы. Таблица виртуальных методов Виртуальные функции реализуются в компиляторе чаще всего в виде массива, который содержит в себе указатели на все виртуальные функции. Чаще всего он обозначается как VTable или vtbl. Для каждого класса с виртуальными функциями существует подобная таблица. Каждый объект такого класса получает в качестве дополнительной информации указатель на эту таблицу. Если это производный класс, то он получает копию такой таблицы. Для всех заново определенных функций устанавливается указатель на собственные функции. Если вызывается функция полиморфного объекта, через указатель на таблицу виртуальных методов можно получить доступ к его классу. Так объект «знает», какие функции ему принадлежат. Они будут вызываться через указатель, расположенный в таблице виртуальных методов. На первый взгляд это кажется расточительным. Но на практике 278
Классы разница во времени выполнения едва ли заметна. Так Рихард Кайзер1 демонстрирует, что при вызове 100 миллионов пустых функций, виртуальные функции требуют 3,69 секунд, а обычным на это требуется 3,03 секунды. Таким образом, виртуальные функции в целом на шесть миллисекунд на миллион вызовов медленнее. На практике это не может играть роли. Поскольку указатель на таблицу виртуальных методов содержит объект, виртуальные функции имеют смысл и разрешены только для нестатических элементов. Виртуальный деструктор Существует нерушимое правило, которое гласит, что каждый класс с виртуальными функциями должен иметь виртуальный деструктор. Как было показано выше, производный класс Tuba обращается к классу Bass через указатель. Если через такой указатель вызвать команду delete, то обычный (не виртуальный) деструктор освободил бы только составные части класса Bass, но не достиг бы класса Tuba. В этом простом примере все остальное не существенно. Если бы класс Tuba использовал внешнюю память, доступную через указатель, она бы не освободилась не виртуальным деструктором. Абстрактные базовые классы В примере тубы со стр. 272, от базового класса Bass можно создать производный объект. При этом не получится чего-то вроде баса. Есть туба, бас-гитара и басовый голос. При этом абстрактное понятие «бас» обозначает только свойства всех этих инструментов, которые могут издавать низкий звук. Поэтому создание объекта баса не имеет смысла. Есть только объекты реальных инструментов, которые все являются производными от баса, так как они создают низкие тона. Можно возле класса Bass написать длинный комментарий о том, что никто не должен создавать объект такого класса. Но можно предотвратить это с помощью компилятора. Для этого нужно присвоить виртуальной функции значение 0. Это означает, чтоона не может быть реализована, поэтому нельзя создать объекты данного класса. 1 Keiser Richard. C++ mit dem Borland C++ Builder. Springer, Berlin Heidelberg, 2002. 279
Глава 5 class Bass { public: virtual void boom() = 0; }; Листинг 5.42. Абстрактный базовый класс Такая элементная функция, как boom() называется «чисто виртуальной», а класс Bass — «абстрактным базовым классом». Нельзя построить ни одного объекта данного класса. Можно только создавать классы, производные от него. При этом класс Bass определяет категорию инструментов, которые имеют общие свойства. Абстрактные базовые классы создаются, чтобы определить обязательные связи определенных классов. В нашем примере с классом Bass был бы определен абстрактный базовый класс, который должен реализовывать элементную функцию boom() каждого производного класса. При этом вполне допустимо, что производные классы имеют мало общего между собой. Возможно, что для моющего средства для автомобиля, моркови и туалетной бумаги потребуется общий базовый класс, поскольку это все товары, которые реализует торговый центр. В таком случае будет создан класс goods, который станет предлагать такую функцию, как определение наличия. Если предстоит инвентаризация, не потребуется проводить ее для каждой из групп товаров. В конце концов вы захотите узнать общую стоимость всего, что есть у вас на складе. С одной стороны, вам нужно, чтобы все товары, которые когда-либо числились на складе, имели элементную функцию, возвращающую цену. Эта функция могла бы быть полиморфно вызвана при инвентаризации. С другой стороны, вы не хотите, чтобы сотрудники из-за лени описывали товар кое-как. Поэтому не должно быть создано объектов абстрактного класса goods, а только классы моющего средства для автомобиля, моркови и туалетной бумаги. Если появится новая группа товаров, следует создать новый класс. Кайзер объясняет, что наследование без виртуальных функций лишь иногда имеет смысл1. Некоторые авторы утверждают, что наследование классов без полиморфизма является не объектно ориентированным, а объектно-базисным программированием. Другие авторы обозначают 1 280 Keiser Richard. C++ mit dem Borland C++ Builder. Springer, Berlin Heidelberg, 2002.
Классы использование классов без участия наследования как объектно-базисное. Создается впечатление, что объектно ориентированное программирование прошло через идеологическую борьбу. Профессиональный программист на практике редко задумывается о вопросах идеологии, он старается построить программу так, чтобы она работала как можно лучше и эффективнее. Это не отговорка, чтобы не заниматься данной темой. Такие правила или идеологии часто следуют из опыта, и не нужно использовать их без предварительной проверки. Объектно ориентированное программирование было создано не без причин. Полиморфизм на практике При изучении полиморфизма иногда создается впечатление, что его можно использовать только в редких, очень конструктивных случаях. Но это совершенно не верно. Многие библиотеки классов вообще немыслимы без полиморфизма. Особенно много классов приложений, окон и диалоговых окон они предлагают для графических интерфейсов. Чтобы вставить в приложение окно или диалоговое окно, следует создать на основании базовых классов собственные и добавить элементы, которые отличают вашу программу от стандартного шаблона. Как пример представляется класс CDialog класса MFC (Microsoft Foundation Classes). Диалоговый класс сам по себе — особый случай окна и является производным от класса CWnd. Если вы хотите создать для своей программы собственное диалоговое окно, операционная система проинформирует вас обо всех событиях, которые произойдут в связи с этим. Большинство из них вас вообще не интересует, они могут рассматриваться классом CDialog. Прежде всего вас интересуют три события. Первое — это открытие диалогового окна. Как только оно открывается, программа должна инициализировать контрольные элементы, например, заполнять Listbox. Второе событие — это нажатие пользователем кнопки OK, что означает подтверждение правильности содержимого диалогового окна. В этот момент нужно прочесть содержимое контрольных элементов и передать его переменным программы. Третье событие наступает, когда пользователь завершает диалог клавишей Esc. В данном случае не существует длительной обработки каких-либо данных, но программа хочет знать, не передумал ли пользователь. Для обработки этих трех событий класс CDialog предлагает элементную функцию OnInitDialog() для старта диалогового окна, функцию OnOK() для нажатия клавиши OK и функцию OnCancel() для обработки 281
Глава 5 выхода из программы. Они вызываются полиморфно, если происходит событие. Для реализации диалогового окна создается собственный класс, который является открытым производным классом от CDialog. Для каждого события, которое нужно обработать, требуется заново переписать функцию обработки. Класс будет выглядеть следующим образом: class tMyDialog : public CDialog { BOOL OnInitDialog(); void OnOK(); void OnCancel(); ... }; Листинг 5.43. Наследование диалога Функции класса CDialog реализуют соответствующую функциональность в стандартных случаях. Так функция OnInitDialog() класса CDialog будет выполнять создание диалогового окна. Поскольку функция переписана, для инициализации ваших контрольных элементов будет произведен полиморфный вызов функции OnInitDialog()вашего класса, а не функции базового класса CDialog. Чтобы обеспечить выполнение стандартных необходимых шагов инициализации диалогового окна, сначала требуется вызвать функцию базового класса. BOOL tMyDialog::OnInitDialog() { CDialog::OnInitDialog(); // создание собственного диалога ... } Листинг 5.44. Диалоговая инициализация Этот механизм передачи события пользовательского диалога через использование полиморфизма не является специализацией MFC, так же, как и в других библиотеках класса. 282
Классы Вероятно, вас может удивить, почему здесь слово bool внезапно написано прописными буквами. Ответ в том, что сотрудники корпорации Microsoft создали собственный тип, потому как тип bool на тот момент времени еще не использовался в качестве стандартного. 5.9. Определение классов и синтаксический граф В этом месте мы еще раз охватим синтаксис определения класса. Для лучшей наглядности используется синтаксический граф (рис. 5.4). Рис. 5.4. Граф синтаксиса для определения класса Определение класса всегда начинается со слова class. Затем следует его класса. Отделяя двоеточием, можно перечислить через запятую имена базовых классов, от которых производится класс-наследник. Далее следует список базовых классов, синтаксис которых приведен на рис. 5.5. Внутри следующих фигурных скобок располагаются элементы класса, которые видны на рис. 5.6. Рис. 5.5. Граф синтаксиса для списка базовых классов Если называется базовый класс, следует также указывать, происходит ли наследование с атрибутами private, protected или public. По умолчанию это всегда private. При множественном наследовании несколько классов, разделенных запятыми, вносятся в список. Каждый 283
Глава 5 из них может иметь свой атрибут наследования, его можно опустить. По умолчанию, этот атрибут имеет значение private. На рис. 5.6 отображен синтаксический граф для элементов определения класса. Рис. 5.6. Граф синтаксиса для элементов класса • Доступ С помощью ключевых слов public, private и protected обозначается тип доступа к элементам. Если указание такого рода отсутствует, то по умолчанию все элементы private. • Объявление переменных Этим устанавливаются элементы класса. Объявление переменных здесь не отличается от их объявления, например, в функциях. В некоторых случаях инициализация проводится не объявлением, а с помощью конструктора. • Объявление функций Если функция полностью определяется в классе, она компилируется в качестве встроенной (см. стр. 183). • Определение функций В большинстве случаев элементные функции только объявляются внутри определения класса. Это означает, что определяются они в другом месте. • Объявление абстрактных функций В отличие от обычной, абстрактная функция является виртуальной и может быть инициализирована 0.
Глава 6 ИНСТРУМЕНТЫ ПРОГРАММИРОВАНИЯ Абсолютно все равно, насколько хорошо вы знаете язык, без компилятора и компоновщика нельзя создать программу. Большинство книг по языку C++ не рассматривают вызов компилятора и других инструментов программирования. Причина в том, что таких инструментов огромное множество. Но поскольку работа с ними также относится к языку, имеет смысл рассмотреть наиболее важные из них. Все описанные здесь программы — бесплатны и доступны во Всемирной паутине, они относятся к стандартным компонентам операционной системы или записаны на диске, прилагающемся к книге. 6.1. Компилятор C++ В операционных системах типа Linux используется компилятор С++ из пакета GCC, GNU Compiler Collection. Как правило, его нужно устанавливать отдельно. Компилятор можно получить в комплекте с любой операционной системой. Сюда относятся распространенные версии UNIX, такие как Solaris, HP-UX, AIX и OS X. В операционную систему Windows данный компилятор уже встроен. Он также входит в состав бесплатной среды Cygwin, которая обеспечивает тесную интеграцию приложений, данных и ресурсов Windows с приложениями, данными и ресурсами UNIX-подобной системы. Кроме того, GNU-компилятор доступен в составе интегрированной среды разработки Bloodshed Dev-C++. Этот компилятор подтверждает свою надежность тем, что является центральным инструментом при разработке продуктов с открытой лицензией. 6.1.1. Вызов компилятора Компилятор вызывается командой g++. В качестве параметра передается имя файла программного кода. g++ myprogramm.cpp 285
Глава 6 Если программа не содержит ошибок, через мгновение на экране возникнет запрос на ввод данных. Компилятор сообщит о себе только в том случае, если ему что-то не понравится в коде. Он переводит программу в машинный язык и компилирует ее. Результат этого процесса записывается в файл a.out. Это стандартное имя для исполняемого файла. В операционной системе Linux его можно вызвать, введя в командной строке точку, слеш и имя файла. ./a.out Если вам оно не нравится, можно с помощью дополнительного свойства задать другое имя исходящего файла. Определение такого свойства начинается со знака минус. В данном случае, свойство выглядит как -o, поскольку речь идет о выводе (от англ. output). Затем следует имя файла. Если файл должен носить имя, например, myprogram.exe1, команда будет иметь вид: g++ -o myprogram.exe myprogram.cpp 6.1.2. Свойства компилятора В системе UNIX компилятор GNU Compiler имеет набор свойств. Здесь был создан определенный стандарт, перенятый также другими разработчиками компиляторов. Самые важные свойства при вызове компилятора: • -o имя файла Файл выходных данных называется имя файла. • -c имя файла Компилирует один-единственный источник кода, состоящий из имени файла и расширения .о. • -IПуть Дополняет путь, в котором будет происходить поиск имени файла. • -LПуть Дополняет путь, по которому будет происходить поиск библиотек. 1 Расширение .exe не является обязательным для Linux. Однако оно и не мешает. При вызове программы ее имя должно быть указано с расширением. 286
Инструменты программирования • -lИмя (строчная l) Использует библиотеку libИмя.а. Поиск файла будет выполняться по стандартному пути (например /usr/lib) и по дополнительному пути -L. • -g К коду добавляется информация для отладчика. Так можно в отладчике обратиться к переменным и функциям по их именам. • -DИмя С помощью этого свойства можно определить имена. Такая команда равнозначна #define. Если имени требуется присвоить значение, следует указать -DИмя=Значение. 6.1.3. Сообщения об ошибках Является скорее правилом, нежели исключением, то, что программа содержит ошибки. В лучшем случае они настолько явные, что их замечает компилятор. Здесь приведены сообщения об ошибках GNU Compiler. Другие компиляторы могут выводить другие сообщения, но в целом они аналогичны. Сообщения об ошибках от компилятора Компилятор различает два вида сообщений. Первый — это ошибки. Если встречается ошибка, программа не может быть скомпилирована. Ошибка должна быть исправлена. Второй вид сообщений — предупреждения. Они указывают на то, что в программе не все безукоризненно. Но компилятор в состоянии преодолеть проблему. Компиляция при предупреждении будет выполнена и программа сможет работать. Здесь представлена выдержка из сообщений об ошибках при компиляции: main.cpp:33: Warning: не скомпилированный параметр «signalNr» main.cpp: в функции >>int main(int, char**)<<: main.cpp:174: Warning: Переменная "db" не используется main.cpp:157: Error: >>Element<< не определен в данной области действия Сначала указывается имя файла, в котором встретилась ошибка. Затем следует номер строки, для того чтобы можно было найти место 287
Глава 6 в коде, где она находится. После чего следует обозначение, ошибка это или предупреждение. И, наконец, появляется само сообщение об ошибке. Перед ошибкой в строке 174 находится указание на то, в какой функции она была обнаружена. Далее представлено несколько типичных сообщений об ошибках: • Отсутствует определение Если используется переменная, для которой отсутствует определение, будет получено сообщение об ошибке. Может быть, что вы действительно забыли определить переменную. Но может также быть, что вы допустили ошибку при написании ее имени. А может случиться, что вы определили переменную, но где-то в другом месте. Компилятор должен знать, как определена переменная, перед ее первым использованием в программе. Определение следует размещать перед использованием, или определять прототип. Такое сообщение об ошибке может касаться не только переменной. Может быть, что вы вызываете функцию, которая ни определена, ни объявлена. • Неиспользуемая переменная Если переменная определена, но ни разу не используется, компилятор «удивится подобному расточительству», и сообщит об этом. Такого же мнения он будет, если переменной в программе только присваивается некоторое значение, и больше ничего. Эти предупреждения помогают навести порядок в коде, если после значительных изменений остаются лишние переменные. main.cpp:161: Ошибка: >>class Database<< не имеет элементов с именем "writeURL" • Отсутствует элемент класса В этом случае компилятору также не хватает определения. Он может отметить, что функция writeURL() хоть и обрабатывается в качестве элемента класса Database, однако не определена в нем. • …expected/ожидается Сообщение об ошибке, которое содержит слово «ожидается», или его английский вариант «expected», указывает, что компилятор несколько удивлен обнаружением определенного элемента в этом месте. Следующее сообщение появилось по причине отсутствующей скобки в операторе if. Queue.cpp:30: Ошибка: expected '(' before >>myValue<< 288
Инструменты программирования Иногда ожидаемые элементы могут быть неочевидны. Это происходит из-за того, что компилятор перебирает все мыслимые варианты. Он не может знать, что планировал в этом месте программист. Ошибки компоновщика Если компилятор удовлетворен исходным кодом, в действие вступает компоновщик. Его задача — из отдельных модулей и библиотек составить полноценную программу. Соответственно, его ошибками, как правило, являются отсутствующие функции или глобальные переменные, которые обещаны в заголовочном файле перед реализацией. /home/arnold/src/cpp/main.cpp:181: undefined reference to 'Queue::getInstance()' Сообщение указывает, что член-функция getInstance() класса Queue не определена, хотя и вызывается в файле main.cpp в строке 181. Причины таких ошибок могут быть следующее: • Если при обработке кода компилятором уже было выдано предупреждение, то имя функции может быть неверно написано или отсутствовать команда #define • Библиотека не подключена. В интегрированной среде разработки библиотеки должны подключаться в настройках проекта, если они не являются стандартными. При использовании инструмента make (см. стр. 309) или при прямом вызове командной строкой, перед именем библиотеки должно быть указано свойство -l (строчная буква L). • Библиотека располагается в каталоге, где компоновщик не ищет библиотеки. Если библиотека не расположена по стандартному пути, ее каталог должен быть внесен в настройки проекта или указан в командной строке компилятора со свойством -L. • Компоновщик привязывает библиотеки только тогда, когда существует открытое указание это делать. При этом компоновщик работает быстро, а код программ становится короче, но программист должен следить, чтобы зависимые друг от друга файлы вызывались компоновщиком в правильной последовательности. 6.2. Препроцессор Препроцессор унаследован от языка C. Перед тем, как компилятор получает исходный текст программы, его просматривает препроцес- 289
Глава 6 сор. Он управляется командами, которые начинаются с символа октоторпа (#). 6.2.1. Связывание файлов: #include Почти во всех листингах вам уже встречалась команда для привязки заголовочного файла. Это команда #include, которая задает его имя. Способности препроцессора доходят до того, что он может рекурсивно привязывать файлы. При этом он не запутается, если в привязанном файле снова встретится команда #include. Следующие за ключевым словом имена файлов обычно размещаются в кавычках или в угловых скобках. При этом кавычки означают, что поиск файла будет проводиться в списке исходных файлов. Угловые скобки указывают на то, что речь идет о стандартном подключении, как например заголовок стандартной библиотеки. Компилятор может найти ее в четко установленном каталоге. Для систем UNIX это /usr/ include. Для системы Windows не существует стандарта. Поэтому файлы заголовков помещаются в поддиректорию компилятора. Он определяет это место при установке. Список каталогов, где будет производиться поиск заголовочных файлов, имена которых указаны в угловых скобках, можно расширить с помощью свойства компилятора -I. При указании файлов после команды #include можно также использовать пути директорий или буквы дисков. Желательно указывать короткие пути. Следует учитывать также и то, что программа будет запускаться и на других компьютерах. Путь не должен обращаться к локальным данным. 6.2.2. Константы и макросы: #define С помощью команды #define определяется имя. При этом речь идет в некотором роде о препроцессорных переменных. С переменными языка C++ они не имеют ничего общего. Эти имена могут быть позднее опрошены командой #ifdef. Следующая команда определяет имя DEBUGCODE. #define DEBUGCODE Определенному имени может соответствовать некоторая константа. Для этого она располагается после имени. Следующее определение устанавливает VERSION в 5. 290
Инструменты программирования #define VERSION 5 В старых компиляторах языка C это была единственная возможность задать константу. В языках ANSI C и C++ появилось ключевое слово const, которое более рекомендуется к использованию. Преимущество такого препроцессорного определения в том, что оно срабатывает до вызова компилятора. Следующий вызов компилятора GNU Compiler также устанавливает имя VERSION в 5. g++ -DVERSION=5 unix.cpp Первый знак такого определения не может быть знаком подчеркивания, поскольку тот используется для специфических системных определений. Также важно следить за тем, чтобы препроцессорные команды не заканчивались точкой с запятой. Макрос Возможности препроцессора заходят дальше. Он позволяет создать нечто наподобие маленькой функции. Следующая команда #define определяет квазифункцию ADD(), которая складывает два передаваемых значения. #define ADD(a, b) ((a) + (b)) Такое определение называется макросом. В макросе с параметрами нельзя указывать пробел между именем и открывающейся скобкой. Макрос отличается от обычной функции тем, что вставляется препроцессором в код программы. Это дает небольшой выигрыш в скорости по сравнению с вызовом функции. При использовании макросов имеется подводный камень, — его входные параметры передаются в виде кода. По этой причине вверху располагается так много скобок. Препроцессор не следит за приоритетами математических функций. Результат представлен в следующем примере: #define FALSEINCR(a) a + 1 ... int i = FALSEINCR(4) * 2; Листинг 6.1. Неожиданный результат 291
Глава 6 Препроцессор просто вставляет код макроса в позицию его вызова и устанавливает значение переменной a равным 4. При этом получается следующая строка: int i = 4 + 1 * 2; Но результатом будет не 10, как это, возможно, ожидалось, а 6. При добавлении скобки макровставка может привести к необычным эффектам. Рассмотрим в следующей строке переменную Number, и подумаем, какое содержимое она получит после выполнения этого кода. #define SQUARE(a) ((a)*(a)) ... int Number = 5; Value = SQUARE(++Number); Листинг 6.2. Неожиданный результат Переменная Value в итоге будет иметь значение 49. Причина в том, что передается не значение переменной в качестве параметра, а код ++Number. Поскольку он повторяется в макросе дважды, значение переменной также будет инкрементироваться дважды. Проблема состоит в следующем: обычно программист ожидает, что при вызове макроса будет работать компилятор, но вместо этого препроцессор вставляет код. Поэтому макросов рекомендуется по возможности избегать. Если требуется выигрыш в скорости макроса по сравнению с вызовом функции, толучше использовать встроенные функции (см. стр. 183). Способность препроцессора вставлять код может быть полезна для проведения синтаксических изменений. Те, кому не нравятся фигурные скобки, с помощью препроцессора могут заменить их на BEGIN и END — команды в языке Pascal. #define BEGIN { #define END } С точки зрения оформления программы такая возможность может быть очень полезной, например, мои первые шаги в языке C были сделаны на компьютере, который не имел клавиш с фигурными скобками. 292
Инструменты программирования Однажды определенное имя может быть разыменовано с помощью команды #undef. #undef DEBUGCODE 6.2.3. Опросы: #if Определение DEBUGCODE может быть использовано для того, чтобы преобразовывать код при компилировании только тогда, когда имя определено. Для этого используется команда #if. Аргументом служит функция define(), которая получает имя в качестве параметра, и проверяет, определено оно или нет. #if defined(DEBUGCODE) ... #endif Полный исходный код между двумя командами будет показан компилятору только тогда, когда определено имя DEBUGCODE. Для краткосрочного комментирования больших частей кода такой конструктор очень полезен. Поскольку он часто используется, существует его сокращенное написание — # ifdef. #ifdef HUHU // разные команды /* также с комментариями */ #endif После вставки команды весь закомментированный код будет помечен светлым. Вложенные конструкции # ifdef не создают при этом никаких проблем для препроцессора. Если вместо этого использовать стандартные скобки комментария, первый знак окончания комментария, который находится между скобками, закончит комментирование. Поэтому здесь препроцессор гораздо более эффективен, чем компилятор. Можно не только проверять, было ли определено имя, но и использовать его содержимое. В следующем примере опрашивается, является ли значение MAX меньше 10. #if MAX<10 293
Глава 6 Как и при использовании обычной команды if, можно связывать выражения с помощью && и ||. Также препроцессор знает команду #else и комбинацию команд if и else: #elif. #if MAX<10 ... #elif MAX==10 ... #else ... #endif 6.2.4. Предопределенные макросы Препроцессор знает некоторые предварительно определенные макросы. Их можно вставить в любое место программного кода. Препроцессор заменит их на соответствующие предписанные команды (табл. 6.1). Таблица 6.1. Стандартные макросы Макрос Определение _LINE_ _FILE_ _DATE_ _TIME_ Текущая строка кода источника Имя файла источника Текущая дата в формате ММ\ДД\ГГГГ Текущее время в формате чч:мм:сс Некоторые макросы установлены компилятором. Так, например, очень полезно с помощью команды #ifdef опросить, для какой операционной системы требуется скомпилировать программу. Так можно предусмотреть системные отличия (табл. 6.2). Таблица 6.2. Системные макросы Макрос Определение unix _MSDOS_ _Windows UNIX-системы, в т.ч. и Linux MS DOS MS Windows В следующем примере системный вызов fork(), который доступен только в операционной системе UNIX, будет скомпилирован лишь тогда, когда целевая машина работает под управлением UNIX. 294
Инструменты программирования #ifdef __unix__ fork(); #endif Производители компиляторов кодируют версии компиляторов в макросах. Так Borland C++ Builder для кодирования номера его версии использует макрос _BCPLUSPLUS_. Этим можно пользоваться, если требуется применять механизмы, которые доступны только в новых версиях. #if __BCPLUSPLUS__ >= 0x530 // работает под C++ Builder начиная с версии 3.0 #endif Хотя многие компиляторы определяют этот макрос, для предотвращения возникновения ошибок разумно один раз быстро проверить, функционирует ли он так, как ожидается. Макрос _cplusplus для всех компиляторов языка C++ обязательно определен в файле stdlib.h. Если компилятор совместим со стандартом ANSI, макрос возвращает шестизначный номер. В противном случае — пятизначный. Компиляторы языка C вообще не имеют этого макроса. Так можно было бы написать программу, которая скрывает расширенные возможности языка C++ от компилятора языка C. Следующий пример реализует функцию min() в качестве шаблона, если речь идет о компиляторе языка C++. В противном случае определяется макрос. #ifdef __cplusplus template <class T> T min(T a, T b) { return a<b?a:b; } #else #define min(a,b) a>b?b:a #endif 6.2.5. Другие препроцессорные команды Команда #line изменяет счетчик строк. Следующая команда устанавливает номер текущей строки равным 2000. #line 2000 С помощью команды #error можно остановить компиляцию. По желанию далее можно разместить код, который будет появляться на экране после выполнения команды. 295
Глава 6 #error #error Код, который потом будет выведен Команда #pragma используется для того, чтобы компилятор мог ввести собственные команды. Ее аргумент будет интерпретирован компилятором. Какие команды он запустит и как на них отреагирует, зависит от разработчика. Существует лишь правило, что компилятор должен игнорировать аргументы, которые ему незнакомы. #pragma OPTION 6.3. Разделение исходного кода • Если исходный код программы содержит несколько классов, для легкости прочтения логичнее разделить его на несколько файлов. В то время как язык Java предписывает программисту использовать для каждого класса отдельный файл исходного кода, в языке C++ вопрос о разделении программы на части остается на усмотрении программиста. Ниже приведены некоторые причины разделять код программы на несколько файлов. В объемном коде сложно найти определенные классы или функции. Разделение облегчает поиск. Файлы должны носить соответствующие имена и быть легко читаемыми. • При разработке программы командой программистов следует избегать ситуаций, когда два программиста работают с одним файлом, при этом один будет нарушать изменения второго и наоборот. Чем лучше исходный код программы разделен на файлы, тем меньше риск, что они будут друг другу мешать. • Файлы должны компилироваться только тогда, когда они были изменены. Чем больше файлов могут оставаться нетронутыми, тем быстрее проходит работа по редактированию, компиляции и тестированию. 6.3.1. Пример: игра «Бермуда» Проще всего продемонстрировать разделение программы на конкретном примере. Для этого разделим код игры «Бермуда» на отдельные файлы. Программа содержит две важные группы: игровое поле и корабли. Они, соответственно, составляют два центральных класса. Тогда весь исходный код для игрового поля помещается в файл field.cpp, 296
Инструменты программирования а исходный код класса кораблей — в файл ship.cpp. Основная программа находится в файле bermuda.cpp. Каждый отдельный файл может быть скомпилирован. Сначала компилируется главный файл bermuda.cpp. Команда для компилятора выглядит так: g++ -c bermuda.cpp Будет выдан целый список ошибок, которые, в первую очередь, появятся из-за того, что главной программе не знаком ни класс игрового поля, ни класс кораблей, а для их успешной инициализации ей требуется знать оба этих класса. Таким образом, каждый из двух файлов будет еще раз разделен. Файл ship.cpp содержит определение функций для кораблей. Класс tShip, который необходим главной программе, должен размещаться в специальном файле интерфейса с именем ship.h. То же самое требуется сделать с игровым полем, чтобы файл field.h также был вложен в код главной программы. Помимо классов здесь определяются и константы для размеров поля и количества кораблей. Теперь нужно позаботиться о том, чтобы в файле bermuda.cpp были подключены файлы ship.h и field.h. Для этого служит старая знакомая команда #include. #include "ship.h" #include "field.h" Теперь компилирование должно пройти успешно. Хотя определение элементных функций классов находится не в заголовочном файле, главная программа может быть скомпилирована. Она должна содержать только прототипы используемых функций и переменных. В качестве результата успешной компиляции создается файл bermuda.o. Этот файл объекта содержит теперь скомпилированную функцию main(), но не содержит ни класса tShip ни игрового поля. В целом он содержит только ссылки на них и информацию о том, как должен выглядеть объект класса tShip. Теперь проведем таким же образом компиляцию файла ship.cpp. g++ -c ship.cpp Компиляция не сработает сразу. Некоторые классы незнакомы компилятору, поскольку они находятся в заголовочном файле. Здесь также 297
Глава 6 требуется подключить файл ship.h. После привязки его с помощью команды #include создается новый файл. Он носит имя ship.о. При попытке компилировать файл field.cpp выяснится, что ему требуются оба заголовочных файла. Здесь также понадобится #include. Следующие строки должны быть добавлены в код файла field.cpp. #include "field.h" #include "ship.h" Здесь снова появится ошибка! Компилятор будет недоволен тем, что класс tShip неизвестен. Почему? Ведь заголовочный файл подключен. Если рассмотреть сообщение об ошибке внимательно, станет ясно, что ошибка находится не в файле field.cpp, а в его заголовочном файле field.h. Причина в том, что при определении класса игрового поля также используется класс tShip. В последовательности команд #include файл ship.h указан после файла field.h. В файле bermuda.cpp было иначе, поскольку класс tShip был уже известен, а именно: подключен в к файле field.h. Означает ли это, что надо всегда следить за тем, в какой последовательности располагаются команды #include? Нет, это может только привести к путанице. Обычно нужно подключать заголовочный файл там, где он требуется. В этом случае нужно написать команду #include для ship.h именно в заголовочном файле field.h, потому что там используется класс tShip. Теперь еще раз скомпилируем файл field.cpp, и снова появится ошибка. Но теперь причина совсем в другом. Компилятор «жалуется», что класс tShip дважды определен в одном элементе компиляции. Вероятно, вы полностью уверены, что класс tShip определен только один раз в файле ship.h. Это тоже абсолютно верно. Но дело в том, что этот файл подключен дважды. Один раз через файл field.h, и еще раз прямо в field.cpp. И если сейчас попробовать скомпилировать файл bermuda.cpp, вы получите точно такое же сообщение об ошибке. Такая ситуация может запросто возникнуть, когда в заголовочном файле подключаются другие заголовочные файлы. Можно при каждой компиляции следить, не подключен ли один заголовочный файл два раза, но это достаточно утомительно и чревато ошибками. Лучше предоставить эту работу компилятору. Никогда не делайте того, что компьютер может сделать лучше. Здесь можно воспользоваться тем, что пре- 298
Инструменты программирования процессор может определять и опрашивать константы. Заключим класс tShip и файл ship.h в команды: #ifndef SHIP_H #define SHIP_H class tShip { ... }; #endif Первая команда #ifndef проверяет, не была ли константа SHIP_H определена ранее. Этого наверняка не произошло, так что компилятор может продолжать работать. В следующей строке будет определена именно эта константа и подключен класс tShip. В конце препроцессор обрабатывает команду #endif, которая принадлежит к первой строке #ifndef. Если позднее при процессе компиляции снова будет подключаться файл ship.h, препроцессор снова будет опрашивать константу SHIP_H, была ли она уже определена или нет. На этот раз она уже определена, поэтому препроцессор переходит к последней строке и прячет содержимое файла от компилятора. Неважно, сколько раз вы подключаете ship.h, класс tShip определяется единожды. Теперь можно скомпилировать все три модуля и получить три файла объектов. Но не хватает самого программного файла. Для его создания нужно связать три файла друг с другом. Для этого снова вызывается компилятор GNU Compiler и ему передаются файлы с именем bermuda и затем все файлы объектов. Компилятор настолько сообразительный, что знает: что здесь он должен не компилировать, а связывать. g++ -o bermuda bermuda.o field.o ship.o Если используется операционная система Windows, в качестве имени файла вместо bermuda следует ввести bermuda.exe. Система Windows нуждается в расширении, чтобы отличить исполняемый файл. В следующей главе рассмотренный пример будет сформулирован в общих понятиях и рассмотрен по порядку. 6.3.2. Распознавание файлов Файл исходного кода программы на языке C обычно имеет расширение .с. Для программ на языке C++ имеются различные расширения. Предложение использовать заглавную букву C потерпело поражение, 299
Глава 6 поскольку в операционной системе Windows нет различия между строчными и прописными буквами в именах файлов. Периодически встречается расширение .сс, но обычно файлы имеют расширение .срр. Заголовочные файлы имеют расширение .h, как в языке C, так и в C++. Некоторые программисты используют также .hpp или .hh. Для заголовочных файлов стандартных библиотек можно опустить расширение1. Компилятор обрабатывает отдельно каждый файл исходного кода и создает из него отдельный файл объекта. Файл объекта в системе UNIX имеет расширение .о, а в системе Windows — .obj. Файлы объектов при компиляции без ошибок привязываются к программе компоновщиком. Чтобы из файла объекта создать работоспособную программу, компоновщику требуются стандартные библиотеки, которые входят в пакет среды разработки. Такие файлы библиотек в Windows имеют расширение .lib. В системе UNIX имя файла библиотеки начинается со слога lib и имеет расширение .а. 6.3.3. Объявление и определение Понятия «объявление» и «определение» значат для классов немного иное, чем для переменных и функций. Оно состоит из слова class и имени класса и означает только, что этот класс существует, но не говорит о том, как он выглядит. С помощью объявления можно установить указатель на класс. Этого, однако, недостаточно, чтобы создать объект класса, поскольку компилятор не знает, насколько большим он должен быть. Определение класса содержит его объявление и составные элементы данных и элементных функций. Такое определение должно предшествовать каждому модулю, которому необходимо получить от класса больше, чем просто указатель на него. Определение класса может повторяться в проекте как угодно часто, однако в исходном коде оно появится только один раз. Поэтомуопределение класса, который важен для нескольких модулей, располагается в заголовочном файле, а не в файле реализации. Проще обстоит дело с глобальными переменными и функциями. Компилятор требует только их объявление. Если оно указано в начале 1 Заголовочные файлы без расширения используют пространство имен std (см. стр. 327). 300
Инструменты программирования исходного кода программы, переменные и функции можно использовать и вызывать в ней сколько угодно раз. Объявление может встречаться в коде программы в любом количестве. Для функций оно состоит из прототипа, то есть заголовка функции, который заканчивается точкой с запятой. Для глобальных переменных прототип определяется ключевым словом extern. Хотя ключевое слово предполагает нечто иное, переменные могут быть определены в том же исходном коде программы, в котором они объявлены extern. int myFunction(int, char *); // объявление функции extern char *Pos; // объявление переменной Листинг 6.3. Прототипы функций и переменных Функции, как и переменные, можно определить в программе только один раз. Исключение составляют встроенные функции и шаблоны (см. стр. 317). Причина в том, что компилятор в обоих случаях уже при вызове обрабатывает эти функции. Для встроенных функций код вставляется в позицию их вызова. Для шаблонов в зависимости от типа параметров будет генерироваться подходящая функция. Одного определения в обоих случаях будет мало. Оно может многократно встречаться в одной программе, но в единице компиляции использоваться только единожды. 6.3.4. Заголовочные файлы Чтобы разделить необходимое объявление и определение между различными исходными кодами программы, существуют заголовочные файлы. Обычно Они подключаются к файлу исходного кода с помощью команды #include и компилируются с исходным кодом. Все команды, которые начинаются с символа октоторпа (#), обрабатываются препроцессором. Команда #include действует так, что файл встраивается на место ее написания. Имя файла указывается в угловых скобках. Иногда речь идет о системных файлах или файлах компилятора. Путь к ним устанавливается условно или в настройках компилятора. Также можно указать имя файла в кавычках. Тогда заголовочные файлы будут принадлежать проекту и находиться в том же каталоге, что и другие файлы проекта. Обычно они имеют расширение .h. Заголовочные файлы стандартных библиотек языка C++ не имеют расширения. 301
Глава 6 В определении пути к файлу допустим прямой слеш (/). В операционной системе Windows также допускается обратный слеш (\). Там, где это может ограничивать переносимость программного обеспечения, рекомендуется использовать прямой слеш. Если заголовочный файл находится в каком-то каталоге, который не принадлежит к стандартным путям, не рекомендуется указывать его прямо в строке include, лучше расширить список путей подключаемых файлов. В интегрированной среде разработки (IDE) для этого существует протокол проектных свойств. В командной строке компилятора он доступен с помощью свойства -I. Если вы не примете это всерьез и напишете в вашем программном коде строку, подобную следующей, программа будет скомпилирована только в среде Windows и скорее всего только на вашем компьютере: #include "F:\mysrc\firma\incl\standards.h" // нехорошо В некоторых случаях требуется подключить старые библиотеки языка C к программам на языке C++. Тогда может оказаться, что вызов функции C++ не подходит к номенклатуре языка C, которую ожидает библиотека. Поскольку язык C++ кодирует также параметры функции, они не совместимы с функциями, которые генерирует компилятор языка C. При этом вызов функций языка C возможен, если их объявление будет подключено через описание extern "C". Часто при подключении соответствующих файлов заголовков это выглядит, как в следующем примере: extern "C" { #include <cheader1.h> #include "cheader2.h" } Заголовочный файл открывает часть модуля, которая требуется для программирования других модулей. Таким образом он выстраивает интерфейс между модулем и частями программы, которые его используют. При этом изменения в модуле не касаются других частей программы до тех пор, пока заголовочный файл остается неизменным. Если же он изменяется, все модули, к которым подключен этот файл, должны быть скомпилированы заново. Чтобы использовать инструмент make (см. стр. 309), требуется также предусмотреть зависимости заголовочных файлов. Если вы работаете с интегрированной средой разработки, такая зависимость будет автоматически установлена. Изменения в за- 302
Инструменты программирования головочных файлах приведут к тому, что все зависимые модули снова нужно будет компилировать. При создании интерфейсов следует разрабатывать их особенно сжато и тщательно, чтобы изменения требовались как можно реже. 6.3.5. Статические функции Как правило, любую функцию можно вызывать в разных модулях. Компилятору требуется только прототип. Если функция предназначается исключительно для одного программного кода и не должна быть известна компоновщику, то для нее используется ключевое слово static. Если многие функции должны использоваться в качестве локальных, в языке C++ предлагается анонимное пространство имен (см. стр. 330), работать с которым проще, чем указывать каждой функции свойство static. 6.3.6. Скрытая реализация Немножко неудачно, что также и приватные операции класса выносятся на всеобщее обозрение. Если класс записан в заголовочном файле, который используется другими модулями, следует определить также закрытые элементы, которые, возможно, будут реализованы позже. Данная глава предлагает решение, как обойти этот огрех языка C++. Возьмем в качестве примера карточную игру. Для других модулей программы нужно создать интерфейс, чтобы перемешать карты и вытащить одну. class CardGame { public: Card *next_card(); void new_mix(); private: Card card[MAXCARDS]; }; Листинг 6.4. Класс CardGame 303
Глава 6 Здесь в закрытом разделе определяется массив карт. К его реализации нельзя получить доступ извне, и он в любой момент может измениться, не сообщая об этом другим модулям. Тем не менее интерфейс класса подвергнется изменениям, хотя они и не затрагивают внешние модули. Это можно обойти с помощью класса реализации. Для этого закрытого класса будет добавлен только указатель в закрытой части для открытых классов. Этому указателю требуется объявление класса, которое через содержимое класса ничего не выдает. class localCardGame; class CardGame { public: Card *next_card(); void new_mix(); CardGame(); ~CardGame(); private: localCardGame *my; }; Листинг 6.5. Карточная игра со скрытой реализацией Все, что не касается закрытой части класса, но и не должно быть открытым, перемещено в специальную классовую реализацию localCardGame. Этот класс хоть и объявляется в заголовке, но не определяется в нем. При этом его содержимое в заголовке не известно. Так и должно быть, поскольку в закрытой части класса CardGame располагается только указатель. Поскольку он занимает столько же памяти, сколько и другие, компилятору не требуется информация о локальном классе. При реализации нужно предусмотреть, чтобы этот указатель получал объект в конструкторе. В деструкторе он должен снова удаляться. В исходном коде программы будет указано следующее: class localCardGame { public: Card card[MAXCARDS]; 304
Инструменты программирования }; CardGame::CardGame() { my = new localCardGame; } CardGame::~CardGame() { delete my; } Листинг 6.6. Реализация Такая дискретность дается нелегко. Для доступа к приватным классам всегда нужно обращаться через указатель my. По этой причине имеет смысл использовать для него короткое имя. 6.4. Компоновщик и библиотеки Библиотеки содержат классы, функции и переменные в машинном коде. Можно создавать также собственные библиотеки. Вместе с компилятором C++ вы получаете набор стандартных библиотек. Если требуется создать базу данных или графический интерфейс, можно получить наборы библиотек от другого разработчика. Во Всемирной паутине можно найти множество библиотек, которые значительно облегчают жизнь программисту. Чтобы использовать содержимое библиотеки для корректного вызова, компилятору требуются заголовочные файлы, которые определяют интерфейс. Чтобы требуемый код был встроен в программу, следует обратиться к компоновщику и прикрепить библиотеки к программе. 6.4.1. Подключение статических библиотек Компоновщик будет искать в библиотеках функции, которые вызываются в программных модулях. Он подключает к программе только необходимые ей объекты. В средах UNIX и Linux компоновщик обозначается ld, а в операционной системе Windows — часто LINK. В целом, имя не имеет значения, 305
Глава 6 поскольку компоновщик в основном вызывается компилятором или интегрированной средой разработки. Компилятору GNU Compiler передаются подключаемые файлы, при этом он знает, что нужно вызывать для них компоновщик. Если компоновщик вызывается в командной строке или командой создания файла, в качестве параметра ему передается свойство -о и имя выходного файла. Затем следуют файлы объектов, которые он должен скомпоновать. Опция -l (строчная буква L) передает в качестве параметра имя требуемой библиотеки. Если в операционной системе Windows передается имя файла библиотеки, то оно, как правило, имеет расширение .LIB, в среде UNIX имя обычно иное. В системе UNIX имя всегда начинается с lib и имеет расширение .а. Оба параметра не могут передаваться свойством -l. Библиотека libxml.a будет подключаться как -lxml. В среде UNIX библиотеки компилятора языка C и библиотека операционной системы обычно находятся в каталоге /usr/lib. Библиотеки других разработчиков проще всего установить также в эту директорию, но можно и разместить их отдельно. В других операционных системах библиотеки принадлежат компилятору, а не операционной системе. Там можно найти файлы в поддиректории lib, расположенной в каталоге, куда установлен компилятор. Если требуется подключить библиотеку, которая находится не по стандартному пути, расположение должно передаваться в свойстве -L. Для интегрированной среды разработки эту информацию можно найти в настройках проекта. Там находится перечень, в котором можно указать библиотеки и добавить к ним путь. 6.4.2. Динамические библиотеки Начиная с победного шествия графических оболочек, стало невозможным подключать все библиотеки к каждому приложению, поскольку в таком случае каждое приложение Windows должно включать собственную среду Windows. Вместо этого в системе существуют динамические библиотеки, которые управляются операционной системой. В среде UNIX они называются «Shared Libraries». В операционной системе Windows — «Dynamic Link Library». Их можно узнать по расширению DLL. 306
Инструменты программирования Использование динамических библиотек системы происходит неявно для программиста. Через подключение заголовочных файлов вызовы автоматически обрабатываются так, что используются динамические библиотеки. Если требуется самостоятельно написать динамическую библиотеку, следует внимательно изучить эту тему. К сожалению, обращение с системными библиотеками крайне зависимо от операционной системы. Различные компиляторы одинаковых систем могут вести себя по-разному. Далее создание и использование системной библиотеки будет описано на примере кухонного рецепта. Среда Windows C помощью любой интегрированной среды разработки (IDE) можно создать DLL-проект, и тем самым выполнить бо́льшую часть работы. Автоматический конструктор IDE создаст функцию DllEntryPoint(), которую должна содержать каждая библиотека DLL. Вам потребуется только написать функции, к которым можно получить доступ извне. Библиотеки DLL для операционной системы Windows не имеют формата C++, а работают с классическим интерфейсом компилятора языка C. Для этого каждый компилятор имеет свой собственный синтаксис. Например, библиотека должна содержать функцию fkt(), которая имеет входной параметр и возвращаемое значение типа double. Компилятор Borland использует следующий тип написания1: __declspec(dllexport) double fkt(double w) { ... В среде Visual C++ корпорации Microsoft функция для DLL выглядит так2: extern "C" __declspec(dllexport) double __stdcall fkt(double w) { ... После генерации проекта создается файл с расширением .DLL, который при выполнении программы должен находиться в рабочем каталоге 1 2 Keiser Richard. C++ mit dem Borland C++ Builder. Springer, Berlin Heidelberg, 2002. Wigard Susanne. Visual C++ 6. bhv, Kaarst, 1999. 307
Глава 6 или папке Windows. Наряду с ним создается файл импорта с расширением .LIB. Последний должен подключатся вызываемой программой, которая запрашивает настоящий DLL. Вызываемая программа требует при этом еще и прототип, чтобы корректно вызывать функции. Он легко выводится из определения функции. В Borland C++ прототип выглядит следующим образом: __declspec(dllexport) double fkt(double w); Сам вызов не отличается от вызова обычной локальной функции. Среда UNIX/Linux Напротив, для среды UNIX и Linux функции не должны быть особо отмечены, чтобы внутри «Shared Library» их можно было вызвать. Для этого требуется установить корректные свойства компилятора1. Следующий пример динамической библиотеки hello просто выводит слово на экран. #include <iostream> using namespace std; void print_hello() { cout << "Hello" << endl; } Листинг 6.7. Программа libhello.cpp Вызов происходит через программу usehello.cpp. Здесь создается прототип функции print_hello(), который в больших программах, конечно, должен был бы подключаться в заголовочном файле. void print_hello(); int main() { print_hello(); } Листинг 6.8. Программа usehello.cpp 1 Майкл К. Джонсон, Эрик В. Троан. Разработка приложений в среде Linux. М.: Вильямс, Диалектика, 2007. 308
Инструменты программирования Секрет заключается в том, как обрабатываются оба модуля. Файл libhello.cpp компилируется сначала в файл объекта. При этом свойство -fPIC заботится о том, чтобы был создан позиционно независимый код. Второй вызов привязывает файл объекта в качестве динамической библиотеки. Центральное свойство здесь -Wl, -soname, libhello.so.0. Оно позволяет создать новые библиотеки с именем libhello.so.0. При этом номер версии сразу же кодируетсясимволом 0. Последние команды связывания позволяют получить доступ к файлам с несколькими именами. Серийный номер задавать необязательно. c++ -fPIC -c libhello.cpp c++ -shared -Wl,-soname,libhello.so.0 -o libhello.so.0.0\ libhello.o -lc ln -sf libhello.so.0.0 libhello.so.0 ln -sf libhello.so.0 libhello.so Листинг 6.9. Команды для создания динамической библиотеки В конце нужно скомпилировать параметр usehello.cpp. c++ usehello.cpp -L. -lhello Листинг 6.10. Компиляция главной программы 6.5. Программа make Когда программа состоит из нескольких частей, быстро достигается точка, в которой кажется логичнее создавать короткий программный файл для компиляции. Вместо этого лучше написать программу создания файла. Это не намного сложнее, но имеет то преимущество, что программа make следит, какие изменения при создании новой версии необходимо предусмотреть. Это значит, что make вызывает компиляцию не чаще, чем это требуется. Проект myprog состоит из файлов исходного кода main.cpp, test. cpp и tools.cpp. Каждый из них имеет заголовочный файл (main.h, test.h и tools.h), которые проект каждый раз подключает сам. При этом файл main.cpp подключает заголовочные файлы каждого из других файлов, а модули подключает main.h. 309
Глава 6 Добавляется файл Makefile, в котором определяется путь компиляции. Программа myprog зависит от файлов main.о, test.о и tools.о. Это определяется в следующем файле таким образом (рис. 6.1). myprog: test.o main.o tools.o test.h haupt.h tools.h test.cpp haupt.cpp tools.cpp test.o haupt.o tools.o meinprog Рис. 6.1. Пример проекта make Программа myprog обозначается как «цель». Далее следующие строки определяют, как создается целевой файл. Файл myprog генерируется, когда компилятор вызывается с файлами объектов в качестве параметров. С помощью свойства -о устанавливается, что результат должен иметь имя myprog. Компилятор отмечает для себя, что здесь он должен только скомпоновать программу, поскольку нет файлов исходного кода. Такие строки должны начинаться с табуляции. Нельзя использовать пробел: myprog: test.o main.o tools.o g++ -o myprog test.o main.o tools.o Если вы вызовите make просто так, то к своему удивлению получите уже полностью скомпилированную программу. На экране появятся следующие строки: gaston> make g++ -c -o test.o test.cpp 310
Инструменты программирования g++ -c -o main.o main.cpp g++ -c -o tools.o tools.cpp g++ -o myprog test.o main.o tools.o gaston> Фактически программа make «знает», как из c-файла сделать o-файл, и поскольку с помощью правил o-файл создать нельзя, make просматривает директорию, есть ли там файлы, из которых можно сгенерировать o-файлы. Если изменить файл test.cpp, то только он будет скомпилирован, а myprog создастся заново. Если изменить test.h, ничего не произойдет. Программа make не знает зависимостей заголовков. Поскольку она следит за изменениями заголовочных файлов, требуются следующие изменения в файле Makefile: test.o : test.cpp test.h main.h tools.o : tools.cpp tools.h main.h main.o : main.cpp main.h test.h tools.h Полные строки команд в данном случае выглядели бы примерно так: test.o : test.cpp test.h main.h g++ -c -o test.o test.cpp Поскольку программа make уже знает требуемые действия, упоминать их не нужно. Программа make является инструментом, с помощью которого можно с минимальными затратами сгенерировать целевой файл из файла исходного кода. Для этого в файле с именем makefile или Makefile определяются связи файлов и устанавливаются вызовы программ, которые генерируют из соответствующих источников целевые файлы. Программа make распознает, если исходный файл программы создан позднее целевого файла, и вызывает программу генерирования, пока целевые файлы не станут новее файлов источника или действие не будет прервано. Makefile имеет вводные параметры в виде следующей структуры (см. врезку).  Цель: зависимости команда генерации 311
Глава 6 Эта основная структура называется правилом. Новое правило должно отделяться от предыдущего пустой строкой. Пустое пространство перед командами генерирования следует реализовать табуляцией. Можно располагать несколько командных строк друг за другом. Все они должны начинаться с табуляции. Командные строки обрабатываются в отдельной оболочке. В некоторых ситуациях это приводит к побочным эффектам, которые следует предусматривать. Пример: try : cd .. ; pwd pwd Результаты обоих вызовов pwd не одинаковы. Обмен с cd .. действует только для текущих строк, ав следующих снова обрабатывается предыдущий каталог: cd .. ; pwd /home/arnold/my/src/unix pwd /home/arnold/my/src/unix/make Следует составлять команды так, чтобы они обрабатывались в одной общей оболочке, для этого нужно написать те же строки и разделить их точкой с запятой. При длинных строках одну строку можно перенести с помощью символа слеш. Символ комментария здесь — октоторп (#). 6.5.1. Макросы в make-файле С помощью макросов можно создать лучше структурированный и более гибкий make-файл. Макросы — это переменные, чьим символьным последовательностям присвоено значение. Они используются со скобками, в которых перед именем указывается знак доллара ($). Посредством связывания имен файлов или свойств код make-файла может стать короче и читаться лучше. В примере файл объекта обрабатывается и дважды перечисляется — один раз в отношении определения myprog, второй раз в вызове компилятора. 312
Инструменты программирования myprog: test.o main.o tools.o g++ -o myprog test.o main.o tools.o Здесь можно определить переменную OBJS, которая обозначает объект. С внедрением переменной OBJS создаются следующие make-файлы: OBJS = test.o main.o tools.o myprog: $(OBJS) g++ -o myprog $(OBJS) Переменные не должны определяться в make-файле. Он может получить доступ к переменным среды, которая установлена для вызванной оболочки. Предопределенный макрос Несколько строк допустимо обрабатывать одним правилом. Например, использовать $(OBJS) в качестве цели. Одна строка может разделяться на несколько строк. В команде генерирования допустимо ссылаться на текущую цель. Для этого существуют предопределенные макросы, показанные в табл. 6.3 на примере файла main.o. Таблица 6.3. Предопределенные make-макросы Макрос Значение $@ $* Имя целевого файла (main.o) Базовое имя целевого файла (main) Правило суффикса Правила суффикса определяют переход от одного расширения файла к другому. Такое правило можно узнать по тому, что в качестве цели друг за другом стоят два расширения. .source.target: Типичный переход — от C-источника к объектам. Источники имеют расширение .с, а объекты — .о. Соответствующее правило суффикса имеет вид: .c.o: g++ -c $< Внутренний макрос $< может использоваться только при суффиксном правиле и обозначать текущую строку. 313
Глава 6 6.5.2. Несколько строк Make-файл может генерировать несколько программ. Эта возможность используется, когда один и тот же исходный код требуется для создания нескольких проектов, которые при этом могут также зависеть друг от друга. Типичным примером служит программа клиент-сервер, которая использует в заголовке одинаковые структуры данных: all: client server client: $(SENDHEADER) $(COMMONOBJS) $(CLTOBJS) ... server: $(SENDHEADER) $(COMMONOBJS) $(SRVOBJS) ... Первая цель — это всегда цель общих make-файлов. В данном случае при вызове make в первую очередь был бы сгенерирован файл all. Поскольку нет никакого связанного действия, будет просто проверяться, выполняется ли зависимость. Соответственно, следующей целью будет построен client, а затем server. Не обязательно, но чаще всего, псевдоцель, которую генерируют все программы обработки make-файлов, называется all. 6.6. Отладка с помощью GNU Debugger Отладчик — это инструмент, которым можно проверить программу во время ее выполнения. Программист запускает отладчик для исследуемой программы. При этом он берет программу за руку, позволяет ей идти и при этом наблюдает. Если программа спотыкается, отладчик демонстрирует, где произошел сбой. Программист может сам установить позицию, в которой произойдет остановка выполнения программы. Отладчик способен при этом показать содержимое переменных и пошагово продолжить выполнение, пока программист не определит, почему программа работает не так, как требуется. В этой главе будет описано, как отладчик GNU Debugger запускается из командной строки. Он доступен как в интегрированной среде разработки Bloodshed, так и в KDevelop. Кроме того, он входит в состав пакета GCC. Отладчик GNU Debugger запускается с исследуемой программой в качестве параметра. Остальные параметры можно добавить в качестве Coredump1. Часто создание таких файлов пропускается, чтобы они не засоряли систему или не пугали неопытного пользователя. По1 Coredump называется файл с именем core, который создает UNIX, если в программе происходит сбой. Этот файл содержит полное описание памяти программы, включая стек и содержимое регистров, и позволяет сделать дальнейшие выводы о причине сбоя. 314
Инструменты программирования скольку этот файл несет в себе неизмеримую пользу для программиста, следует разрешить его создание командой ulimit -c 1000000 в файле .profile. Значение после -с устанавливает его максимальный размер1. Если отладчик запущен, становятся доступными различные его команды. С помощью run можно запустить программу, командой kill снова ее остановить. С помощью команды quit отладчик будет покинут. Чтобы прервать работу программы, в критическом месте следует установить точку останова. Команда break ожидает в качестве параметра номер строки в листинге или имя функции. Номер строки можно получить из листинга программы, который вызывается командой list. Если точка останова достигнута, с помощью команды step или next можно пройти программу шаг за шагом. При этом команда step будет заходить внутрь встречающихся функций, а команда next — нет. Путем ввода команды continue программа запускается до следующей точки останова или до конца. Если требуется окинуть взглядом переменные, самая простая команда для этого — print. Командой watch можно выбрать переменные для наблюдения. Она демонстрирует содержимое, как только оно изменяется. Таблица 6.4. Команды отладчика GNU Debugger Команда Действие quit run kill print переменная watch переменная list break имя_функции break номер_строки clear имя_функции clear номер_строки next step continue Покинуть отладчик Запустить программу Остановка выполнения программы Показать содержимое переменных Наблюдение за переменной Показать код Установить точку останова Установить точку останова Удалить точку останова Удалить точку останова Выполнить следующую строку Выполнить следующую строку с возможностью захода в функцию Завершить выполняющийся процесс Отладчик может взять под свое покровительство выполняющуюся программу. Для этого его нужно запустить и ввести команду at или attach. Процесс будет остановлен и свяжется с отладчиком. attach PID 1 В случае, если вы не настолько хорошо знакомы со средой UNIX или Linux, чтобы выполнить эти настройки, спросите администратора или обзаведитесь книгой по UNIX. 315
Глава 6 Также можно задать PID в качестве второго параметра при вызове отладчика. Далее следует пример1: gaston> ps -ef | grep one arnold 1339 1223 0 11:37 pts/3 00:00:00 one gaston> gdb one 1339 GCC gdb 20010316 Copyright 2001 Free Software Foundation, Inc. /home/arnold/my/src/unix/ipc/1339: No such file or directory. Attaching to program: /home/arnold/src/unix/one, process 1339 Reading symbols from /lib/libc.so.6...done. Loaded symbols for /lib/libc.so.6 0x400f3d64 in read () from /lib/libc.so.6 (gdb) next Single stepping until exit from function read, ... Этот несколько сокращенный протокол демонстрирует, как процесс one протекает под контролем отладчика. Сначала PID обозначается в качестве one и запускается отладчик. Затем появляется сообщение о правах копирования. Далее отладчик пытается открыть файл 1339. Поскольку это не выходит, он пробует получить соответствующий процесс, что ему удается. В IDE точка останова устанавливается с помощью нажатия клавиши мыши слева возле строки программы и точно так же удаляется. Часто появляется большая красная точка. Ниже меню проекта можно найти пункт меню запуска отладчика. Если IDE находится в режиме отладки, появляются соответствующие кнопки для пошагового просмотра программы и пункт меню для вывода содержимого памяти. 1 Команда ps используется в UNIX системах и показывает список процессов. В данном случае программа получит номер процесса one. 316
Глава 7 ДРУГИЕ ЭЛЕМЕНТЫ ЯЗЫКА C++ Итак, основы для программирования на языке C++ заложены. Рассмотрено обращение с переменными и полиморфизм. Также и самоопределение операторов больше не должно быть для вас проблемой. В этой главе мы осветим другие важные элементы языка C++. Обобщенное программирование рассматривает алгоритмы и описывает их без определения типа данных. Пространства имен служат для лучшего разделения больших проектов, а обработка исключений ошибок относится к теме программной гигиены. 7.1. Обобщенное программирование Существуют последовательности команд, которые одинаковы для различных типов данных. Так, определение минимума двух значений абсолютно независимо от используемого типа данных и всегда одинаково до тех пор, пока для типа данных можно употребить оператор сравнения. Также сортировку допустимо проводить одинаково почти для всех типов данных. В конце концов, стеку тоже полностью безразлично, какие данные в нем используются. В программах, рассмотренных в книге до этого момента, тип тесно связан с алгоритмом. То есть каждое действие для другого типа данных требовалось бы переписать заново. С помощью шаблонов язык C++ предлагает инструмент для определения алгоритма в общем виде, без привязки его к конкретному типу данных. Шаблоны используются в случаях, когда одно и то же действие требуется провести для различных типов данных. Такую форму, при которой определение алгоритма не зависит от типа, называют обобщенное программирование. Шаблон строится для алгоритма. Язык C++ «знает» шаблонные функции, для которых программист оставляет открытыми типы параметров, а также шаблоны классов. По- 317
Глава 7 следние определяют, к примеру, структуры данных, которые могут хранить различные типы. 7.1.1. Шаблонные функции Простая форма шаблона — шаблонные функции. Они определяют некоторый алгоритм и оставляют открытым используемый им тип данных. Это функции с такими действиями, которые одинаковы для различных типов данных. Классический пример обобщенной функции — функция min(). Функция выбирает минимальный параметр из двух заданных. Процесс одинаков для всех типов, для которых может использоваться знак сравнения. Перед тем, как будет показано определение шаблона, рассмотрим сначала обычную функцию min(), которая использует переменные типа long в качестве параметров. long min(long a, long b) { return (a < b) ? a : b; } Листинг 7.1. Функция min() для типа long Теперь создадим шаблонную функцию: template <class T> T min(T a, T b) { return (a < b) ? a : b; } Листинг 7.2. Шаблонная функция min() Шаблонная функция начинается с ключевого слова template. В угловых скобках следует шаблонный параметр. Он состоит из ключевого слова class или typename и имени. Оно будет использоваться в шаблоне в качестве представителя оставшегося неопределенным типа. 318
Другие элементы языка C++ Традиционно для этого берется прописная буква Т. Но имя, конечно, можно выбрать самостоятельно. В позиции, где в программе должен использоваться тип, в шаблоне идет имя шаблонного параметра. Функция min() использует оставшийся неопределенным тип сразу три раза. Оба параметра и возвращаемое значение имеют тип Т. Слов typename и class полностью равнозначны. Ключевое слово typename вводится, чтобы обозначить, что здесь также могут использоваться типы, которые не определены в качестве классов. Хотя некоторые компиляторы еще не знают ключевое слово typename. На рис. 7.1 показан синтаксический граф шаблонной функции. Граф определения функции, представленный на рисунке, изображен на рис. 4.1 на стр. 163. Рис. 7.1. Граф синтаксиса шаблонной функции Когда вызывается шаблонная функция, компилятор создает такую интерпретацию, которая подходит к вызывающему ее типу данных. При вызове не задается явно, что должен быть создан объект для типа int. Компилятор определяет тип значения, которое передается в качестве параметра, и делает из этого вывод, для какого типа должна быть сгенерирована функция. В следующем примере тип Т в определении шаблонной функции будет заменен стандартным типом int. int i=15; int j=7; cout << min(i,j) << endl; Листинг 7.3. Команды шаблонной функции min() В следующем примере функция swap() демонстрирует, что можно использовать локальные переменные еще неизвестных типов, и что параметр может быть передан также по ссылке. 319
Глава 7 template <typename T> void swap(T& a, T& b) { T help; help = a; a = b; b = help; } Листинг 7.4. Шаблонная функция swap() Можно использовать несколько разных типов. Для этого они перечисляются в угловых скобках: template <class T1, class T2> int myFunction(T1 t1, T2 &t2) { ... Не всегда из передаваемых параметров четко понятно, для какого типа вызывается шаблон. Проблема возникает тогда, когда шаблон не имеет входных параметров, а только возвращаемое значение. Чтобы установить, с каким типом он должен использоваться в таком случае, при вызове функции целевой тип указывается в угловых скобках между именем функции и скобками параметров. int a = Create<int>(); Если шаблон использует несколько открытых типов, они могут быть заданы перечислением через запятую. Но можно задавать только первый параметр. Следующий вызов myFunction() указывает, что первый параметр должен пониматься в качестве short. int a = myFunction<short>(12, 5.5); Если требуется установить, что первый параметр должен быть интерпретирован в качестве short, а второй как float, вызов функции будет иметь вид: int a = myFunction<short,float>(12, 5.5); С помощью шаблонных параметров можно не только установить типы, но также передать значения или выражения. В следующем примере функция SuperArray() передает, сколько места должно быть выделено под массив. 320
Другие элементы языка C++ template <class T, int max> int SuperArray(T t) { T a[max]; ... } int main() { SuperArray<int, 100>(12); } Шаблоны предназначаются в качестве замены внушающих опасение макросов (см. стр. 325). Поскольку последние функционируют на основе простой замены кода, источники ошибок слабо обозримы. Прежде всего, макросы не могут предоставить защиту типа. Даже если шаблонная функция оставляет тип открытым, она гарантирует, что все одинаковые типы остаются одинаковыми. Если тип Т1 используется в качестве float, при этом вызове все переменные типа Т1 обслуживаются как float. 7.1.2. Шаблоны классов Можно целый класс определить как шаблон. Так решается проблема, которая уже попадалась в классе примера tStack (см. стр. 229). При создании класса должно быть установлено, какими типами управляет стек. В шаблоне класса можно использовать механизм стека для разных типов. Следующая реализация стека базируется на массиве вместо связанного списка. // стек для некоторого класса T template<class T> class Stack { public: // конструктор с инициализацией констант maxStack Stack() : maxStack(256) 321
Глава 7 { s = new T[maxStack]; index=0;} ~Stack() {delete[] s;} bool pop(T *); // изъять bool push(T ); // поместить private: const int maxStack; // константа для размера стека T *s; // сам стек int index; // текущая позиция в массиве стека }; // получить элемент из стека и освободить память template<class T> bool Stack<T>::pop(T *get) { if (index==0 ) return false; // стек пуст? *get = s[--index]; // получить элемент return true; } // записать новый элемент в стек template<class T> bool Stack<T>::push(T set) { if (index>=maxStack) return false; // стек переполнен? s[index++] = set; // записать элемент return true; } int main() // тестирования стека { Stack<int> iStack; // создание стека для целых чисел iStack.push(8); // запись тестовых значений iStack.push(4); iStack.push(2); // прочесть значения из стека и вывести их на экран for (int i=0; i<3; i++) { int a; if (iStack.pop(&a)) 322
Другие элементы языка C++ { cout << a << endl; } } } Листинг 7.5. Шаблон класса стека Элементные функции push() и pop() объявляются в шаблонном классе, а затем определяются отдельно. При этом они, как и сам класс, создаются при объявлении шаблона. Далее между именем класса и двойным двоеточием в угловых скобках указывается имя шаблонного типа. template<class T> bool Stack<T>::push(T set) В функции main() создается стек для переменных типа int. Для этого сначала в качестве типа используется имя класса Stack. Затем в угловых скобках называется тип, для которого будет создан стек. После этого, как и в любом определении переменной, следует ее имя. Stack<int> iStack; При дальнейшей работе программы в стек записывается пара значений и затем считывается из него обратно. При этом незаметно, что стек является шаблоном. Шаблонные классы с параметрами В примере выше мы установили размер стека 256. Между угловыми скобками можно передать это значение в качестве параметра, а в данном примере — в качестве значения max. При создании объекта оно должно определяться в качестве константы. С помощью инициализатора в конструкторе оно будет использоваться для установки значения константы класса maxStack. #include <iostream> using namespace std; template<class T, int max> class Stack { public: 323
Глава 7 Stack() : maxStack(max) // устанавливается при //определении { s = new T[maxStack]; index=0;} ~Stack() {delete[] s;} bool pop(T *); // изъять bool push(T ); // поместить private: const int maxStack; // константа размера стека T *s; // сам стек int index; // текущая позиция в массиве стека }; // получить элементы из стека и освободить память template<class T, int max> bool Stack<T,max>::pop(T *get) { if (index==0 ) return false; // стек пуст? *get = s[--index]; // изъять элемент return true; } // записать новый элемент в стек template<class T, int max> bool Stack<T,max>::push(T set) { if (index>=maxStack) return false; // стек переполнен? s[index++] = set; // записать элемент return true; } int main() // тестирование { Stack<int, 2> iStack; // стек с двумя целыми значениями int a; iStack.push(8); // записать! iStack.push(4); iStack.push(2); // уже тесно! // вывод стека for (int i=0; i<3; i++) 324
Другие элементы языка C++ { if (iStack.pop(&a)) { cout << a << endl; } } } Листинг 7.6. Шаблонный класс с параметрами (templstack2.cpp) В функции main() создается стек из двух элементов. Для тестирования в него записываются три значения. Затем проверяется, можно ли их считать. Упражнение • Напишите шаблонный класс Stack заново так, чтобы он был реализован линейным списком. Для этого можно использовать класс tStack (см стр. 229). Само собой разумеется, что при этом не требуется ограничение размера. Пример решения приведен на стр. 513. 7.1.3. Макропрограммирование с помощью команды #define Обобщенное программирование началось не с языка C++. В языке C также можно с помощью препроцессорных макросов разработать обобщенные функции. По причине совместимости эта возможность осталась и в языке C++, но не следует ее использовать для новых программ. Команда #define является базисом макропрограммирования, поскольку все команды, которые начинаются с символа октоторпа (#), выполняются препроцессором. Он может выполнить только вставку кода, но не имеет никаких знаний по языку C или C++. После команды #define указывается имя макроса. Если у макроса имеются параметры, к имени присоединяются скобки. В них для каждого параметра указывается имя. Параметр не может иметь тип. Поскольку #define выполняется препроцессором и способен только вставить код, об определении типа вообще не может быть и речи. В скобках допустимо задать несколько параметров, которые разделяются запятой. 325
Глава 7 После пробела вставляется код, который препроцессор добавляет на место вызова в исходном коде. Для этого он считывает строку полностью. Если места не хватает, в качестве последнего знака можно указать обратный слеш (\). Тогда препроцессор рассматривает следующую строку в качестве продолжения предыдущей (рис. 7.2). Рис. 7.2. Граф синтаксиса #define Очень часто обобщенную функцию min() можно встретить в виде макроса. Она будет реализована таким или подобным образом: #define min(a,b) a>b?b:a Листинг 7.7. Пример макроса min() Везде, где в программе встречается функция min(), ее вызов будет заменен на команду условия. При этом первый параметр вызова будет вставлен в виде кода там, где в макросе указано значение а. Аналогичное действие будет проведено со вторым параметром. Поскольку препроцессор производит только вставки кода, параметры при передаче не будут оцениваться. При этом легко может произойти, что вследствие вставки они окажутся в контексте, где будут интерпретированы иначе, чем ожидает программист. Далее представлен пример этого. Макрос, с точки зрения программиста на языке C, имеет следующие преимущества: 1. Он может писать код для различных типов. 2. Не требуется код для вызова функции. Макрос выполняется незначительно быстрее функции. В языке C++ оба аргумента действительны, но не могут рассматриваться как весомые аргументы. При написании кода для различных типов используются шаблоны (см. стр. 317). Если требуется сократить время вызова функции, используются встроенные функции (см. стр. 183). 326
Другие элементы языка C++ С точки зрения программиста на языке C++ макросы имеют следующие недостатки: 1. Поскольку макрос является чистой вставкой кода, которую препроцессор проводит перед компиляцией, компилятор не может контролировать, являются ли параметры вызова функции верными. 2. Как только макрос делается достаточно большим, выражения становится трудно прочесть, поскольку невозможно получить промежуточные значения переменных. 3. Можно с легкостью неверно установить приоритеты, поскольку параметры вызова не проверяются. Пример: #define mul(a,b) a*b int a=-5; int c = mul(a+4,2); Листинг 7.8. Коварный макрос Результат в любом случае будет равен не –2, а –3! Причина в том, что выражение а+4 будет пересчитано при вызове –1, поскольку происходит вставка кода: c = a+4*2; Отсюда получится -5+8. 4. Сообщения об ошибках от компилятора указывают не на позиции в коде макроса, которые являются причиной проблемы, а на места кода, где вызывается макрос. 5. За счет ограничения строки код макроса часто очень сжат и из-за этого почти нечитабелен. 7.2. Пространство имен Поскольку не исключено, что два созданных независимо друг от друга исходных кода используют одинаковые имена, в языке C++ введены пространства имен. При этом объявление и определение может быть объединено под одним именем и отделено от другого. 327
Глава 7 7.2.1. Определение пространства имен Чтобы одну часть кода разместить под одним пространством имен, используется ключевое слово namespace, после чего следует имя и затем код в фигурных скобках. Следующий пример мог быть указан в файле util.cpp. namespace util { int counter; void find_begin(string s) { ... } } Листинг 7.9. Код в пространстве имен util Внутри пространства имен для программиста ничего не меняется. Функция find_begin() может прямо обращаться к глобальной переменной counter. Поскольку функции и переменные находятся в одном пространстве имен, прототипы должны определяться точно так же. В соответствующем заголовочном файле util.h указано было бы, к примеру, следующее: namespace util { int counter; void find_begin (string s); int find_end(string s); } Листинг 7.10. Прототип пространства имен util Функция find_end() определена не в файле util.cpp. Если она будет реализована в другом месте, принадлежащее ей пространство имен может быть так же обозначено, как это обычно для элементных функций классов. 328
Другие элементы языка C++ int util::find_end(string s) { ... } Листинг 7.11. Функция для пространства имен util 7.2.2. Доступ Если функции и переменные должны вызываться извне, из пространства имен util, для каждого элемента нужно указать util::. all = util::find_end(filename); all += util::counter + file::counter; Листинг 7.12. Доступ к пространству имен В первой строке функция find_end() вызывается из пространства имен util. Результат будет записан в локальную переменную all. Вторая строка демонстрирует, как получить доступ к одноименной переменной другого пространства имен. 7.2.3. Особые пространства имен Стандартные библиотеки языка C++ используют для всех функций и переменных пространство имен std. При использовании типов, функций или переменных из стандартных библиотек необходимо для каждой из них указать std::. Также с помощью команды using можно пояснить, что требуется использовать пространство имен std, чтобы все определенные в нем переменные и функции объявлять без имени пространства имен. Код имеет вид: using namespace std; Приведенные в примере имена пространств имен util или file не особенно оригинальны. Поскольку пространства имен придуманы именно для того, чтобы одинаковые имена различных модулей использовать без конфликтов, было бы фатально, если бы имя одного пространства имен совпадало с другим. По этой причине следует использовать как 329
Глава 7 можно более выразительное и однозначное имя. Недостаток в том, что это, вероятно, действительно длинное имя придется указывать при вызове. Трудности можно избежать, если перед вызовом объявить псевдоним: namespace file=CustomerConfigurationFile; 7.2.4. Анонимное пространство имен Пространства имен можно применять так, чтобы имена, которые используются для локальных функций, скрыть от других модулей. В языках C и C++ другие модули также могут получить доступ к функции, пока она не определена в качестве статической (см. стр. 303). Вместо того чтобы определять так каждую функцию, можно поместить несколько функций в одно пространство имен. Поскольку данное пространство имен недоступно извне, его можно использовать, не задавая имени. За счет этого все функции внутри одного исходного файла кода могут получить доступ прямо к элементам пространства имен. Функции и переменные в пространстве имен доступны при указании пространства имени. Без него они недоступны, если требуется использование одних и тех же функций и переменных для некоторого набора отдельных методов (функций), их можно заключить в одно пространство имен и не указывать его при каждом обращении. #include "util.h" #include <iostream> using namespace std; namespace { int little_help(int a) { cout << "Little help" << a << endl; } } // namespace void util::service() 330
Другие элементы языка C++ { little_help(12); } Листинг 7.13. Анонимное пространство имен (namespace/util.cpp) Соответствующий заголовочный файл с именем util.h содержит только пространство имен util и доступную в нем функцию service(). namespace util { void service(); } Листинг 7.14. Заголовочный файл (namespace/util.h) Вызов осуществляется, как уже видно, с помощью указания имени util и двойного двоеточия. #include "util.h" int main() { util::service(); } Листинг 7.15. Вызов (namespace/main.cpp) 7.2.5. Граф синтаксиса На рис. 7.3 представлен граф синтаксиса определения пространства имен. Рис. 7.3. Граф синтаксиса namespace 331
Глава 7 7.3. Защита от сбоев при помощи ключевых слов try и catch «Невозможно написать программу, защищенную от дурака, поскольку дураки очень изобретательны». Действительно, бороться с законом Мерфи1 непросто. Программист должен не только разработать алгоритм, но также предусмотреть и исключить все попытки неверного ввода, системные ошибки и невозможность использования на других платформах. Язык C++ предлагает механизм, который позволяет защитить блок программного кода от сбоев. Все возникающие ошибки будут передаваться блоку обработки. Защитный командный блок начинается с ключевого слова try2. Блок обработки ошибок начинается ключевым словом catch3. В блоке команд производится попытка выполнения программы, и блок обработки ошибок ловит сбои, словно сетью. try { int divisor=0; int dividend=1; int quotient = dividend/divisor; // здесь появляется ошибка! } catch (...) { cout << "Проблема обнаружена" << endl; } Листинг 7.16. Деление на 0 в обычном случае приведет к сбою программы. Такая ошибка называется исключительной ситуацией. Если она возникает в блоке try, то сразу обрабатывается в блоке catch4. Конечно, программа может быть защищена от сбоев проверкой делителя перед выполнением деления. Но с помощью команды try обработка ошибки будет 1 2 «Если что-то может пойти не так, то это непременно произойдет». В пер. с англ. — «пытаться». Прим. ред. 3 В пер. с англ. — «ловить». Прим. ред. 4 Во многих версиях компилятора GNU Compilier (в любом случае для gcc-2.9.2. и gcc-2.95.2, и вероятно для более новых) деление на 0 и исключительные ситуации для чисел с плавающей запятой не обрабатываются командой catch(…). 332
Другие элементы языка C++ сознательно изъята из нормального выполнения программы. Такое разделение функционального блока и блока обработки ошибок позволяет создавать программы, код которых легко читается и за счет этого более предсказуем. Следующее преимущество состоит в том, что не каждый шаг программы должен отдельно проверяться на возможность возникновения ошибки. Пример чудовищной перегрузки проверками ошибочных ситуаций представлен в листинге на стр. 390. Три точки после команды catch не означают, что у автора кончились мысли, а говорят об общей обработке ошибок. В скобках команды catch указывается тип исключительной ситуации, которая должна обрабатываться этим блоком. Можно создавать различные блоки catch для различных типов исключительных ситуаций. Три точки являются сокращением для всех типов, при этом можно обработать все не определенные причины ошибок. Этот общий блок catch должен всегда указываться самым последним. Обработка ошибок влияет не только на сам блок, но и на все функции, вызываемые в нем. За счет этого блок может располагаться в центральном месте программы. Кроме того, такие блоки можно вставлять в любое место программного кода. Как только код вызывает ошибку, используется следующий, подходящий к типу исключительной ситуации, оператор catch. Чтобы найти этот следующий блок, последовательность вызовов функций будет пройдена в обратном порядке. Поэтому всегда используется тот блок обработки ошибок, который указан следующим после строки с ошибкой. 7.3.1. Создание собственных исключительных ситуаций Не все ошибки сразу приводят к исключительной ситуации. Но только она может быть обработана с помощью команды catch. Поэтому очень интересно определять собственные исключительные ситуации. Команда throw определяет такое исключение. Она сразу прерывает работу программы и переходит прямо к соответствующему блоку обработки. #include <iostream> using namespace std; void do_smth(int Problem) { 333
Глава 7 if (Problem>0) { throw 0; } } int main() { try { do_smth(1); } catch(...) { cout << "Возникла ошибка!" << endl; } } Листинг 7.17. Исключение, сгенерированное throw Команда throw в функции do_smth() вызывает исключение, если значение параметра Problem больше 0. Обработка происходит сразу же в блоке catch. Иногда недостаточно просто вызвать программу обработки. Чтобы обработать ошибку, требуется информация, такая как, например, имя файла, который послужил причиной ошибки, или ее номер. Эта информация может быть передана в блок catch с помощью аргумента команды throw. В первом примере передается целое число. Блок catch теперь получает параметр в качестве функции, в данном случае типа int. #include <iostream> using namespace std; void do_smth(int Problem) { if (Problem>0) { 334
Другие элементы языка C++ throw 5; } } int main() { try { do_smth(1); } catch(int a) { cout << "Исключение: " << a << endl; } } Листинг 7.18. Команда throw передает номер ошибки Если блок catch получает параметр типа int, он обрабатывает только исключения, которые с помощью команды throw вызываются с помощью числа как аргумента. Чтобы обработать другие параметры, определяется другой блок catch с другими параметрами. Чтобы обработать все оставшиеся исключения, в самом конце можно разместить еще один блок catch с тремя точками в качестве параметра. Точно так же, как и при перегрузке функций (см. стр. 183) подходящий блок catch выбирается по подходящим параметрам. Вообще, catch всегда имеет параметр. Автоматически подходящий тип, как в функциях, здесь не срабатывает. Если, например, использовать throw с аргументом 1.2, подходящий catch должен иметь параметр double, а не float, потому что константы с плавающей запятой обрабатываются компилятором как double. Следующий пример демонстрирует несколько блоков catch для различных типов. // программа демонстрации различных блоков catch #include <iostream> using namespace std; // do_smth использует различные типы, в // зависимости от значения параметра ошибки. 335
Глава 7 void do_smth(int Problem) { switch (Problem) { case 0: throw 5; break; // передает int case 1: throw (char *)"test.dat"; break; // передает char * case 2: throw 2.1; break; // передает double case 3: throw 'c'; break; // передает char } } // тестовая программа int main() { // задать номер проблемы int number; cout << "Введите число от 0 до 3:" << endl; cin >> number; // блок try ловит значение do_smth try { do_smth(number); } catch(int i) // обработка для int { cout << "Integer " << i << endl; } catch(char *s) // обработка для char* { cout << "Символьная последовательность " << s << endl; } catch(double f) // обработка для double { cout << "Число с плавающей запятой " << f << endl; 336
Другие элементы языка C++ } catch(...) // обработка для остальных исключений { cout << "Общий случай " << endl; } } Листинг 7.19. Несколько блоков catch (throwtyp.cpp) Если запустить программу, можно, выбирая числа от 0 до 3, определить, какое исключение будет обработано. При 1 вам предложат символьную последовательность, которая затем обработается в блоке catch в качестве параметра s. При 3 будет передан char, для которого не предусмотрен catch. Поэтому для данного случая будет применен общий catch. 7.3.2. Разработка классов ошибок Можно создавать также собственные классы, которые допустимо использовать в качестве параметра для catch. Сами по себе пустые классы ошибок могут быть очень полезны за счет правильно выбранного для них имени. #include <iostream> class no_data_already { }; void do_smth(int Problem) { ... throw no_data_already(); ... } int main() { try 337
Глава 7 { do_smth(0); } catch(no_data_already& ) { std::cout << "Информация отсутствует " << std::endl; } } Листинг 7.20. Собственный тип ошибки Класс no_data_already ничего не содержит. Он не передает в обработчик ошибок ничего, кроме собственного имени. Но уже по нему можно заключить, что здесь произошла ошибка, причиной которой послужило отсутствие данных в потоке. Теперь эта ситуация будет обработана отдельно от остальных ошибок. Данное имя поясняет каждому читателю кода, что делает throw и какое исключение ловит catch. Если в программе должен использоваться только один источник информации, имени класса может быть более чем достаточно. При этом программа обработки не нуждается в дополнительной информации. На основе этого легко добавить в класс ошибок также информацию о причине ошибки. Такие данные может использовать блок обработки. Также в классах ошибок возможно построить функции для обработки исключений, которые затем будут вызываться в блоке catch. В следующем примере целое число будет добавлено в объект ошибки и затем показано с помощью функции show_error(). #include <iostream> using namespace std; class no_data_already { public: no_data_already(int a) { nr = a; } void show_error() { cout << nr << endl; } private: int nr; 338
Другие элементы языка C++ }; void do_smth(int Problem) { if (Problem==0) { throw no_data_already(8); } } int main() { try { do_smth(0); } catch(no_data_already& error) { error.show_error(); } } Листинг 7.21. Активный класс ошибки Чтобы обработать различные ситуации возникновения ошибок, предлагается написать единый базовый класс для всех ошибок. В нем реализуется то, что является общим для всех подобных ситуаций. Тут будет очень полезно свойство наследования. Из этого также следует, что нет необходимости писать для каждой ошибки отдельный блок catch, если использовать полиморфизм для вызова соответствующей функции обработки ошибки определенного класса. #include <iostream> using namespace std; class meaCulpa // базовый класс ошибок { public: 339
Глава 7 virtual void show_error() = 0; }; class any_data_already : public meaCulpa { public: no_data_already(int a) { nr = a; } void show_error() { cout << nr << endl; } private: int nr; }; class missing_source : public meaCulpa { public: missing_source() {} void show_error() { cout << "Источник данных отсутствует " << endl; } }; void do_smth(int Problem) throw (no_data_already, missing_source) { if (Problem==0) { throw no_data_already(8); } if (Problem==1) { throw missing_source(); } } int main() { try { do_smth(0); 340
Другие элементы языка C++ } catch(meaCulpa& error) { error.show_error(); } } Листинг 7.22. Полиморфизм класса ошибки (tryclass.cpp) Как обычно в полиморфизме, для вызова виртуальной функции используется указатель на передаваемый объект. Объект вызывает через собственную таблицу виртуальных методов соответствующую функцию show_error() (см. стр. 269). Такой же подход предлагают стандартные библиотеки. Используются классы, которые базируются на классе exeption. Далее описано, как можно создать на базе него производный класс. В листинге 7.22 показано, что к функции do_smth() добавляется код, поясняющий, какое исключение обрабатывает функция. Эта информация должна указывать на то, что функция содержит команду throw. В скобках перечисляется, какие исключения будут обработаны. Компилятор не проверяет, действительно ли они содержатся. Он не заметит ни одну пропущенную команду throw. Вообще компилятор будет «недоволен», если функция попытается обработать исключение, которое не было определено при объявлении. Из этого следует, что функция, чье объявление throw не содержит в скобках тип, не должна выполнять эту команду. 7.3.3. Исключения стандартных библиотек Стандартные библиотеки языка C++ реализуют обработку исключений. При этом также определяются классы ошибок, которые строятся на базовом классе. Он называется exeption. Благодаря ему любое исключение стандартной библиотеки может быть обработано с помощью catch с параметром exeption&. Собственные производные от класса exeption Имеет смысл создавать собственные исключения на основе класса exeption. Для этого реализуется виртуальная функция what(). Она возвращает указатель на С-строку, которая хранит информацию об 341
Глава 7 ошибке в виде открытого кода. Здесь представлено определение класса exeption. class exception { public: exception() throw(); exception(const exception&) throw(); exception& operator=(const exception&) throw(); virtual ~exception() throw(); virtual const char * what() const throw(); }; Чтобы создать подобный производный класс, сначала должен быть подключен заголовочный файл exeption, который определяет класс. Следующий пример демонстрирует класс meaCulpa, который является производным от класса exeption. Как особые исключения наследуются no_data_already, missing_source. #include <iostream> #include <exception> #include <string> #include <sstream> using namespace std; // мой собственный класс, производный от exception class meaCulpa : public exception { public: meaCulpa(string s) {this->s = s;} virtual ~meaCulpa() throw() {} virtual const char * what() const throw() {return s.c_str();} private: 342
Другие элементы языка C++ string s; }; // особая ошибка, когда отсутствуют данные class no_data_already : public meaCulpa { public: no_data_already(int a) : meaCulpa(" ") {nr = a;} virtual ~ no_data_already() throw() {} virtual const char * what() const throw(); private: int nr; string s; }; // функция what() будет переписана для собственного сообщения об ошибке const char * no_data_already::what() const throw() { ostringstream getNr; getNr << "Данные отсутствуют. Номер ошибки: " << nr; return getNr.str().c_str(); } // Следующий тип ошибки производный от meaCulpa class missing_source : public meaCulpa { public: missing_source() : meaCulpa("Источник отсутствует") {} }; // функция do_smth() симулирует оба типа ошибок // в зависимости от параметра void do_smth(int Problem) throw (no_data_already, missing_ source) { if (Problem==0) { 343
Глава 7 throw no_data_already(8); } if (Problem==1) { throw missing_source(); } } int main() { // ввести номер ошибки int choice; cout << "Ввести цифру от 0 до 3:" << endl; cin >> choice; // блок try ловит исключение в do_smth() try { do_smth(choice); } // идентифицируются только собственные ошибки catch(meaCulpa& error) { cout << error.what() << endl; } } Листинг 7.23. Классы ошибок, производные от класса exeption (exeption.cpp) Класс ошибки no_data_already ожидает в конструкторе целое число. Для класса meaCulpa конструктор вызывается со строкой в качестве параметра. Функция what() реализуется заново классом no_data_ already. Номер ошибки в сопровождении короткого кода будет преобразован в символьную последовательность и возвращен в качестве строки. Класс ошибки missing_source передает в конструктор класса meaCulpa сообщение, за счет чего не требуется заново реализовывать функцию what(). 344
Другие элементы языка C++ Обзор стандартных классов ошибок В стандартных библиотеках языка C++ определены следующие классы, которые являются производными от класса exeption: • runtime_error Ошибка рабочего времени программы. Производные этого класса: • ios_base::failure Класс ошибки класса потока. Доступен не для всех компиляторов. • range_error Переход за границы области. Обрабатывается функцией at(). • overflow_error Переполнение при расчетах. • underflow_error Потеря значимости при расчетах. • logic_error Логическая ошибка. Производные классы: • domain_error Ошибка области. • invalid_argument Недопустимый аргумент. • length_error Превышен максимально допустимый размер. • out_of_range Доступ с недопустимым индексом. Язык C++ имеет также собственные стандартные исключения: • bad_alloc Возникает, когда new не может выделить память. • bad_cast Связана с dynamic_cast. • bad_exeption Если возникает ошибка при обработке ошибок. • bad_typeid Связан с typeid. 345
Глава 7 Стандартные классы ошибок могут быть использованы в собственных функциях в качестве аргументов команды throw. Тогда имеет смысл передавать конструктору собственное сообщение об ошибке: #include <stdexcept> int main() { try { throw range_error("Что-то здесь тесновато!"); } catch(range_error& e) { cout << e.what() << endl; } } Исключения стандартного класса fstream Обработка исключений необходима в момент доступа к данным, поскольку существует бесчисленное количество причин, почему доступ не срабатывает1. Класс fstream также готов к обработке исключительных ситуаций. Выполнение обработки ошибок должно быть сначала разрешено для всех объектов потоков данных. Для этого вызывается функция exceptions(). В качестве параметров можно передать одну или несколько констант: ios::failbit, ios::badbit или ios::eofbit. В последнем случае они отделяются друг от друга вертикальной чертой. Параметром для catch является константа ios::failure&. Типичный программный интерфейс выглядит при этом следующим образом: fstream f; try { 1 Согласен, несколько необычно, что обсуждение библиотек данных осуществляется после обработки ошибок. Тем не менее это такая же ситуация, как с курицей и яйцом. Вы можете сразу перейти к стр. 381, если не хотите ожидать. 346
Другие элементы языка C++ f.exceptions(ios::failbit|ios::badbit); f.open("test.dat", ios::in); f << "Этот текст записывается в файл" << endl; f.close(); } catch(ios::failure&) { if (f.fail()) cout << "fail" << endl; if (f.bad()) cout << "bad" << endl; } Листинг 7.24. Класс fstream с исключениями (fstreamexeption.cpp) К сожалению, некоторые старые компиляторы не знакомы с ios::failure. Как видно из листинга, не так уж драматично, когда класс ios::failure не получает информацию из файла. Объект fstream в любом случае должен быть доступен для прямого доступа блоку catch. При этом точно так же может использоваться класс exeption. Но можно исходить из того, что все компиляторы должны поддерживать данное исключение. 7.4. Низкоуровневое программирование Одной из целей разработки языка C++ было расширить язык C так, чтобы можно было поддерживать новые концепции программирования и при этом не потерять производительность и эффективность языка C. Одна из его особенных возможностей — это обращение с низкоуровневыми структурами. Все-таки язык C был создан именно для того, чтобы разрабатывать на нем операционную систему UNIX. Эти возможности сохранились в языке C++. 7.4.1. Битовые операторы Бит имеет только два состояния: 1 или 0, включен или выключен. Так можно установить или сбросить бит. Если два бита связаны между собой, то можно использовать булевы операции И и ИЛИ. В отличие от булевых значений, биты соединяются в байты. Поэтому ИЛИ должно работать для каждого бита, байта или вообще целой переменной long. 347
Глава 7 Поэтому оператор логического И (&&) отличается от оператора бинарного И (&). То, что несколько битов объединены в длинное слово, видно по аппаратной среде, если получить доступ к контроллеру или его регистрам. Так можно управлять периферийными устройствами или опрашивать их состояние. В некоторых API несколько флагов1 также объединяются между собой в переменную, чтобы они могли получить только один параметр. В обоих случаях необходимавозможность получения доступа к конкретному биту конкретного байта. Битовые операции имеют такие же имена и функции, как и булевы операции, которые используются в условиях и циклах. Битовые операции должны иметь доступ к одному биту и поэтому в языках C и C++ используется несколько иной оператор. В табл. 7.1 представлены такие операторы. Таблица 7.1. Битовые операторы Оператор Функция Действие & | ~ ^ AND OR NOT XOR 1 если все операнды равны 1 1 если хотя бы один операнд равен 1 Инверсия каждого бита 1 если только один из двух операндов равен 1 Если два значения связаны битовым оператором, речь идет о двоичной системе счисления. Чтобы понять результат, рассмотрим участвующие в вычислении значения в двоичном виде. Бинарные операторы можно применять только для целых значений. В следующем примере используются переменные типа unsigned char из восьми бит. Функция AND может быть использована для того, чтобы отфильтровать определенные биты в байте. При этом говорят о так называемой маске. Поскольку функция AND только тогда имеет результат 1, когда оба операнда равны 1, область первого будет фильтроваться так, как это задано вторым операндом. Пример: 01101110 AND 00111100 ------------00101100 1 348 Флаги — это параметры, которые могут иметь только два состояния.
Другие элементы языка C++ Здесь маска накладывается на четыре центральных бита. Таким же образом можно с помощью функции AND удалить один бит в байте. 00101101 AND 11110111 ------------00100101 Здесь удаляется четвертый справа бит первого операнда. Для этого маска должна состоять из единиц, и только четвертый бит справа должен быть равен 0. Можно также использовать объединение AND, если требуется опросить состояние одного бита. Когда нужно проверить пятый бит справа, необходимо задать маску 24, то есть 16. В программе это выглядит так: if (RegisterContent & 16) Если пятый бит равен 0, тогда общее выражение также равно 0. Если пятый бит установлен в 1, то результатом наложения маски со значением 16 будет 16. Поскольку это не 0, значит бит равен 1. Когда бит должен быть переключен, используется операция XOR. Ее результатом будет 1, если только один из двух операндов равен 1. Когда требуется инвертировать один бит, в маске он должен быть 1, а остальные биты равняться 0. 00101101 XOR 00001000 ------------00100101 Если определенный бит должен быть установлен, используется функция OR. Здесь также во втором операнде устанавливается в 1 только тот бит, который требуется установить в первом операнде. Все остальные значения остаются неизменными за счет нулей. 00100101 OR 00001000 ------------00101101 349
Глава 7 Оператор OR используется, например, для операций с файлами. При открытии файла тип доступа связывается с помощью символа вертикальной черты (|), то есть двоичным ИЛИ. fstream f(..., ios::out|ios::binary|ios::in); Это означает, что константы ios::out, ios::binary и ios::in кодируются двоичным кодом. Кодировки могут быть следующими (табл. 7.2). Таблица 7.2. Примеры кодирования Константа Десятичная Двоичная Значение ios::in ios::out ios::binary ios::trunc ios::app 1 2 4 8 16 00000001 00000010 00000100 00001000 00010000 Открыть файл для чтения Открыть файл для записи Файл не является кодовым Файл будет отчищен при открытии Новые данные записываются в конец файла Если требуется открыть бинарный файл для чтения и записи, и чтобы новые данные записывались в конеце, константы следует связать с помощью операции ИЛИ и передать это значение в качестве параметра: ios::in|ios::out|ios::binary|ios::app С помощью ИЛИ-объединения создается число, которое отражает, какие свойства включены, а какие нет. ios::in 00000001 ios::out 00000010 ios::binary 00000100 ios::app 00010000 ----------------------OR: 00010111 В байте можно передать восемь свойств. Если использовать переменную типа long, возможных вариантов может быть до 32. 7.4.2. Операторы сдвига Двойные знаки «меньше» или «больше» используются в языке C++ в основном для связи с операторами ввода и вывода. В языке C они использовались для побитного сдвига целочисленных значений. 350
Другие элементы языка C++ Следующий пример демонстрирует, как в программе определяются операторы сдвига. value = 24 >> 1; Операция действует так, что двоичный код числа 24 сдвигается вправо на один разряд. Результат такого действия равен 12. Для этого рассмотрим сначала 24 в двоичном представлении: 2410 = 25 + 24 = 000110002 Если сместить 00011000 вправо на один бит, получится значение 00001100. Это десятичное представление числа 12: 000011002 = 24 + 23 = 810 + 410 = 1210 Число после оператора сдвига указывает, на сколько бит оно должно быть смещено. Как было показано выше, свободные биты слева заполняются нулями. Оператор << сдвигает биты влево, во всем остальном работает точно также как и оператор >>. Здесь также свободные биты замещаются нулями. value = 12 << 2; В данном случае результатом будет 48. Здесь число сдвигается на две позиции. Сдвиг на один бит дает число 24. Следующий — удваивает значение. Сдвиг на одну позицию влево равносилен умножению числа на 2, но реализуется процессором значительно эффективнее. Поэтому часто умножение и деление на степень двойки в системах, где важна скорость выполнения программы, реализуется с помощью операторов сдвига. Некоторые компиляторы вводят такую оптимизацию сами, так что скорость выполнения не изменяется. 7.4.3. Доступ по аппаратным адресам До этого я избегал рассматривать содержимое указателей. В низкоуровневом программировании значение, которое содержит указатель, особенно интересно. Например, регистр статуса первого интерфейса принтера для компьютера с процессором Intel расположен по адресу 0х379. Для обычных приложений он недоступен. Но если вам однажды потребуется написать драйвер, то по этому адресу вы можете получить 351
Глава 7 информацию, готов ли принтер принимать следующее задание на печать. Доступ к этому адресу прост: объявляется переменная указатель и ей напрямую присваивается этот адрес. unsigned char *PrinterStatusRegister; unsigned char Status; PrinterStatusRegister = 0x379; Status = *PrinterStatusRegister; if (Status & 0%10000000) { // принтер свободен } else { // принтер занят } Листинг 7.25. Доступ к состоянию принтера В итоге из переменной Status можно прочесть, какое содержимое имеет регистр состояния принтера LPT1. C помощью маски для старших битов можно, например, установить, какое состояние имеет интерфейс, когда принтер занят. 7.4.4. Битовые структуры Чтобы получить доступ к данным в двоичном коде, можно определить структуру, чьи элементы имеют двоичное расширение. Такое кодирование часто используется в области контроллеров или сообщений состояния, которое помещено в регистры. struct tRegister { unsigned int Target:3; unsigned int reserved1:1; 352
Другие элементы языка C++ unsigned int busy:1; unsigned int conditionmet:1; unsigned int check:1; unsigned int reserved2:1; } StatusByte; Этот байт статуса моделирует регистр контроллера. В трех старших битах он содержит цель. Затем следует резервный бит. Затем — биты «занят», «условие» и «проверка». Последний бит также резервный. Фактически для данной структуры требуется только один байт. Но цель не в том, чтобы экономить память, а в том, чтобы продемонстрировать структуру регистра. К составным частям битовой структуры можно получить доступ как к элементам обычной: StatusByte.Target = 5;
Глава 8 БИБЛИОТЕКИ «Во время пожара в единственной библиотеке на Мельмаке сгорели обе книги. И ни одну из них так и не перерисовали полностью» (Сериал «Альф»). В языке C++ имеются библиотеки, которыми комплектуется компилятор. Они включают в себя классические библиотеки, унаследованные от языка C, которые по причине совместимости все еще используются. Особенными для языка C++ являются библиотеки вывода, файлов, строк и так называемые STL-библиотеки. Если решение задачи уже есть в библиотеке, следует использовать его и не разрабатывать заново. Даже самоуверенные программисты должны признать, что библиотеки разработчиков компилятора значительно тщательнее проверены потребителями, чем код, написанный отдельным программистом. Использование библиотек происходит в два шага. Сначала к исходному коду программы следует подключить заголовочный файл с помощью препроцессорной команды #include. В нем расположены прототипы для библиотек. Вторым шагом компоновщик определяет сами файлы библиотек. Так требуемые части добавляются к программе. 8.1. Символьные последовательности и строки В работе программиста то и дело встречаются последовательности букв в форме кодов и названий. Учитывая высокую практическую актуальность, интеграция строк в язык программирования выглядела несколько хаотично. C++ сначала унаследовал символьные последовательности в виде массивов элементов типа char. Класс string, каким мы его знаем сегодня, был введен годами позже. К тому времени другие производители разработали классы строк. 354
Библиотеки За счет внедрения интернациональных символьных последовательностей распространенная практика хранить одну букву в одном байте стала невозможной, поскольку азиатские языки имеют гораздо больше, чем 256 символов. По этой причине во многих устройствах был установлен стандарт UNICODE с 16 битами для символа. С другой стороны, европейской фирме не требуется выделять в два раза больше памяти для каждого кода в компьютерной системе, если контакты с Китаем крайне редки и никто из служащих, пользующихся компьютером, не владеет китайским. Чтобы каждая буква не занимала 16 бит, но, тем не менее, сохранялась возможность использовать азиатские символы, в системе кодирования UTF-8 обычные буквы занимают 1 байт. Для особых символов используется несколько байт. К ним помимо азиатских букв относятся нетипичные символы европейских языков (такие, как немецкий умлаут). Для них требуется теперь на байт больше, чем ранее, и могут внезапно возникнуть проблемы при использовании приложений электронной почты на портативных устройствах или со старым программным обеспечением, где они кодируются одним байтом. 8.1.1. Стандартный класс string Конечно, в языке C++ можно и дальше использовать классические символьные последовательности языка C. С одной стороны, нельзя не заметить, что как только кодовая константа устанавливается в кавычки, она будет автоматически преобразована компилятором в массив символов типа char с заключительным нулем. С другой стороны, класс string определен для языка C++ однозначно в качестве стандарта. Так какому из стандартов следовать? Поскольку многие программисты, использующие язык C++, писали ранее на языке C, они больше доверяют символьным последовательностям, и те встречаются во многих программах на языке C++. Но долгий переход к другому языку не должен выступать в качестве решающего аргумента. Рассмотрим внимательнее преимущества и недостатки: • Просто и понятно Массив для каждого программиста является базовой технологией, которая ему понятна. Ясно, где находится код и сколько места он занимает. • Скорость Скорость перебора с инкрементируемым указателем С-строки сложно превзойти. Остальным способам доступа до нее далеко. 355
Глава 8 • Никаких внешних ссылок Если использовать массив char в качестве составной части класса, то вся символьная строка располагается в объекте и не требует конструкторов копирования. При использовании указателя на char, как и для многих классов string, в любом случае потребуется конструктор. • Негибкие длины Длина массива устанавливается константой при объявлении. Ее последующее изменение невозможно. Если символьная последовательность длиннее, чем предполагалось, программа должна это распознать, выделить новую память и скопировать туда символьную строку. • Неэффективное использование памяти По причине негибкости длины программист делает массив настолько большим, чтобы места хватило в даже самом худшем случае. Это ведет к тому, что массив получается гораздо больше, чем это на самом деле требуется. • Опасность выхода за пределы Границы массива не пересекаются. Если указатель выходит за пределы символьной последовательности, когда, например, был забыт завершающий ноль, в худшем случае будет изменено содержимое других переменных. • Не подготовлен к стандарту UNICODE Тип char кодирует символ в одном байте. При переходе на стандарт UNICODE в худшем случае потребуется корректировать каждую строку и перерабатывать код. Перечисленные выше недостатки устранены в классе string. Разумеется, не без потерь. Так, скорость перебора классической символьной последовательности с указателем недоступна в классе. Однако простота копирования оператором присваивания, легкость добавления информации в строку с помощью оператора плюс и ряд дополнительных функций делают переход на строки гораздо более приятным. Создание и инициализация переменной типа string Чтобы объявить объект типа string, требуется сначала подключить файл string. Он имеет необычное для большинства заголовочных фай- 356
Библиотеки лов стандартных библиотек языка C++ расширение .h. Класс string находится в пространстве имен std. Чтобы при объявлении строки не требовалось каждый раз писать std::string, следует определить пространство имен std в самом начале кода с помощью команды using: #include <string> using namespace std; string myName; Для создания объекта класса string не нужно задавать количество резервируемых символов. Если требуется больше места, объект сам позаботится о том, чтобы получить необходимую дополнительную память. Класс string доступен многим конструкторам. По этой причине символьные последовательности можно объявить при определении со стандартными значениями, заданными по умолчанию. Если символьная строка по какой-то причине состоит из повторяющегося символа, конструктору сначала передается количество символов, а затем он сам. string str(80, '-'); Листинг 8.1. Строка из 80 символов тире Такой вариант весьма полезен, если требуется вывести, например, линию разрыва: #include <string> #include <iostream> using namespace std; int main() { cout << string(40, '=') << endl; } Листинг 8.2. Вывод 40 символов равенства 357
Глава 8 Совместимость с С-строками Объект типа string во многих случаях совместим с массивом char. Доступ к одному из элементов строки реализуется, как и для С-строк, с помощью квадратных скобок. Можно инициализировать строки со значениями С-строк. С помощью члена функции c_str() можно установить указатель на char, который ссылается на код объекта типа string. Используйте такой указатель только для считывания кода. Никогда не пытайтесь подобным образом изменить код объекта string! Следует учитывать, что указатель вообще «пагубная» вещь. Как только строка изменится, может оказаться, что указатель ссылается на пустоту. Так что пользуйтесь им сразу после его создания! #include <string> std::string str = "Инициализация С-строки"; char *cstringPointer = str.c_str(); Присваивание Строка может быть скопирована в другую переменную типа string обычным присваиванием. Не требуется никакой специальной функции strcpy()или циклов с перебором строки поэлементно, не нужно думать о том, вставлен ли в конец строки завершающий ноль, и контролировать, хватит ли для копии места в памяти. Можно даже присвоить строке С-строку без дополнительного конвертирования. Вместо оператора присваивания можно использовать элементную функцию assign(). В качестве параметра она получает значение, которое должно быть присвоено строке. С помощью функции swap() можно произвести обмен содержимым двух переменных типа string. Параметром является ссылка на переменную, с которой должен производиться обмен. Склеивание строк Строки можно прикрепить друг к другу, если указать между ними знак плюс. #include <string> using namespace std; 358
Библиотеки ... string myName = "new"; char oldName[25] = "old"; string newString; ... newString = myName; // "new" newString = oldName; // "old" newString = myName + oldName; // "newold" newString += oldName; // "newoldold" Вместо оператора += можно использовать элементную функцию append(). #include <string> #include <iostream> using namespace std; int main() { string fort = "abc"; string back = "def"; vorn.append(back); // как: fort += back; "abcdef" } Доступ к элементу К отдельной букве символьной последовательности можно получить доступ с помощью индексного оператора, то есть квадратных скобок, точно так же, как при работе с классическими С-строками. В качестве альтернативы квадратным скобкам для считывания одной буквы можно использовать функцию at(). #include <string> #include <iostream> using namespace std; int main() { string str = "Hand"; 359
Глава 8 str[1] = 'e'; str[2] = 'l'; str.at(3) = 'p'; cout << str << endl; // "Help" } Использование указателя на char по известной причине невозможно. В качестве альтернативы, класс string предлагает некоторый тип указателя, который именуется итератором. Тип итератора несет в себе класс string. Он имеет вид string::iterator. В большинстве случаев итератор устанавливается на начало строки. Для этого класс string предлагает функцию begin(). Она не имеет параметров и возвращает итератор. Он может быть инкрементирован в качестве обычного указателя. Чтобы определить конец строки, имеется функция end(), которая возвращает итератор, указывающий на последнюю действительную букву строки. Как только итератор становится возвращаемым значением функции end(), он переходит в конец строки. С помощью итератора, как и с помощью указателя, можно получить доступ к любому элементу строки, если указать перед его именем символ астериска (*). В следующем цикле строка перебирается с помощью итератора и буква за буквой выводится на экран. #include <iostream> #include <string> using namespace std; int main() { string s = "Test"; string::iterator i; for (i=s.begin(); i!=s.end(); i++) { cout << *i ; } cout << endl; } Листинг 8.3. Считывание строки с помощью итератора (stringit.cpp) 360
Библиотеки Срок службы итератора ограничен — как только строка изменяется в размере, он может потерять свою актуальность. По этой причине следует при каждом пробеге обновлять итератор с помощью функции begin(). Вместе с рассмотренным итератором существует итератор реверса, который имеет иной тип, отличный от типа обычного итератора. Он определен в классе string как string::reverse_iterator и просматривает строку в обратном направлении, несмотря на то, что его значение при этом инкрементируется оператором ++. Чтобы установить такой итератор на конец строки, используется элементная функция rbegin(). Конец определяется функцией rend(). В следующей программе строка выводится в обратном порядке. #include <iostream> #include <string> using namespace std; int main() { string s = "Test"; string::reverse_iterator i; for (i=s.rbegin(); i!=s.rend(); i++) { cout << *i ; } cout << endl; } Листинг 8.4. Пробег в обратном направлении с помощью итератора (stringrev.cpp) Главное отличие между указателем и итератором состоит в том, что нельзя манипулировать строкой с помощью указателя, как уже это делалось для массива типа char. Объект класса string состоит не только из символов. Если использовать указатель на строку, он станет ссылаться не на первую букву, а на объект, который ею владеет. Конечно, можно вернуть значение начала строки с помощью функции c_str(), как уже было показано. Но не следует пытаться с помощью этого указателя изменить внутреннюю символьную последовательность объекта 361
Глава 8 string, поскольку может быть так, что c_str() возвращает указатель на копию.  При доступе к переменной типа string следует избегать указателей на тип char. Управление строками Класс string имеет различные функции для изменения символьных последовательностей. Функция clear() удаляет все символы в строке. void clear(); Пример: string str = "Встряхнуть, не перемешивать"; str.clear(); Переменная str в итоге будет содержать пустую символьную последовательность. Функция insert() вставляет строку s в позицию р. string& insert(size_type p, string& s); Пример: string str = "Встряхнуть, не перемешивать"; str.insert(11, "но "); Переменная str будет хранить символьную строку «Встряхнуть, но не перемешивать». Функция erase() удаляет n символов с позиции р. string& erase(size_type p, size_type n); Пример: string str = "Встряхнуть, не перемешивать"; str.erase(11, 4); Переменная str будет содержать строку «Встряхнуть, перемешивать». Функция replace() заменяет, начиная с позиции р, количество символов n в строке s. Изменяемая строка дополнительно возвращается в качестве ссылки. 362
Библиотеки string& replace(size_type p, size_type n, string& s); Пример: string str = "Встряхнуть, не перемешивать"; string sub = str.replace(11, 2, "но"); После выполнения операции, переменные str и sub получают значение «Встряхнуть, но перемешивать». Функция substr() возвращает часть строки объекта string, которая начинается с позиции р и имеет длину из n знаков. Оригинал при этом не меняется. string &substr(size_type p, size_type n=npos) Пример: string str = "Встряхнуть, не перемешивать"; string sub = str.substr(11, 2); string rest = str.substr(11); После этой команды переменная str все еще содержит оригинальную строку. Переменная sub содержит слово «не». Если нет второго параметра, строка будет прочитана до конца. Переменная rest будет содержать текст «не перемешивать». Сравнение В отличие от С-строк, объекты string могут сравниваться с помощью обычных операторов сравнения, которые уже знакомы нам из сравнения числовых значений (табл. 8.1). Не требуется больше никаких специальных функций, вроде strcmp(). Таблица 8.1. Операторы сравнения для строк Оператор Значение == != < > <= >= Равно Не равно Стоит впереди в алфавите Стоит после в алфавите Стоит впереди или на той же позиции в алфавите Стоит позади или на той же позиции в алфавите Для сравнения достаточно, чтобы один из двух операторов был типа string. Его можно сравнивать как с классической С-строкой, так и с переменной типа char. Порядок не играет роли. 363
Глава 8 string s; char cstring[256]; ... if (s < cstring) ... if (cstring < s) Элементная функция compare() сравнивает объект типа string с другой строкой. Возвращаемое значение равно 0, когда обе строки одинаковы. Если возвращаемое значение отрицательное, строка объекта меньше той, с которой он сравнивается. Если число положительное, то строка объекта больше. Функция служит в первую очередь в качестве непосредственной замены strcmp(), которая выполняет аналогичное сравнение для С-строк. Элементная функция empty() сообщает, пуста ли строка объекта, возвращая в таком случае значение true. Длина символьной последовательности Функции length() и size() возвращают количество символов содержащихся в объекте string. Функция resize() изменяет длину строки на то число, которое передается ей в качестве параметра. Если значение меньше, чем первоначальная длинна строки, она сократится. В противном случае строка будет расширена и заполнена нулевыми байтами. Поиск и информация Элементная функция find() возвращает позицию, которую занимает передаваемая в качестве параметра искомая строка s. Если дополнительно задается параметр p, поиск выполняется с позиции р. size_type find(const string &s, size_type p=0); Если символьная последовательность s не найдена, функция возвращает значение string::npos. Оно в любом случае располагается вне длины строки, поэтому так можно узнать, не произошел ли переход за границы строки. 364
Библиотеки #include <string> #include <iostream> using namespace std; int main() { string str = "Карл у Клары украл кораллы" "Клара у Карла украла кларнет"; string such = "украл"; unsigned int pos = 0; while (pos!=string::npos) { pos = str.find(such, pos); if (string::npos==pos) { cout << "не найдено!" << endl; } else { cout << pos << endl; ++pos; // предотвращение бесконечного цикла } } } Листинг 8.5. В поиске Функция rfind() работает так же, как и функция find(), только поиск производится с конца строки к началу. Функция find_first_of() работает, как функция find(). Но при этом она возвращает первую позицию, на которой располагается в объекте искомый символ или строка. Помимо find_first_of() существуют различные другие функции, которые работают похожим образом. Так, функция find_last_of() возвращает позицию искомого элемента, поиск которого проводится с конца к началу. Функция find_first_not_of() возвращает первый символ, который не находится в искомой строке, find_last_not_of() выполняет то же самое, но поиск происходит в обратном порядке. 365
Глава 8 Преобразование чисел и символьных последовательностей Очень часто программистам встречается ситуация, когда требуется преобразовать число в символьную последовательность или наоборот. Если пользователь вводит число, естественно он набирает на клавиатуре последовательность определенных символов, то есть строку. Для дальнейшей обработки ее требуется преобразовать в число. При использовании cin эту ситуацию улаживает класс iostream. В других случаях, как, например, в графических интерфейсах, элементы оболочки передают в программу символьную последовательность и та должна сама преобразовать ее в число. Точно так же элементы графических интерфейсов ожидают символьную последовательность для вывода информации. Если там должны быть числа, требуется сначала преобразовать их в строку. Для решения таких задач в стандартных библиотеках языка C++ предлагается два класса — istingstream и ostringstream, которые преобразуют файл в символьную последовательность с помощью операторов ввода >> и вывода <<. Для преобразования символьной последовательности в числовую переменную используется объект класса istringstream. Он может быть инициализирован при объявлении со значением строки. Затем требуется только направить оператор ввода >> от объекта на числовую переменную. #include <sstream> using namespace std; int my_value = 0; string s("123"); istringstream inStream(s); inStream >> my_value; Если требуется пройти обратный путь, то есть сгенерировать из числовой переменной строку, следует использовать объект класса ostringstream. C помощью оператора << содержимое числовой переменной будет отправлено в потоковый объект. Элементная функция str() позволяет прочесть данную символьную последовательность. #include <sstream> using namespace std; 366
Библиотеки int my_value = 976; string s; ostringstream outStream; outStream << my_value; s = outStream.str(); Чтобы строковые переменные соответствовали требованиям вывода, можно использовать манипуляторы (см. стр. 378) Более старые версии языка C++ предлагают похожую функциональность в классе strstream. Но стандарт категорически не рекомендует его использование1. Управление распределением памяти класса string Класс string распределяет память для символьных последовательностей самостоятельно. Поэтому многих критических ошибок, которые могли привести к сбою программы, написанной на языке С, можно избежать. Так, исключен переход за пределы выделенной памяти или запись в чужую область. Если строка больше выделенной памяти резервированной в классе, выделяется новая память, строка копируется, а старая память очищается. Если размер строки то и дело незначительно изменяются, постоянное выделение новой памяти и освобождение старой может привести к значительным потерям производительности. В таких случаях программист может задать для символьной последовательности определенный размер памяти. Для этого вызывается функция reserve(). Ее параметры задают минимальное количество выделяемой для строки памяти. Таким же механизмом обладает контейнер STL vector (см. стр. 410). Сколько места в памяти занимает объект, можно узнать с помощью функции capacity(). Не следует путать это число с длиной строки. void reserve(size_type size); size_type capacity(); Листинг 8.6. Прототипы функций выделения и определения памяти для класса string 1 Kaiser Richard. C++ mit dem Borland C++ Builder. Springer, Berlin Heidelberg, 2002. 367
Глава 8 Размещение в стандартные классы Класс string принадлежит к стандартной библиотеке языка C++, а точнее к STL, и является производным от базового класса basic_ string. Этот класс реализует функциональность строковых функций, но способен также с помощью шаблона изменять тип одной буквы. По этой причине можно использовать одинаковую функциональность, если вместо простого восьмибитного кода будет применяться стандарт UNICODE из 16 бит. От класса basic_string происходят класс string для восьмибитовых строк и wstring для стандарта UTF-16 или UNICODE. Можно использовать оба следующих определения типов: typedef basic_string<char> string; typedef basic_string<wchar_t> wstring; То, что класс string принадлежит к STL, можно определить по использованию итераторов и собственных элементных функций. Так что он является хорошим вводом в STL. 8.1.2. Другие библиотеки строк К сожалению, наряду со стандартными библиотеками существуют еще и собственно разработанные. Первые версии языка C++ не содержали библиотеки string. Это привело к различным собственным вариантам реализации строк. Поскольку не существовало никакого стандарта, для больших библиотек классов вводились их собственные классы строк. Они не были изменены и после ввода класса string. Причиной этого служит то, что собственные классы подходят классовым библиотекам значительно лучше. Другая причина в том, что существует ряд программ, которые используют эти строковые классы. Производитель, с одной стороны, не мог оставить своих клиентов без поддержки просто потому, что появился новый стандарт. С другой стороны, ему желательно привязать программиста к своему проекту и тем самым затруднить переход на другой компилятор. Классы MFC используют класс с именем CString, а Borland C++ Builder — класс с именем AnsiString. Оба класса предлагают конструктор преобразования, чтобы можно было работать со стандартными строками и преобразовывать их при доступе к интерфейсу библиотек класса. Преимущество этот образа действия в том, что можно содержать бо́льшую часть модуля проекта в портативном виде. 368
Библиотеки 8.1.3. Классические функции языка C При обсуждении обработки массивов уже пояснялось, что С-строки реализуются в качестве массивов символов. Для этого типа язык C содержит огромное количество библиотек функций. Поскольку классическая С-строка является массивом типа char, программист обязан оценивать, насколько большой она может быть. Массив должен быть подготовлен к самой невыгодной ситуации. Максимальный размер должен быть определен при объявлении массива. Если он определяется во время выполнения программы, память можно динамически выделить функцией new. Когда массив становится больше, требуется выделять новую память, копировать строку и освобождать старую память. Иначе выражаясь: управление памятью символьных последовательностей принадлежит к задачам реализации приложения. Фактическое окончание строки устанавливается независимо от размера массива с помощью нулевого символа. В случае если он забыт, и в массиве случайно не встречается 0, будут использоваться данные, которые не имеют отношения к символьной последовательности. Это сложно обнаружить, поскольку перезапись области чужой переменной не приведет сразу же к ошибке, и причина не обязательно будет понятна, если возникнет исключительная ситуация. Тем не менее для классических С-строк имеется большое количество различных функций, так что программисту не нужно все операции со строками писать самостоятельно. Чтобы воспользоваться этими функциями требуется подключить заголовочный файл string.h. Эти функции в основном не особенно сложны и в некоторых программах можно найти напрямую запрограммированные циклы. В следующем примере символьная последовательность копируется в качестве функции strcpy(). while(*source) { *target++ = *source++; } *target = 0; Листинг 8.7. Функция strcpy(), реализованная самостоятельно 369
Глава 8 Функция strcpy() стандартной библиотеки в качестве параметров ожидает сначала цель, а затем источник. Как шпаргалку можно отметить, что последовательность «цель»-«источник» соответствует последовательности присваивания. Других параметров strcpy() не содержит, функция не может определить, кто больше: цель или источник. В таком случае произойдет выход за границы массива. Поэтому использование функции strcpy() ведет к риску безопасности. Если злоумышленник передаст программе в качестве вводного значения или параметра излишне длинную строку, функция strcpy() выйдет за пределы массива и может изменить переменные, которые не должны быть доступны пользователю. Из-за этого могут возникнуть неожиданные последствия или сбой программы. Если программа помимо расчетной имеет защитную функцию (ввод пароля, брандмауэр или нечто похожее), то в итоге процесс вычисления может оказаться лишен защиты. Чтобы это предотвратить, существует функция strncpy(), которая в качестве расширенных параметров ожидает максимальную длину копируемой области. Таким образом перехода границы целевого массива легко избежать. К сожалению, функция strncpy()не завершает строку нулем, если длина строки источника превышает третий параметр. Будет скопировано ровно столько символов, сколько задается третьим параметром. Чтобы целевой массив был в любом случае завершен корректно, после вызова strncpy()следует последний элемент целевого массива устанавливать в 0. Следующий пример демонстрирует, как это сделать: char *source; char target[MAX]; ... strncpy(target, source, MAX); target[MAX-1] = 0; // надежно и никогда не помешает Чтобы соединить две строки используется функция strcat(). Первый параметр содержит первую строку и является в тоже время целевой переменной. Таким образом, в уже имеющуюся строку будет добавлен второй параметр. В этом случае также существует функция, которая в качестве третьего параметра имеет длину строки. Она называется strncat(). Третий параметр задает максимальное количество символов строки-источника, которые будут добавлены к целевой строке. То есть речь идет не о максимальной длине строки. Следующий пример использует функцию strlen(), возвращающую длину строки: 370
Библиотеки char *source; char target[MAX]; ... strncat(target, source, MAX-strlen(Ziel)); target[MAX-1] = 0; // надежно и никогда не помешает Функция strlen() возвращает длину строки, которая передается в качестве параметра. С помощью функции strcmp() можно сравнить две строки между собой. Возвращаемое значение равно 0, когда строки идентичны. Если первая строка лексически больше второй, будет возвращено положительное число. В обратном случае — отрицательное. «Лексически меньше» означает, что символьная последовательность отсортирована по алфавиту. С помощью функции strstr() можно в одной строке найти другую. Первый параметр — строка, в которой будет происходить поиск. Второй — искомая строка. Функция strstr() возвращает указатель на первое место, где найдена искомая строка. Если она не найдена, возвращается 0. Далее представлены прототипы самых важных строковых функций, которые можно использовать в языке C: #include <string.h> char *strcat(char *target, const char *source); int strcmp(const char *s1, const char *s2); char *strcpy(char *target, const char *source); size_t strlen(const char *s); char *strncat(char *target, const char *source, size_t n); int strncmp(const char *s1, const char *s2, size_t n); char *strncpy(char *target, const char *source, size_t n); char *strstr(const char * haystack, const char * needle); Преобразование строки в число Часто требуется преобразовывать строки в числа. Например, если данные, введенные пользователем в диалоговом окне, передаются в программу в виде символьной последовательности, а она ожидает числовое значение. 371
Глава 8 Функция atoi()1 ожидает в качестве параметра С-строку и возвращает значение типа int. Символьная последовательность должна начинаться с цифры или знака числа. Функция прерывает работу, как только встречается символ, не являющийся цифрой. Аналогом этой функции выступает atol(), которая возвращает тип long. Для чисел с плавающей запятой имеется функция atof(), возвращаемое значение которой имеет тип double. #include <stdlib.h> int atoi(const char *s); long atol(const char *s); double atof(const char *s); Функция atof() имеет тот недостаток, что она принимает только точку в качестве символа, отделяющего целую часть от дробной. В русскоязычном сообществе для этого чаще используется запятая, однако она будет интерпретироваться функцией atof() в качестве конца числа, как будто дробная часть отсутствует. Для использования запятой в таких числах можно написать свою функцию atof(), которая вторым параметром принимает любой разделяющий символ. double atof(const char *ValueString, char DecimalSign) { double Value = 0.0; bool Negativ = false; // обработать знак числа if (*ValueString=='-') { Negativ = true; ValueString++; } else if (*ValueString=='+') { ValueString++; } // определить числа перед запятой 1 372 Слово «atoi» является сокращением от «ascii to int».
Библиотеки while (*ValueString>='0' && *ValueString<='9') { Value *= 10; Value += (*ValueString++ - '0'); } if (*ValueString == DecimalSign ) { // определить числа после запятой double Potency=1.0; ValueString++; while (*ValueString>='0' && *ValueString<='9') { Potency /= 10; Value += (*ValueString++ - '0') * Potency; } } // добавить знак числа if (Negativ) { Value = -Value; } return Value; } Листинг 8.8. Перегрузка функции atof() Упражнение • Напишите функцию atof() с такими же параметрами, как показано выше. Используйте при этом функции строк, чтобы найти запятую и заменить ее точкой и вызвать после этого стандартную функцию atof(). Пример решения на стр. 515. Преобразования чисел в строки Для преобразования числа в строку часто используется функция sprintf(). Имя «printf» указывает на ее происхождение. Функция 373
Глава 8 printf()1 использовалась в языке C для создания форматированного вывода. Функция sprintf() имеет аналогичное действие, но результат не выводится на экран, а записывается в С-строку. Третий вариант — функция fprintf() записывает результат в файл. Из-за происхождения этих функций требуется предварительно подключить файл stdio.h. #include <stdio.h> int printf(const char *Format, ...); int fprintf(FILE *stream, const char *Format, ...); int sprintf(char *String, const char *Format, ...); Функция sprintf() должна быть здесь рассмотрена подробнее, поскольку она часто используется и в языке C++ для форматирования строк или таблиц. При выводе чисел с фиксированной запятой она более гибкая, чем манипуляторы (см. стр. 378). Первый параметр функции sprintf() — С-строка, которая содержит результат после вывода. Необходимо следить за ее размерами, поскольку сама функция не может проверить, превышена ли емкость строки. Второй параметр — строка формата, которая определяет, как должны быть представлены следующие параметры. Остальные параметры содержат значения, которые следует обработать. Три точки при этом не обозначают растерянность, а являются элементом языка C, обозначающим любое количество параметров (см. стр. 179). Форматированная строка является обычной С-строкой, которая на определенных позициях содержит специальные управляющие последовательности, сообщающие, какой тип имеют значения в параметрах и как они должны выравниваться. Эти управляющие последовательности начинаются с символа процента (%). Пример: sprintf(OutStr, "%s %s имеет клиентский номер %d и %7.2f EUR на счету", FirstnameStr, NameStr, ClNr, Money); Переменная OutStr должна быть массивом типа char и получать значение после выполнения команды sprintf(). Форматированная строка получает сначала две управляющие последовательности (%s). Два следующих параметра — FirstnameStr и NameStr — должны быть С-строками. Код «имеет клиентский номер» в форматированной строке будет добав1 374 От англ. «print formatted» — форматированный вывод. Прим. ред.
Библиотеки лен далее в строку OutStr. Затем следует целочисленное десятичное число (%d). Параметр ClNr должен иметь тип int. Последняя управляющая последовательность (%7.2f) устанавливает число с плавающей запятой с семью знаками перед ней и двумя после. Форматированная строка копируется в целевую переменную, и управляющие последовательности заменяются соответствующей информацией. Они начинаются со знака процента (%) и заканчиваются буквой, определяющей тип. В следующей таблице представлены типовые обозначения (табл. 8.2). Таблица 8.2. Обозначение типов в форматированной строке printf Обозначение типа Значение d x f e с s Десятичное представление целого значения Шестнадцатеричное представление целого значения Представление с фиксированной запятой значения типа double Экспоненциальное представление значения типа double Символьное представление Представление в виде С-строки Каждая из управляющих последовательностей содержит значение в качестве параметра, который передается в функцию. При этом оно должно быть совместимо с управляющей последовательностью. Сама функция не может это определить, а компилятор — проконтролировать. Если будет передано неверное значение или тип, в худшем случае это приведет к сбою программы. Так что здесь требуется осмотрительность. Между знаком процента и обозначением типа можно размещать флаги, ширину и точность вывода, в качестве спецификации типа. Флаг контролирует тип заполнения выделенного свободного пространства (табл. 8.3). Таблица 8.3. Флаги Флаг Обозначение 0 − Пробел + Заполнение нулями Выравнивание по левому краю Вывод пробела вместо положительного знака числа Выводить знак числа и для положительных значений Затем следует числовая константа, которая задает минимальное количество позиций для столбца. Если ожидаются числа с плавающей запятой, можно задавать две константы, которые устанавливают количество символов до и после запятой, разделяя их точкой. 375
Глава 8 Затем задается модификатор, который определяет тип. При этом «h» означает тип short, а «l» означает long. Символ «L» означает long double. Самая простая форма преобразования — это представление целочисленной переменной в виде строки. Она реализуется так: sprintf(TargetString, "%d", Value); 8.2. Класс iostream для продвинутых Ввод и вывод были в общем виде рассмотрены в разделе 1.5. Язык C++ использует концепцию потока данных, которая также поддерживается новыми операционными системами. 8.2.1. Ввод командой cin При считывании символьной последовательности можно не только получать объекты типа string, но также и помещать С-строки в массив char. При работе с С-строками нужно быть осторожным, поскольку размер массива неизвестен и его сложно предусмотреть. Слишком длинная строка ввода может привести к переполнению, то есть к записи в чужую область памяти. #include <iostream> #include <string> using namespace std; int main() { char a[5], b[20]; // или: string a, b; cin >> a >> b; cout << a << "," << b << endl; } Листинг 8.9. Чтение символьной последовательности Объект ввода можно опрашивать в качестве логической переменной. Тогда она будет возвращать false, если достигнут конец строки. 376
Библиотеки Такое действие является аналогом проверки на достижение конца файла. while (cin >> Value) { } Листинг 8.10. Логический характер Эквивалентом прямого опроса объекта ввода является вызов функции cin.eof(). Индикатор EOF1 вводится с клавиатуры с помощью сочетания клавиш Ctrl+D в среде UNIX или Ctrl+Z в операционной системе MS-DOS или Windows. Поскольку такой вид программного управления нелегко дается обычному пользователю, этот способ действия предлагается только в том случае, когда файлы при стандартном вводе перевернуты. Считывание строки При считывании данных через функцию cin, так называемые «невидимые» символы, то есть все пробелы, табуляции и символы перехода на следующую строку, удаляются как символы разделения. Это может быть обременительно, когда требуется считать строку, которая также содержит пробелы. Для полного считывания строки имеется глобальная функция getline(). В качестве первого параметра она ожидает потоковый объект. Второй параметр имеет тип string. Преимущество ее состоит в том, что не требуется контролировать границы. Дополнительно можно задать третий параметр, который устанавливает, какой символ должен быть завершающим. По умолчанию это символ конца строки. int main() { string theString; getline(cin, theString); cout << theString << endl; } 1 От англ. словосочетания «end of file» — конец файла. Прим. ред. 377
Глава 8 Потоковые классы имеют элементную функцию getline(). Ее первый параметр не string, a классическая С-строка, то есть указатель на тип char. Вторым параметром ожидается размер массива. Здесь также можно дополнительно задать завершающий символ. int main() { char theString[MAXSTR]; cin.getline(theString, sizeof(theString)); cout << theString << endl; } Функция getline() во всех вариантах возвращает ссылку на поток. 8.2.2. Манипуляторы Манипуляторы служат для предварительной обработки вывода и его форматирования. На стр. 68 уже было представлено несколько манипуляторов. Их можно вставлять между операторами потокового вывода. Чтобы использовать манипуляторы, требуется подключить файл iomanip. Манипулятор endl создает переход на следующую строку и заботится о том, чтобы строки, выводимые ранее, сразу передавались на устройство вывода. При этом освобождается временно занимаемая память буфера операционной системы. Манипулятор setw() получает параметром ширину, которую должен занимать объект, выводимый следующим. Если размер больше требуемого, вывод выравнивается по правому краю. Оставшееся незанятым место будет заполнено пробелами. Пробел можно заменить с помощью манипулятора setfill(), который ожидает параметром символ. Он остается действительным, пока не будет вызван следующий манипулятор setfill() с другим значением. Выравнивание можно задать манипуляторами left или right. cout << setw(8) << i << endl; cout << setfill('-'); cout << setw(8) << i << endl; cout << left << setw(8) << i << endl; 378
Библиотеки Вывод программы будет выглядеть следующим образом, если переменной i присвоить значение 725. 725 -----725 725----Посредством манипулятора можно установить, какой базис имеют следующие выводимые данные. Для десятичных чисел это dec, для восьмеричных — oct, для шестнадцатеричных — hex. При этом будут выведены только числа. Типичные для языков C и C++ опознавательные константы, такие, как 0х для шестнадцатеричной и 0 для восьмеричной систем, не будут представлены. Но такое действие можно включить, если использовать манипулятор showbase. При помощи noshowbase оно будет снова отключено. cout << oct << endl; cout << setw(8) << i << endl; cout << setfill('-'); cout << setw(8) << i << endl; cout << hex << left << setw(8) << i << endl; Вывод программы для переменной i, равной 725, выглядит следующим образом: 1325 ----1325 2d5----Константа true выводится обычно в виде 1, а false в виде 0. Но в некоторых компиляторах с помощью манипулятора boolalpha можно установить, что true и false будут выводиться словами. Манипулятор noboolalpha переключит вывод снова на 1 и 0. Для чисел с плавающей запятой тоже имеются манипуляторы. Здесь также функционирует setw() для установки ширины вывода. Вид представления числа можно установить манипулятором scientific. При этом числа будут выводиться в экспоненциальном представлении с одной позицией перед десятичной точкой. Манипулятор fixed переключает представление чисел на числа с фиксированной запятой. Манипулятор setprecision() устанавливает точность (табл. 8.4). 379
Глава 8 Таблица 8.4. Манипуляторы Манипулятор Действие endl setw(n) left right setfill(c) dec oct hex showbase boolalpha noboolalpha Добавляет переход на новую строку Ширина столбца для следующего выводимого объекта равна n Выравнивание по левому краю Выравнивание по правому краю Использовать в качестве символа заполнения пустых позиций Десятичное представление Восьмеричное представление Шестнадцатеричное представление Выводит 0х перед шестнадцатеричным числом Выводит true и false в кодовом виде Выводит true как 1, а false как 0 В качестве параметра ему передается количество позиций числа, которые должны быть выведены (табл. 8.5). Таблица 8.5. Манипуляторы для чисел с плавающей запятой Манипулятор Действие showpoint noshowpoint scientific fixed setprecision(n) Представление с десятичной точкой и позициями после запятой Выводится только значащая часть Число с плавающей запятой в математическом виде Фиксированные позиции после запятой Точность; n — количество позиций Вместо манипуляторов, помещаемых в поток вывода, можно использовать также элементные функции объекта потока (табл. 8.6). В целом они не особо отличаются названиями от манипуляторов, но есть исключения1: Таблица 8.6. Манипуляторы и элементные функции Манипулятор Элементная функция setw(n) setfill(c) setspecision(n) width(n) fill(c) precision(n) Особенно интересна элементная функция setf(). C ее помощью можно моделировать манипуляторы. cout.setf(ios::left); // соответствует: cout << left; 1 380 Стефан Р. Дэвис. С++ для чайников. М.: Диалектика, Вильямс, 2009.
Библиотеки Манипулятор по сравнению с функцией имеет то преимущество, что его можно поместить прямо в поток вывода. Распознать его перед оператором вывода можно по соответствию одному из прототипов1. ios & function(ios &); istream & function(istream &); ostream & function(ostream &); Листинг 8.11. Прототипы для манипуляторов Когда вы пишите функцию, которая имеет параметры и возвращаемое значение, вы тем самым создаете манипулятор. Его имя — это имя функции. Так что если требуется построить манипулятор left для GNU-компилятора языка C++, можно написать следующую функцию с именем left(). ios& left(ios& i) { i.setf(ios::left); return i; } Листинг 8.12. Самостоятельно написанный манипулятор left 8.3. Операции с файлами Программа держит свою информацию в переменных. К сожалению, они сохраняются до первого отключения электроэнергии или сбоя операционной системы. Когда программа заканчивает работу, ее содержимое так или иначе уже является историей. Чтобы завтра можно было снова получить те же значения, рекомендуется их записывать. Для этого можно потоком занести данные в файл. Процесс осуществляется, как и для вывода на экран, командой cout. Такая форма называется последовательной, поскольку данные помещаются в файл друг за другом, в том же порядке, в котором они были записаны. Кроме того, их можно записать и в любое место файла. Позднее блок данных можно считать, если расположить внутренний файловый указатель на эту позицию. Такой тип 1 Jakobs Holger. Einführung in ISO C++, PDF-Datei v. 12.4.2000. 381
Глава 8 действий типичен для предложений данных, особенно, если они должны быть отсортированы в определенном порядке. Для операций с классами используется объект класса fstream. Если происходит только запись в файл, то вместо него можно использовать класс ofstream. Чтобы непосредственно ввести данные в файл предлагается класс ifstream. Для объектов этих классов можно использовать операторы ввода >> и вывода <<. Перед доступом требуется открыть файл элементной функцией open(). Операцию открытия можно перенаправить конструктору класса fstream, в котором объект при объявлении получает имя файла в качестве параметра. После обработки файла его требуется закрыть функцией close(). Это задание автоматически перенимает деструктор, так что необходыми вызывать close() только в том случае, если нужно закрыть файл, прежде чем его объект будет ликвидирован. 8.3.1. Открытие и закрытие Листинг 8.13 сначала объявляет объект класса fstream. Затем функцией open() файл открывается. Следующая строка передает в файл код, после чего тот закрывается функцией close(). #include <fstream> using namespace std; int main() { fstream f; f.open("test.dat", ios::out); f << "Этот код записывается в файл" << endl; f.close(); } Листинг 8.13. Запись кода в файл Первым параметром функции open() является имя файла, который должен быть открыт. Второй параметр задает режим его открытия. В примере файл открывается для записи. Параметры open() можно задать при объявлении объекта. При этом вызывается конструктор, который прямо открывает файл. f.open("test.dat", ios::out); 382
Библиотеки Имя файла определяется в качестве С-строки, то есть указателя на тип char. В большинстве случаев оно передается из диалогового окна выбора файла или задается пользователем. Если имя файла определено в программе в качестве константы, следует обратить внимание на то, что символ перехода в другой каталог (\) должен быть написан два раза, иначе он будет интерпретирован в строке как особый символ языка. Поэтому для перехода между каталогами рекомендуется использовать прямой слеш (/). Он допустим для такого применения и в системе Windows. Это не только облегчает чтение, но и способствует переносу программы в другие операционные системы, так как в среде UNIX для обозначения перехода между каталогами используется прямой слеш. Второй параметр передает режим, в котором открывается файл. Константа ios::out в примере указывает, что файл открыт для записи. Константа ios::in используется для режима чтения. Режим ios::trunc обозначает, что содержимое файла при открытии будет удалено. Он используется только в том случае, если файл должен быть заново перезаписан. Так же дело обстоит с режимом ios::app. При этом все записывающиеся данные будут добавлены в конец файла. Этот режим имеет смысл использовать в комбинации с ios::out, который имеет особое значение прежде всего при записи протокольных файлов. Таким образом данные всегда добавляются в конец. Вместо того, чтобы сначала получить размер файла и затем определить позицию записи, достаточно просто вызвать функцию. Режим ios::ate делает так, что указатель позиции при открытии устанавливается на конец файла (табл. 8.7). Для комбинации нескольких режимов используется побитовое связывание оператором ИЛИ. То есть достаточно разместить между константами символ вертикальной черты (|). Таблица 8.7. Режимы открытия файлов Константа Значение ios::in ios::out ios::trunk ios::app ios::ate Для чтения Для записи Файл будет очищен при открытии Записываемые данные помещать в конец файла Указатель позиции установить на конец 383
Глава 8 Имеются два особенных класса потока. Класс ifstream заключает в себе режим открытия файла ios::in, то есть специализируется на чтении данных. Противоположностью является класс ofstream, который используется для записи файлов. В этом случае необязательно явно задавать ios::out при открытии файла. Как правило, язык C++ исходит из того, что файл представлен кодом. Из-за этого переходы на следующую строку организуются по-разному в зависимости от платформы. В среде UNIX переход на следующую строку кодируется символом 10 таблицы ASCII (перевод строки). Среда OS X использует символ 13 для возврата каретки, а операционная система Windows используют комбинацию из обоих символов. При чтении и записи файлов при необходимости будут проведены преобразования, которые, однако, являются фатальными, если речь идет не о коде, а о бинарном файле. Чтобы избежать этого, при его открытии используется ios::binary. Файл автоматически закрывается деструктором. Поэтому вызов функции close() в целом излишен. Недостаток явного закрытия в том, что есть потоковый объект, который не имеет больше доступа к файлу. Поэтому открытие конструктором и закрытие деструктором выглядит значительно более элегантно. С другой стороны, умный программист постарается поскорее закрыть файл, открытый для записи, поскольку ему легко навредить. Если до закрытия файла произойдет сбой в программе, его данным может быть нанесен урон. 8.3.2. Чтение и запись Смысл и цель файла состоят в том, чтобы хранить данные. При этом кодовые файлы отличаются от файлов данных. Кодовые файлы можно обработать операторами ввода и вывода. Их можно читать и обрабатывать любым редактором. Файлы данных, напротив, обычно определяются блоками определенной длины и так же считываются. Поток данных С объектом потока данных можно работать с помощью операторов ввода и вывода, как вам уже знакомо по функциям cin и cout. В следующем примере еще раз показана запись в файл оператором потокового вывода: 384
Библиотеки fstream Datei(..., ios::out); ... Datei << "Запись в файл" << Variable << endl; Для форматирования записи в файл также можно использовать манипуляторы (см. стр. 378). Чтение из файла производится оператором ввода аналогичным образом. Оператор ввода >> работает с объектом fstream так же, как с cin. Файл считывается так, словно информация вводится с клавиатуры. Сюда же относится то, что пробел, табуляция и переход на следующую строку интерпретируются в качестве разрыва ввода. Это действительно и в ситуации, когда поток данных записывается в переменную символьной последовательности. Чтобы все-таки прочитать из файла кодовую строку с пробелами используется функция getline(). Первый параметр передает указатель на char. То есть функция работает с классической С-строкой. Второй параметр — максимальное количество знаков, которые записываются в буфер. #include <fstream> #include <iostream> using namespace std; int main(int argc, char *argv[]) { fstream f; char cstring[256]; f.open(argv[1], ios::in); while (!f.eof()) { f.getline(cstring, sizeof(cstring)); cout << cstring << endl; } f.close(); } Листинг 8.14. Считывание файла 385
Глава 8 Наряду с этим имеется глобальная функция getline(), которая получает объект типа ifstream в качестве параметра. Она также работает со стандартным классом string и принимает объект этого класса в качестве второго параметра. Пример выше в данном случае выглядел бы так: #include <fstream> #include <iostream> #include <string> using namespace std; int main(int argc, char *argv[]) { ifstream f; // объект файла string s; // открыть файл с заданными параметрами f.open(argv[1], ios::in); while (!f.eof()) // пока файл не закончился { getline(f, s); // считать строку cout << s << endl; // вывести ее на экран } f.close(); // файл снова закрыть } Листинг 8.15. Считывание файла (getline.cpp) Файлы данных Для записи двоичных файлов способ передачи данных менее пригоден. Здесь используются функции read() и write(), с помощью которыхзаписываются и считываются четкие блоки данных. Для них в качестве главного буфера памяти используются чаще всего объекты данных или структуры. tData Data; fstream f(..., ios::out|ios::binary|ios::in); f.write(&Data, sizeof(Data)); ... f.read(&Data, sizeof(Data)); 386
Библиотеки При вызове функции fwrite() объект Data будет записан в файл с текущей позиции. При дальнейшем вызове read()информация из файла будет считана в объект Data. Класс, который используется в качестве буфера, должен, конечно, содержать все данные. Как только он получает указатель, в памяти сохраняется адрес, а не сами данные. Такой указатель не всегда очевиден, порой он спрятан за именем типа. Так, например, объект класса string содержит ссылку на символьную последовательность, но не саму последовательность. Соответственно для буфера данных годится скорее классическая С-строка, то есть четкий массив char, нежели string или указатель на char. Следующая тестовая программа сохраняет первый параметр, с которым она была вызвана, в файле testfile. Если программа вызывается без параметра, сначала из файла считывается последнее сохраненное имя, которое затем будет выведено на экран. Программа представлена в двух версиях. В той, где используется С-строка для приема файла в tData, все функционирует безупречно. Если удалить символ комментария перед объявлением переменной для Data и закомментировать предшествующие ему объявления переменных, данные станут помещаться в string объект, потому что теперь будет использоваться класс tStrData. В программе при этом возникнут сбои, поскольку символьная последовательность не находится больше в пределах tStrData и не записывается в файл. #include <fstream> #include <string> #include <iostream> using namespace std; #include <string.h> // этот класс принимает символьную последовательность в // классический массив char и хранит при этом данные. class tData { public: void Set(char *para) { 387
Глава 8 strncpy(data, para, sizeof(data)); data[sizeof(data)-1] = 0; } void Show() { cout << data << endl; } private: char data[255]; }; // Здесь используeтся string. Объект такого типа // не является на самом деле фактическими данными class tStrData { public: void Set(char *para) { data = para; } void Show() { cout << data << endl; } private: string data; }; int main(int argc, char**argv) { // tStrData Data; // это не работает! tData Data; // это не работает! // файл с именем testfile будет открыт fstream f("testfile", ios::out|ios::binary|ios::in); // будет ли передан параметр вызова? if (argc>=2) 388
Библиотеки { Data.Set(argv[1]); // поместить в Data // сохранить объект в файле f.write((char *)&Data, sizeof(Data)); } // аргумент отсутствует? Тогда прочесть файл! if (argc==1) { // считать данные из файла в объект f.read((char *)&Data, sizeof(Data)); Data.Show(); // ... и вывести } } Листинг 8.16. Тест: сохранение структур данных (teststr.cpp) Следует также подумать о том, что файловые структуры, в общем, более долговечны, чем версии программ. Как только структура буфера файлов изменяется, файлы, которые были созданы в предыдущей версии программы, становится невозможно прочесть. Это может быть критичным, если в них хранилась информация клиентов. Пользователь ожидает, что после обновления программы ранее созданные им файлы можно будет, как минимум, прочесть. Каждый процесс1 владеет позиционным указателем для каждого открытого файла. Обычно он после открытия файла ссылается на его начало и при считывании или записи перемещается в направлении конца файла. Чтобы получить позицию указателя используется функция seekg() класса ifstream. Класс fstream для изменения позиции использует функцию seekp(). С ее помощью можно записать или прочесть требуемый блок данных в любом месте файла. Для этого сначала организуется поисковый вызов, а затем производится считывание или запись. Первый параметр поисковой функции — это позиция, которую должен получить указатель файла. Дополнительно может быть задан второй параметр. Он определяет, в каком направлении следует вычислять новую позицию. Параметром могут служить следующие константы (табл. 8.8). 1 Процесс — это запущенная программа. Если она запускается неоднократно, то выполняется несколько процессов одной программы. 389
Глава 8 Таблица 8.8. Позиционное направление Константа Значение ios::beg ios::cur ios::end Рассматривать с начала файла Рассматривать с текущей позиции Рассматривать с конца файла Функции без параметров tellg() и tellp() сообщают, на какой позиции в данный момент находится указатель. Доступ к файлу будет значительно быстрее, если операционная система не будет записывать все данные сразу на жесткий диск, а сохранит их в оперативной памяти до того момента, когда можно будет записать несколько блоков сразу. На сегодняшний день это стандартный подход для всех операционных систем. Иногда, однако, требуется записать блок данных сразу на жесткий диск. Этого можно добиться элементной функцией flush() класса fstream. 8.3.3. Наблюдение состояния При работе с файлами могут возникнуть проблемы, к которым программа должна быть подготовлена. Так, файл, который программе нужно прочесть, может вообще не существовать или быть короче, чем ожидается. Сюда же относится отсутствие прав на запись или свободного место на диске. Объект потока можно в любой момент опросить элементной функцией good() на предмет возникновения ошибок при последней операции с файлом. Функция good() не имеет параметров и возвращает логическое значение. В другом случае объект потока может сам опросить свое состояние. Если он содержит 0, это соответствует возвращаемому значению false функции good(). fstream File; ... if (File.good()) { // все отлично ... if (File) { // тоже хорошо 390
Библиотеки Следующая программа открывает файл, записывает туда блок информации, снова возвращается к началу файла и считывает только что записанные данные. При этом она проверяет каждый шаг сразу после его выполнения. fstream f("test.dat", ios::out|ios::binary|ios::in); if (!f.good()) { cerr << "Ошибка при открытии файла test.dat" << endl; } f.write(&Data, sizeof(Data)); if (!f.good()) { cerr <<" Ошибка при записи файла test.dat"<< endl; } f.seekg(0, ios::beg); f.read(&Data, sizeof(Data)); if (!f.good()) { cerr << " Ошибка при чтении файла test.dat" << endl; } Если убрать из примера строки с командой поиска, то команда чтения вызовет ошибку, если файл в начале был пуст. Команда записи устанавливает указатель позиции файла в его конец. Следующая команда чтения не может прочесть достаточное количество данных, чтобы заполнить переменную буфера Data. Это состояние обозначается как EOF1. Достижение конца файла ни в коем случае не катастрофично. Это вполне обычно, считывать файл блок за блоком, пока не будет достигнут его конец. Чтобы отличить конец файла от других ошибок, имеется отдельная функция eof(). Она возвращает false до тех пор, пока не будет достигнут конец файла. Следующий цикл считывает данные файла. while (!f.eof()) { f.read(&Data, sizeof(Data)); } f.clear(); 1 От англ. «End Of File» — конец файла. 391
Глава 8 После возникновения ошибки функция good() возвращает ее до тех пор, пока состояние ошибки объекта потока не будет переключено функцией clear(). В примере выше это проводится после выхода из цикла, который происходит после достижения индикатора EOF. Наряду с концом файла бывают ситуации, когда запись или чтение не удается, потому что вызов не распространяется на требуемый диапазон. В таком случае элементная функция fail() класса fstream возвращает значение true. Серьезные ошибки можно установить при опросе элементной функции bad() класса fstream. При возникновении такой ошибки дальнейшие операции обрываются. Вызвав элементную функцию exceptions() класса fstream можно выяснить, какие состояния ошибок должны быть обработаны в блоке обработки исключений. Параметром функции является одна из констант ios::falibit, ios::badbit или ios::eofbit. При необходимости их можно объединить двоичным ИЛИ. Следующий пример производит обработку исключений, если установлен failbit или badbit. f.exceptions(ios::failbit|ios::badbit); По теме обработки исключений вы найдете исчерпывающую информацию в разделе 7.3 на стр. 331. Обращение с исключениями класса fstream рассмотрено в отдельном разделе на стр. 346. 8.3.4. Доступ к файлам в стандарте ANSI C Конечно, файлы появились не с момента разработки языка C++, и естественно, уже в языке C имелись функции для их обработки. Многие программисты, работающие на C++, начинали с языка C, и поэтому в программах на языке C++ можно часто встретить привычный для языка C способ обработки файлов. Знание такого доступа к файлам важно, если понадобится расширить или исправить старую программу. Однако новые программы лучше писать с использованием класса fstream. Перечислим его достоинства: • Операции проверяются на типовую защищенность; • Можно использовать обработку исключений; • Если использовать ostream и istream, уже при компилировании устанавливается, что в файл, открытый для чтения, не будет по ошибке произведена запись. 392
Библиотеки Поскольку язык C не знает, что такое классы, доступ к файлам осуществляется с помощью набора функций. При открытии файла вы получаете так называемый идентификатор. Это значение требуется передавать при каждой операции доступа к файлу, чтобы операционная система знала, с каким файлом она работает. Данный идентификатор в функциях стандарта ANSI C представляет собой указатель на тип FILE. Следующий пример демонстрирует, как записать в файл, а затем прочитать из него блок данных. #include <stdio.h> int main() { FILE *f; f = fopen("test.dat", "rwb"); if (f) { fwrite(buffer, sizeof(buffer), 1, f); fseek(f, 0, SEEK_SET); fread(buffer, sizeof(buffer), 1, f); fclose(f); } } Листинг 8.17. Работа с файлом Файл открывается функцией fopen() и закрывается функцией fclose(). Они имеют следующие прототипы: FILE *fopen(const char* FileName, const char* Mode); int fclose(FILE *FileHandle); Первый параметр функции fopen() — имя файла. Второй — режим открытия, который передает один или несколько символов в виде символьной последовательности. В табл. 8.9 представлен перечень. Функция fwrite() записывает блок данных в файл. Функция fread() считывает блок данных из файла. Функции имеют следующие прототипы: 393
Глава 8 size_t fread(void *Buffer, size_t Length, size_t n, FILE *f); size_t fwrite(void *Buffer, size_t Length, size_t n, FILE *f); Таблица 8.9. Режимы открытия для fopen() Символ Значение r w a r+ w+ b Открыть для чтения Удалить содержимое и открыть для записи Данные будут добавлены Помимо чтения разрешить также запись Удалить содержимое и открыть для записи и чтения Двоичный файл (преобразование символов окончания строк отсутствует) Первый параметр — указатель на буфер, в котором хранятся данные в оперативной памяти. Поскольку тип параметра — указатель на void, можно передавать адрес любой структуры данных. Два следующих параметра определяют размер буфера. Сначала задается размер отдельного блока, затем их количество. Когда используются файлы данных, буфер которых является структурой данных, параметр Length обычно определяется функцией sizeof() для класса, а количество n устанавливается в 1. Если обрабатывается кодовый файл, имеет смысл присвоить параметру Length значение 1, а количеству n задать желаемый размер буфера. Возвращаемое значение обеих функций — количество блоков, которые можно фактически обработать. Если n не соответствует возвращаемому значению, значит что-то пошло не так. Последний параметр — идентификатор файла. Для работы с кодовыми файлами имеются две специальные функции, которые в плане использования несколько проще: fgets() и fputs(). Функция fgets() считывает кодовый файл в буфер. Чтобы предотвратить его переполнение, длина ограничивается параметром,. Функция fputs() записывает в файл символьную последовательность. При этом длина ограничивается завершающим символом 0, как это обычно происходит в С-строках. char *fgets(char *Buffer, int Length, FILE *f); char *fputs(char *Buffer, FILE *f); C помощью функции fprintf() можно записать в файл форматированные данные. Параметры этой функции аналогичны параметрам функции printf() (см. стр. 373), за исключением идентификатора файла. 394
Библиотеки char *fprintf(FILE *f, char *Format, ...); Перемещение позиционного указателя файла возможно с помощью функции fseek(). Для файлов на языке C существует только один общий указатель для чтения и записи. int fseek(FILE *f, long Offset, int RelPos); Первый параметр — идентификатор файла, второй — интервал, третий — константа, которая определяет, к чему относится интервал (табл. 8.10). Таблица 8.10. Отношение позиции в функции fseek() Константа Значение SEEK_SET SEEK_CUR SEEK_END Начало файла Текущая позиция Конец файла Функция feof() сообщает, достигнут ли конец файла. В качестве единственного параметра функция получает идентификатор файла. Возвращаемое значение равно 0, если конец файла не достигнут. int feof(FILE *f); 8.3.5. Команды файловой системы Следующие функции относятся к стандарту ANSI для языков C и C++, они позволяют работать с файловой системой. Удаление файла: remove() Функция remove() удаляет файл, имя которого передается в нее параметром. #include <stdio.h> int remove(const char *filename); Функция возвращает 0 при успешном выполнении операции, и –1 при возникновении ошибки. Причина последней хранится в переменной errno. Большинство системных функций в случае сбоя возвращают значение меньшее 0. Если возвращаемое значение не достаточно содержательное, причина ошибки кодируется в глобальной переменной errno. Константы, которые может принимать errno, перечислены в за- 395
Глава 8 головочном файле errno.h. Так, при вызове remove() переменная errno может принимать значение следующих констант (табл. 8.11). Таблица 8.11. Константы ошибок Константа Значение EACCES ENOENT EROFS Не хватает прав для записи Файл не существует Файл находится в среде, защищенной от записи Переименование: rename() Функция для переименования файлов называется rename(). В системе UNIX с ее помощью также можно перемещать файлы внутри одной файловой системы. #include <stdio.h> int rename(const char *filename, const char *newname); Функция возвращает 0 при успешном завершении действия, и –1 при ошибке. Причина ошибки записывается в переменную errno. Другие функции Команды для работы со справкой не принадлежат, к сожалению, к стандарту ANSI. В среде UNIX они в любом случае доступны и стандартизированы согласно POSIX1. Также они поддерживаются многими компиляторами в среде Windows. По этой причине здесь перечислены ключевые слова, которые требуются, если необходимо получить помощь компилятора, когда возникает вопрос, поддерживается ли данная функция. В среде UNIX перед функцией задается команда man, после чего система выдает ее прототип и функции. Хотя чтение каталога и не предусматривается стандартом ANSI, оно предусмотрено POSIX. Этот стандарт UNIX реализован также для некоторых C компиляторов. Поскольку чтение из каталога то и дело необходимо использовать в программе, здесь следует его коротко рассмотреть (табл. 8.12). 1 POSIX (Portable Operating System Interface) — портативный интерфейс операционной системы — является одним из стандартов для интерфейсов UNIX, которые введены институтом инженеров по электротехнике и электронике IEEE (Institute for Electrical and Electronic Engineers). 396
Библиотеки Таблица 8.12. Функции каталогов Функция Значение mkdir chdir rmdir opendir readdir closedir Создание каталога Смена каталога Удаление пустого каталога Открывает каталог для поиска файла Считывает следующую запись в каталоге Закрывает каталог Следующая программа выводит все имена файлов каталога на экран: #include <dirent.h> #include <iostream> using namespace std; int main() { DIR *hdir; struct dirent *entry; hdir = opendir("."); do { entry = readdir(hdir); if (entry) { cout << entry->d_name << endl; } } while (entry); closedir(hdir); } Листинг 8.18. Считывание каталога (dir.cpp) Указатель на DIR — требуемый идентификатор для каталога. Функция opendir() получает в качестве параметра его имя1. После открытия 1 В некоторых компиляторах возникают трудности, когда требуется прочитать иной каталог, помимо текущего. Это нужно проверять при написании программы. 397
Глава 8 каталога функция readdir() возвращает либо указатель на переменную структуры записи директории dirent, либо 0, если в каталоге нет записей. В конце рекомендуется закрыть директорию. Единственный гарантированный стандартом POSIX элемент переменной dirent — имя файла d_name. Этого в целом достаточно, поскольку с помощью имени можно получить остальную информацию, используя такие функции, как, например, stat(). 8.3.6. Получение свойств файла Системная функция UNIX stat() переносится в библиотеках языка C также для других систем. В целом она не принадлежит к стандарту ANSI. С помощью этой функции можно получить информацию о файлах. Ее вызов в любом случае допустим в системе UNIX. Для среды Windows она доступна в компиляторе Borland и GCC. Среда разработки Visual C++ предлагает ее вызов под именем _stat(). С помощью функций stat() и fstat() можно получить информацию о файле. Результат помещается в структуру типа stat1. Необходимо создать переменную этого типа и передать ее адрес функции stat(). Последняя имеет следующий прототип: #include <sys/types.h> #include <sys/stat.h> int stat(char *filename, struct stat *buffer); Результат хранится в переменной типа stat, на которую указывает параметр buffer. Определение структуры содержит следующие элементы: struct stat { dev_t st_dev /* (P) устройство, на котором хранится файл */ ushort st_ino /* (P) индексный дескриптор файла */ ushort st_mode /* (P) тип файла */ short st_nlink /* (P) количество ссылок файла */ ushort st_uid /* (P) владелец файла User-ID (uid) */ ushort st_gid /* (P) ID группы (gid) */ 1 398 В Visual C++ такая структура имеет имя _stat.
Библиотеки dev_t st_rdev /* основной или дополнительный номер устройства */ off_t st_size /* (P) размер в байтах */ time_t st_atime /* (P) дата последнего доступа */ time_t st_mtime /* (P) дата последнего изменения */ time_t st_ctime /* (P) дата последнего изменения статуса */ }; Составные части этой структуры могут различаться в зависимости от операционной системы. Элементы, обозначенные символом (Р) предписаны для UNIX стандартом POSIX. • st_dev и st_ino В системе UNIX элементы st_dev и st_ino однозначно определяют место расположения файла. st_dev — устройство, то есть раздел жесткого диска. Элемент st_ino определяет индексный дескриптор файла, его собственный оригинальный номер на жестком диске в системе UNIX. В системе Windows в элементе st_dev хранится номер дискового накопителя, значение элемента st_ino всегда равно 0. • st_mod Правые двенадцать бит элемента st_mode определяют, может ли программа считывать, записывать или выполнять файл. В среде UNIX эта информация может содержать права пользователей, групп и остального мира. Таблица 8.13 описывает, как правые девять бит элемента st_mode кодируют права1: Таблица 8.13. Права для файла Чтение Запись Выполнение Пользователь Группа Мир 4 2 1 4 2 1 4 2 1 В среде Windows права пользователей также оцениваются. Они сообщают, может ли файл быть прочитан или записан. По расширению файла система Windows определяет, можно ли его выполнить и записывает эту информацию в элемент st_mode. 1 Остальные биты специфицированы системой UNIX и описываются в руководстве stat и в книгах по среде UNIX. 399
Глава 8 В следующих четырех битах кодируется тип файла. Для разделения используется константа S_IFMT. С ее помощью можно наложить маску на эти биты. В конце можно сравнить значение со следующими константами (табл. 8.14). Таблица 8.14. Константы типа файлов Константа Тип файла S_IFSOCK S_IFLNK S_IFREG S_IFBLK S_IFDIR S_IFCHR S_IFIFO Sockets Символьные ссылки Стандартные файлы Блоки устройств Каталоги Символьные устройства FIFO Следующая программа сообщает права в качестве первого параметра передаваемого файла и устанавливает, идет ли речь о файле, каталоге или символьной ссылке. Последние являются особенностью файловой системы среды UNIX, которой нет в операционной системе Windows. #include <sys/types.h> #include <sys/stat.h> int main(int argc, char **argv) { struct stat Status; stat(argv[1], &Status); printf("Права доступа: %o \n", Status.st_mode & ~S_IFMT); switch(Status.st_mode & S_IFMT) { case S_IFREG: puts("Файл"); break; case S_IFLNK: puts("Символьная ссылка"); break; case S_IFDIR: puts("Каталог"); break; default: puts("Другое"); } } Листинг 8.19. Определение типа файла и прав доступа Константы S_IFREG (обычный файл) и S_IFDIR (каталог) поддерживаются также и системой Windows. 400
Библиотеки Так можно различать файлы и директории. Остальные типы файлов неизвестны для среды Windows. • st_nlink В этой переменной хранится количество жестких ссылок на файл. Поскольку в операционной системе Windows ссылки отсутствуют, там это значение всегда равно 1. • st_uid и st_gid Эти переменные сообщают пользователя и группы пользователей. Значением является число, которое установлено соответственно в файлах /etc/passwd и /etc/group. В операционной системе Windows оно всегда равно 0. • st_rdev Здесь кодируется основной или дополнительный номер устройства, если речь идет о расположении файла на устройстве. В операционной системе Windows здесь хранится номер привода. • st_size Если речь идет о стандартном файле, то в этой переменной располагается его размер в байтах. • st_atime, st_mtime и st_ctime Каждый доступ к файлу для чтения или записи обновляет переменную st_atime. Каждое изменение содержимого файла отмечается в переменной st_mtime. Дата изменения пользователя, прав, количества ссылок и тому подобного (то есть всего, что не касается содержимого файла) сохраняется в переменной st_ctime. 8.4. Математические функции Во многих, в том числе и старых языках программирования, имеются математические функции. Язык C разрабатывался для низкоуровневого программирования. Там математические функции не имели большого спроса. Поэтому они были помещены в отдельную библиотеку. Это уменьшает программу, которая не использует математические функции. 8.4.1. Стандартная математическая библиотека Чтобы можно было использовать функции математической библиотеки, требуется в начале программы подключить файл math.h. #include <math.h> 401
Глава 8 Тригонометрические функции Прототипы функций углов собраны в табл. 8.15. Таблица 8.15. Тригонометрические функции Объявление Функция double double double double double double double double double double Арккосинус Арксинус Арктангенс Арктангенс двух переменных Косинус Гиперболический косинус Синус Гиперболический синус Тангенс Гиперболический тангенс acos(double); asin(double); atan(double); atan2(double, double); cos(double); cosh(double); sin(double); sinh(double); tan(double); tanh(double); Все параметры передаются в радианной мере. Если требуется градусная мера, необходимо пересчитывать ее самостоятельно. К счастью, это несложно. Пересчет градусной меры α в радианную меру х выполняется по следующей формуле: x = (α / 180) π. Чтобы компилятор не подавился такой формулой, следует ее, пожалуй, сформулировать в следующем виде: rad = grad/180*3,1415926535; Пересчет радианной меры х в градусную меру α предсказуем: α = (x / 180) π. В программе это выглядит так: grad = rad*180/3,1415926535; Экспоненты, корни и логарифмы Функция exp(a) возвращает значение ea, где e является экспонентой (числом Эйлера, имеющим в десятичной системе счисления значение 2,718…): double exp(double a); Если требуется вычислить некоторую экспоненту ab, используется функция pow(): double pow(double a, double b); 402
Библиотеки Функция sqrt()1 возвращает квадратный корень числа с плавающей запятой. double sqrt(double a); Функция log() рассчитывает натуральный логарифм а, то есть логарифм числа a с базисом числа Эйлера e: double log(double a); Для расчета логарифма с базисом 10 имеется функция log10(): double log10(double a); Функция frexp() раскладывает число с плавающей запятой a так, что a = f · 2b. При этом f является возвращаемым значением функции и лежит в промежутке между 1/2 и 1. double frexp(double a, int *b); Значение b передается параметром через адрес. Вызов функции имеет вид: int b; f = frexp(a, &b); Функция ldexp() является обратной функции frexp(): double ldexp(double ai, int b); Другие функции Функция абсолютного значения возвращает значение передаваемой величины, если она положительна, и умножает ее на –1, если отрицательна. То есть функция всегда возвращает только положительное значение. Существует функция abs() для целых чисел из библиотеки stdlib и функция fabs() для чисел с плавающей запятой из математической библиотеки. #include <stdlib.h> int abs(int j); long labs(long k); Функция fabs() отличается от abs() типом параметров возвращаемого значения и тем, что для ее использования требуется подключить файл math.h. 1 Сокращение происходит от английского выражения «square root» (квадратный корень). 403
Глава 8 #include <math.h> double fabs(double a); Расчет остатка для целых чисел осуществляется оператором %. Он возвращает остаток от их деления. Для чисел с плавающей запятой такой расчет выполняется функцией fmod(). double fmod(double a, double b); Значение с плавающей запятой a раскладывается в функции modf() на целую и дробную части. Целая записывается в переменную b, а дробная является возвращаемым значением функции. double modf(double a, int* b); Функция ceil() возвращает округленное к большему значению целое число. double ceil(double); Функция floor() возвращает округленное к меньшему значению целое число. double floor(double); 8.4.2. Комплексные числа Комплексные числа состоят из действительной и мнимой частей. Класс должен содержать обе составляющие, чтобы определить комплексное число. Стандартная библиотека языка C++ предлагает класс шаблона, который может работать с тремя типами данных: float, double и long double. Тип данных указывается в угловых скобках после имени шаблона complex. #include <complex> using namespace std; complex<double> myComplex(-1, 3); Комплексное число myComplex инициализируется конструктором с реальной частью равной –1 и мнимой частью, равной 3. Элементная функция real() возвращает реальную, а функция imag() — мнимую часть комплексного числа. Для комплексных чисел также определены стандартные математические операторы +, −, / и *. Кроме того, могут использоваться операто- 404
Библиотеки ры равенства и неравенства (табл. 8.16). Операторы «больше» и «меньше» не определены ни в языке C ни в C++. Таблица 8.16. Функции для комплексных чисел Функция Действие norm() abs() conj() arg() polar() Возвращает квадрат абсолютного значения комплексного числа Абсолютное значение, корень из функции norm() Комплексное сопряженное число Угол в полярных координатах Комплексное число для полярных координат 8.5. Функции времени Функции времени не относятся к библиотекам языка C++, а являются унаследованными от языка C. В целом эта область происходит из функций среды UNIX. Однако с языком C они распространились на другие операционные системы. Оттуда же берут начало функции стандарта ANSII языка C, что является большим преимуществом в плане мобильности программного обеспечения, прежде всего при смене операционной системы или компилятора и при разработке портативного программного обеспечения. 8.5.1. Дата и время Предположительно, самое распространенное использование функций времени в программах — для получения текущей даты и времени. Функция time() возвращает текущее время. Возвращаемое значение передает прошедшие секунды с 1.1.1970. Эта функция имеет тип time_t и соответствует типу long. Однако если в программе на самом деле не требуется выводить непосредственно количество секунд с этого момента, не рекомендуется пользоваться таким соответствием. #include <time.h> time_t time(time_t *t); В качестве параметра можно либо передать адрес переменной типа time_t, в которую будет записан результат вычисления функции, либо просто использовать 0 и записывать результат в обычную переменную. Поскольку лишь немногие способны оценить количество секунд с 1.1.1970, существуют полезные функции пересчета. С помощью функ- 405
Глава 8 ции localtime() можно посредством переменной типа time_t заполнить структуру с именем tm. Она содержит элементы для составляющих частей даты и времени. #include <time.h> struct tm *localtime(const time_t *timer); Структура tm, указатель на которую возвращает функция localtime(), содержит все необходимые данные, такие как дата или день недели. Значения начинаются с 0, за исключением дня месяца. Это значение начинается с 1. Так было сделано, вероятно, для того, чтобы программисты были более осмотрительными и внимательнее читали документацию. struct tm { int tm_sec; /* секунды - [0,61] */ int tm_min; /* минуты - [0,59] */ int tm_hour; /* часы - [0,23] */ int tm_mday; /* день месяца - [1,31] */ int tm_mon; /* месяц в году - [0,11] */ int tm_year; /* год с 1900 */ int tm_wday; [0,6] */ /* дни с воскресенья (рабочий день) - int tm_yday; /* дни с нового года (1.1.) - [0,365] */ int tm_isdst; /* флаг летнего времени */ } Следующая программа использует описанные функции для вывода даты и времени. #include <iostream> using namespace std; #include <time.h> int main() { time_t TimeLabel; tm *now; TimeLabel = time(0); 406
Библиотеки now = localtime(&TimeLabel); cout << now->tm_mday << '.' << now->tm_mon+1 << '.' << now->tm_year+1900 << " - " << now->tm_hour << ':' << now->tm_min << endl; } Листинг 8.20. Вывод времени Если текущее время не требуется форматировать особым образом, например, оно используется в качестве пометки в протоколе, имеются функции asctime() и ctime(). Обе создают из полученной о дате информации символьную последовательность в американском формате. Символьная последовательность всегда состоит из 26 байт и заканчивается символом перехода на новую строку и завершающим 0. Функция asctime() содержит информацию о времени в структуре tm. Функция ctime() использует тип time_t в качестве параметра. #include <time.h> char *asctime(const struct tm *t); char *ctime(const time_t *timep); 8.5.2. Остановка времени С помощью функции time() можно получить текущее время с точностью до секунды. Если требуется еще более точное значение, можно использовать функцию gettimeofday(). Она возвращает в структуре timeval также микросекунды. Насколько такое измерение является верным, зависит также и от аппаратного обеспечения. #include <sys/time.h> int gettimeofday(struct timeval *time, void *tp); Первыми параметрами структуры являются секунды и микросекунды, они определены следующим образом: struct timeval { unsigned long tv_sec; /* секунды с 1.1.1970 */ long tv_usec; /* микросекунды */ }; 407
Глава 8 Второй параметр изначально предназначался для временно́й зоны. В целом, различные переходы на летнее время не очень просто реализовать, поэтому лучше задавать второй параметр 0. Следующий фрагмент программного кода демонстрирует, как можно провести измерение времени работы программы с помощью функции gettimetoday(). #include <iostream> using namespace std; #include <sys/time.h> int main() { timeval start, end; gettimeofday(&start, 0); ... gettimeofday(&end, 0); cout << start.tv_sec << ':' << start.tv_usec << endl; cout << end.tv_sec << ':' << end.tv_usec << endl; } Листинг 8.21. Сообщение времени работы программы Функция clock() передает потребовавшееся на выполнение программы процессорное время. Это значит, что время, за которое выполняются другие процессы, не входит в подсчет времени выполнения программы. Для этого сообщаются такты центрального процессора, прошедшие после старта программы. Чтобы преобразовать их в секунды, значение нужно разделить на константу CLOCKS_PER_SEC. Эта функция подходит, прежде всего, для измерения производительности. #include <time.h> clock_t clock(void); Следующая программа демонстрирует, как измеряется время центрального процессора. Сначала измеряется стартовое значение, поскольку нельзя гарантировать, что такты еще не были посчитаны. В кон- 408
Библиотеки це значение измеряется повторно. Программа также выводит константу CLOCKS_PER_SEC. В системах POSIX она всегда равна 1 000 000. #include <iostream> using namespace std; #include <time.h> int main() { clock_t start, end; start = clock(); ... end = clock(); cout << start << endl; cout << end << ':' << CLOCKS_PER_SEC << endl; } Листинг 8.22. Измерение тактов центрального процессора
Глава 9 СТАНДАРТНАЯ БИБЛИОТЕКА ШАБЛОНОВ (STL) STL — стандартная библиотека. Она предлагает структуры данных и методы, которые ранее снова и снова разрабатывались программистами заново. Между тем от каждого профессионального программиста C++ ожидается использование STL. STL — гибкое собрание классов шаблонов. Она предлагает к использованию так называемые контейнеры, которые помогаюn организовать однородные данные. Одна из простых форм контейнера — массив, который относится к богатствам едва ли не каждого языка программирования. Однако массив с его точным размером крайне негибок. STL предлагает ряд лишенных этого недостатка контейнеров, которые приходят на смену массиву и самостоятельно созданным связанным спискам. Чтобы получить доступ к данным контейнера, STL использует итераторы, которые можно представить в качестве вида указателей. Для контейнеров в STL имеются различные функционалы. К ним относятся функции поиска, сортировки, операторы добавления и удаления элементов. В STL функционалы обозначаются в качестве алгоритмов. STL относится к стандарту языка C++ и поддерживается любым компилятором стандарта ISO. 9.1. Контейнер класса vector Вектор похож на массив, однако значительно более гибок. Тут, как и в массиве, все элементы расположены рядом друг с другом в памяти, и можно получить доступ к элементу прямо по его позиционному номеру. Даже квадратные скобки функционируют так же, как в массиве. Новым является только то, что размер вектора не требуется четко задавать константой до компиляции. Он может быть не только вычислен во 410
Стандартная библиотека шаблонов (STL) время работы программы, но также изменен впоследствии. В качестве примера построен простой вектор из целочисленных переменных. #include <vector> #include <iostream> using namespace std; const int MAX = 8; int main() { vector<int> myvec(MAX); // вектор для 8 целых значений myvec[0] = 9; // как в массиве myvec[3] = 9; for (int i=0; i<MAX; ++i) { cout << i << ": " << myvec[i] << endl; } } Листинг 9.1. Вектор для целых значений Все выглядит так же, как в массиве. Только лишь описание позволяет определить происхождение вектора от класса шаблона. Создается шаблон vector типа int и в конструкторе параметров задается размер. Затем его можно легко изменить. Также размер можно опустить. Тогда следует вовремя увеличить вектор, прежде чем записывать в него данные. Прежде чем использовать вектор, требуется подключить файл vector. Как во всех стандартных библиотеках класс vector находится в пространстве имен std. Поэтому следует либо установить это имя глобальной командой using namespace std; либо перед каждым использованием vector ставить префикс std::. К элементу вектора можно получить доступ с помощью квадратных скобок точно так же, как это используется для массивов. В качестве альтернативы квадратным скобкам предлагается функция at(), которую можно использовать точно так же для доступа к элементам массива. Главное различие между ними заметно в случае, когда индекс слиш- 411
Глава 9 ком велик для вектора. При использовании квадратных скобок сложно предсказать, что может произойти. Например, окажется повреждена чужая область памяти. Или же доступ будет проигнорирован. При ошибке доступа программа не получит сообщения о возникшем сбое. Доступ функцией at(), напротив, будет проверен, и в случае ошибки появится исключение out_of_range1. До тех пор, пока очевидно, что границы вектора в безопасности, использование квадратных скобок является преимуществом, поскольку доступ без проверки значительно быстрее. В обратном случае функция at() убережет вас от сложных ошибок. #include <vector> using namespace std; ... int MaxVec = 5; vector<int> Numbers(MaxVec); ... Numbers[MaxVec+1] = 12; // не будет воспринято Numbers.at(MaxVec+1) = 12; // будет проверено 9.1.1. Изменение размера Как уже упоминалось, размер вектора может быть изменен в процессе выполнения программы. Элементная функция resize() получает в качестве параметра будущий размер вектора. Если параметр больше, чем текущий размер, будут добавлены следующие элементы. В противном случае вектор будет обрезан, при этом значения в его отделяемой части будут потеряны. Функция max_size() возвращает максимальный размер, который может принимать вектор. Текущий размер вектора можно установить в любой момент функцией size(). Она не имеет параметров и возвращает размер вектора. Функция empty() возвращает true, если вектор не содержит ни одного элемента. С помощью функции push_back() можно добавить в вектор отдельный элемент. В качестве параметра функция получает значение типа, с которым определен вектор. При этом его размер увеличивается на один элемент. 1 412 Тема обработки исключений рассмотрена на стр. 331.
Стандартная библиотека шаблонов (STL) Чтобы прочесть последний элемент используется функция back(). Она не имеет параметров и возвращает только значение последнего элемента. При этом вектор не уменьшается. Противоположностью данной функции является front(), которая возвращает первый элемент вектора. Посредством функции pop_back() удаляется последний элемент вектора. Эта функция также не имеет параметров. Тип возвращаемого ею значения — void, то есть она ни возвращает значение последнего элемента, ни сообщает о том, было ли действие проведено успешно. В таком случае перед вызовом pop_back() значение должно быть считано функцией back(). Приложению важно точно установить, что не происходит удаления из пустого стека. Для этого предлагается функция size(). На основе такой функциональности вектор можно представить в форме стека. Листинг 9.2 демонстрирует, как это делается: #include <iostream> #include <vector> using namespace std; int main() { vector<int> values; values.push_back(8); values.push_back(7); values.push_back(6); do { cout << ": " << values.back(); values.pop_back(); } while (values.size()); cout << endl; } Листинг 9.2. Функция стека для вектора Если требуемый вышеописанными функциями размер памяти для вектора больше, тот должен быть скопирован, поскольку он размещен линейно в памяти. Но это не ваша проблема, поскольку класс сделает 413
Глава 9 это автоматически. Что может, однако, привести к проблемам, если постоянное копирование снизит скорость выполнения программы. Для того, чтобы каждое увеличение не вызывало копирования, можно резервировать память посредством элементной функции reserve(), которая способна расширить вектор. Емкость памяти, предназначенную для вектора, возвращает элементная функция capacity(). Емкость нельзя путать с размером, который определяет элементная функция size(), так как она возвращает только размер фактически занимаемой памяти. values.resize(MaxVec+3); // здесь увеличивается размер values.reserve(20); // теперь емкость 20 cout << values.size() << " - " << values.capacity() << endl; 9.1.2. Итераторы Итераторы ведут себя точно так же, как и указатели. Они используются для получения доступа к элементу контейнера. Классы контейнеров предлагают к использованию итераторы для доступа к элементу контейнера. Итератор создается через контейнер и тип элемента. Чтобы объявить итератор для вектора целых значений можно использовать следующую строку: vector<int>::iterator it; Для итератора определены операторы * и ++. С помощью оператора ++ можно перемещаться итератором по контейнеру, а оператором * можно получить доступ к элементу. То есть оба оператора используются точно так же, как и для указателей. В массиве задается указатель на его первый элемент, и туда записывается соответствующий адрес. Чтобы определить последний элемент, требуется указатель на первый элемент увеличить на число элементов массива. Это не работает для контейнера. Чтобы установить итератор на первый элемент, класс контейнера предлагает функцию begin(). Чтобы достигнуть конца контейнера, имеется функция end(). Итератор, который она возвращает, указывает, однако, не на последний элемент, а на позицию сразу после него в неопределенной области вектора. #include <iostream> #include <vector> using namespace std; 414
Стандартная библиотека шаблонов (STL) int main() { int MaxVec = 5; int i=0; vector<int> values(MaxVec); vector<int>::iterator it = values.begin(); while(it!=values.end()) { *it = i++; // соответствует: values[i] = i++; it++; } for (it = values.begin(); it!= values.end(); it++) { cout << *it << " "; } cout << endl; } Листинг 9.3. Перемещение сквозь вектор с помощью итератора Чтобы просмотреть вектор в обратном направлении, существуют функции rbegin() и rend(). Они возвращают итератор, который идет в обратном направлении при его инкрементировании. 9.1.3. Другие функции Функция insert() добавляет элементы в вектор. Она может вызываться с различными параметрами. В большинстве случаев она вызывается с итератором и элементным объектом и служит для того, чтобы элемент был добавлен на место, указанное итератором. Когда перед элементом указано число, добавляется соответствующее ему количество объектов. Если задаются три итератора, то первый — место, куда добавляется элемент. Два других ограничивают элементы, которые будут копироваться. Функция erase() удаляет элементы из вектора. Если в качестве параметра передается только один итератор, будет удален элемент, на 415
Глава 9 который ссылается указатель. Если передаются два итератора, будет удалена область между ними. Возвращаемое значение erase() — итератор, который указывает на элемент следующий после удаленного. Функция clear() удаляет все элементы вектора. Как и для erase() при этом вызываются деструкторы удаляемых элементов. С помощью функции assign() вектор теряет все свои элементы и получает новые. Если функция вызывается с двумя итераторами, будут скопированы элементы другого вектора, которые ими ограничены. Другой вариант — когда в функцию передается число и элемент. Вектор будет заполнен этим элементом. Функция swap() обменивает содержимое векторов одинакового типа (табл. 9.1). Таблица 9.1. Функции контейнера vector Функция Действие obj& operator[] Непроверяемый доступ к элементу obj& at(size_type) Проверяемый доступ к элементу obj& front() Возвращает первый элемент вектора obj& back() Возвращает последний элемент вектора size_type size() Возвращает занимаемый размер вектора size_type max_size() Возвращает максимальный размер void resize(size_type size) Изменяет размер на значение size size_type capacity() Возвращает размер резервированной памяти bool empty() Проверяет, пуст ли вектор reserve(size_type size) Изменяет емкость памяти на n-элементов void push_back(&obj) Добавляет объект в конец вектора void pop_back() Удаляет объект в конце вектора obj& front() Возвращает ссылку на элемент в начале списка obj& back() Возвращает ссылку на элемент в конце списка iterator begin() Возвращает итератор первого элемента iterator end() Возвращает итератор позиции после последнего элемента iterator rbegin() Возвращает итератор последнего элемента iterator rend() Возвращает итератор позиции перед первым элементом void assign( InputIterator first, InputIterator last) Присваивает вектору данные, которые размещены между двумя итераторами void insert(iterator, obj&) Добавляет объект в позицию итератора 416
Стандартная библиотека шаблонов (STL) Функция Действие iterator erase(iterator first, iterator last) Удаляет область, ограниченную итераторами iterator erase(iterator) Удаляет элемент, на который указывает итератор void swap(vector<T, Allocator>& vec) Обменивает содержимое двух векторов void clear() Удаляет все элементы и вызывает их деструкторы 9.2. Контейнер класса deque Контейнер deque1 ведет себя как вектор и означает, что элементы в можно добавлять не только в конец, но в и начало контейнера. Можно представить себе очередь как два вектора. Первый реализует начало, а второй — конец очереди. Номера индексов просчитываются от первого элемента к последнему. Если один добавляется в начало, номер каждого элемента будет увеличен на 1, а новый элемент получает номер 0. У пользователя очередью создается впечатление, будто все элементы были сдвинуты на одну позицию (рис. 9.1). pop_back() 12 13 14 15 16 17 18 19 20 21 back() push_back() pop_front() 11 10 9 8 7 6 5 4 3 2 1 0 front() push_front() Рис. 9.1. Очередь В начале списка имеются функции, которые также соответствуют функциям для конца списка. Функция front() уже рассмотрена для векторов, она возвращает первый элемент. С помощью push_front() добавляется новый элемент в начало очереди. Функция push_front() ожидает в качестве параметра новый элемент. Кроме того, имеется функция pop_front(), которая удаляет элемент в начале очереди. Что1 От англ. «double ended queue» — двунаправленная очередь. 417
Глава 9 бы произвести те же операции в конце очереди, используются известные по вектору функции back(), push_back() и pop_back(). Следующий пример демонстрирует буфер FIFO1. Каждая очередь является таким буфером. Тот, кто пришел первым, идет первым в очереди. Таким образом, элементы появляются в том порядке, в котором они были записаны. В примере тысяча элементов тысячу раз записывается в буфер, а затем снова считывается. Функция clock() считает такты процессора, то есть измеряет время работы программы. #include <iostream> #include <deque> using namespace std; #include <time.h> int main() { clock_t start, end; start = clock(); deque<int> buffer; for(int i=0; i<1000; i++) { for (int j=1; j<1000; j++) { buffer.push_front(j); } do { buffer.pop_back(); } while (buffer.size()); } end = clock(); cout << start << ":" << end << endl; } Листинг 9.4. Очередь FIFO (stl/deque.cpp) 1 418 Акроним от фразы First In, First Out — «первым пришел — первым ушел». Прим. ред.
Стандартная библиотека шаблонов (STL) Если использовать вместо deque контейнер list из следующего раздела главы, можно убедиться, что очередь значительно быстрее. Причина заключается во внутренней структуре. Функции insert() и erase() позволяют вставлять и удалять элементы внутри очереди. В целом это не слишком эффективно. Быстрый прямой доступ требует, чтобы элементы располагались в памяти один за другим. Такое достоинство при добавлении и удалении оказывается недостатком, поскольку память должна при этом сдвигаться. Если подобные операции требуется проводить часто, нужно использовать контейнер list. В отличие от вектора, в очереди невозможно установить или опросить емкость. Элементные функции reserve() и capacity() не определены в классе deque. Это происходит из-за того, что класс не может определить, с какого конца он должен начинаться в области памяти. Функция resize() для увеличения контейнера при этом не страдает. Она также существует и для очереди. Подобным образом с помощью функции size() можно установить текущий размер очереди (табл. 9.2). Таблица 9.2. Функции контейнера deque Функция Действие obj& operator[] Непроверяемый доступ к элементу obj& at(size_type) Проверяемый доступ к элементу obj& front() obj& back() Возвращает первый элемент вектора Возвращает последний элемент вектора size_type size() Возвращает занимаемый размер вектора size_type max_size() Возвращает максимальный размер void resize(size_type size) Изменяет размер на значение size bool empty() Проверяет, пуст ли вектор void push_back(&obj) Добавляет объект в конец вектора void pop_back() Удаляет объект в конце вектора void push_front(obj) Добавляет элемент в начало списка void pop_front() Удаляет элемент в начале списка obj& front() Возвращает ссылку на элемент в начале списка obj& back() Возвращает ссылку на элемент в конце списка iterator begin() Возвращает итератор первого элемента iterator end() Возвращает итератор позиции после последнего элемента 419
Глава 9 Функция Действие iterator rbegin() Возвращает итератор последнего элемента iterator rend() Возвращает итератор позиции перед первым элементом void assign( InputIterator first, InputIterator last) Присваивает вектору данные, которые размещены между двумя итераторами void insert(iterator, obj&) Добавляет объект в позицию итератора iterator erase(iterator first, iterator last) Удаляет область, ограниченную итераторами iterator erase(iterator) Удаляет элемент, на который указывает итератор void swap(vector<T, Allocator>&vec) Обменивает содержимое двух векторов void clear() Удаляет все элементы и вызывает их деструкторы 9.3. Контейнер класса list В то время как вектор сравним с массивом, контейнер list можно сравнить с двусвязным списком и таким же образом в большинстве случаев реализовать. Связанный список уже был представлен в этой книге при рассмотрении программирования стека. Каждый элемент списка с помощью указателя ссылается на следующий. Элементы могут располагаться по отдельности в любом месте памяти. Поэтому контейнер не требуется копировать, когда его размер увеличивается. Новый элемент будет запрошен и добавлен в цепь. Чтобы добраться до последнего элемента программа должна перебрать все элементы списка. Двусвязный список построен так, что каждый элемент содержит два указателя. Первый ссылается на следующий элемент, второй — на предыдущий. Это ведет к определенным дополнительным затратам при добавлении элемента, но также позволяет перемещаться по списку в обоих направлениях. В отличие от очереди или вектора, преимущество списка, прежде всего, проявляется в ситуации, когда элемент в середине списка требуется удалить или добавить. При этом не нужно сдвигать элементы в списке, чтобы выделить место или заполнить образовавшуюся пустоту. При добавлении функцией insert() для нового элемента выделяется память. Он добавляется в цепь в то место, на которое ссылается итератор. Соседние элементы не обязаны при этом располагаться по порядку, и поэтому их не требуется сдвигать. То же самое действительно и для удаления из середины списка функцией erase()— пространство не нужно заполнять. В целом, получать доступ к конкретному элементу списка по его позиции не вполне эффективно. Для того чтобы добраться до пятидеся- 420
Стандартная библиотека шаблонов (STL) того элемента, программа должна просмотреть все предшествующие сорок девять. По этой причине список не поддерживает доступ к элементу через квадратные скобки. Чтобы обозначить позицию в списке, на которую должен быть помещен новый элемент или с которой требуется удалить уже имеющийся, необходим итератор. Каждый контейнер содержит определение подходящего ему итератора с очевидным именем iterator. Чтобы его объявить, требуется сначала назвать тип контейнера. Затем следуют два двоеточия, чтобы указать, что имеется в виду тип итератора, определенный в этом классе, а после идет его имя. Следующая строка объявляет итератор для списка целых значений. list<int>::iterator IntegerListIterator; Чтобы использовать итераторы, их следует инициализировать. Обычно сперва они устанавливаются на начало списка. Итератор начала контейнера возвращается элементной функцией begin(). Чтобы просмотреть контейнер, итератор инкрементируется. Элементная функция контейнера end() возвращает позицию итератора после последнего элемента списка. Как только параметр перебора элементов равен возвращаемому значению функции end() проход по списку завершается. На этом основании строится стандартный цикл for для просмотра списка. list<int>::iterator Iter; for(Iter=List.begin(); Iter!=List.end(); Iter++) 9.3.1. Добавление и удаление элементов Если внутри цикла была найдена позиция, на которую должен быть помещен новый элемент, то первым параметров функции insert() будет итератор Iter. Он указывает на элемент списка, перед которым должен быть помещен новый. List.insert(Iter, NewElement); Для удаления элемента из списка используется функция erase(). Ее единственный параметр — итератор, указывающий на элемент списка, который требуется удалить. Поскольку этот итератор будет недействителен после удаления, функция erase() возвращает итератор, который указывает на следующий за удаленным элемент. Он необходим, если список требуется просматривать далее после удаления элемента. Iter = List.erase(Iter); 421
Глава 9 В следующем листинге проводится пара экспериментов со списком, чтобы продемонстрировать работу различных функций. #include <iostream> #include <list> using namespace std; int main() { list<int> count; list<int>::iterator lIter; // объявлен итератор srand(0); // в список будет записано 10 случайных значений for(int i=0; i<10; i++) { count.push_back( rand() % 9 + 1 ); } // перебрать и вывести for(lIter=count.begin(); lIter!=count.end(); lIter++) { cout << *lIter << endl; } // перед каждым числом 8 добавить -1, удалить каждое число 2 for(lIter=count.begin(); lIter!=count.end(); lIter++) { if (8==*lIter) { count.insert(lIter, -1); } else if (2==*lIter) { J lIter = count.erase(lIter); } } // перебрать и вывести 422
Стандартная библиотека шаблонов (STL) for(lIter=count.begin(); lIter!=count.end(); lIter++) { cout << *lIter << endl; } } Листинг 9.5. Примеры работы со списком Внутри последнего цикла проявляется еще одно свойство итератора: его можно использовать в качестве указателя для доступа к объекту. Чтобы добраться до элемента, на который он указывает, перед итератором ставится символ астериска (*). Если требуется удалить несколько элементов, предлагается использовать функцию remove(). В качестве параметра ей передается объект. Все равные ему элементы будут удалены. Функция remove_if() более гибкая. В качестве параметра она получает предикат. Предикат — это функция, которая возвращает логическое значение. Параметр предиката в данном случае имеет тип элемента списка. Внутри предиката объект проверяется и возвращает true, если тот должен быть удален. 9.3.2. Перемещение элементов: splice С помощью функции splice() элементы одного списка перемещаются в другой. Операция такого рода дает очень эффективную реализацию списков, поскольку не требуется фактически перемещать данные, а только изменить указатели. В зависимости от параметра, функция splice() выполняет легко варьируемые операции. Так можно переместить отдельный элемент или весь список. Чтобы продемонстрировать различные варианты, подготовлены два списка. Список source содержит элементы 0, 1, 2, 3, а список target — элементы 100 и 200. Итератор pos указывает на элемент 200 списка target. Итератор source_begin указывает на 1, итератор source_end указывает на 3. Функция splice() вставляет в текущий список перед позицией pos элементы списка source. void splice(iterator pos, list<T>& source); 423
Глава 9 Итератор pos должен быть итератором списка, для которого выполняется splice(). Вызов имеет вид: target.splice(pos, source); После этого список target содержит элементы 100, 0, 1, 2, 3, 200. Список source будет пуст. С помощью функции splice() можно также переместить отдельный элемент одного списка в другой. void splice(iterator pos, list<T>& source, iterator source_begin); С этим параметром функция splice() вставляет в текущий список перед позицией pos тот элемент, который указан в позиции source_ begin в списке source. При этом элемент source_begin будет удален из списка source. Вызов имеет вид: target.splice(pos, source, source_begin); После этого в списке target содержатся элементы 100, 1, 200. Список source содержит 0, 2, 3. В последнем варианте будут перемещены следующие друг за другом элементы списка source. void splice(iterator pos, list<T>& source, iterator source_begin, iterator source_end); Здесь все элементы списка source от source_begin до source_end перемещены в позицию pos. Как обычно для итераторов , source_end в качестве ограничителя также указывает на позицию позади последнего перенятого элемента. target.splice(pos, source, source_begin, source_end); После этого список target хранит элементы 100, 1, 2, 200. Список source содержит 0, 3. 9.3.3. Добавление отсортированного списка Элементная функция merge() позволяет в один отсортированный список поместить другой. При этом целевой список содержит все эле- 424
Стандартная библиотека шаблонов (STL) менты в отсортированном виде. Вставляемый список пуст после выполнения функции. target.merge(source); Функции merge() можно добавить в качестве параметра еще одну функцию, которая заботится о сортировке элементов. Эта функция имеет два параметра — элементы списка, и возвращает логическое значение. list<int> source; ... bool vergl(int a, int b) return a<b; .., target.merge (source, vergl); 9.3.4. Сортировка и последовательность Функция sort() сортирует элементы списка. Помимо этой функции члена контейнера, библиотека STL предлагает также общую функцию sort(), которая может сортировать любой контейнер. Функция sort() контейнера list может извлечь выгоду из внутренней структуры списка. Как и merge(), функция sort() может передавать функцию, которая проводит сортировку. list<int> source; ... bool vergl(int a, int b) return a<b; .., target.sort (vergl); С помощью функции reverse() элементы списка выстраиваются в обратном порядке. При этом интересно то, что все итераторы остаются актуальными после выполнения функции (табл. 9.3). 425
Глава 9 Таблица 9.3. Функции контейнера list Функция Действие obj& front() Возвращает первый элемент вектора obj& back() Возвращает последний элемент вектора size_type size() Возвращает занимаемый вектором размер size_type max_size() Возвращает максимальный размер void resize(size_type size) Изменяет размер на значение size bool empty() Проверяет, пуст ли вектор void push_back(&obj) Добавляет объект в конец вектора void pop_back() Удаляет объект в конце вектора void push_front(obj) Добавляет объект в начало вектора void pop_front() Удаляет объект в начале вектора obj& front() Возвращает ссылку на элемент в начале списка obj& back() Возвращает ссылку на элемент в конце списка iterator begin() Возвращает итератор первого элемента iterator end() Возвращает итератор позиции после последнего элемента iterator rbegin() Возвращает итератор последнего элемента iterator rend() Возвращает итератор позиции перед первым элементом void assign(InputIterator first, InputIterator last) Присваивает вектору данные, которые размещены между двумя итераторами void insert(iterator, obj&) Добавляет объект в позицию итератора iterator erase(iterator first, iterator last) Удаляет область, ограниченную итераторами iterator erase(iterator) Удаляет элемент, на который указывает итератор void swap(vector<T, Allocator>&vec) Обменивает содержимое двух векторов void clear() Удаляет все элементы и вызывает их деструкторы void splice(iterator, list&) Перемещает элементы из другого списка void merge(list&) Добавляет другой список void sort() Сортирует список void unique() Удаляет все повторяющиеся элементы void reverse() Переставляет элементы в обратном порядке void remove(obj&) Удаляет элементы void remove_if(praedikat) Удаляет элементы по условию 426
Стандартная библиотека шаблонов (STL) 9.4. Контейнер классов set и multiset Контейнер set помещает добавляемый элемент сразу по порядку сортировки. В этом контейнере один элемент может встречаться только один раз. В multiset возможно повторение одинаковых элементов. Контейнер set является ассоциативным. Это значит, что он сортирует элементы по ключевому значению в бинарном дереве. Если требуется разместить данные в структуре по порядку и получить к ним быстрый доступ, то контейнер set — идеальное решение. В списке добавление неотсортированного элемента хоть и осуществляется очень эффективно, но поиск требуемой позиции занимает много времени, а сортировка достигается только функцией sort(). 9.4.1. Добавление и удаление Операторы добавления insert() и удаления erase() в контейнере set не требуют других итераторов, а ожидают значение, которое должно быть добавлено или удалено. Функция insert() возвращает итератор, который указывает на добавленное значение. Следующий пример добавляет случайные значения во множество set. Повторяющиеся значения не будут добавлены, поскольку речь идет о set, а не о multiset. В конце значение 4 будет снова удалено из множества. #include <iostream> #include <set> using namespace std; void show(set<int> &count) { set<int>::iterator iter; for(iter=count.begin(); iter!=count.end(); ++iter) { cout << *iter << " - " ; } cout << endl; } 427
Глава 9 int main() { set<int> count; int new_number; srand(0); for(int i=0; i<10; i++) { new_number = rand() % 9 + 1 ; count.insert(new_number); cout << new_number << " - " ; } cout << endl; show(count); count.erase(4); show(count); } Листинг 9.6. Добавление и удаление в классе set (stl/set.cpp) 9.4.2. Поиск и сортировка Поиск в бинарном дереве весьма эффективен. Поэтому элементная функция find() контейнера set также значительно быстрее, чем общая функция find(), которая предназначена для всех контейнеров. Она получает в виде параметра искомое значение и возвращает итератор, который указывает на элемент. Если элемент в контейнере set отсутствует, итератор идентичен возвращаемому значению функции end(). Чтобы контейнер set мог отсортировать элементы в правильном порядке, для их типа требуется определить знак «меньше». Или же при объявлении контейнера set может быть задан предикат, который определяет отношение «меньше». Для этого в качестве объекта функции строится класс, который возвращает результат сравнения. 428
Стандартная библиотека шаблонов (STL) В следующем примере создан предикат, который вместо сравнения «меньше» строит отношение «больше». При этом последовательность элементов множества будет расположена в обратном порядке. class other { public: bool operator() (int a, int b) { return a > b; } }; void show(set<int,other > &values) { set<int>::iterator iter; for(iter=values.begin(); iter!=values.end(); ++iter) { cout << *iter << " - " ; } cout << endl; } int main() { set<int,other > values; ... Листинг 9.7. Класс сравнения (stl/setsort.cpp) Класс other используется в качестве второго параметра. Таким образом, элементы будут отсортированы по уменьшению. В классе other речь идет о совершенно типичном объекте функции. Видно, что оператор вызова определен. Поскольку тип возвращаемого значения bool, речь идет о предикате. По функции show()также можно видеть, как контейнер передается в функцию в виде параметра (табл. 9.4). 429
Глава 9 Таблица 9.4. Функции контейнера set Функция Действие size_type size() Возвращает занимаемый вектором объем size_type max_size() Возвращает максимальный объем bool empty() Проверяет, пуст ли вектор iterator begin() Возвращает итератор первого элемента iterator end() Возвращает итератор последнего элемента iterator rbegin() Возвращает итератор позиции перед первым элементом iterator rend() Возвращает итератор позиции после последнего элемента void insert(iterator, obj&) Добавляет объект в позицию итератора iterator erase(iterator first, iterator last) Удаляет область, ограниченную итераторами iterator erase(iterator) Удаляет элемент, на который указывает итератор void swap(vector<T, Allocator>&vec) Обменивает содержимое двух векторов void clear() Удаляет все элементы и вызывает их деструкторы key_compare key_comp() Возвращает объект сравнения контейнера value_compare value_comp() Возвращает значение объекта сравнения контейнера iterator find(key_type&) Производит поиск первого элемента равного параметру size_type count(key_type&) Производит подсчет элементов, равных параметру iterator lower_bound() Производит поиск элемента меньшего, чем параметр iterator upper_bound() Производит поиск элемента большего, чем параметр pair<iterator,iterator>equal_ range( key_type&) Возвращает итератор элемента, равного параметру 9.5. Контейнер классов map и multimap Контейнер map принимает два вида элементов. Первый — ключ, который, как и в set, подставляется в бинарном дереве. Второй вид — элемент данных, который можно найти по ключу. 430
Стандартная библиотека шаблонов (STL) Так при определении контейнера map задаются два типа: первый для ключа и второй для значения. Квадратные скобки перегружены для map так, чтобы можно было разместить в них значение ключа. Тогда общее выражение определяет элемент данных. По этой причине использование map очень наглядно. #include <iostream> #include <string> #include <map> using namespace std; int main() { map<string,string> number; number["HH"] = "Hansestadt Hamburg"; cout << number["HH"] << endl; cout << "Размер: " << number.size() << endl; cout << number["HG"] << endl; cout << "Размер: " << number.size() << endl; } Листинг 9.8. Пример класса map (stl/map.cpp) Если запустить программу, то на экран будет выведено: Hansestadt Hamburg Размер: 1 Размер: 2 Совершенно очевидно будет в таком случае задать элемент для ключа HG, используя квадратные скобки. Если требуется только проверить, существует ли такой элемент в принципе, следует использовать функцию find(): if (number.find("KI")== number.end()) { cout << "Не найдено!" << endl; } 431
Глава 9 Таблица 9.5. Функции контейнера map Функция Действие obj& operator[](key_type&) Доступ к объекту по ключу size_type size() Возвращает занимаемый размер вектора size_type max_size() Возвращает максимальный размер bool empty() Проверят, пуст ли вектор iterator begin() Возвращает итератор первого элемента iterator end() Возвращает итератор позиции после последнего элемента iterator rbegin() Возвращает итератор последнего элемента iterator rend() Возвращает итератор позиции перед первым элементом void insert(iterator, obj&) Добавляет объект в позицию итератора iterator erase(iterator first, iterator last) Удаляет область между двумя итераторами iterator erase(iterator) Удаляет элемент, на который указывает итератор void swap(vector<T, Allocator>&vec) Обменивает содержимое двух векторов void clear() Удаляет все элементы и вызывает их деструкторы iterator find(key_type&) Ищет первый элемент равный параметру size_type count(key_type&) Подсчитывает количество элементов, равных параметру key_compare key_comp() Возвращает объект сравнения контейнера value_compare value_comp() Возвращает значение объекта сравнения контейнера iterator lower_bound() Производит поиск элемента меньшего, чем параметр iterator upper_bound() Производит поиск элемента большего, чем параметр pair<iterator,iterator> equal_range( key_type&) Возвращает итератор элемента, равного параметру 9.6. Контейнер-адаптер Контейнер-адаптер создает специальные интерфейсы, которые накладываются на контейнеры. Он всегда использует некоторый определенный тип контейнера в качестве основы, однако в тоже время может накладываться на другой контейнер. Так стек, например, может быть реализован на основе вектора. Стек предлагает пользователю ограниченный интерфейс. Адаптер не использует итераторов. 432
Стандартная библиотека шаблонов (STL) На базе контейнеров vector, deque и list реализуются соответственно контейнеры-адаптеры stack, queue и priority_queue. 9.6.1. Контейнер-адаптер stack Стек хранит элементы в порядке их записи и возвращает в обратном. В качестве интерфейса контейнер-адаптер stack предлагает следующие функции (табл. 9.6). Таблица 9.6. Функции адаптера stack Функции Действие void push(obj) Помещает объект в стек obj& top() Возвращает из стека верхний объект. При этом из стека он не удаляется. void pop() Удаляет из стека верхний объект. Функция не возвращает значений Чтобы прочесть объект из стека и затем изъять его, применяются функции top() и pop() друг за другом. Маленький пример демонстрирует, как это используется. #include <iostream> #include <stack> using namespace std; int main() { stack<int> myStack; myStack.push(3); myStack.push(2); myStack.push(1); while (myStack.empty()) { cout << myStack.top() << endl; myStack.pop(); } } Листинг 9.9. Адаптер стека (stl/stack.cpp) 433
Глава 9 Стандартно стек реализуется на базе очереди. Если требуется реализовать стек на основе списка, требуется подключить list. При определении стека задается, какой контейнер служит базисом. Здесь нужно внести следующие изменения: #include <list> #include <stack> ... stack<int, list<int> > myStack; Пробел между двумя знаками «меньше» перед словом myStack нельзя опускать. Иначе может случиться, что компилятор примет двойной символ «меньше» за оператор ввода. 9.6.2. Контейнер-адаптер queue Аналогично стеку существует адаптер, работающий в качестве FIFO — очереди. Она дополнительно к функциям стека имеет также функции front() и back(), которые возвращают один элемент. При этом функция top() для очереди отсутствует (табл. 9.7). Таблица 9.7. Функции FIFO контейнера-адаптера queue Функция Действие void push(obj) Помещает объект в конец буфера void pop() Удаляет из стека первый записанный в него объект. Возвращаемое значение отсутствует obj& front() Возвращает первый объект. При этом он не удаляется obj& back() Возвращает последний объект. При этом он не удаляется #include <iostream> //#include <list> #include <queue> using namespace std; int main() { queue<int> FIFO; // queue<int, list<int> > FIFO; 434
Стандартная библиотека шаблонов (STL) FIFO.push(4); FIFO.push(3); FIFO.push(2); FIFO.push(1); while (!FIFO.empty()) { cout << FIFO.front() << endl; FIFO.pop(); } } Листинг 9.10. Контейнер queue (stl/queue.cpp) Закомментированные строки можно использовать, если требуется установить базисом список queue. Обычно для этого используется очередь. 9.6.3. Контейнер-адаптер priority_queue Этот адаптер отличается от queue тем, что записываемые элементы получают приоритет. При этом функции данного адаптера такие же, как у стека. Обычная ожидающая очередь реализуется в качестве priority_ queue. Разница заключается в образе действия: такая очередь проверяет приоритет элементов с помощью оператора «меньше» и возвращает элемент с наивысшим приоритетом как первый. В следующем примере в очередь с приоритетом записываются целые числа, которые выводятся затем в отсортированном порядке. На практике здесь бы, конечно, объявлялись классы данных, для которых перегружался бы оператор «меньше» и возвращался их приоритет. #include <iostream> #include <queue> using namespace std; int main() { priority_queue<int> FIFO; 435
Глава 9 FIFO.push(4); FIFO.push(7); FIFO.push(5); FIFO.push(1); while (!FIFO.empty()) { cout << FIFO.top() << endl; FIFO.pop(); } } Листинг 9.11. Очередь с приоритетом (stl/pqueue.cpp) 9.7. Типы итераторов Поскольку доступ к различным контейнерам осуществляется различным образом, каждый имеет соответствующий ему итератор со стандартным именем iterator. Типы итераторов называются RandomAccessIterator, BidirectionalIterator, ForwardIterator, InputIterator и OutputIterator. В табл. 9.8 перечислены операторы, которые допустимы для соответствующих типов итераторов. Таблица 9.8. Итераторы Output Чтение Доступ Запись Итерация Сравнение *p= ++ Input Forward Bidirectional RandomAccess =*p -> ++ ==, !== =*p -> *p= ++ ==,!= =*p -> *p= ++,-==,!= =*p ->, [] *p= ++,--,+,-, +=,-= ==,!=,<,>,<=,>= За типом iterator, который предлагает контейнер, можно разместить один из перечисленных выше операторов. Какие итераторы предлагает контейнер, зависит от возможности доступа, который он имеет. В табл. 9.9 указано, какие контейнеры какими типами итераторов предлагаются. 436
Стандартная библиотека шаблонов (STL) Таблица 9.9. Контейнеры и итераторы Контейнер Итератор vector deque string list map multimap set multiset RandomAccessIterator RandomAccessIterator RandomAccessIterator BidirectionalIterator BidirectionalIterator BidirectionalIterator BidirectionalIterator BidirectionalIterator 9.8. Алгоритмы библиотеки STL Библиотека STL наряду с контейнерами распоряжается рядом собственных функций, которые можно использовать для всех видов контейнеров. Сюда относятся даже массивы. Доступ к элементам осуществляется через итераторы. Для массивов используются обычные указатели. Такие функции обозначаются в STL как алгоритмы. Пользователя, знакомого с информатикой, может сбить с толку это название, поскольку там алгоритмы представляют собой пошаговое графическое описание действий для решения задачи. В этой особой связи можно оправдать данное имя, поскольку эти функции принадлежат библиотеке и работают со всеми ее типами. Чтобы использовать алгоритмы STL требуется подключить заголовочный файл algorithm и установить пространство имен std: #include <algorithm> using namespace std; 9.8.1. Поиск: find() Функция find() производит поиск элемента, заданного третьим параметром, в контейнере, чьи границы заданы двумя первыми параметрами. Возвращаемое значение — итератор на первый найденный элемент. Прототип функции имеет вид: template <class InputIterator, class T> InputIterator find(InputIterator first, InputIterator last, const T& value); Листинг 9.12. Прототип find() 437
Глава 9 В простом случае, где контейнером служит массив, итератором является указатель. Указатель first ссылается на первый элемент, который учитывается. Указатель last ссылается на позицию после последнего элемента. Возвращаемый указатель ссылается на первый элемент, который равен искомому значению. Когда требуется найти все элементы, можно инкрементировать возвращаемый указатель и использовать его в качестве первого параметра при следующем вызове функции. Если в контейнере не содержится искомого элемента, возвращаемый указатель ссылается на позицию после последнего элемента. То есть он идентичен указателю нижней границы. Следующая программа ищет в целочисленном массиве число 4. Результатом является указатель на элемент. #include <iostream> #include <algorithm> using namespace std; int main() { int values[9] = {1,2,3,4,5,6,7,8,9}; int *found; int *last = values+9; found = find(values, last, 4); if (found!=last) { cout << *found << " - " << endl; } else { cout << "Не найдено!" << endl; } } Листинг 9.13. Поиск числа в массиве (stl/find.cpp) 9.8.2. Сортировка Функция sort() сортирует область контейнера, которая передается с помощью итераторов. В качестве контейнера для простоты будем 438
Стандартная библиотека шаблонов (STL) использовать массив целых чисел. В качестве итератора — указатель на int. #include <iostream> #include <algorithm> using namespace std; int main() { int values[9] = {3,8,1,9,5,6,4,2,9}; sort(values, values+9); for (int i=0; i<9; i++) { cout << values[i]<< endl; } } Листинг 9.14. Сортировка массива Алгоритм STL sort() требует прямой доступ к элементу через квадратные скобки. Это значит, ему нужен итератор RandomAccessIterator. Далее функция sort() может использоваться только для тех типов элементов, для которых определен оператор «меньше». Если требуется запустить sort() для контейнера с собственно написанным классом, необходимо перегрузить оператор «меньше». Алгоритм библиотеки, который был представлен выше на примере массива, точно так же работает для вектора. #include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { int MaxVec = 9; vector<int> values(MaxVec); srand(0); 439
Глава 9 for(int i=0; i<MaxVec; i++) { values[i] = rand() % 9 + 1; } sort(values.begin(),values.end()); for (int i=0; i<MaxVec; i++) { cout << values[i] << endl; } } Листинг 9.15. Сортировка вектора 9.8.3. Двоичный поиск Если в контейнере находится отсортированная область, то для поиска в ней некоторого значения допустимо использовать функцию binary_search(). Этот поиск очень быстрый, однако зависит от того, можно ли получить прямой доступ к элементу контейнера. Поэтому данная функция примененима к вектору или массиву, но не может использоваться, например, для списка. if (binary_search(values.begin(),values.end(), SearchValue)) Функция возвращает логическое значение, которое сообщает, найдено ли значение. 9.8.4. Копирование: copy() Функция copy() копирует область одного контейнера в другой. int main() { int values[9] = {1,2,4,9,5,6,7,8,9}; int target[5]; copy(values, values+5, target); } Листинг 9.16. Копирование 440
Стандартная библиотека шаблонов (STL) Здесь будут скопированы первые пять элементов массива values в массив target. При этом функция copy() не контролирует, превышена ли емкость целевого объекта. 9.8.5. Перестановка: reverse() Функция reverse() переставляет элементы контейнера так, что после вызова они располагаются в обратном порядке. Следующий пример использует сразу два алгоритма STL. Сначала аргумент программного вызова с помощью функции copy() копируется в локальный массив char с именем word. Затем к этому массиву применяется функция reverse(), которая переворачивает символьную последовательность. #include <iostream> #include <algorithm> #include <string> using namespace std; int main(int argc, char **argv) { char word[80]; copy(argv[1], argv[1]+strlen(argv[1])+1, word); reverse(word, word+strlen(word)); cout << word << endl; } Листинг 9.17. Перестановка передаваемого параметра (stl/reverse.cpp) При вызове программы любой код передается в виде параметра. Программа выдает его в обратной последовательности (в обратном порядке). 9.8.6. Заполнение: fill() Функция fill() заполняет контейнер значениями. Параметрами функции являются начало и конец заполняемой области и значение, которым она заполняется. 441
Глава 9 int main() { int values[9] = {1,2,4,9,5,6,7,8,9}; fill(values, values+9, 4); } Листинг 9.18. Заполнение Каждый элемент массива values будет заполнен значением 4. 9.8.7. Функция equal() С помощью функции equal() можно проверить равенство двух областей. В качестве параметра передается начало и конец первой области, затем элемент в начале второй области, с которого должно начинаться сравнение. if (equal(values, values+3, vgl)) Команды после if выполняются, если три первых значения массива values равны трем первым значениям массива vgl. 9.8.8. Функция в качестве параметра: find_if() Некоторые функции STL вызывают функции, передающиеся в виде параметров. Один из простых примеров — функция find_if(). Ее третий параметр — функция, тип возвращаемого значения которой должен быть bool. Кроме того, ее параметр обязан соответствовать типу, который принимает контейнер. В пропозициональной логике булев член обозначается как предикат. Это обозначение используется в STL для функций с возвращаемым типом bool. Функция find_if(), как и функция find(), просматривает область контейнера. При этом в качестве третьего параметра передается не значение, а функция, которая вызывается функцией find_if() для каждого элемента просматриваемой области. Если функция возвращает true, find_if() завершается и возвращает идентификатор (в данном простом случае — указатель) найденного значения. Поскольку функция 442
Стандартная библиотека шаблонов (STL) find_if() может передать проверяемый элемент в качестве предиката, он должен принимать параметр типа элементов, из которых состоит контейнер. С помощью функции find_if() можно производить поиск элементов, у которых одинаковы только определенные составные части , или же их поиск осуществляется на основании свойств, которые нельзя описать простой логической комбинацией. Следующая программа использует в качестве предиката функцию is_nine(). Она должна иметь передаваемый параметр типа int и возвращать значение bool. Как уже указывает имя, функция принимает значение true, если передаваемый параметр равен 9. #include <iostream> #include <algorithm> using namespace std; bool is_nine(int parameter) { return (parameter==9); } int main() { int values[9] = {1,2,3,9,5,6,7,8,9}; int *found; found = find_if(values, values+9, is_nine); if (found!= values+9) { cout << *found << " - " << endl; } else { cout << "Не найдено" << endl; } } Листинг 9.19. Поиск числа в массиве 443
Глава 9 Поскольку функция очень гибкая, таким образом можно производить поиск по любым критериям. Например, предикат может сообщать, является ли число простым. В некоторых случаях, тем не менее, функция недостаточно гибка. Рассмотрим ситуацию, когда требуется найти первое число, которое меньше предшественника в контейнере. В предыдущем примере это было бы 5. В функции это можно сформулировать так: bool little(int parameter) { static last = INT_MIN; bool back_value; back_value = parameter < last; last = parameter; return back_value; } При первом запуске это работает прекрасно. В целом функция не нужна в конце, поскольку она уже записала в свою статическую переменную last значение 5. Но если она будет использована заново для такого же массива, в качестве искомого числа будет сообщено значение 1, поскольку оно меньше 5. Чтобы решить эту проблему, можно определить переменную last в качестве глобальной, которая должна сбрасываться перед каждым вызовом find_if(). Поскольку глобальных переменных следует избегать, такое решение не слишком элегантно1. Если речь идет о проблемах инициализации, предлагается использовать класс. Здесь переменная может быть легко сброшена в конструкторе. Чтобы соответствовать требованиям find_if(), оператор функции перегружается и передается объект класса. Программа выглядит следующим образом: #include <iostream> #include <algorithm> #include <limits.h> using namespace std; 1 Использование глобальных переменных всегда проблематично, если программа в будущем должна работать в несколько потоков. 444
Стандартная библиотека шаблонов (STL) class tLittle { public: tLittle() { last = INT_MIN; } bool operator()(int a); private: int last; }; bool tLittle::operator()(int a) { bool back_value; back_value = a < last; last = a; return back_value; } int main() { int value[9] = {1,2,3,9,5,6,7,8,9}; int *found; tLittle back_case; found = find_if(value, value+9, back_case); if (found!=value+9) { cout << *found << endl; } else { cout << "Не найдено" << endl; } } Листинг 9.20. Использование объекта функции (stl/findif.cpp) Нужно упомянуть, что для каждого вызова find_if() нужно использовать новый объект класса tLittle, чтобы конструктор сбрасывал значение переменной last. Альтернатива — добавление в класс функ- 445
Глава 9 ции reset(), которая это перенимает. Можно также вставить следующую строку: found = find_if(value, value+9, tLittle()); Здесь вызывается конструктор класса tLittle, передающий функции временный объект, который затем снова вызывается в качестве предиката. Тогда можно опустить также строки с определением переменной back_case. 9.8.9. Функция for_each Функция for_each() выполняет функцию, которую она получает в качестве третьего параметра, для всех элементов контейнера, чьи границы ей передаются в качестве первых двух параметров. Следующий пример вызывает функцию show(), которая выводит эти элементы на экран. Результатом программы является вывод массива на экран. #include <iostream> #include <algorithm> using namespace std; int show(int a) { cout << a << endl; } int main() { int values[9] = {1,2,4,9,5,6,7,8,9}; for_each(values, values+9, show); } Листинг 9.21. Вывод с помощью for_each() (stl/foreach.cpp) Параметр функции show() принимает элемент контейнера. Возвращаемое значение должно быть такого же типа. Функция for_each() передает возвращаемое значение последней функции программе, которая ее вызывает. 446
Стандартная библиотека шаблонов (STL) 9.9. Класс шаблона bitset Класс шаблона bitset также относится к STL, но не является контейнером, поскольку не принимает пользовательских типов данных для их организации. Он предлагает возможность управлять битовыми структурами. Параметр, принимаемый шаблоном, является не типом, а целочисленной константой, которая указывает, сколько бит занимает bitset. С помощью функции set() можно устанавливать отдельные биты и сбрасывать их функцией reset(). Элементная функция flip() переключает бит из true в false и наоборот. В качестве параметра передается позиция желаемого бита. Если не передан параметр, функция влияет на все биты bitset. Элементная функция test() возвращает значение типа bool, которое сообщает, установлен ли бит, чья позиция передается в функцию параметром (табл. 9.10). Таблица 9.10. Функции класса bitset Функция Действие bool test(size_t pos) Возвращает true, если бит установлен bitset<N>& set(size_t pos, int var=1) Устанавливает бит в позиции pos bitset<N>& reset(size_t pos, int var=0) Удаляет бит в позиции pos bitset<N>& flip(size_t pos) Переключает бит в позиции pos Класс bitset можно обрабатывать с помощью индексного оператора, как обычный массив. При этом доступ более очевиден. Элементная функция count() возвращает количество установленных бит в bitset, функция size() — размер bitset в битах. Функция any() возвращает true, если установлен хотя бы один бит в bitset. Функция none() возвращает true, если не установлен ни один бит (табл. 9.11). Таблица 9.11. Другие функции класса bitset Функция Действие size_t count() Количество установленных бит в bitset size_t size() Количество бит в bitset bool any() Возвращает true, если установлен хотя бы один бит bool none() Возвращает true, если не установлен ни один бит Класс bitset использует функции to_ulog() и to_string() для передачи объекта bitset в виде целого числа или символьной последовательности. 447
Приложение А ЯЗЫК C++ ДЛЯ ТЕХ, КТО ТОРОПИТСЯ Если вы хотите быстрее разобраться в теме или уже имеете начальные знания программирования, тогда это приложение для вас. Здесь в быстром темпе рассматривается язык C++. Более подробное описание каждой темы вы найдете в соответствующих разделах книги. Чтобы сжать материал, пришлось, разумеется, пожертвовать некоторыми деталями. Кроме того, отдельные описания более наглядны, чем научно точны. А.1. Программа Первая программа выводит на экран сообщение, требует ввод строки, рассчитывает квадрат и выводит его значение на экран. #include <iostream> using namespace std; int main() { int input; int Square; cout << "Введите число: "; cin >> input; Square = input * input; cout << "Квадрат числа равен " << Square << endl; } Листинг А.1. Квадрат числа После того, как код этой программы набран в редакторе и сохранен, например, с именем first.cpp, ее требуется скомпилировать. В операционных системах UNIX или Linux достаточно выполнить команду: 448
Язык C++ для тех, кто торопится > c++ erst.cpp > После успешной компиляции программу надо запустить. Если используется IDE, можно запустить ее прямо оттуда. Из командной строки программа будет сохранена с именем a.out, и запущена: > ./a.out Введите число: После запуска программы на экране появится запрос «Введите число:». Программа ожидает от пользователя ввода числа, завершающегося нажатием клавиши Enter. Затем на экране появляется сообщение «Квадрат числа равен » и следует результат вычисления. Пробежимся по программе строка за строкой. #include <iostream> Первая строка содержит команду #include. Она подключает файл iostream в исходный код программы. Он требуется, чтобы привязать информацию о программных библиотеках. В данном случае речь идет о библиотеке для ввода и вывода (iostream). Основные функционалы для этого не требуется писать самостоятельно, они передаются компилятором из стандартных библиотек и их можно просто брать оттуда. Если требуется использовать ввод или вывод, в начале программы нужно ввести эту команду и подключить файл iostream. Он содержит информацию, которая требуется компилятору, чтобы работать с библиотекой. Какие файлы должны подключаться командой #include можно найти в описании используемых функций. using namespace std; Здесь устанавливается пространство имен std в глобальном пространстве имен. Стандартные библиотеки добавляют перед всеми именами префикс std::. Вышеуказанная строка позволяет получить доступ к именам библиотек без указания этого префикса. int main() { Имя main() начинает главную функцию программы. Слово int означает тип функции, который для начала вам не требуется. Каждая программа на языке C или C++ имеет только одну функцию main(). 449
Приложение А С нее после запуска программы начинается выполнение. Каждая функция, включая main(), содержит ряд команд, которые заключены в фигурные скобки. Открытая фигурная скобка указана на следующей строке и объявляет начало программы. Принадлежащая ей закрывающаяся фигурная скобка располагается в конце листинга. Как можно заметить, следующие строки программы отделены отступами. Это делать необязательно. В языке C++ можно упорядочить код программы так, как хочется. В целом, с практической точки зрения рекомендуется после каждой открытой фигурной скобки делать отступ на каждой следующей строке программы и убирать его после закрытия скобки. Так относящиеся друг к другу скобки всегда расположены на одном уровне, и можно быстро заметить, если скобка случайно забыта. int input; int Square; Первая строка в теле функции содержит первую команду. Каждая команда заканчивается точкой с запятой. Данная команда — объявление переменной. Здесь устанавливается, что есть переменная с именем input, которая может хранить целое число. То, что переменная может содержать целое число, сигнализирует тип int, который указан перед ее именем. Ключевое слово int происходит от английского слова «integer», это понятие, обозначающее целое число, то есть без знаков после запятой. Следующая строка также является объявлением переменной. Здесь определяется Square. Имена переменных можно выбирать любые. Я назвал первую переменную input, потому что далее в нее будет записаны вводимые пользователем данные. Вторая носит имя Square, поскольку позднее она будет содержать значение квадрата числа. Подробную информацию об именах переменных вы найдете на стр. 28. Наряду с типом int имеется целочисленный тип short для малых чисел и long для больших (см. стр. 35). Для чисел с плавающей запятой, то есть таких, которые имеют знаки после запятой, имеется тип float и тип с большей точностью double (см. стр. 41). Для отдельной буквы существует тип char, для буквы стандарта UTF-16 — тип wchar_t. Буквы можно узнать в исходном коде по тому, что они заключены в одинарные кавычки ('A'). Символьная последовательность, то есть набор букв, напротив, заключается в двойные кавычки ("Программирование — это интересно"). В переменной типа char можно помимо букв хранить маленькие целые числа (см. стр.39). 450
Язык C++ для тех, кто торопится Таблица А.1. Основные типы Тип Данные int short long float double long double char wchar_t Целые числа Целые числа (обычно от -32.768 до 32.767) Целые числа (обычно от -2.147.483.648 до 2.147.483.647) Числа с плавающей запятой (например -2,5 или 3,14159) Числа с плавающей запятой высокой точности Числа с плавающей запятой особо высокой точности Буквы или целые числа от -128 до 127 UNICODE буквы для интернациональных наборов символов cout << " Введите число: "; Вывод на экран реализуется в языке C++ через объект cout. Объект вывода cout всегда указывается слева, затем следуют два знака «меньше» (<<), которым обозначается оператор потока, а затем то, что должно выводиться на экран. В данном случае — это код, заключенный в кавычки. Они не выводятся на экран,но все, что в них указано, будет выведено символ за символом. В этом случае программа сообщает пользователю, что она от него хочет. cin >> input; Противоположностью объекта cout является объект cin. С помощью двух символов «больше» (>>) вводимые данные передаются в переменную. Переменная input получает свое значение прямо с клавиатуры. Дальнейшую информацию по теме ввод и вывод можно найти на стр. 65 или на стр. 376. Square = input * input; Команды в следующей строке рассчитывают квадрат числа. С левой стороны знака равенства находится строка расчета. Результат того, что указано справа от него, присваивается переменной Square. Поэтому знак равенства называется оператором присваивания. С правой стороны находится выражение. Так называется вычисление, которое возвращает сохраняемый результат. Астериск (*) — это символ умножения, то есть здесь происходит перемножение содержимого переменной input на само себя. Результат записывается в переменную Square. Поскольку в переменной input сохранена информация, введенная пользователем с клавиатуры, после выполнения этой строки желаемый результат находится в переменной Square. 451
Приложение А Наряду с символом астериска (*) для умножения, используется слеш (/) для деления, плюс (+) для сложения и минус (−) для вычитания. В табл. А.2 представлен перечень основных операторов. Больше информации по этой теме вы найдете на стр. 58. Таблица А.2. Основные операторы Оператор Значение + − * / = << >> Сложение Вычитание Умножение Деление Присваивание Передача (обычно после cout) Передача (обычно после cin) Для определенных расчетов в языке C++ имеются сокращения. Так, с помощью выражения а+=5 содержимое переменной a будет увеличено на 5. Аналогичное выражение — а=а+5 — такое написание функционирует также с другими операторами: −=, *= и /=. Так, командой а/=2 содержимое переменной a будет разделено на 2. Если требуется увеличить переменную на 1, можно написать а++. Это работает также и с минусом а−− — здесь переменная a будет уменьшена на 1. cout << "Квадрат числа равен " << input << endl; Теперь результат требуется вывести на экран. В конце концов пользователь хочет узнать, что вычислил компьютер. Для этого снова используется команда cout. Здесь сначала выводится код в кавычках. Как и прежде, кавычки при этом не отображаются, зато выводятся все символы, которые программист в них разместил. Содержимое будет не интерпретировано, а выведено непосредственно. Затем снова появляется оператор передачи, после чего следует слово «Square». Поскольку оно указано не в кавычках, будет получен доступ к переменной Square и выведено ее содержимое. Ключевое слово endl завершает строку. Следующий вывод производится с новой строки. А.2. Условия и циклы Программа не обязана выполняться строка за строкой. Можно вызывать или повторять части программы в зависимости от состояний переменных. 452
Язык C++ для тех, кто торопится А.2.1. Условия и булевы выражения Посредством проверки условия можно назначить выполнение блока команд в зависимости от результата проверки. Например, можно проверить, не равен ли делитель нулю. if (divisor==0) { cout << "Деление на 0" << endl; } else { quotient = dividend / divisor; } Листинг А.2. Защита от деления на 0 Проверка условия начинается ключевым словом if, в скобках располагается выражение условия, в зависимости от которого будет выполняться следующий блок команд. Здесь проверяется, равно ли содержимое переменной divisor нулю. Если это так, то программа выдает сообщение об ошибке «Деление на 0». Следующая команда else начинает блок команд, который должен быть реализован, если условие не выполняется. Использование команды else не обязательно, его можно опустить. Подробную информацию по операторам if и else вы найдете на стр. 71. Для проверки равенства двух значений в языке C++ используется два символа равно (==), эта комбинация была введена для того, чтобы можно было отличить ее от оператора присваивания. Не путайте! Компилятор в лучшем случае выдаст предупреждение, если вместо сравнения в скобках будет указано присваивание. Символ неравенства состоит из восклицательного знака и знака равно (!=). Можно сравнить два значения символом «меньше» (<) — является ли первое значение меньше второго. Аналогично с символом «больше» (>) вы можете вычислить, является ли первое число больше второго. При добавление знака равенства можно также проверить условие: меньше или равно (<=) и больше или равно (>=) (см. стр. 83). 453
Приложение А if (input>=5 && input<=10) Листинг А.3. Фрагмент кода Два амперсанда (&&) означают символ «И» и реализуют И-объединение двух логических выражений. В примере показано, как проверить, является ли содержимое переменной input больше или равно 5 и меньше или равно 10. Оба условия должны соблюдаться, чтобы выполнилось общее условие. Так проверяется, находится ли введенное значение в интервале от 5 до 10.  Общее условие И-объединения будет истинным, если оба составляющих его условия также истинны. ИЛИ-объединение определяется в языке C++ через две вертикальные черты (||). Общее выражение будет истинным, если хотя бы одно из составных условий истинно. Данное объединение ИЛИ не является объединением ИЛИ-ИЛИ.  Общее условие ИЛИ-объединения будет истинным, если хотя бы одно из составных условий истинно. Если требуется выразить противоположное условие, можно заключить исходное в скобки и указать перед ними восклицательный знак. Отрицание логического выражения обозначается в языке C++ с помощью восклицательного знака. Если требуется выразить, что введенное значение не находится в интервале между 5 и 10, можно просто написать так: if (!(input>=5 && input<=10)) Листинг А.4. Фрагмент программы Скобки, перед которыми указан восклицательный знак, определяют, что должно быть инвертировано все условие, а не только первая его часть. Скобки можно использовать в логических выражениях для расстановки приоритетов. Более подробную информацию по условиям и объединениям вы найдете на стр. 87. В табл. А.3 перечислены логические операторы. 454
Язык C++ для тех, кто торопится Таблица А.3. Булевы операторы Оператор Значение == != < <= > >= ! && || Равенство Неравенство Меньше Меньше или равно Больше Больше или равно Отрицание булева выражения (NOT) И-объединение (AND) ИЛИ-объединение (OR) А.2.2. Цикл while В цикле команда или блок команд может повторяться так долго, как это указано в его условии. #include <iostream> using namespace std; int main() { int InValue = 0; while (InValue<1 || InValue>6) { cout << "Введите число от 1 до 6!"; cin >> InValue; } } Листинг А.5. Ввод числа Программа станет повторять цикл while , пока переменная InValue будет меньше 1 или больше 6. Иначе говоря, программа покинет цикл, если значение этой переменной будет находиться в диапазоне от 1 до 61. Речь идет о проверке пользовательского ввода. Как и для обычной проверки, здесь условие также заключено в скобки сразу после команды while. 1 Относительно этого преобразования см. закон де Моргана на стр. 91. 455
Приложение А Внутри цикла находится команда на ввод числа в диапазоне от 1 до 6. После этого пользователь может ввести число, которое запишется в переменную InValue. Эти две команды будут повторяться, пока выполняется условие. При этом разработчиком обеспечено, что внутри цикла можно встретить событие, которое заботится о том, что он однажды будет покинут. Иначе программа окажется в бесконечном цикле. Важно также то, что происходит перед циклом. За счет присваивания 0 переменной InValue обеспечивается заход в цикл. Если бы переменная InValue имела значение в диапазоне от 1 до 6 еще до начала цикла, то заход в него не произошел бы вообще. Подробная информация о цикле while находится на стр. 94. А.2.3. Цикл for Цикл for является особой формой цикла while. Инициализация, условие пребывания в цикле и команда перехода на следующий шаг находятся в первой команде цикла. Тем самым достигается, что не будет забыт ни один из важных аспектов цикла. Цикл for более комплектный, чем цикл while, и при этом легче читается, если к нему привыкнуть. Цикл for особенно полезен там, где следует производить перебор элементов. Следующая программа выводит на экран список первых десяти квадратов числа. #include <iostream> using namespace std; int main() { int i; for (i=1; i<=10; i++) { cout << i << ": " << i*i << endl; } } Листинг А.6. Квадраты числа 456
Язык C++ для тех, кто торопится Если запустить эту программу, в левой колонке появятся числа от 1 до 10. В каждой строке после числа будет стоять двоеточие, пробел и квадрат этого числа. В скобках оператора for указано три элемента, разделенных точкой с запятой. Первый элемент — команда инициализации. Она вызывается единожды перед началом выполнения цикла. Здесь переменная перебора обычно инициализируется с начальным значением. В середине, между двумя точками с запятой, указано условие цикла. Это условие, по которому цикл должен выполняться дальше. При переборе здесь будет проверяться, находится ли переменная все еще в требуемом диапазоне. Третий элемент — это завершающая команда. Она выполняется после каждого выполнения тела цикла. Обычно она заботится, чтобы значение переменной изменялось так, чтобы условие цикла однажды перестало выполняться, и программа вышла из него. Например, здесь производится увеличение переменной счетчика. Подробная информация о цикле for указана на стр. 99. А.3. Массивы Массив — это объединение нескольких переменных одного типа. При этом они стоят в ряд друг за другом. Целый массив имеет имя. Чтобы обратиться к отдельному элементу массива, нужно указать номер его позиции в квадратных скобках. Типичный пример массива — числа лото. Здесь речь идет о шести целых равноценных числах, которые интересны только при рассмотрении их всех вместе. Можно создать переменную массива с именем lotto. Она должна иметь достаточно места для хранения шести чисел. Для обращения к отдельному числу, называется имя переменной lotto и в квадратных скобках указывается позиция, в которой сообщается число. Например, lotto[3]. Во всех массивах первый элемент расположен в позиции 0. Поэтому lotto[0] будет первым числом, а lotto[3] — четвертым. На рис. А.1 представлено это объединение. 5 lotto[0] 12 lotto[1] 13 28 lotto[2] lotto[3] 32 lotto[4] 43 lotto[5] Рис. А.1. Массив для чисел лото 457
Приложение А В следующем примере объявляется переменная массива и заполняется числами лото с рис. А.1. В конце они выводятся в цикле for. #include <iostream> using namespace std; int main() { int lotto[6]; lotto[0] = 5; lotto[1] = 12; lotto[2] = 13; lotto[3] = 28; lotto[4] = 32; lotto[5] = 43; int i; for (i=0; i<6; i++) { cout << lotto[i] << " "; } cout << endl; } Листинг А.7. Числа лото Объявление массива аналогично объявлению переменной с правой привязкой. Сначала называется тип элементов, затем имя массива, после чего указываются типичные квадратные скобки. Между скобками указывается размер массива, то есть количество элементов. В нашем примере создается массив для шести элементов. Они будут перебираться с индексами от 0 до 5. Наиболее часто встречающийся массив состоит из символов типа char. Одна буква занимает один байт. Если расположить их рядом друг с другом получится текст, например имя, адрес или пароль. Для построения таких кодовых переменных создается массив типа char и заполняется элементами с позицией 0. Конец кода помечается байтом, в котором хранится ноль. Его нельзя путать с символом '0'. 458
Язык C++ для тех, кто торопится График на рис. А.2 демонстрирует массив char, в котором первые три элемента имеют значения 'K', 'a' и 'i'. В четвертом, с индексом 3, находится 0. Состояние двух других элементов не определено. ’K’ ’a’ ’i’ 0 [0] [1] [2] [3] ’z’ [4] ’M’ [5] Рис. А.2. Заполненный массив char Следующая программа уточняет, как нужно вводить код. Для этого используется функция cin.getline(). В конце имя сначала выводится, а затем копируется в другую переменную. Для проверки копия также выводится на экран. #include <iostream> using namespace std; int main() { char Name[80]; cout << "Как Вас зовут?" << endl; cin.getline(Name, 80); cout << "Добрый день, " << Name << "!" << endl; char Copy[80]; int i; for (i=0; Name[i] && i<79; i++) { Copy[i] = Name[i]; } Copy[i] = 0; cout << "Это Ваша копия: " << Copy << endl; } Листинг А.8. Приветствие (hi.cpp) В начале снова подключается файл iostream. Затем начинается функция main(). 459
Приложение А char Name[80]; Для вводимого имени создана переменная массива Name, которая может хранить до 80 символов. Поскольку каждая символьная последовательность требует завершающий ноль, в нашем распоряжении имеется 79 символов. cout << "Как Вас зовут?" << endl; cin.getline(Name, 80); Сначала программа выдает на экран сообщение, в котором просит пользователя ввести его имя. В следующей строке введенное значение считывается и записывается в переменную Name. Здесь снова используется объект ввода cin. Однако теперь ввод передается в переменную не оператором >>. Причина состоит в том, что запись в переменную оборвется, если встретится пробел. Вместо этого требуется передать всю строку. Так будет передано полное имя, фамилия и отчество, разделенные пробелами. Для этого вызывается функция cin.getline(). Между скобками сначала должен указываться массив, в который записывается строка. Второй параметр — максимальное количество букв. Он нужен для того, чтобы кто-нибудь не ввел больше букв, чем может поместиться в массиве. cout << "Добрый день, " << Name << "!" << endl; Теперь выводится приветствие, к которому будет добавлено только что введенное имя. Это заметно по тому, что имя указано не в кавычках. В таком случае всегда имеется в виду переменная. Здесь также видно, что массив char можно вывести непосредственно, без отдельного доступа к каждой букве. char Copy[80]; int i; В вышеприведенном коде определены две переменные. Первая — массив, который объявлен точно так же, как и Name. Вторая переменная, i, используется в качестве счетчика и индекса. for (i=0; Name[i] && i<79; i++) Здесь начинается цикл for, который копирует содержимое переменной Name в переменную Copy. Цикл сначала устанавливает индекс i равным 0. 460
Язык C++ для тех, кто торопится Между точками с запятой указано условие, по которому программа остается в цикле. Оно выглядит несколько оригинально. Рассмотрим сначала левую часть до И-объединения оператором &&. Name[i] возвращает символ, который находится на текущей позиции. Поскольку каждая символьная последовательность заканчивается 0, а 0 интерпретируется в языке C++ как false, цикл заканчивается сразу же, как только в оригинале встречается конец строки. Для И-объединения достаточно того, чтобы одна из частей условия была false, тогда все условие не допустит того, чтобы программа оставалась в цикле. Чтобы она продолжала работу и далее, необходимо выполнение условия, что символьная последовательность должна быть меньше 79 символов. Индекс массива Copy должен быть меньше 79, поскольку массив объявлен так, что в нем может храниться не более 80 элементов. Поскольку первый элемент, как и в любом массиве, имеет позицию 0, самый последний символ располагается на позиции 79. Раз это символьная последовательность, должно быть место для завершающего нуля. Так что индекс будет меньше 79. В третьей части в качестве замыкающей команды переменная i увеличивается на 1. Этим гарантируется, что цикл закончится, как только i станет равно 79 или больше. { Copy[i] = Name[i]; } Фигурные скобки принадлежат циклу for и отделяют команды, которые должны выполняться в цикле. В данном случае это одна строка. Здесь i-тая буква массива Name будет скопирована в соответствующий элемент массива Copy. Поскольку цикл заботится о том, чтобы индекс i изменялся от 0 до последней буквы, все имя будет перемещено. Copy[i] = 0; В этой строке в копию добавляется завершающий ноль. Цикл копирует только содержимое. Как только в оригинале достигается 0, цикл прерывается, и копия не перенимает его. Это достигается в данной строке после цикла. Индекс i после цикла на 1 больше, чем индекс последнего скопированного символа. По теме символьных последовательностей см. информацию на стр. 127 и класс string на стр. 355. 461
Приложение А А.4. Функции С помощью функции можно объединить несколько команд так, чтобы их можно было запускать из любого места программы одной командой. При этом большие программы становится легче прочесть. Если в функции находится последовательность команд, которые часто требуется повторять, то так программа становится еще и короче. А.4.1. Разделение программ В примере программа из листинга А.8 разложена на три функции. Первая принимает вводимые пользователем данные, вторая копирует Name, а третья выдает сообщение. #include <iostream> using namespace std; char Name[80]; char Copy[80]; void InputName() { cout << "Как Вас зовут?" << endl; cin.getline(Name, 80); cout << "Добрый день, " << Name << "!" << endl; } void do_copy() { int i; for (i=0; Name[i] && i<79; i++) { Copy[i] = Name[i]; } Copy[i] = 0; } void show_copy() { 462
Язык C++ для тех, кто торопится cout << "Это Ваша копия: " << Copy << endl; } int main() { InputName(); do_copy(); show_copy(); } Листинг А.9. Приветствие разделено на функции (fhi.cpp) Команды не изменились. Только порядок стал другим. Содержимое функции main() стало значительно более наглядным. В ней находятся только три вызова функций. Рассмотрим снова команды по отдельности: char Name[80]; char Copy[80]; Обе переменные массива здесь являются глобальными. Поскольку они определены в начале программы вне всех функций, к ним можно получить доступ из любой позиции программы. void InputName() { cout << "Как Вас зовут?" << endl; cin.getline(Name, 80); cout << "Добрый день, " << Name << "!" << endl; } Это функция InputName(). Имена функций подчиняются тем же правилам, что и имена переменных (см. стр. 32). Перед ними всегда указывается тип возвращаемого значения. Поскольку эта функция не имеет такового, ее тип — void1. В скобках содержатся параметры. Данная функция не имеет параметров, поэтому скобки пустые. Можно также поместить в них слово void. Оно будет означать то же самое. Затем следует открытая фигурная скобка. Все команды, которые размещаются здесь до соответствующей закрывающейся фигурной скобки, будут выполнены при вызове функции InputName(). Следующие три строки 1 Это слово можно перевести как «пустой» или «не занятый». 463
Приложение А уже известны вам из листинга А.8. Первая выводит на экран сообщение пользователю, что он должен делать. Вторая строка считывает введенные данные в переменную массива Name, а третья использует переменную Name для соответствующего вывода пользователю. Следующая закрывающаяся фигурная скобка завершает функцию. void do_copy() { int i; for (i=0; Name[i] && i<79; i++) { Copy[i] = Name[i]; } Copy[i] = 0; } Функция do_copy() также является функцией без возвращаемого значения и параметров. В ней объявляется переменная i. Она является локальной переменной функции. Вне функции ее нельзя увидеть. Если в других функциях также имеются переменные с именем i, они не будут мешать друг другу. Последняя функция не несет в себе ничего нового, поэтому перейдем сразу к функции main(). int main() { InputName(); do_copy(); show_copy(); } Вызов функции можно размещать в любом месте программы с любой частотой. Если, например, ввод имени еще раз потребуется в другом месте программы, достаточно просто еще раз вызвать функцию InputName(), и требуемая команда будет выполнена. Можно строить функции, которые применимы в программе несколько раз, при этом вы сэкономите много времени при наборе кода. Сюда также относится то, что функции не требуется проверять при повторном вызове, поскольку они уже проверены. 464
Язык C++ для тех, кто торопится А.4.2. Возвращаемое значение В примере функциям не требовалось возвращаемое значение. Во многих случаях, однако, функция должна передавать в программу, которая ее вызвала, результат своего выполнения. Это может быть результат вычисления, сообщение об ошибке и т. д. float numerator, denominator; float Div() { return numerator/denominator; } int main() { float quotient; numerator = 26.8; denominator = 4; quotient = Div(); } Листинг А.10. Передача параметров Задание программы — разделить одно число на другое. При этом числитель и знаменатель являются глобальными переменными. Деление проводится функцией Div(), которая возвращает результат1. float Div() Возвращаемое значение функции Div() имеет тип float. Тип функции всегда указывается в начале ее объявления. Затем следует имя функции и скобки. Возвращаемое значение передается в программу с помощью команды return. return numerator/denominator; Команда return сразу завершает функцию. Если имеется возвращаемое значение, после return следует размещать то, что функция воз1 Конечно, это проблематичнее, чем прямо на месте произвести деление. Но если быть последовательным, то чтобы разделить два числа, вам не требуется и писать программу на C++. Вы наверняка просто воспользуетесь калькулятором или листом бумаги и ручкой. 465
Приложение А вращает в программу в виде результата. В примере возвращаемое значение — результат деления переменных numerator и denominator. В главной функции main() сначала определяются глобальные переменные, а затем вызывается функция. quotient = Div(); В этой строке переменная quotient получает значение, которое передает команда return функции Div(). Несмотря на то, что функция не получает параметров, скобки являются обязательными при обозначении ее вызова. Если необходимо вызвать функцию, но ее возвращаемое значение при этом не требуется, то не нужно указывать переменную и оператор присваивания перед вызовом. Так функция может быть вызвана аналогично функции типа void. В случае функции Div() такой вызов выглядел бы следующим образом: Div(); Однако в нашем примере такой вызов абсолютно не имеет смысла, поскольку функция не делает ничего, кроме расчета возвращаемого значения. Тем не менее вызов синтаксически безупречен. В другом случае, когда возвращаемое значение для программы не играет никакой роли, такой способ вызова имеет преимущество, поскольку не требуется создавать дополнительную переменную, которая не будет использоваться в будущем. А.4.3. Параметры В последнем примере значения, которые использовались в функции Div() находились в глобальных переменных до вызова. Такой метод в высшей степени неэлегантен и может привести к неоднозначным последствиям. Взамен было бы лучше передавать переменные прямо в функцию. Для этой цели имеются параметры. С их помощью в функцию передаются значения. float Div(float numerator, float denominator) { return numerator/denominator; } int main() 466
Язык C++ для тех, кто торопится { float quotient; myfloat=26.8; quotient = Div(myfloat, 4); } Листинг А.11. Передача параметров Переменные, с которыми работает функция, теперь можно увидеть в заголовочной строке определения функции. Соответственно глобальные переменные больше не требуются. float Div(float numerator, float denominator) Объявление параметров в скобках выглядит как объявление переменных, которые разделены запятой. Фактически, переменные параметров ведут себя как локальные переменные. Значения, которые располагаются в скобках вызова функции, копируются в эти переменные. Тема локальных переменных рассмотрена на стр. 30. quotient = Div(myfloat, 4); Функция Div() получает значение переменной myfloat через локальную переменную numerator, поскольку при вызове функции оно будет в нее скопировано. Число 4, то есть второй параметр вызова, будет скопировано в переменную denominator. Передача адреса Передача параметров в функцию — улица с односторонним движением. Программа, вызывающая функцию, может передать в нее значения, но не наоборот. Обратный путь возможен только с помощью возвращаемого значения. В некоторых случаях полезно, если функция может получить доступ к переменной основной программы через передаваемый параметр, например, в случае, когда ей требуется изменить более одного значения. Для того чтобы можно было получить доступ из функции к внешним переменным, используется небольшой трюк. Программа передает в функцию адрес своей переменной. При копировании он не изменяется. Но функция может по этому адресу получить доступ к реальной переменной и изменить ее. Чтобы установить адрес переменной, следует указать амперсанд (&) перед ее именем. Теперь требуется также переменная, в которой можно 467
Приложение А сохранить этот адрес. Такие переменные называются указателями. Их можно узнать при объявлении по символу астериска (*) между типом и именем. Астериск здесь обозначает нечто вроде «указывает на». Так строка int *p; говорит: «переменная p указывает на переменную типа int». В конце концов, у вас должна быть возможность изменить значение переменной через указатель на нее. Это возможно, если перед переменной указателя поставить символ астериска (*). С помощью комбинации астериска и переменной указателя будет обработана переменная, на которую он ссылается. void shorten(int * numerator, int *denominator) { int divide; divide = ggt(*numerator, *denominator); *numerator= *numerator/divide; *denominator= *denominator/divide; } int main() { int z=14; int n=6; shorten(&z, &n); } Листинг А.12. Передача параметров Функция shorten() принимает два указателя на int и сокращает оба значения, на которые они ссылаются. То есть функция изменяет значения z и n, хотя они являются переменными функции main(). Программа передает адрес переменных в функцию. В определении параметров видно, что переменные numerator и denominator определены в качестве указателей на тип int. Они принимают адрес переменной целого типа и не имеют иного значения, кроме этого адреса. После передачи параметров указатель numerator получает адрес переменной z. В функции каждый раз, когда нужно получить доступ к переменной z, теперь следует указывать астериск перед именем переменной numerator. При этом обращение будет осуществляться не к адресу, а к значению, которое хранится по нему. 468
Язык C++ для тех, кто торопится Функция shorten()в свою очередь вызывает функцию ggt(). Она должна вернуть наибольший общий делитель двух чисел. Если вам захочется узнать, как он рассчитывается, рекомендую заглянуть на стр. 112. Возможно, вы сами захотите написать такую функцию в качестве маленького упражнения. Если вам это не удастся, на стр. 495 вы найдете решение. Дальнейшую информацию по теме параметров указателей вы найдете на стр. 170, по теме переменных указателей — на стр. 185. Массив в качестве параметра Следующая программа вычисляет среднее арифметическое значение из чисел массива с плавающей запятой и возвращает результат. Для этого используется функция, которая получает в качестве параметра массив. double avg(int number, double values[]) { int i; long double Summ=0; for (i=0; i<number; i++) { Summ += values[i]; } return Summ/number; } Листинг А.13. Расчет среднего арифметического значения Первый параметр функции avg() представляет собой целое число, которое передает размер массива. Требуется, чтобы внутри функции было известно, сколько элементов имеет массив. Его квадратные скобки пусты. Это указывает на то, что речь идет о целом массиве, и что может быть передан массив любого размера. В качестве переменной для суммы внутри функции используется переменная типа long double. Этот тип данных точнее, чем double и принадлежит к стандартным типам языка C++. 469
Приложение А Среднее арифметическое число получается вследствие расчета внутри цикла for суммы элементов массива и деления ее на количество элементов. Более подробная информация о массивах представлена на стр. 172. А.5. Классы Большинство объектов в мире являются составными, поэтому их нельзя поместить в переменную типа числа с плавающей запятой или в массив. Человек, с точки зрения программы, имеет имя, адрес, дату рождения и доход. Автомобиль состоит, к примеру, из марки, типа, мощности и цены. Для определения таких объектов требуется объединить в одной переменной несколько типов. Для этого в языке C++ используются классы. Следующий пример демонстрирует класс для автомобиля. class Auto { public: char brand [20]; char typ[30]; int kW; float prise; }; Листинг А.14. Класс для автомобиля С таким определением создается тип Auto. Теперь можно создать переменную типа Auto. При этом говорят, что создан объект типа Auto. Имя переменной может быть myAuto. Этот объект способен содержать в переменной brand символьную последовательность "Renault", в переменной typ — символьную последовательность "Logan", мощность 79 kW и цену 400 000 рублей. Вся эта информация будет храниться в объекте типа Auto. Помимо нее автомобиль может иметь также количество сидений, дату изготовления, дату следующего техосмотра и расход топлива. Но не все эти данные важны для каждой программы. В классе содержится только та информация, которая обрабатывается в программе. 470
Язык C++ для тех, кто торопится Рассмотрим другой пример. Некоторое программное обеспечение должно хранить список товаров. Одна единица товара состоит из описания, штрихкода и цены. const int MaxName=40; const int MaxCode=20; class tGoods { public: char Name[MaxBez]; char BarCode[MaxCode]; float price; }; int main() { tGoods chocolate; chocolate.price = 0.70; } Листинг А.15. Товары Рассмотрим снова строки по отдельности: const int MaxName=40; const int MaxCode=20; Сначала определяются две константы — MaxName и MaxCode. Объявление констант идентично объявлению переменных с присвоением им значения, только для них используется также ключевое слово const. При этом значение не может меняться в дальнейшем. Константы будут использоваться далее для установления длины символьных последовательностей. class tWare { public: За ключевым словом class следует имя класса. Можно использовать любое, но оно должно соответствовать правилам составления имен 471
Приложение А (см. стр. 32). Часто перед именем класса указывается буква t, c или C, чтобы тем самым указать, что речь идет о классе, а не о переменной. С помощью открытых фигурных скобок начинается определение элементов класса. Метка public: задает, что все дальнейшие определения открыты. Эта тема будет подробнее рассмотрена далее. char Name[MaxBez]; char BarCode[MaxCode]; float price; }; Это три элемента класса. Они подчиняются правилам обычного объявления переменных. Определение класса завершается закрывающейся фигурной скобкой и точкой с запятой. tGoods chocolate; chocolate.price = 0.70; После объявления класса программа может создавать собственные переменные типа tGoods. tGoods — это класс, а chocolate — объект класса tGoods. Чтобы получить доступ к элементу price нужно поставить точку между именем объекта и именем элемента. Таким образом, здесь устанавливается цена шоколада, равная 0,70. К классу относятся не только элементы данных, но и функции. Решение, что функции относятся к объектам данных, — основная идея объектно ориентированного программирования. Большинство функционалов применяются к объекту. Прежде всего они отличаются друг от друга объектом, к которому применимы. Езда — это функционал. Но человек ездит не сам по себе, а на велосипеде, на поезде, на машине. Тип объектов при этом имеет важное влияние на тип езды. В случае класса tGoods необходимо обеспечить изменение цены. Вообще, в случае такого действия, как изменение цены, требуется также записывать, когда именно оно было произведено. По этой причине класс tGoods имеет функцию NewPrice(). Она должна изменять цену и протоколировать это. Чтобы обеспечить изменение цены только таким образом, требуется разрешить изменение стоимости только функцией NewPrice(). Для этого она будет защищена от внешнего воздействия, то есть определена как закрытая. Тогда прямой доступ к переменной price, как показано 472
Язык C++ для тех, кто торопится в листинге А.15, будет запрещен компилятором. Элемент данных price может при этом изменяться только элементной функцией класса (в данном случае NewPrice()). class tGoods { public: char Name[MaxBez]; char BarCode[MaxCode]; void NewPrice(float new_price); private: float price; }; void tGoods::NewPrice(float new_price) { Protocol(price, new_price); price = new_price; } int main() { tGoods chocolate; chocolate.NewPrice(0.70); } Листинг А.16. Изменение стоимости Рассмотрим изменения, которые были внесены в программу. void NewPrice(float new_price); private: float price; Внутри класса создается при ее объявлении функция NewPrice(). При этом сначала указывается заголовок функции, а потом точка с запятой. Это означает, что компилятору только сообщается, что имеется такая функция с данными параметрами, а реализуется она в другом месте. 473
Приложение А Затем появляется метка private:. Все следующие после нее определения и объявления доступны только элементным функциям класса. При этом переменная price защищена от прямого доступа. void tGoods::NewPrice(float new_price) Здесь определяется (реализуется) элементная функция NewPrice() класса tGoods. Для того чтобы отличить ее от других функций, которые не относятся к классу, перед именем функции указаны имя класса и два двоеточия. { Protocol(price, new_price); price = new_price; } Здесь следует тело функции в фигурных скобках. Сначала вызывается функция Protocol(), чье содержимое нам неизвестно и не должно нас интересовать. Внутри элементной функции можно получить прямой доступ к элементам данных класса. Запомните, что функция должна вызываться в качестве составной части объекта. Если она получает доступ к переменной price, то это цена объекта, к которому относится функция. tGoods chocolate; chocolate.NewPrice(0.70); Вызов функции NewPrice() осуществляется в качестве доступа к объекту данных класса. Сначала называется объект, для которого вызывается функция, затем указывается точка и только потом следует вызов функции. А.5.1. Конструктор Классы позволяют объявлять элементные функции, которые автоматически вызываются, когда создается объект класса. При этом программист может гарантировать, что объект будет всегда корректно инициализирован. Конструктор имеет имя класса и не имеет возвращаемого значения, а также типа void. В классе tGoods имеется две символьные последовательности, одна — для штрихкода и другая — для описания товара. Символьные 474
Язык C++ для тех, кто торопится последовательности должны быть инициализированы так, чтобы они оказались пустыми при запуске программы. Поскольку такая последовательность всегда завершается нулевым символом, достаточно установить нулевой элемент последовательности в ноль. class tGoods { public: tGoods(); // объявление конструктора char Name[MaxBez]; char BarCode[MaxCode]; void NewPrice(float new_price); float stock; private: float price; }; tGoods:: tGoods() { Name[0] = 0; BarCode[0] = 0; price = 0.0; stock = 0.0; } void tGoods::NewPrice(float new_price) { Protocol(price, new_price); price = new_price; } int main() { tGoods chocolate; chocolate.NewPrice(0.70); } Листинг А.17. Конструктор 475
Приложение А Объявление конструктора осуществляется в классе в качестве объявления открытой элементной функции. Некоторые части листинга рассмотрим подробнее: class tGoods { public: tGoods(); // объявление конструктора Перед именем конструктора не должно стоять типа данных, и даже типа void. Поскольку этот конструктор не имеет параметров, его называют стандартным. tGoods:: tGoods() { Реализация конструктора аналогична реализации любой элементной функции. Имя класса отделено двойным двоеточием от имени функции, которое в данном случае является именем класса. Name[0] = 0; BarCode[0] = 0; price = 0.0; stock = 0.0; } Команды конструктора абсолютно типичны. Все элементы данных класса устанавливаются на одно начальное значение. Символьные последовательности пусты, поскольку их первым элементом записывается завершающий ноль. tGoods chocolate; Как только создается переменная chocolate, для этого объекта вызывается конструктор tGoods:: tGoods(). При этом он перед самым первым доступом заботится о том, чтобы все элементы имели одно определенное состояние. Конструкторы с параметрами Конструкторы могут снабжаться параметрами. Тогда объект уже при создании будет инициализирован с определенными параметрами. Они перечисляются в скобках при создании переменной. 476
Язык C++ для тех, кто торопится В следующем листинге добавлен конструктор, который в качестве параметров имеет имя и штрихкод. При этом объект при создании уже будет снабжен этой информацией. class tGoods { public: tGoods(); tGoods(char *Nm, char *Cd); char Name[MaxBez]; char BarCode[MaxCode]; void NewPrice(float new_price); float stock; private: float price; }; tGoods:: tGoods() { Name[0] = 0; BarCode[0] = 0; price = 0.0; stock = 0.0; } tGoods:: tGoods(char *Nm, char *Cd) { strcpy(Name, Nm); strcpy(BarCode, Cd); price = 0.0; stock = 0.0; } void tGoods::NewPrice(float new_price) { Protocol(price, new_price); price = new_price; 477
Приложение А } int main() { tGoods chocolate("Milk_chocolate", "4008400404127"); chocolate.NewPrice(0.70); } Листинг А.18. Изменение цены Новый конструктор отличается от старого только входными параметрами. public: tGoods(); tGoods(char *Nm, char *Cd); ... tGoods:: tGoods() ... tGoods:: tGoods(char *Nm, char *Cd) Конструктор с параметрами должен инициализировать элементы price и stock. Стандартный конструктор не будет вызываться ни до, ни после него. Конструктор с параметрами таким образом является самостоятельным и может вызываться в качестве альтернативы стандартному конструктору. tGoods chocolate("Milk_chocolate", "4008400404127"); Класс tGoods имеет два конструктора. Первый инициализирует название товара и его штрихкод при создании объекта. Второй не требует никаких параметров и является стандартным конструктором. Он вызывается, если при объявлении переменной не задается никаких параметров. Удалив стандартный конструктор из класса tGoods, нельзя будет создать объект класса tGoods, не задав ему параметров имени и штрихкода. Так можно добиться, чтобы при создании всегда задавались эти два параметра. Подробная информация о конструкторах на стр. 220. Представим ситуацию, что существуют две функции с одинаковыми именами. В языке C++ это допустимо до тех пор, пока они отличаются параметрами, по которым можно узнать, какую из функций требуется вызвать. Подобная техника называется перегрузкой функций. Она рабо- 478
Язык C++ для тех, кто торопится тает не только с конструкторами, но и с любыми функциями. Это также не обязательно должен быть элемент класса. Подробная информация об этом приведена на стр. 183. А.5.2. Наследование Не все товары одинаковы. Так, для напитков в бутылках имеется залоговая стоимость емкости. Если она должна быть сохранена в товаре, то можно дополнить класс tGoods элементом deposit. Но он не требуется для других товаров. Можно скопировать класс tGoods, добавить элемент deposit и переименовать. Тогда получатся два независимых класса. Но в языке C++ для этого имеется более элегантное решение — создать новый класс tDepositGoods и объяснить, что он перенимает все свойства от класса tGoods. В классе tDepositGoods определяется только то, чем он отличается от класса tGoods. При этом говорят, что класс tDepositGoods унаследовал свои свойства от tGoods. Также можно сказать, что класс tDepositGoods производный от класса tGoods, который в свою очередь является базовым классом. Чтобы описать это в программе, при объявлении производного класса добавляется двоеточие, слово public и имя базового класса. class tDepositGoods : public tGoods { public: tDepositGoods(char *Nm, char *Cd) : tGoods(Nm, Cd) { Deposit = 0.0; } void NewDeposit (float newDeposit) { Deposit = newDeposit; } private: float Deposit; }; Листинг А.19. Производный класс 479
Приложение А Рассмотрим строки по отдельности: class tDepositGoods : public tGoods Класс tDepositGoods наследует все элементы, которые имеет класс tGoods. Каждый объект класса tDepositGoods имеет описание, штрихкод, цену и количество. От базового класса также наследуется функция NewPrice(). Поскольку класс tDepositGoods является производным от класса tGoods, перед стартом его собственного конструктора будет запущен конструктор базового класса. Но это, однако, здесь совсем не желательно. Поскольку этот конструктор получает сразу же описание и штрихкод, эту информацию следует также передавать дальше базисному классу. tDepositGoods(char *Nm, char *Cd) : tGoods(Nm, Cd) { Deposit = 0.0; } Здесь к заголовку конструктора добавлен так называемый инициализатор. За двоеточием вызывается конструктор базового класса. Используемые инициализатором параметры — это те, которые получает сам конструктор tDepositGoods(). Конструктор tDepositGoods() требуется только для того, чтобы заботиться о дополнительных элементах. В данном случае, определение конструктора проводится не вне определения класса, а интегрировано в класс. Это вполне приемлемо для небольших функций. Точка с запятой после определения такой функции не указывается, поскольку она находится в теле класса. Совместимость с базовым классом Объекты производных классов получают все открытые составные части базовых классов. По этой причине объекту производного класса можно прямо присвоить объект базового класса. Но при этом все дополнения производного класса будут потеряны. int main() { tDepositGoods Milk_chocolate; tGoods TargetGoods; tGoods *PointerGoods; 480
Язык C++ для тех, кто торопится TargetGoods = Milk_chocolate; // deposit исчезает. PointerGoods = &Milk_chocolate; // deposit остается. } Переменная TargetGoods содержит информацию переменной Milk_chocolate, которую класс tDepositGoods унаследовал от класса tGoods. Все расширения (здесь — deposit) будут утеряны. Переменная TargetGoods не в состоянии сохранить deposit. Возможность присваивания делает интуитивно более понятным, что депозитные товары также являются товарами. При этом товар не всегда может быть депозитным. Поэтому в обратном порядке это не работает. Переменная PointerGoods является указателем на тип tGoods. Он может также ссылаться на объект производного класса. Факт, что этот указатель ссылается на Milk_chocolate, не изменяет объект. Он сохраняет свой депозит. Компилятор, однако, возмущенно отклонит попытку получить доступ к deposit через указатель PointerGoods, поскольку указатель на tGoods не знает никакой переменной deposit. А.5.3. Полиморфизм При инвентаризации каждому товару назначается цена, включая и товары с депозитом. В этом случае окупится то, что мы не создавали для них отдельный класс, а определили tDepositGoods в качестве производного от класса tGoods. Поскольку производный класс можно в известном отношении обработать в качестве базового. Если имеется функция для назначения цен при инвентаризации товаров класса tGoods, ее можно также применить для класса tDepositGoods. Чтобы установить цены товаров при инвентаризации, напишем элементную функцию InventPrice(). Для обычных товаров она будет возвращать определенный процент от стоимости. Для товаров с депозитом к этому значению будет добавляться также значение залога за емкость. Поскольку инвентаризационное значение зависит от типа товара, для каждого производного класса функцию следовало бы определить заново, если переоценка товара данного класса отличается от стандартной. Для инвентаризации создается отдельный массив article, в который помещаются указатели всех имеющихся товаров. Указатель на tGoods может ссылаться на любой объект, который является производным от tGoods. При этом объект не теряется своих отличительных особенностей. Теперь только требуется добиться того, чтобы компилятор не 481
Приложение А применял к нему опрометчиво функцию InventPrice() класса tGoods. Для этого перед определением функции InventPrice() нужно указать ключевое слово virtual. Тогда функция будет выбираться в зависимости от обрабатываемого объекта. class tGoods { public: ... virtual float InventPrice(); ... }; Листинг А.20. Виртуальная функция С помощью ключевого слова virtual компилятору сигнализируется, что при доступе к этой переменной не следует обращаться к классу tGoods, объект должен сам определить, какая из функций InventPrice() ему принадлежит. Если отдельный объект устанавливает, какая функция будет запущена, это называется полиморфизмом. В следующем листинге представлен цикл для перебора массива указателей на товары. tGoods * article[max_article]; int number_of_articles; for (int i=0; i<number_of_articles; i++) { Summ += article[i]->stock * article[i]->InventPrice(); } Листинг А.21. Инвентаризация Стрелка, формируемая из символов «больше» и тире, или «меньше» и тире (->, <-) используется, когда через указатель требуется получить доступ к элементу объекта. Двумя условиями можно добиться того, что будут вызываться функции, принадлежащие к классу, объект которого обрабатывается. 482
Язык C++ для тех, кто торопится 1. Объект сохраняется таким образом, что к нему можно получить доступ только через указатель на тип базового класса. 2. Вызываема функция определяется в базовом типе ключевым словом virtual. Следующие команды записывают адрес объекта класса tDepositGoods в массив article, который используется вверху для проведения инвентаризации. tDepositGoods bottle("Beer", "") article[i] = & bottle; Через полиморфизм ответственность за выбор элементной функции перекладывается на объект. Подробная информация по теме полиморфизма доступна на стр. 271. А.6. Шаблоны С шаблонами мы переходим к так называемому обобщенному программированию. Его можно обозначить в качестве подхода, противоположного объектно ориентированному программированию, поскольку здесь описываются алгоритмы, не зависящие от типов данных.  Шаблон — это независимая от типа данных модель выполняемого процесса. Компилятор устанавливает при вызове, для какого типа должен быть реализован шаблон, и создает из шаблона и типа требуемую функцию или класс. На стр. 469 была представлена функция, которая вычисляет среднее арифметическое значений массива. Функция работает с переменными типа float. Метод, который используется в этой функции, подходит не только для чисел с плавающей запятой. Таким же образом можно рассчитать среднее арифметическое для целых значений. Чтобы уточнить различие приведем здесь еще раз функцию со стр. 469. double avg(int number, double value[]) { int i; long double Summ=0; 483
Приложение А for (i=0; i<number; i++) { Summ += value[i]; } return Summ/number; } Листинг А.22. Расчет среднего арифметического С помощью функции шаблона можно определить этот алгоритм для нескольких типов. Функция шаблона начинается ключевым словом template. Если сравнить обе функции между собой, легко определить, по какому шаблону функция приобретает анонимный тип. template <class T> T avg(int number, T value[]) { int i; T Summ=0; for (i=0; i<number; i++) { Summ = Summ + value[i]; } return Summ/number; } Листинг А.23. Расчет среднего арифметического через шаблон Сильнее всего функции в листингах А.22 и А.23 различаются заголовками. Здесь тип float заменен на суррогатный тип Т. template <class T> T avg(int number, T value[]) Однако перед использованием Т он вводится в качестве типа шаблона. Перед каждой функцией шаблона всегда указывается ключевое слово template. Затем в угловых скобках определяется, какой тип объявляется анонимным. Для этого используется знакомое понятие class. 484
Язык C++ для тех, кто торопится Здесь оно имеет мало общего с понятием класса и подразумевает тип. Новые компиляторы также принимают в качестве альтернативы ключевое слово typename, которое лучше описывает ситуацию. Имя Т является типом, с которым функция будет работать в будущем. Теперь начинается сам заголовок функции. Тип возвращаемого значения Т, то есть такой же, как и для второго параметра. Затем следует имя функции и параметры в скобках. Количество элементов при расчете среднего арифметического всегда будет иметь тип int. Только передаваемый массив должен иметь переменный тип. Какой тип примет Т впоследствии, зависит от параметров при вызове функции. int i; T Summ=0; Внутри функции анонимный тип Т применяется еще раз для объявления переменных, которые используются при расчете суммы. При вызове больше не требуется ничего контролировать. Тип шаблона Т будет заменен типом, с которым вызывается функция. Эта функция работает с double или int. Она может также вызываться для любого класса, для которого определено сложение, деление и присваивание, поскольку данные операции производятся над переменными типа Т внутри функции. В языке C++ можно определить такие операторы для собственного класса. Как это делается, рассмотрено на стр. 238. Аналогом функций шаблонов могут выступать классы шаблоны. На этом месте я завершаю быстрый ввод в программирование на языке C++. Классы шаблоны не очень подходят для того, чтобы рассматривать их поверхностно. Информацию по ним вы найдете на стр. 321.
Приложение Б УСТРОЙСТВО КОМПИЛЯТОРА Для операционной системы Windows на диске, прилагающемся к книге, вы найдете интерфейс командной строки Cygwin и среду разработки Bloodshed Dev-C++. Если вы используете операционную систему Linux, то все необходимые инструменты программирования содержатся на диске с дистрибутивом вашей операционной системы. Здесь представлен краткий обзор этих программ. Общие указания по эксплуатации инструментов программирования были уже представлены в главе 6. Б.1. KDevelop Разработчики программ в среде Linux или UNIX с графической оболочкой KDE1 могут использовать мощную среду KDevelop, которая имеет в своем распоряжении GNU-компилятор. Современные дистрибутивы Linux больше не исходят из того, что по другую сторону клавиатуры находится программист, поэтому KDevelop требуется устанавливать отдельно. Тем не менее вы найдете KDevelop на диске с дистрибутивом вашей операционной системы Linux, его не требуется дополнительно покупать или загружать из Всемирной паутины. Установка посредством мастера инсталляции не сложнее установки редактора кода. После запуска KDevelop похож внешним видом на обычную среду разработки (IDE). Самое большое окно справа вверху предназначено для ввода программного кода, при этом различные элементы языка выделяются различными цветами. В левой части расположено обзорное окно, в котором отображаются используемые в проекте классы. Под окном редактора находится многофункциональное окно, в котором, например, могут выводиться как сообщения от компилятора, так и информация отладчика. Б.1.1. Новый проект Чтобы написать программу, необходимо создать новый проект. При этом нужно определить, предназначается программа для графической 1 Так же и пользователи графической оболочки GNOME могут использовать KDevelop. Однако данная IDE не будет достаточно гибко интегрирована в эту оболочку. 486
Устройство компилятора оболочки или для командной строки. В первом случае вам понадобятся специальные знания соответствующей графической оболочки. Чтобы создать новый проект, в меню следует выбрать пункт Проект (Project), а в открывшемся подменю пункт Новый (New). Затем появится диалоговое окно, изображенное на рис. Б.1. Рис. Б.1. Выбор типа проекта В дереве каталогов слева внизу выбрана программа «Простая программа Hello World». C правой стороны расположено окно предварительного просмотра и лаконичное описание выбранного файла. В нижней области диалогового окна задается, имя и расположение приложения. В первой строке уже введено слово «conversion». По умолчанию, KDevelop сохраняет рабочую директорию проекта Hello в домашний каталог1. Если нажать кнопку Далее (Next), можно провести дальнейшие установки, которые для начала можно и не трогать. В итоге на экране появится рабочая область проекта, который уже содержит главную функцию main(). 1 В системе UNIX и соответственно Linux каждый пользователь имеет собственную рабочую директорию, которая находится в каталоге /home и обычно имеет имя, совпадающее с именем пользователя. 487
Приложение Б Б.1.2. Компиляция и старт Теперь можно ввести первую программу. Перед тем, как ее можно будет запустить, она должна быть скомпилирована. Для этого следует выбрать пункт меню Файл (File), и в открывшемся подменю пункт Создать (Create), вместо этого можно нажать соответствующую кнопку на панели инструментов или воспользоваться клавишей F8. Чтобы запустить программу, нажмите на панели инструментов кнопку, на которой изображена коричневая шестеренка. При этом откроется отдельное окно консоли, в котором выполняется программа, как это показано на рис. Б.2. Рис. Б.2. Запуск программы После завершения выполнения программы, окно остается открытым и в нем появляется надпись «Press Enter to continue!». Как только клавиша Enter будет нажата — окно исчезнет. В IDE KDevelop имеется встроенный отладчик. Чтобы установить точку останова в программе, щелкните мышью по серой области рядом со строкой кода, перед которой программа должна остановиться. Отлад- 488
Устройство компилятора чик можно запустить также через главное меню. После его запуска можно просмотреть выполнение программы в пошаговом режиме. Б.2. Bloodshed Dev-C++ Эта интегрированная среда разработки предназначена для операционной системы Windows и связывает GNU-компилятор с IDE, которая ориентирована на среду разработки для Visual C++. В принципе, можно использовать для нее и другие компиляторы. Среда разработки включает редактор, который позволяет форматировать вводимый код. Она содержит классовый навигатор и позволяет быстрее обнаруживать ошибки. Кроме того, в этой системе можно разрабатывать приложения для Windows на базе API, атакже статические и динамические библиотеки (DLL). Программное обеспечение общедоступно и свободно распространяется по лицензии GNU. Б.2.1. Установка На диске, прилагающемся к книге, в каталоге Программы/Bloodshed Dev-C++ находится файл, требующийся для запуска в операционной системе Windows. При запуске будет запрошен выбор языка. Вероятно, вы предпочтете русский. Затем появится лицензионное соглашение, в котором требуется указать, что пользователь согласен с его условиями. Следующим шагом выбираются компоненты для установки. Пока вы точно не знаете, что можно пропустить, имеет смысл выбрать все предлагаемые компоненты. Программа инсталляции запросит целевой каталог и предложит использовать путь C:\Dev-Cpp1. После завершения установки приложение запустится и будет выведено диалоговое окно с советом дня. В пункте меню Справка (Help) можно найти информацию о Dev-C++. Там описаны приемы работы с этой IDE. Б.2.2. Создание проекта Перед тем, как начать вводить код в редактор, требуется создать проект. В проекте IDE выбирается, создается ли консольное приложение, приложение для Windows, библиотека или DLL. Файл проекта со1 Организованнее было бы конечно установить ее в каталог Programms. 489
Приложение Б держит информацию о требуемых исходных файлах, их отношениях, свойствах, например путь размещения, а также сведения о конфигурации IDE. Чтобы создать проект, требуется выбрать команду меню Файл ⇒ Создать ⇒ Проект (File ⇒ Create ⇒ Project). При этом появится диалоговое окно, в котором будет предложено выбрать один из нескольких типов проекта. Здесь требуется установить, будет ли ваша программа запускаться из командной строки, или вы хотите написать приложение для Windows или библиотеку DLL. Диалоговое окно выбора представлено на рис. Б.3. Рис. Б.3. Создание нового проекта Для выполнения примеров из книги выберите консольное приложение. Затем задайте имя файла без расширения. После этого появится диалоговое окно, в котором вы можете сохранить файл проекта, имеющий расширение .dev. Кстати, здесь также можно разрабатывать проекты Visual C++. Для консольного приложения в пустой программный файл автоматически помещается общий код, как это показано на рис. Б.4. Он незначительно отличается от того, который часто используется в книге. В конце концов, нет разницы, какую из версий вы будете использовать. Рассмотрим листинг, предлагаемый для консольного приложения Dev-C++. 490
Устройство компилятора Рис. Б.4. Заполненный автоматически новый файл проекта #include <cstdlib> #include <iostream> using namespace std; int main(int argc, char *argv[]) { system("PAUSE"); return EXIT_SUCCESS; } Листинг Б.1. Пустой консольный проект Подключение файла cstdlib.h требуется всегда, когда используются функции стандартной библиотеки языка C. Подключение не мешает, но в книге встречается не часто, поскольку библиотеки языка C в программировании на языке C++ используются редко. В строке перед командой return функция system() вызывается с параметром "PAUSE", что обеспечивает ожидание нажатия клавиши пользователем. Это делается для того, чтобы окно программы не закрывалось сразу после ее выполнения. Редактор форматирует код различными цветами. Так символьные последовательности выделены красным цветом, команды препроцессора — зеленым, а ключевые слова языка C++ форматируются полужирным начертанием. 491
Приложение Б Б.2.3. Компиляция и старт В следующем шаге программу требуется скомпилировать и запустить. Оба действия можно выполнить через пункт меню Выполнить (Run). Для компиляции в этом пункте меню выбирается подпункт Скомпилировать (Compile), можно также использовать сочетание клавиш Ctrl+F9. Я допустил ошибку в коде программы, чтобы продемонстрировать, как на нее будет реагировать IDE (рис. Б.5). Рис. Б.5. Сообщения об ошибках Строка с ошибкой выделяется темно красным цветом. В нижней части рабочего окна программы размещается панель, на которой перечисляются ошибки. Если в вашей программе имеется много ошибок в разных местах кода, их можно легко найти, если дважды щелкнуть мышью по сообщению об ошибке. Редактор сразу установит курсор в строку с ошибкой, даже в случае, если она находится в другом файле. После того, как ошибка была устранена, я написал небольшой цикл, который выводит цифры от 0 до 9. Здесь при компиляции не возникает ошибок. Программа запускается при выборе команды меню Выполнить ⇒ Выполнить (Run ⇒ Run) или нажатии сочетания клавиш Ctrl+F10 (рис. Б.6). 492
Устройство компилятора Рис. Б.6. Запуск программы Откроется новое окно и программа выполнится. Из-за наличия команды system("PAUSE"); появится запрос нажать любую клавишу. После нажатия клавиши окно будет закрыто, и вы снова окажетесь в IDE. Дистрибутив среды Bloodshed Dev-C++ вы найдете на диске, прилагающемся к книге. Также, последнюю версию среды можно загрузить с сайта www.bloodshed.net. Б.3. Cygwin Cygwin — это не просто компилятор, а полноценная среда UNIX для операционной системыWindows. Вы найдете программу на диске, прилагающемся к книге, в каталоге Программы/Cygwin. Запустите файл setup.exe. В первом диалоговом окне не нужно ничего делать, кроме как нажать кнопку Далее (Next). Следующим шагом требуется указать, происходит установка из Интернета или из локальной директории. Здесь следует выбрать третий пункт, поскольку установка производится с диска. Установочная программа предлагает в качестве целевого каталога директорию C:\Cygwin. По умолчанию компилятор устанавливается для всех пользователей, а кодовые файлы сохраняются в формате UNIX, то есть используют символ переноса строки в качестве разделителя строк. 493
Приложение Б Затем программа установки запросит расположение, из которого следует получать пакеты для установки. Если выбрать локальную дирректорию, необходимо нажать кнопку Обзор (Browse) и в появившемся диалоговом окне выбрать сначала привод оптических дисков, а затем каталог Software/Cygwin/Packages. Следующим шагом программа установки произведет поиск пакетов, имеющихся на диске. Это займет некоторое время. Затем появится диалоговое окно, в котором перечислены все найденные пакеты. Выберите все и нажимайте кнопку Далее (Next) до тех пор, пока в окне не появится кнопка Установить (Install). При этом вы установите гораздо больше компонентов, чем требуется. Однако пространство на жестком диске в наши дни не так дорого, а взамен вы получите наслаждение работать со всеми полезными инструментами, которые только существуют в UNIX. Вы можете, конечно, установить только основной пакет, но тогда впоследствии придется дополнительно устанавливать редактор, компилятор и, вероятно, компоновщик. Далее программа поинтересуется, необходимо ли разместить ярлык на рабочем столе и в меню Пуск (Start). После чего можно нажать кнопку Готово (Finish). В процессе установки на экране появится окно командной строки, в котором отобразятся установочные коды, после чего окно закроется. По завершении установки возникнет диалоговое окно с сообщением «Инсталляция завершена». В меню Пуск (Start) и на рабочем столе вы найдете ярлык для старта программы. После запуска Cygwin появится командная строка особого вида: BASH. Это оболочка родом из среды UNIX. В этой среде вы можете действовать так же, как в среде UNIX. В вашем распоряжении имеется редактор vi. Для запуска компилятора используется команда g++. Можно также запускать графические программы UNIX, поскольку Cygwin содержит X11-Server.
Приложение В ПРИМЕРЫ РЕШЕНИЙ ЗАДАЧ Свое собственное, хоть и плохое, решение задачи несет больше пользы знаниям, чем чужое отличное решение, которое вы можете использовать. Поэтому рекомендуется сначала попытаться решить задачу самостоятельно, прежде чем заглядывать в этот раздел. Контрольные вопросы Вопрос со стр. 23. Вопрос: Почему программа, которая переводится в машинный код интерпретатором, обычно медленнее, чем программа, переводимая компилятором? Ответ: Интерпретатор переводит программу в машинный код во время ее выполнения. Функция ggt() Задание со стр. 469. #include <iostream> using namespace std; int ggt(int a, int b) { int help; while (b>0) { if (b>a) { // обменять a и b 495
Приложение В help = a; a = b; b = help; } a = a - b; } return a; } void shorten(int *numerator, int *denominator) { int factor; factor = ggt(*numerator, *denominator); *numerator = *numerator/ factor; *denominator = *denominator/ factor; } int main() { int z=14; int n=6; shorten(&z, &n); cout << z << " " << n << endl; } Листинг В.1. Сокращение с ggt() Программа НДС Задание со стр. 69. #include <iostream> using namespace std; const float MwSt = 16; int main() { float Netto, Tax, Brutto; 496
Примеры решений задач cout << "Введите цену нетто!" << endl; cin >> Netto; Tax = Netto * MwSt / 100; cout << "Налог: " << Tax << endl; Brutto = Netto + Tax; cout << "Брутто: " << Brutto << endl; } Листинг В.2. Программа MwSt (mwst.cpp) Программа НДС с проверкой ввода Задание со стр. 114. #include <iostream> using namespace std; const float MwSt = 16; int main() { float Netto, Tax, Brutto; cout << "Введите цену нетто!" << endl; cin >> Netto; if (Netto > 0) { Tax = Netto * MwSt / 100; cout << "Налог: " << Tax << endl; Brutto = Netto + Tax; cout << "Брутто: " << Brutto << endl; } else { cout << "Только положительные величины!" << endl; } } Листинг В.3. Программа MwSt с проверкой ввода (ifmwst.cpp) 497
Приложение В Сложный процент Задание со стр. 114. #include <iostream> #include <iomanip.h> using namespace std; const int YearNow = 2003; const int Years = 20; const float Interest = 5; const float Payment = 5000; int main() { float Capital = Payment; for (int i=0; i<Years; i++) { cout << setw(4) << i+YearNow << " - " << setw(12) << Capital << endl; Capital += Capital * Interest / 100 + Payment; } } Листинг В.4. Программа сложного процента (payment.cpp) Угадывание чисел Задание со стр. 114. Первая часть задания состоит в том, чтобы найти случайное число в диапазоне от 1 до 1000. Программа должна повторяться до тех пор, пока пользователь не угадает это число. Поскольку ввод числа пользователем находится в середине тела цикла, проверка должна находиться в его конце. Здесь предлагается цикл do-while. Внутри него запрашивается число. Чтобы выдать результирующее сообщение, проверяется больше ли введенное число задуманного. Затем проверяется, меньше ли оно. Проверка на равенство уже избыточна, поскольку тогда цикл будет покинут. Церемония награждения располагается в конце программы. Тот, кто там оказался — угадал число. 498
Примеры решений задач #include <iostream> using namespace std; #include <stdlib.h> int main() { int Get; int SearchNumber; srand(4); SearchNumber = rand() % 1000 + 1; do { cout << "Число от 1 до 1000!" << endl; cin >> Get; if (Get < SearchNumber) { cout << "Слишком маленькое!" << endl; } if (Get > SearchNumber) { cout << "Слишком большoе!" << endl; } } while (Get != SearchNumber); cout << "Угадал!" << endl; } Листинг В.5. Программа угадывания числа (guess.cpp) Отсортированные числа лото Задание со стр. 125. #include <iostream> using namespace std; #include <stdlib.h> 499
Приложение В const int BallNumber = 6; const int MaxNumber = 49; int main() { int lotto[BallNumber]; int i, j; int help; bool newNumber; srand(38); // перемешаем! // выберем шесть чисел лото for(i=0; i<BallNumber; i++) { // цикл проверяет, нет ли повторяющихся чисел do { lotto[i] = rand() % MaxNumber + 1; newNumber = true; // думаем позитивно! // проверяем все предыдущие числа for (j=0; j<i; j++) { if (lotto[j]==lotto[i]) { newNumber = false; // уже есть! } } } while (!newNumber); } // теперь числа сортируем методом «пузырька» for(i=BallNumber-1; i>0; i--) { for (j=0; j<i; j++) { if (lotto[j]>lotto[j+1]) { 500
Примеры решений задач help = lotto[j]; lotto[j] = lotto[j+1]; lotto[j+1] = help; } } } // вывод отсортированных чисел for (i=0; i<BallNumber; i++) { cout << lotto[i] << " "; } cout << endl; } Листинг В.6. Сортировка чисел лото (sortlotto.cpp) Оптимизированная сортировка методом «пузырька» Задание со стр. 125. #include <iostream> using namespace std; #include <stdlib.h> const int MAX=12; int main() { int field[MAX], help; int i, j; srand(56); for(i=0; i<MAX; i++) { field[i] = rand() % 100 + 1; } bool sort = false; 501
Приложение В for(i=MAX-1; !sort && i>0; i--) { sort = true; for (j=0; j<i; j++) { cout << j << " - " << j+1 << endl; if (field[j]>field[j+1]) { sort = false; help = field[j]; field[j] = field[j+1]; field[j+1] = help; } } } for (i=0; i<MAX; i++) { cout << field[i] << " "; } cout << endl; } Листинг В.7. Оптимизированная сортировка методом «пузырька» (bubbleopt.cpp) Ввод дроби Задание со стр. 131. #include <iostream> using namespace std; const int MAX=256; int main() { char input[MAX]; int i=0; 502
Примеры решений задач double Value = 0; double denominator = 0; cin.getline(input, MAX); // прочесть знаменатель while (input[i]>='0' && input[i]<= '9') { Value *= 10; Value += input[i] - '0'; i++; } // Если появляется символ /, значит имеется знаменатель if (input[i]== '/') { // прочесть знаменатель i++; while (input[i]>= '0' && input[i]<= '9') { denominator *= 10; denominator += input[i] - '0'; i++; } } // разделить на знаменатель, если он был найден if (denominator!=0) { Value /= denominator; } cout << Value << endl; } Листинг В.8. Ввод дроби (infraction.cpp) Функция swap Задание со стр. 175. Напишите функцию swap(), которая получает два параметра по ссылке и обменивает их содержимое. 503
Приложение В void swap(int &a, int &b) { int help; help = a; a = b; b = help; } #include <iostream> using namespace std; int main() { int a = 4, b = 3; swap(a, b); cout << a << " " << b << endl; } Листинг В.9. Функция swap (swap.cpp) Локальный стек Задание со стр. 177. Расширьте программу для реализации стека так, чтобы можно было использовать не один, а любое количество стеков в программе, в т.ч. и локальных. #include <iostream> using namespace std; struct tListKnots { int data; tListKnots *next; }; void push(tListKnots **Anchor, int data) { 504
Примеры решений задач tListKnots *node = new tListKnots; node->data = data; node->next = *Anchor; *Anchor = node; } int pop(tListKnots **Anchor) { int content =0; if (*Anchor) { tListKnots *old = *Anchor; *Anchor = (*Anchor)->next; content = old->data; delete old; } return content; } int main() { tListKnots *Anchor = 0; push(&Anchor, 2); push(&Anchor, 5); push(&Anchor, 18); cout << pop(&Anchor) << endl; cout << pop(&Anchor) << endl; cout << pop(&Anchor) << endl; } Листинг В.10. Стек (stackex.cpp) Вероятно, вас немного смущает, что параметр Anchor имеет два символа астериска. Причина в том, что Anchor изменяется внутри функции. Чтобы изменения затронули также и переменную вызываемой функции, ее нужно передавать либо через указатель, либо через ссылку. 505
Приложение В Игра «Бермуда»: поиск кораблей Задание со стр. 191. Основополагающая идея игры состоит в том, что вводятся параметры направления поиска. Они называются xdiff и ydiff. Внутри функции они будут просто добавляться к значениям х и у. Поскольку неизвестно, какая координата будет изменяться в каком направлении, в условии цикла while будут проверяться все варианты. int search(tShip Ship[], int x, int y, int xdiff, int ydiff) { x+=xdiff; y+=ydiff; while(x<X && x>=0 && y<Y && y>=0) { if (hier_is_a_ship(ship, x, y)) { return 1; } x+=xdiff; y+=ydiff; } return 0; } Листинг В.11. Диагональный поиск Чтобы все направления были просмотрены, можно при вызове определить два вложенных цикла for, чьи индексы изменяются от –1 до +1. Требуется обработать только тот случай, когда обе разности равны 0, чтобы цикл в функции search() не был бесконечным. Как минимум, хорошо было бы поместить опрос первой строкой функции search(). int search_ship(tShip Ship[], int x, int y) { int Number=0; 506
Примеры решений задач if (hier_is_a_ship(Ship, x, y, true)) { return MaxShip; } else { for (int i=-1; i<=1; i++) { for (int j=-1; j<=1; j++) { if (!(i==0 && j==0)) { Number += search(Ship, x, y, i, j); } } } } return Number; } Листинг В.12. Поиск кораблей Поиск в бинарном дереве Задание со стр. 202. Проще всего решить эту задачу так, чтобы функция fit_in() для добавления листа сокращалась, и просто возвращался 0. Далее при рекурсивном вызове присваивание переставлялось бы в возвращаемое значение. tTree * Search(tTree *Leaf, int Content) { if (Leaf==0) return 0; if (Content < Leaf->Content) { return Search(Leaf->left, Content); } 507
Приложение В else if (Content > Leaf->Content) { return Search(Leaf->right, Content); } return Leaf; } Листинг В.13. Бинарное дерево (bintree.cpp) Калькулятор для float Задание со стр. 210. Полная программа уже была представлена для чисел с плавающей запятой. Но функция определения числа не принимала цифры после запятой. Это касалось только функции search_token(). Чтобы не сильно перегружать ее, распознавание числа в следующем листинге вынесено в отдельную функцию read_value(). Эта функция работает точно также, как и программа со стр. 130. Она считывает сначала позиции перед запятой, затем смотрит, следует за ними точка или запятая, и считывает затем позиции после запятой. По этой причине функция интерпретирует как точку, так и запятую в качестве символа разделителя дробной и целой части. void read_value() { while (*srcPos>='0' && *srcPos<'9') { TokenValue *= 10; TokenValue += *srcPos-'0'; srcPos++; } if (*srcPos==',' ||*srcPos=='.') { double NK = 1; srcPos++; while (*srcPos>='0' && *srcPos<='9') { 508
Примеры решений задач NK *= 10; TokenValue += (*srcPos-'0')/NK; srcPos++; } } } tToken search_token() // производит поиск лексемы с текущей позиции // здесь также устанавливаются и записываются в // глобальные переменные TokenValue числовые константы { aktToken = ERROR; // пропустить пробел while (*srcPos==' ') srcPos++; if (*srcPos==0) { aktToken = END; } else { switch (*srcPos) { case '(': aktToken=LPAR; break; case ')': aktToken=RPAR; break; case '*': aktToken=MUL; break; case '/': aktToken=DIV; break; case '+': aktToken=PLUS; break; case '-': aktToken=MINUS; break; } if (*srcPos>='0' && *srcPos<'9') { aktToken=NUMBER; TokenValue = 0.0; read_value(); 509
Приложение В } if (aktToken != NUMBER) { srcPos++; } } return aktToken; } Листинг В.14. Распознавание чисел с плавающей запятой в калькуляторе (floatcalc.cpp) Изменение search_token() минимально. Как только числовая константа распознается по первому числу, aktToken присваивается значение NUMBER, переменная TokenValue инициализируется, и вызывается функция read_value(). В начале функции search_token() указан небольшой цикл, который проверяет, имеется ли пробел перед числом, и пропускает его в случае наличия. При вызове программы следует только следить за тем, чтобы общее выражение находилось в кавычках, иначе интерпретатор командной строки разделит введенную строку в месте пробела. Вопрос к знаку минус Вопрос: Почему интерпретатор не путает минус, хотя иногда он может быть знаком числа, а иногда математическим оператором? Ответ: Интерпретатор опрашивает знак минус в двух местах, сначала в функции bracket(). Если синтаксический анализатор опускается, то он находится перед операндом. Тут может быть указана левая скобка. Если здесь находится минус, он может быть представлен только в качестве знака числа, поскольку нет «открытого» оператора. В другом случае минус распознается функцией PlusMinus(). Сюда синтаксический анализатор приземляется, когда уже найден один из операндов и происходит поиск следующего. Так что здесь минус интерпретируется только в качестве знака вычитания. 510
Примеры решений задач Буфер: FIFO Задание со стр. 231. #include <limits.h> #include <iostream> using namespace std; class tNode { public: int d; tNode *next; }; class tFifo { public: tFifo(); ~tFifo(); void push(int); int pop(); private: tNode * Head; tNode * Tail; }; tFifo::tFifo() { Head = Tail = 0; } tFifo::~tFifo() { tNode *last = Head; while (Head) { last = Head; Head = Head->next; 511
Приложение В delete last; } } void tFifo::push(int d) { tNode *node = new tNode; node->d = d; node->next = 0; // добавить в конец if (Tail) { Tail->next = node; } if (Head==0) { Head = node; } Tail = node; } int tFifo::pop() { int content=-1; if (Tail==Head) Tail = 0; if (Head) { tNode *old = Head; Head = Head->next; content = old->d; delete old; } return content; } int main() { 512
Примеры решений задач tFifo fifo; fifo.push(2); fifo.push(5); fifo.push(18); cout << fifo.pop() << endl; cout << fifo.pop() << endl; cout << fifo.pop() << endl; } Листинг В.15. Реализация FIFO Пример стека в качестве списка Задание со стр. 235. Чтобы перенести стек из массива в список, сначала требуется определить класс для каждого узла, который принимает данные и содержит связь со следующим элементом. Здесь перемещается класс tNode. Соответственно, в классе Stack для якоря вспомогательный указатель и доступ к next использует тип tNode<T>. #include <iostream> using namespace std; template<class T> class tNode { public: T d; tNode<T> *next; }; template<class T> class Stack { public: Stack() { Anchor = 0; } ~Stack(); bool pop(T *); bool push(T ); private: 513
Приложение В const int maxStack; tNode<T> *Anchor; int index; }; template<class T> Stack<T>::~Stack() { tNode<T> *last = Anchor; while (Anchor) { last = Anchor; Anchor = Anchor->next; delete last; } } template<class T> bool Stack<T>::pop(T *get) { if (Anchor) { tNode<T> *old = Anchor; Anchor = Anchor->next; *get = old->d; delete old; return true; } return false; } template<class T> bool Stack<T>::push(T set) { tNode<T> *node = new tNode<T>; node->d = set; node->next = Anchor; Anchor = node; return true; } 514
Примеры решений задач int main() { Stack<int> iStack; int a; iStack.push(8); iStack.push(4); iStack.push(2); for (int i=0; i<3; i++) { if (iStack.pop(&a)) { cout << a << endl; } } } Листинг В.16. Пример стека в виде списка (templstack.cpp) Функция atof() с десятичной запятой Задание со стр. 373. Напишите функцию atof() с параметрами, аналогичными вышеописанным. Но используйте функции строк для поиска запятой и замены ее точкой, результат передавайте функции стандартной библиотеки atof(). #include <iostream> using namespace std; #include <string.h> #include <stdlib.h> double atof(const char *NumberString, char DecimalSign) { const int MAX=80; char Buffer[MAX]; strncpy(Buffer, NumberString, MAX); 515
Приложение В Buffer[MAX-1] = 0; char *KommaPos = 0; char KommaStr[2] = "A"; KommaStr[0] = DecimalSign; KommaPos = strstr(Buffer, KommaStr); if (KommaPos) { *KommaPos = '.'; } return atof(Buffer); } int main(int argc, char **argv) { if (argc>1) { cout << atof(argv[1], ',') << endl; } } Листинг В.17. Функция atof() с двумя параметрами Поскольку функция в интерфейсе с помощью ключевого слова const защищена от того, что строка будет изменена, перед заменой запятой требуется создать копию. Хотя компилятор через указатель KommaPos может и не заметить этот доступ, но пользователь программы точно окажется недоволен, если что-то будет нарушено. Чтобы из параметра char DecimalSign сделать строку такой, как требует второй параметр в функции strstr(), вводится локальная строка KommaStr и инициализируется одноразрядной строкой. Этот символ будет заменен на десятичный, итогда можно сразу производить его поиск. Использование стандартной функции atof() допустимо только потому, что наша функция atof() имеет другие параметры. Иначе здесь это привело бы к рекурсии. Компилятор языка C не справился бы с такой ситуацией, поскольку наша функция atof() перегружает оригинальную функцию. Язык C не может перегружать функции. 516
Приложение Г ГЛОССАРИЙ ANSI Сокращение от American National Standards Institute (Американский Национальный Институт Стандартов). Многие языки программирования стандартизированы этим институтом. Так обеспечивается возможность перевода на машинный язык программ, отвечающих стандарту ANSI, созданных в компиляторах различных производителей. C Язык программирования C, наравне с разработкой системы UNIX, создавался в качестве портативного языка программирования высокого уровня, с помощью которого можно также писать низкоуровневые программы. Поскольку UNIX написан не на языке ассемблера, как это было распространено в те времена, операционная система не привязана к аппаратному обеспечению. CPU Central Processing Unit — центральный процессор. Это основополагающий элемент компьютера, который выполняет программы. GNU Сокращение GNU расшифровывается как «GNU is Not UNIX» (GNU — не UNIX). Целью GNU, задолго до разработки Linux, было создание операционной системы со всеми необходимыми инструментами, которую можно свободно распространять и чей исходный код открыт. Компилятор GNU для языков C и C++, make, и другие важные инструменты можно использовать почти на всех платформах. С другой стороны, GNU имеет особую лицензию, которая гарантирует, что данное программное обеспечение может использоваться свободно. В ядре она содержит условие, которое обеспечивает свободное распространение программного обеспечения и возможность получить при необходимости все исходные коды системы. Лицензия GNU открыто разрешает из- 517
Приложение Г менение программного обеспечения, однако требует, чтобы все изменения также подчинялись лицензии GNU. IDE Интегрированная среда разработки, аббревиатура происходит от английского выражения «Integrated Development Environment». При приобретении компиляторов для операционной системы Windows или OS X вы получите не просто компилятор языка C++, а систему, которая состоит из редактора, компилятора, компоновщика, отладчика и некоторых дополнительных инструментов, собранных в одной программе. POSIX POSIX описывает официальные стандарты UNIX, полученные от IEEE (Институт инженеров по электротехнике и электронике — (англ. Institute of Electrical and Electronics Engineers)). RAD Быстрая разработка приложений, аббревиатура происходит от английского выражения «Rapid Application Development». RAD-системы разрешают программисту создавать окна для графических программ с помощью мыши. Когда программе следует отреагировать на действие пользователя, программист должен взять клавиатуру и определить это действие. Самый распространенный представитель семейства — среда Delphi для языка программирования Pascal. Версия для языка C++ называется C++ Builder. Алгоритм Описание действий. Является базисом каждой компьютерной программы. См. стр. 17. Аргумент Параметр при вызове программы или функции. Ассемблера язык Язык процессора. Он дословно переводится в машинный код, который состоит только из единиц и нулей. Поэтому программы, написан- 518
Глоссарий ные на языке ассемблера, очень быстрые, но и сложны в обслуживании и не портативны. См. стр. 17. Атрибут В литературе по объектно ориентированному программированию элементы данных объекта часто называются атрибутами. Этим указывается, что элемент данных определяет свойства объекта. Библиотека Библиотека состоит из нескольких тематически объединенных файлов объектов, которые можно подключить к собственной программе. При подключении динамических библиотек встраивается только тело функции. Библиотечные функции имеются в распоряжении после вызова их операционной системой. Выражение Выражение возвращает пригодный для использования результат. В самом простом случае выражение является переменной или константой, но это также может быть функция с возвращаемым значением или расчет. Заголовочный файл Заголовочный файл содержит определение для исходного кода программы или для объекта данных. Он подключается к исходному файлу программы командой #include. Так можно использовать функции файла объекта. Идентификатор Понятие «идентификатор» происходит от английского термина «handle», что означает «рукоятка» или «ручка». Например, при открытии файла мы получаем идентификатор, по которому операционная система может определить, какой файл программа «держит в руках». В большинстве случаев не имеет смысла рассматривать содержимое идентификатора, поскольку оно имеет ценность только для операционной системы, например, в качестве индекса внутренней структуры данных. Команда Команда — это законченное описание действия языка программирования. В языках C и C++ команда всегда заканчивается точкой с запятой. 519
Приложение Г Компилятор Компилятор переводит исходный код, написанный программистом, в машинный. Из файла исходного кода создается файл объекта. Часто компилятором называют комбинацию из компилятора и компоновщика. См. стр. 16. Компоновщик Компоновщик связывает несколько файлов объектов и библиотек с программой. См. стр. 16. Листинг Листинг — распечатанный код программы. Макрос Техника обобщенного программирования, в языке C реализуемая с помощью препроцессора и команды #include. В языке C++ с этой целью используются шаблоны. Метод Функции класса в литературе по объектно ориентированному программированию называются методами. Это обозначение производит правильное впечатление, что функции доступны только через объект и поэтому являются действием объекта. Модульное программирование Концепция модульного программирования состоит в том, чтобы объединить данные и функции в модули так, чтобы они могли использоваться только через конкретный интерфейс. При этом работает принцип скрытности. Наружу выдается ровно столько, сколько требуется программисту других модулей для использования этого конкретного модуля. Все остальное является деталями реализации, которые могут быть в любой момент изменены автором модуля без информирования об этом его коллег. Обобщенное программирование В обобщенном программировании создаются программные части, которые можно использовать для нескольких различных типов данных. В языке C для этого используются макросы, а в языке C++ — шаблоны. 520
Глоссарий Объектно ориентированное программирование Объектно ориентированное программирование ставит в центре программы объект. Пока процедурное программирование рассматривает алгоритмы, которые работают с файлами, ООП концентрируется на объекте, который сам вырабатывает действия. Объявление Объявление — это, с одной стороны, определение, с другой — перемещение определения в память. Как только переменная или функция единожды объявлена, ее можно определять сколько угодно раз. Объявление переменной связано с резервированием памяти. Объявление функции содержит команды, которые выполняются при ее вызове. ООП Объектно ориентированное программирование. Определение Определение служит для того, чтобы объявленную в другом месте программы переменную сделать известной. При этом задается вся информация о типах, которые требует компилятор, чтобы обеспечить корректное использование. Определение отличается от объявления тем, что при последнем выделяется память, а при определении только сообщается компилятору, с чем он имеет дело. Переменная Переменная — это именованная память. Здесь можно хранить числа или код, изменять их и снова считывать. Постфикс См. префикс. Препроцессор Составная часть компилятора, то есть переводчика. Он просматривает программу перед компиляцией, и проводит, прежде всего, подстановку кода. Команды препроцессора начинаются с символа октоторпа (#). 521
Приложение Г Префикс Предстоящая последовательность символов. Имя файла библиотеки, например, всегда начинается с lib. То есть оно имеет префикс lib. Оператор инкремента может располагаться перед или после переменной. Если оператор указан перед переменной, его называют префикс, если после — постфикс. Производительность Под производительностью понимают скорость работы системы или программы. Процедура Процедурами в языках C и C++ называются функции, которые не возвращают значений. Другие языки программирования, прежде всего Pascal, отличают процедуры от функций по ключевым словам (procedure, function). Процедурное программирование Процедурное программирование пытается разделить решение задачи на отдельные функции, которые вызываются другими функциями. С ним тесно связан нисходящий метод программирования (см. стр. 185). Сегодня понятие процедурного программирования часто используется для описания программ, которые разработаны не по объектно ориентированной логике. Процесс Активная программа. При запуске процесс получает собственный номер. При этом он имеет отдельную область памяти, состояние процессора и защищен от внешнего доступа операционной системой. Рекурсия Рекурсией называется самовызов функции и особая техника программирования. Наряду с этим существуют рекурсивные структуры данных, к ним относится особенно часто используемая структура дерева.. Поскольку перебор элементов такой структуры проще всего реализовать рекурсией, деревья называют рекурсивными структурами и просмотр одной полной ветви дерева — рекурсивным. 522
Глоссарий Синтаксис Синтаксис описывает группирование нескольких команд. Стек Структура памяти, аналогичная стопке книг. Элементы в ней можно поместить сверху и оттуда же изъять. Поэтому самый верхний элемент всегда записывается последним и считывается первым. Такой тип памяти еще называют FILO (акроним от фразы First In, Last Out — «первым пришел — последним ушел»). Также стек используется при вызове функций и создании локальных переменных. Команда return находит адрес возврата, поскольку всегда должен быть переход в основную программу, которая вызвала функцию. Там также хранятся параметры и локальные переменные, которые освобождаются при выходе из функции. Структурное программирование Структурное программирование искоренило использование команды goto. Такой переход стал ненужным с появлением циклов и условий с ветвью else. При этом ход программы стал более очевидным и удобным для обслуживания. Файл объекта Каждый исходный код преобразуется компилятором отдельно. При этом создается файл объекта, который состоит из команд машинного языка. Чтобы создать из него готовую для запуска программу, требуется подключить другие файлы объектов и стандартных библиотек. Это задание выполняет компоновщик.
Приложение Д ЛИТЕРАТУРА • Barclay Ken, Savage John. Object Oriented Design With C++. Prentice Hall, 1996 • Borrmann Alf, Komnick Stefan, Landgrebe Gunnar, Matérne Jan, Rätzmann Manfred, Sauer Jörg. Rational Rose und UML. Galileo Press, 2001. • Dal Cin Mario, Lutz Joachim, Risse Thomas. Programmierung in Modula-2. Teubner, 1986. • Dal Cin Mario. Grundlagen der systemnahen Programmierung. Teubner, 1988. • Jakobs Holger. Einführung www.c-plusplus.de • Kaiser Richard. C++ mit dem Borland C++ Builder. Springer, 2002. • Kaiser Ulrich, Kecher Christoph. C/C++ — Das umfassende Lehrbuch. Galileo Press, 2006. • Oestereich Bernd. Objektorientierte Softwareentwicklung.Oldenburg, 1999. • Wigard Susanne. Visual C++ 6. bhv, 1999. • Willemer Arnold. UNIX — Das umfassende Handbuch. Galileo Press, 2007. • Willms André. C++ STL. Galileo Press, 2000. • Wirth Niklaus. Algorithmen und Datenstrukturen mit Modula-2. Teubner, 1986. • Wolf Jürgen. C++ von A bis Z. Galileo Press, 2006. • Брайан Керниган, Деннис Ритчи. Язык программирования C. М.: Вильямс, 2012. • Бьерн Страуструп. Язык программирования C++. М.: Бином, 2011. 524 in ISO C++. PDF. Загрузка:
Литература • Джесс Либерти, Брэдли Джонс. Освой самостоятельно C++ за 21 день. М.: Вильямс, 2010. • Майкл К. Джонсон, Эрик В. Троан. Разработка приложений в среде Linux. М.: Вильямс, Диалектика, 2007. • Стефан Р. Дэвис: C++ для чайников. Диалектика. М.: Вильямс, 2009. Во Всемирной паутине информацию по языку программирования C++ можно найти на следующих ресурсах: • ru.cppreference.com/w/ • www.dmoz.org • www.open-std.org
ПРЕДМЕТНЫЙ УКАЗАТЕЛЬ ASCII, 40 bool, 84 case, 79 catch, 332 char, 40 cin, 68, 377 class, 214 const, 55 continue, 104 cout, 66 C-строка, 374 default, 80 double, 43 do-while, 98 else, 73 end of file, 377 enum, 158 exeption, 341 explicit, 224 extern, 301 false, 84 float, 42 for, 100 friend, 232 if, 71 include, 66 inline, 184 int, 29 list, 419, 420 long, 36 make, 289, 309 526 map, 430 multiset, 427 mutable, 260 new, 138, 152 priority_queue, 433 private, 225 protected, 265 public, 225 queue, 433 return, 161 set, 427 short, 36 stack, 433 static, 256, 303 STL, 410 string, 127 struct, 146 switch, 79 this, 219 throw, 333 true, 84 try, 332 typedef, 160 typename, 318 unsigned, 36 vector, 411 virtual, 275 void, 163 wchar_t, 40 while, 94
Предметный указатель Абстрактные базовые классы, 279, 280 Алгоритм, 18 Анонимное пространство имен, 330 Базовый класс, 261 Байт, 35 Бибилиотека, 17 Бинарное дерево, 198 Бит, 35 Блок, 27 Булевые выражения, 83 Двусвязный список, 420 Декремент, 243 Деструктор, 220 Инкремент, 243 Интерфейс, 229 Класс, 214 Объявление, 300 Оператор адреса, 137 Оператор вызова, 254 Оператор индекса, 252 Определение, 300 Отладчик, 165 Параметры, 163 Перегрузка операторов, 240 Переменная, 28 Полиморфизм, 278 Предикат, 423 Предопределенные параметры, 178 Препроцессор, 16, 291 Префикс, 61, 243 Присваивание, 57 Пространство имен, 66, 328 Прототип, 165, 288 Рекурсия, 195 Классы ошибок, 337 Комментарии, 24 Компановщик, 17, 289 Компилятор, 16 Константа, 47 Конструктор, 220 Конструктор копирования, 234 Контейнер, 410 Контейнер-адаптер, 433 Связанный список, 155 Макрос, 291, 321 Указатель, 135 Указатель функции, 211 Манипулятор, 68 Массив, 116 Математические функции, 401 Многомерный массив, 131 Символьная последовательность, 127 Сортировка методом пузырька, 122 Сортировка пузырьком, 122 Ссылка, 174 Стандартный конструктор, 220 Статические переменные, 193 Стек, 165 Файл, 381 Наследование, 261 Файл объекта, 16 Форматированный вывод, 68 Функция, 62, 161 Общение. См. Обсуждение Цикл, 94 Объектно-ориентированное программирование, 21, 213 Элементная функция, 216
Ïðîèçâîäñòâåííî-ïðàêòè÷åñêîå èçäàíèå ÌÈÐÎÂÎÉ ÊÎÌÏÜÞÒÅÐÍÛÉ ÁÅÑÒÑÅËËÅÐ Àðíîëüä Âèëëåìåð ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ ÍÀ Ñ++ (îðûñ òiëiíäå) Äèðåêòîð ðåäàêöèè Å. Êàïü¸â Îòâåòñòâåííûé ðåäàêòîð Â. Îáðó÷åâ Õóäîæåñòâåííûé ðåäàêòîð Å. Ìèøèíà Ñâåäåíèÿ î ïîäòâåðæäåíèè ñîîòâåòñòâèÿ èçäàíèÿ ñîãëàñíî çàêîíîäàòåëüñòâó ÐÔ î òåõíè÷åñêîì ðåãóëèðîâàíèè ìîæíî ïîëó÷èòü ïî àäðåñó: http://eksmo.ru/certification/     :        Ïîäïèñàíî â ïå÷àòü 29.07.2013. Ôîðìàò 70x1001/16. Ïå÷àòü îôñåòíàÿ. Óñë. ïå÷. ë. 42,78. Òèðàæ ýêç. Çàêàç