Текст
                    Массивы „
flyW" Указатели
(А Ч "£>
ЧЧЧ^и*
Ч ®, ч <
ЧЧДгЧ
Ч- ч
%л
°Ч
Классы
C++
Джесс Либерти,
Дэвид Б. Хорват, ССР
SAMS
ВИЛЬЯМС
№4
ЧЕТВЕРТОЕ ИЗДАНИЕ

SAMS__________ Teach Yourself Jesse Liberty David B. Horvath, CCP ours FOURTH EDITION SAMS 800 East 96th St., Indianapolis, Indiana 46240 USA
SAMS _______________ Освой самостоятельно Джесс Либерти Дэвид Б. Хорват, ССР часа ЧЕТВЕРТОЕ ИЗДАНИЕ Издательский дом “Вильямс" Москва ♦ Санкт-Петербург ♦ Киев 2007
ББК 32.973.26-018.2.75 Л55 УДК 681.3.07 Издательский дом “Вильямс” Зав. редакцией С. Н. Тригуб Перевод с английского и редакция В.А. Коваленко По общим вопросам обращайтесь в Издательский дом “Вильямс” по адресу: info@williamspublishing.com, http://www.williamspublishing.com 115419, Москва, а/я 783; 03150, Киев, а/я 152 Либерти, Джесс, Хорват, Дэвид Л55 Освой самостоятельно C++ за 24 часа, 4-е издание. : Пер. с англ. — М.: Издательский дом “Вильямс”, 2007. — 448с.: ил. — Парад, тит. англ. ISBN 978-5-8459-0949-7 (рус.) Эта книга поможет самостоятельно изучить язык C++, его принципы и концепции. Здесь из- ложены фундаментальные основы программирования, описаны принципы управления вводом- выводом, циклы, массивы, объектно-ориентированные подходы, а также создание полнофунк- ционального приложения. Все главы содержат листинги программ, результаты их выполнения и анализ кода. Приведены ответы на часто задаваемые вопросы, а также упражнения и контроль- ные вопросы. Изложение книги ие предполагает наличия у читателя предварительных знаний в области C++, а четкая организация материала позволит быстро и просто изучить язык. ББК 32.973.26-018.2.75 Все названия программных продуктов являются зарегистрированными торговыми марками соответ- ствующих фирм. Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами, будь то электронные или механические, включая фо- токопирование и запись на магнитный носитель, если на это нет письменного разрешения издательства Sams Publishing. Authorized translation from the English language edition published by Sams Publishing, Copyright © 2005. All rights reserved. No part of ihis book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from the publisher. Russian language edition is published by Williams Publishing House according to the Agreement with R&l Enterprises International, Copyright © 2007 ISBN 978-5-8459-0949-7(pyc.) ISBN 0-672-32681-7 (англ.) © Издательский дом “Вильямс”, 2007 © by Sams Publishing, 2005
Оглавление Введение 18 ЧАСТЬ I. ВВЕДЕНИЕ В C++ 21 ЧАС 1. Первые шаги 22 ЧАС 2. Структура программы на языке C++ 35 ЧАС 3. Переменные и константы 47 ЧАС 4. Выражения и операторы 60 ЧАС 5. Функции 79 ЧАС 6. Ветвление процесса выполнения программ 99 ЧАСТЬ II. КЛАССЫ 117 ЧАС 7. Простые классы 118 ЧАС 8. Подробнее о классах 131 ЧАСТЬ III. УПРАВЛЕНИЕ ПАМЯТЬЮ 143 ЧАС 9. Указатели 144 ЧАС 10. Подробнее об указателях 160 ЧАС 11. Ссылки 171 ЧАС 12. Подробнее о ссылках и указателях 185 ЧАСТЬ IV. ДОПОЛНИТЕЛЬНЫЕ СРЕДСТВА 199 ЧАС 13. Дополнительные возможности функций 200 ЧАС 14. Перегрузка операторов 211 ЧАС 15. Массивы 226 ЧАСТЬ V. НАСЛЕДОВАНИЕ И ПОЛИМОРФИЗМ 243 ЧАС 16. Наследование 244 ЧАС 17. Полиморфизм и производные классы 263 ЧАС 18. Расширенное наследование 276 ЧАС 19. Связанные списки 295 ЧАСТЬ VI. СПЕЦИАЛЬНЫЕ ТЕМЫ 307 ЧАС 20. Специальные классы, функции и указатели 308 ЧАС 21. Препроцессор 335 ЧАС 22. Объектно-ориентированный анализ и проектирование 359 ЧАС 23. Шаблоны 383 ЧАС 24. Исключения, обработка ошибок и другое 399 ЧАСТЬ VII. ПРИЛОЖЕНИЯ 417 ПРИЛОЖЕНИЕ А. Двоичные и шестнадцатеричные числа 418 ПРИЛОЖЕНИЕ Б. Глоссарий 426 Предметный указатель 433
Содержание Введение 18 Для кого написана эта книга 18 Нужно ли изучить сначала язык С? 18 Зачем изучать язык C++? 18 Соглашения, принятые в этой книге 19 Что содержит прилагаемый CD 20 Служба поддержки 20 От издательства 20 ЧАСТЬ I. ВВЕДЕНИЕ В C++ 21 ЧАС 1. Первые шаги 22 Подготовка к программированию 22 Несколько уточнений по поводу C++, ANSI C++, ISO C++ и Windows 23 Установка и настройка компилятора 24 Установка с прилагаемого CD 24 Настройка компилятора Borland C++BuilderX 25 Компиляция при помоши компилятора C+ + BuilderX 26 Компилятор и редактор кода 27 Компиляция и компоновка исходного кода 28 Компиляция в интегрированной среде разработки 28 Компоновка программы 28 Цикл разработки 29 Hello.cpp — первая программа на языке C++ 29 Ошибки компиляции 32 Вопросы и ответы 33 Коллоквиум 33 Контрольные вопросы 34 Упражнения 34 Ответы на контрольные вопросы 34 ЧАС 2. Структура программы на языке C++ 35 Почему язык C++ является правильным выбором 35 Процедурное, структурное и объектно-ориентированное программирование 36 Язык C++ и объектно-ориентированное программирование 37 Элементы простой программы 38 Директива tfinclude 39 Построчный анализ 40 Комментарии 41 Типы комментариев 41 Использование комментариев 41 Функции 42 Обращение к функции 42 Использование функций 43 Применение параметров функции 44 Вопросы и ответы 45 Коллоквиум 45
Содержание 7 Контрольные вопросы 45 Упражнения 45 Ответы на контрольные вопросы 46 ЧАС 3. Переменные и константы 47 Что такое переменная? 47 Резервирование памяти 48 Размер целых чисел 48 Типы signed и unsigned 49 Основные типы переменных 50 Определение переменной 50 Чувствительность к регистру 51 Ключевые слова 51 Создание нескольких переменных одновременно 52 Присвоение значений переменным 52 Ключевое слово typedef 53 В каких случаях использовать тип short, а в каких — long? 54 Переполнение регистра беззнаковой переменной 55 Переполнение регистра знаковой переменной 56 Константы 56 Литеральные константы 57 Символьные константы 57 Перечисляемые константы 58 Вопросы и ответы 58 Коллоквиум 59 Контрольные вопросы 59 Упражнения 59 Ответы на контрольные вопросы 59 ЧАС 4. Выражения и операторы 60 Операторы 60 Непечатаемые символы 60 Блоки кода и составные операторы 61 Выражения 61 Операторы 62 Оператор присвоения 63 Математические операторы 63 Объединение операторов присвоения и математических операторов 64 Операторы инкремента и декремента 64 Префиксные и постфиксные операторы 65 Приоритет 66 Вложение круглых скобок в сложных выражениях 67 Операторы отношения 68 Условный оператор If 69 Ключевое слово else 69 Усложнение оператора if 71 Использование фигурных скобок для вложенных операторов if 72 Подробнее о логических операторах 75 Логический оператор AND 75 Логический оператор OR 75 Логический оператор NOT 75 Приоритет операторов отношения 76 Подробнее об истине и лжи 76 Вопросы и ответы 77
8 Содержание Коллоквиум 77 Контрольные вопросы 77 Упражнения 78 Ответы на контрольные вопросы 78 ЧАС 5. Функции 79 Что такое функция? 79 Объявление и определение функций 79 Объявление функции 80 Определение функции 82 Коротко об определении функций 82 Использование переменных в функциях 83 Локальные переменные 83 Глобальные переменные 86 Аргументы функций 86 Использование функций как параметров 86 Параметры — это также локальные переменные 87 Возвращение значений из функций 88 Значения параметров, используемые по умолчанию 90 Перегрузка функций 92 Встраиваемые функции 93 Стек и функции 95 Вопросы и ответы 96 Коллоквиум 98 Контрольные вопросы 98 Упражнения 98 Ответы на контрольные вопросы 98 ЧАС 6. Ветвление процесса выполнения программ 99 Циклы 99 Оператор goto 99 Почему следует избегать использования оператора goto? 99 Оператор цикла while 100 Более сложный оператор while 100 Операторы break и continue 102 Цикл while(I) 104 Оператор цикла do..while 105 Оператор цикла for 107 Инициализация, условие и приращение 108 Более сложные операторы for 109 Пустой цикл for III Вложенные циклы 112 Оператор switch 113 Вопросы и ответы 115 Коллоквиум 115 Контрольные вопросы 115 Упражнения 116 Ответы на контрольные вопросы 116 ЧАСТЫ1. КЛАССЫ П7 ЧАС 7. Простые классы 118 Что такое тип 118 Создание новых типов 119
Содержание 9 Классы и их члены 119 Объявление класса 120 Соглашения об именовании классов 120 Определение объекта 121 Классы и объекты 121 Доступ к членам класса 121 Закрытые и открытые члены класса 122 Реализация методов класса 123 Конструкторы и деструкторы, или создание и удаление объектов 126 Стандартные конструкторы 126 Конструктор, предоставляемый компилятором 126 Вопросы и ответы 129 Коллоквиум 129 Контрольные вопросы 129 Упражнения 129 Ответы на контрольные вопросы 130 ЧАС 8. Подробнее о классах 131 - Постоянные функции-члены 131 Интерфейс и реализация 132 Где объявлять класс и располагать реализацию методов 132 Встраиваемая реализация 133 Классы, содержащие другие классы как данные-члены 135 Вопросы и ответы 140 Коллоквиум 140 Контрольные вопросы 140 Упражнения 140 Ответы на контрольные вопросы 141 ЧАСТЫII. УПРАВЛЕНИЕ ПАМЯТЬЮ 143 ЧАС 9. Указатели 144 Указатели и их назначение 144 Сохранение адреса в указателе 147 Имена указателей 148 Оператор косвенного доступа 148 Указатели, адреса и переменные 149 Манипулирование данными при помощи указателей 150 Адреса 151 Для чего нужны указатели? 153 Стек и динамически распределяемая память 153 Ключевое слово new 154 Ключевое слово delete 155 Утечка памяти 157 Вопросы и ответы 158 Коллоквиум 158 Контрольные вопросы 158 Упражнения 159 Ответы на контрольные вопросы 159 ЧАС 10. Подробнее об указателях 160 Создание объектов в динамической памяти 160 Удаление объектов 160 Доступ к переменным-членам при помощи указателя 162
10 Содержание Данные-члены в динамической памяти I63 Указатель this 164 Паразитные, дикие и зависшие указатели 166 Постоянные указатели 166 Константы как указатели и как функции-члены 167 Постоянный указатель this 169 Вопросы и ответы 169 Коллоквиум 169 Контрольные вопросы 169 Упражнения 169 Ответы на контрольные вопросы 170 ЧАС 11. Ссылки 171 Что такое ссылка? 171 Создание ссылок 171 Использование в ссылках оператора обращения к адресу (&) 173 На что можно ссылаться? 175 Нулевые указатели и ссылки 176 Передача функции аргументов по ссылке 176 Вариант функции swap() для работы с указателями 177 Реализация функции swap() для работы со ссылками 179 Понятие заголовка и прототипа функции 180 Возвращение нескольких значений 180 Возвращение значений по ссылке 182 Вопросы и ответы 183 Коллоквиум I83 Контрольные вопросы 184 Упражнения 184 Ответы на контрольные вопросы 184 ЧАС 12. Подробнее о ссылках и указателях 185 Передача ссылок как средство повышения эффективности 185 Передача постоянного указателя 188 Ссылки как альтернатива указателям 190 Когда использовать ссылки, а когда — указатели 192 Не возвращайте ссылку на объект, находящийся вне области видимости! 192 Возвращение ссылки на объект в динамической памяти 194 Кто владеет указателем? 196 Вопросы и ответы 197 Коллоквиум 197 Контрольные вопросы 197 Упражнения 197 Ответы на контрольные вопросы 197 ЧАСТЬ IV. ДОПОЛНИТЕЛЬНЫЕ СРЕДСТВА 199 ЧАС 13. Дополнительные возможности функций 200 Перегрузка функций-членов 200 Использование значений по умолчанию 202 Выбор между значениями по умолчанию и перегруженными функциями 204 Перегрузка конструкторов 204 Инициализация объектов 205 Конструктор копий 205 Вопросы и ответы 209
Содержание 11 Коллоквиум 209 Контрольные вопросы 210 Упражнения 210 Ответы на контрольные вопросы 210 ЧАС 14. Перегрузка операторов 211 Перегрузка операторов 211 Создание функции инкремента 212 Перегрузка постфиксных операторов 213 Различие между префиксом и постфиксом 213 Оператор суммы 215 Перегрузка оператора суммы operator+ 217 Ограничения на перегрузку операторов 218 Что можно перегружать 218 Оператор присвоения 219 Преобразование типов данных 221 Оператор int() 223 Вопросы и ответы 224 Коллоквиум 224 Контрольные вопросы 225 Упражнения 225 Ответы на контрольные вопросы 225 ЧАС 15. Массивы 226 Что такое массив? 226 Доступ к элементам массива 226 Запись данных за пределами массива 228 Ошибка последнего столба 228 Инициализация массивов 228 Массивы объектов 229 Многомерные массивы 230 Инициализация многомерных массивов 231 Немного о памяти 233 Массивы указателей 233 Объявление массивов в области динамической памяти 234 Указатель на массив и массив указателей 235 Имена массивов и указателей 235 Удаление массивов из динамической памяти 237 Массивы символов 238 Функции strcpyO и strncpyO 239 Строковые классы 241 Вопросы и ответы 241 Коллоквиум 242 Контрольные вопросы 242 Упражнения 242 Ответы на контрольные вопросы 242 ЧАСТЬ V. НАСЛЕДОВАНИЕ И ПОЛИМОРФИЗМ 243 ЧАС 16. Наследование 244 Что такое наследование? 244 Наследование и происхождение 245 В мире животных 246 Синтаксис происхождения классов 246 Закрытый или защищенный? 248
12 Содержание Конструкторы и деструкторы 250 Передача аргументов в базовые конструкторы 252 Переопределение функций 256 Перегрузка или переопределение? 258 Сокрытие метода базового класса 258 Вызов базового метода 259 Вопросы и ответы 261 Коллоквиум 261 Контрольные вопросы 261 Упражнения 261 Ответы на контрольные вопросы 262 ЧАС 17. Полиморфизм и производные классы 263 Реализация полиморфизма при помощи виртуальных методов 263 Как работают виртуальные функции 267 Попытка доступа к методам из базового класса 268 Отсечение 268 Виртуальные деструкторы 270 Виртуальные конструкторы копий 271 Цена виртуальных методов 274 Вопросы и ответы 274 Коллоквиум 274 Контрольные вопросы 274 Упражнения 275 Ответы на контрольные вопросы 275 ЧАС 18. Расширенное наследование 276 Проблемы одиночного наследования 276 Абстрактные типы данных 280 Чистые виртуальные функции 283 Реализация чистых виртуальных функций 286 Сложная иерархия абстракций 289 Какие классы являются абстрактными? 293 Вопросы и ответы 293 Коллоквиум 294 Контрольные вопросы 294 Упражнения 294 Ответы на контрольные вопросы 294 ЧАС 19. Связанные списки 295 Связанные списки и другие структуры 295 Применение связанных списков 296 Делегирование ответственности 296 Компоненты связанных списков 297 Так что же из этого следует? 304 Вопросы и ответы 305 Коллоквиум 305 Контрольные вопросы 305 Упражнения 306 Ответы на контрольные вопросы 306 ЧАСТЬ VI. СПЕЦИАЛЬНЫЕ ТЕМЫ 307 ЧАС 20. Специальные классы, функции и указатели 308 Статические данные-члены 308 Статические функции-члены 310
Содержание 13 Объединение классов Доступ к членам вложенного класса Контроль доступа к вложенным членам Цена объединения Копирование при передаче по значению или передача по ссылке Дружественные классы Дружественные функции Указатели на функции Упрощенный вызов Массивы указателей на функции Передача указателей на функции другим функциям Использование ключевого слова typedef с указателями на функции Указатели на функции-члены Массивы указателей на функции-члены Вопросы и ответы Коллоквиум Контрольные вопросы Упражнения Ответы на контрольные вопросы ЧАС 21. Препроцессор Препроцессор и компилятор Просмотр промежуточного файла Использование директивы #define Использование директивы #define для констант Использование директив #define и #ifdef для проверки Директива препроцессора #else Подключение файлов и предупреждение ошибок подключения Определение в командной строке Директива #undef Условная компиляция Макрофункции Зачем столько круглых скобок? Функции, шаблоны или макросы? Операции со строками Оператор взятия в кавычки Конкатенация Встроенные макросы Макрос assert() Вопросы и ответы Коллоквиум Контрольные вопросы Упражнения Ответы на контрольные вопросы ЧАС 22. Объектно-ориентированный анализ и проектирование Цикл разработки Моделирование системы сигнализации Концептуализация Анализ и требования Низкоуровневое и высокоуровневое проектирование Другие объекты Какие классы нужны? Как передается сигнал тревоги? Цикл событий 312 317 317 318 318 318 319 319 321 322 324 326 329 331 333 334 334 334 334 335 335 335 335 336 336 337 338 339 339 339 339 340 341 342 342 342 343 343 357 357 357 358 358 359 359 360 360 360 361 362 362 363 363
14 Содержание Проект системы PostMaster 365 Дважды отмерь, один раз отрежь 366 Разделяй и властвуй 366 Формат сообщений 367 Предварительное проектирование классов 367 Корневые и некорневые иерархии 368 Проектирование интерфейсов 370 Создание прототипа 371 Правило 80/80 371 Разработка класса PostMasterMessage 372 Интерфейс прикладных программ 372 Программирование в больших группах 373 Продолжение проекта 374 Работа с управляющей программой 375 Вопросы и ответы 381 Коллоквиум 381 Контрольные вопросы 381 Упражнения 382 Ответы на контрольные вопросы 382 ЧАС 23. Шаблоны 383 Что такое шаблоны? 383 Экземпляры шаблона 384 Определение шаблона 384 Использование экземпляров шаблона 390 Стандартная библиотека шаблонов 396 Вопросы и ответы 397 Коллоквиум 397 Контрольные вопросы 397 Упражнения 397 Ответы на контрольные вопросы 398 ЧАС 24. Исключения, обработка ошибок и другое 399 Ошибки, недоработки и просчеты 399 Реакция на непредвиденное 400 Исключения 400 Применение исключений 401 Применение блоков try и catch 405 Обработка исключений 405 Применение нескольких обработчиков catch 406 Обработка по ссылке и полиморфизм 406 Стиль программирования 410 Фигурные скобки 410 Длинные строки 411 Операторы switch 411 Текст программы 411 Имена идентификаторов 412 Правописание и регистр символов имен 412 Комментарии 413 Доступ 413 Определения классов 414 Подключение файлов 414 Макрос assert() 414 Ключевое слово const 414
Содержание 15 Следующие шаги 415 Куда обратиться за помощью и консультацией? 415 Рекомендованная литература 415 Вопросы и ответы 416 Коллоквиум 416 Контрольные вопросы 416 Упражнения 416 Ответы на контрольные вопросы 416 ЧАСТЬ VII. ПРИЛОЖЕНИЯ 417 ПРИЛОЖЕНИЕ А. Двоичные и шестнадцатеричные числа 418 Другие системы счисления 419 Еще об основаниях 419 Двоичная система счисления 420 Почему именно основание 2? 421 Биты байты и полубайты 421 Что такое килобайт? 422 Двоичные числа 422 Шестнадцатеричная система счисления 422 ПРИЛОЖЕНИЕ Б. Глоссарий 426 Предметный указатель 433
Об авторах Джесс Либерти (Jesse Liberty) — автор множества книг по разработке программного обеспечения, включая ряд бестселлеров по C++ и .NET. Являясь президентом ассо- циации Liberty Associates, Inc. (http://www.LibertyAssociates.com), он занимается разработкой уникального программного обеспечения, консультациями и преподаванием. Дэвид Б. Хорват (David В. Horvath), ССР — старший консультант из Филадель- фии, штат Пенсильвания. Обладая более чем 15-летним опытом в этой области, он был также адъюнкт-профессором университета очного и дистанционного обучения по таким направлениям, как C++, Unix/Linux и технологии баз данных. Его дости- жения были отмечены университетом штата Пенсильвания в 1998 году. Он также принимал участие в проведении международных семинаров и коллоквиумов. Дэвид является автором книги UNIX for the Mainframer (Prentice-Hall/PTR), соавтором (в паре с Джессом Либерти) книги Teach Yourself C++ for Linux in 21 Days, а также книг Unix Unleashed (несколько изданий), Red Hat Linux Unleashed (несколько изданий). Learn Shell Programming in 24 Hours, Linux Unleashed и Linux Programming Unleashed (второе из- дание). Он также опубликовал множество статей в журналах. Когда Дэвид не сидит над клавиатурой, он работает в саду, плавает в бассейне и участвует в жизни не имеющих отношения к политике обществ. Он был женат око- ло 17-ти лет, имеет несколько собак и котов (количество которых не постоянно). Свя- заться с Дэвидом можно по адресу cppin24@cobs.com, однако, пожалуйста, без спама!
Эта книга посвящена Эдит, Стеиси, Робину и Рэйчел Джесс Либерти Эта книга посвящена моему брату Энди и его жене Пег по случаю их второй годовщины со дня свадьбы, 13 июля 2004 года Я хотел приурочить книгу к их свадьбе, но проект оказался просрочен. Дэвид Б. Хорват Благодарности Издание каждой книги позволяет еще раз выразить благодарность тем людям, без чьей поддержки и помощи ее создание было бы невозможно. Первыми среди них яв- ляются Стеиси, Робин и Рэйчел Либерти. Автор считает своим долгом поблагодарить также всех сотрудников издательств Sams, Que, O'Reilly и Wrox, которые проявили высокий профессионализм, участвуя в выпуске его книг. Редакторы издательства Sams проделали фантастическую работу; автор хотел бы выразить личную благодарность Кэрол Аккерман (Carol Ackerman), Кристи Франклин (Christy Franklin) и Полу Стрикланду (Paul Strickland). Особая благо- дарность Ричарду Халперт (Rich Halpert). И в заключение автор хотел бы поблагода- рить миссис Калиш (Mrs. Kalish), которая в 1965 году научила своих шестиклассников (в том числе и автора) операциям с двоичными числами: в то время ни она, ни он не знали, зачем это может понадобиться. Джесс Либерти Кроме тех сотрудников издательства Sams, которых благодарил Джесс, хотелось бы выразить признательность и другим, принимавшим участие в этом проекте. Речь идет о Сонглин Киу (Songlin Qiu) и Лоретте Ятес (Loretta Yates). Безусловно, эта книга не была бы написана без поддержки и понимания членов моей семьи, особенно жены Мэри. Обычно семьи страдают от того, что “я должен ра- ботать над книгой и не могу тратить время на ххх вместе с вами”. Такова цена проек- та. Именно по вечерам и выходным фактически выпадает время для писанины. Дэвид Б. Хорват
Введение Эта книга поможет самостоятельно изучить программирование на языке C++. Всего за 24 занятия по одному часу каждое читатель изучит такие фундаментальные концепции, как управление вводом и выводом, циклы, массивы, объектно- ориентированное программирование и шаблоны, а также научится создавать прило- жения на языке C++. Обладающие удобной структурой и понятно изложенные, заня- тия снабжены примерами кода, которые сопровождаются получаемыми при их вы- полнении результатами и анализом кода, что позволяет полнее проиллюстрировать тему занятия. Примеры синтаксиса выделены для удобства читателя. В конце каждого занятия приводятся наиболее часто задаваемые вопросы и ответы на них, а также различные упражнения и контрольные вопросы, ответы на которые находятся в конце каждой главы. Для кого написана эта книга Для изучения этой книги не требуются предварительные знания в области програм- мирования на языке C++. Изложенный здесь материал содержит основы программиро- вания, поэтому, работая с книгой, можно изучить не только сам язык, но и концепции, положенные в основу программирования на языке C++. Снабженное множеством при- меров синтаксиса и детальным анализом кода, это издание послужит превосходным пу- теводителем в путешествии по изучению столь популярного языка, как C++. Вне за- висимости от того, обладает ли читатель определенным опытом программирования или начинает с нуля, четкая организация книги облегчит изучение языка C++. Нужно ли изучить сначала язык С? У многих возникает вопрос: “Поскольку C++ является продолжением языка С, возможно, сначала необходимо освоить язык С?” Страуструп и большинство других программистов, использующих язык C++, считают, что лучше этого не делать, ведь язык С основан на концепции структурного программирования, а язык C++ является полностью объектно-ориентированным. Но если читатель уже знаком с программиро- ванием на языке С, то это не проблема: прочитав первые главы, можно ознакомиться со всеми различиями этих языков. Зачем изучать язык C++? Это еще один довольно распространенный вопрос, который обычно сопровождает- ся примерно следующими словами: “В конце концов, язык X новее и лучше, он со временем заменит язык C++”. Ответ очень прост “Вовсе нет!”. Для замены языка COBOL было предназначено множество языков, но до сих пор в применении нахо- дятся миллионы строк кода приложений COBOL (в свое время их было разработано достаточно много). На языке C++ тоже было написано много приложений, и в на- стоящее время он все еще весьма популярен. Но даже если в будущем язык C++ ус- тареет, изучать его стоит все равно, поскольку большинство “новейших улучшенных” языков происходят именно от C++. Такие языки, как Java и С#, например, исполь- зуют синтаксис языка C++, однако по ряду причин изучать их с самого начала сложнее. Знание основ C++ поможет быстрее освоить подобные языки. Речь идет не только о Java и С#, но и о таких языках, как awk, Perl и, конечно, С. Одной из важнейших причин изучения языка C++ является переносимость его кода, что позволяет исполь-
Введение 19 зовать его на практически любой современной платформе, от PC до крупномасштаб- ных центров данных Unix на базе мейнфреймов. Но даже если переносимость кода (способность выполнения на различных платформах без изменений) для читателя не столь важна, полученные знания не окажутся лишними! Соглашения, принятые в этой книге Здесь используются соглашения, общепринятые в компьютерной литературе. Новые термины в тексте выделяются курсивом. Чтобы обратить внимание чита- теля на отдельные участки текста, также применяется курсив. Текст программ, функций, переменных, URL Web-страниц и другой код пред- ставлен моноширинным шрифтом. Все, что придется вводить с клавиатуры, выделено полужирным моноширинным шрифтом. Знакоместо в описаниях синтаксиса выделено курсивом. Это указывает на необ- ходимость заменить знакоместо фактическим именем переменной, параметром или другим элементом, который должен находиться на этом месте BINDSIZE=(максимальная ширина колонки)*(номер колонки). Пункты меню и названия диалоговых окон представлены следующим образом: Menu Option (Пункт меню). В листингах каждая строка пронумерована. Это сделано исключительно для удобства описания. В реальном коде нумерация отсутствует. Текст некоторых строк кода в листингах иногда бывает слишком длинным и не помешается в одной строке. Таким образом, отсутствие номера в начале строки свидетель- ствует о том, что строка является продолжением предыдущей, и в реальном ко- де их разрывать не следует. Текст некоторых абзацев этой книги выделен специальным шрифтом. Это приме- чания, советы и предостережения, которые помогут обратить внимание на наиболее важные моменты в изложении материала и избежать ошибок при работе. Л !tt L ы? против Знаете ли вы, что... Примечания содержат полезную и интересную информацию, которая по- зволяет более подробно рассмотреть отдельные детали. Между прочим Выделенная с помощью этой пиктограммы информация сделает про- граммирование на языке C++ более эффективным. Обратите внимание Предостережения содержат информацию, которая поможет избежать возможных неприятностей. Ъя*и_______т осторожны! Рекомендуется Не рекомендуется Использовать эти рекомендации для поиска наиболее эффективного решения поставленных задач. Пропускать важные замечания и предупреждения, приведенные в этом столбце.
20 Введение Результат В таких разделах представлен результат выполнения программы, код которой был приведен в листинге. Анализ В этих разделах представлен анализ кода, содержащегося в листинге. Что содержит прилагаемый CD? Прилагаемый CD содержит: исходный код всех рассматриваемых в книге примеров; снабженный IDE бесплатный компилятор от Borland: C++BuilderX. Хотя все приведенные здесь примеры кода были опробованы на этом компиля- торе, книга вовсе не о нем. Приведенные здесь примеры можно компилировать практически на любом компиляторе, совместимом со стандартом C++. Обрати- те внимание: допустимы не все компиляторы, поскольку некоторые устарели, а другие не совместимы со стандартом, поэтому они не смогут откомпилиро- вать код некоторых примеров; ответы и решения большинства упражнений содержатся в конце каждого занятия. Служба поддержки Более подробная информации об этой книге, а также о других публикациях изда- тельства Sams Publishing размещена на Web-сайте www.samspublishing.com. Чтобы получить ее, достаточно ввести без дефисов ISBN (0672326817) или название книги в поле Search (Поиск). От издательства Вы, читатель этой книги, и есть главный ее критик и комментатор. Мы ценим ваше мнение и хотим знать, что было сделано нами правильно, что можно было сделать луч- ше и чтобы еще вы хотели увидеть изданным нами. Нам интересно услышать и любые другие замечания, которые вам хотелось бы высказать авторам. Мы ждем ваших комментариев. Вы можете прислать письмо по электронной почте или просто посетить наш Web-сервер, оставив на нем свои замечания, — одним словом, лю- бым удобным для вас способом дайте нам знать, нравится или нет вам эта книга, а так- же выскажите свое мнение о том, как сделать наши книги более подходящими для вас. Посылая письмо или сообщение, не забудьте указать название книги и ее авторов, а также ваш e-mail. Мы внимательно ознакомимся с вашим мнением и обязательно учтем его при отборе и подготовке к изданию следующих книг. Наши координаты: E-mail: info@williamspublishing. com \ЛЛЛЛЛ/ http: //www. williamspublishing.com
ЧАСТЬ I Введение в C++ В этой части... Час 1. Первые шаги Час 2. Структура программы на языке C++ Час 3. Переменные и константы Час 4. Выражения и операторы Час 5. Функции Час 6. Ветвление процесса выполнения программ
ЧАС 1 Первые шаги На этом занятии вы узнаете: как установить и использовать компилятор; каковы этапы разработки программы на языке C++; как набрать, откомпилировать и скомпоновать первую программу на языке C++. Подготовка к программированию Язык C++, возможно, больше любого другого языка, такого, например, как Java, требует от программиста предварительного проектирования программы на этапе, пред- шествующем написанию кода. При решении тривиальных задач, рассматриваемых в первых нескольких главах этой книги, можно обойтись и без проектирования Но дос- таточно сложные проблемы, с которыми профессиональные программисты сталкивают- ся в реальной жизни чуть ли не каждый день, действительно требуют предварительного проектирования, и чем тщательнее оно будет проведено, тем более вероятно, что програм- ма сможет их решить, причем с минимальными затратами времени и денег. При добро- совестно проведенном проектировании создается программа, которую легко отладить и изменить в будущем. Было подсчитано, что около 90% стоимости программного продук- та составляет стоимость отладки и настройки. Качественный проект программы позво- лит значительно уменьшить эти расходы, а, значит, и стоимость всего проекта в целом. Первый вопрос, который нужно задать при подготовке к проектированию про- граммы, звучит примерно так: “Какую проблему предстоит решить?” Каждая про- грамма должна иметь четкую, ясно сформулированную цель, и это относится даже к простейшим из них, приведенным в этой книге. Второй вопрос каждый уважающий себя программист поставит следующим образом: “Можно ли решить эту проблему, используя существующие программные средства?” Может быть, для решения данной проблемы достаточно воспользоваться своей старой программой, ручкой и бумагой или купить у кого-то уже готовую программу? Зачастую такая идея оказывается на- много удачнее создания абсолютно новой программы. Программист, предлагающий подобную альтернативу, никогда не останется без работы: умение находить эконом- ные решения проблем обеспечит ему популярность. Даже при создании абсолютно нового программного обеспечения можно сущест- венно сэкономить силы и средства, используя уже существующие библиотеки классов (библиотеки определений объектов). Это великолепный способ ускорения процесса разработки, чем частично и объясняется популярность языка C++.
Час 1. Первые шаги 23 Сайт DeveloperWorks от IBM Хотя сайт DeveloperWorks от IBM (http://www.ibm.com/developer- works/) посвящен в основном языку Java, там можно найти также пре- восходные примеры совместного использования библиотек. Уяснив проблему и придя к выводу, что для ее решения необходимо написать аб- солютно новую программу, можно считать себя готовым к этапу проектирования. Несколько уточнений по поводу C++, ANSI C++, ISO C++ и Windows C++ — это язык программирования. Windows, Unix (AIX, Solaris, HP-UX и т.д.), Linux и Mac OS X — это операционные системы. Изучив язык C++, замечаешь, что его код можно переносить на другие машины вне зависимости от установленной на них операционной системы. В данной книге никаких ограничений на используемую операционную систему не накладывается. Здесь приведен код, соответствующий стандарту ANSI/ISO C++. ANSI/ISO C++ — это другое название стандарта C++, международного стандарта, согласованного со всеми платформами и средами разработки. ---- Что такое ANSI и ISO? прочим ANSI — это Американский национальный институт стандартов (American National Standards Institute), a ISO — это Международная организация по стандартизации (International Organization for Standardization). Как сле- дует из их названий, эти организации предоставляют различные типы стандартов не только для Соединенных Штатов, но и для других стран мира. Применение языка, полностью соответствующего международно- му стандарту, очень выгодно, поскольку все описания и правила также отвечают этому стандарту. Код вошедших в эту книгу примеров соответствует стандарту ANSI/ISO C++, а, следовательно, будет компилироваться практически любым компилятором. Поэтому применение таких специфических для разных сред разработки и операционных сис- тем элементов, как окна, списки, графические объекты и т.д., здесь ограничено. Полученный при выполнении программы результат отображается стандартным устройством вывода. Для этого компилятору необходимо указать, что он должен соз- дать консольное приложение. Это относится и к компилятору C++BuilderX от Borland. Некоторые компиляторы, предназначенные для операционных систем Windows Мас и т.п., используют для этого специальные окна (называемые окнами быстрого про- смотра), просто окна или окна консоли. Компилятор (compiler) — это программное обеспечение, упоминаемое на протяже- нии всей книги. Оно читает код в формате, понятном человеку, и преобразует его в машинный код, создавая объектный файл, который впоследствии будет скомпоно- ван и запущен. Компоновщик (linker) — это программа, которая компонует созданные компилятором файлы объектного кода в исполняемый файл. Подобный компилятор находится на прилагаемом к этой книге CD. Следующий раздел посвяшен описанию процесса установки и настройки компилятора C++BuilderX от Borland. Он предназначен для операционной системы Windows и предос- тавляет интегрированную среду разработки (Integrated Development Environment — IDE), графический интерфейс которой позволяет пользователю редактировать, компилиро- вать и отлаживать код C++. Однако читатель вполне может воспользоваться и любым
24 Часть I. Введение в C++ другим компилятором, никак не зависимым от операционной системы Windows, — это вопрос его личных предпочтений. Установка и настройка компилятора Чтобы воспользоваться интегрированной средой разработки C++BuilderX, предос- тавляемой на диске вместе с этой книгой, необходимо, чтобы на машине была уста- новлена операционная система Microsoft Windows. Если используется другая операци- онная система, этот раздел можно пропустить. Читателю понадобится компилятор, подходящий для той операционной системы, которую он использует. Чтобы получить информацию о доступных компиляторах необходимо изучить документацию установ- ленной операционной системы или связаться с компанией, которая ее предоставляет. Если используется операционная система Windows, можно установить IDE и ком- пилятор C++ от Borland, о котором и пойдет речь в этой книге. Этот графический ин- терфейс упоминается далее также как компилятор Borland, Borland C++BuilderX, C++BuilderX и IDE C++BuilderX IDE. Все эти названия являются синонимами. Установка с прилагаемого CD Вставьте прилагаемый CD в дисковод. Если он не сработал автоматически, то найдите и запустите программу install.exe вручную. Файл install.html (или readme.txt) содержит дополнительную информацию о последнем выпуске этого инструмента. Для установки необходимо предпринять следующие действия: 1. щелкните на кнопке Next (Дальше); 2. примите условия лицензионного соглашения; 3. щелкните на кнопке Next (Дальше) (автор рекомендует использовать значения по умолчанию); 4. в раскрывающемся списке Install Set (Комплект установки) выберите пункт Full (Полный), а затем щелкните на кнопке Next (Дальше); 5. выберите место для установки (или оставьте принятое по умолчанию), а затем щелкните на кнопке Next (Дальше); 6. выберите место для расположения пиктограмм (или оставьте принятое по умолчанию), а затем щелкните на кнопке Next (Дальше); 7. и, наконец, щелкните на кнопке Install (Установить). Осталось дождаться появления окна Setup Complete (Установка завершена). Установленную копию необходимо зарегистрировать. В результате компания Borland перешлет содержащий ключ файл, который следует сохранить в корневом ка- талоге операционной системы (следуйте присланным в сообщении инструкциям, они довольно просты)' Теперь, когда IDE C++BuilderX установлен, необходимо настроить программу, чтобы сделать ее использование простым и удобным. 1 Использовать в качестве ключа имеет смысл не переданный файл Reg389. txt, а сохра- ненное в виде файла само сообщение Borland Product Registration.eml. Подробные ин- струкции находятся в самом файле сообщения. Впрочем, несмотря на успешную регистрацию, у меня он так и не заработал. — Прим. ред.
Час 1. Первые шаги 25 Настройка компилятора Borland C++BuilderX Сначала откройте IDE C++BuilderX. Необходимые для этого пункты меню пере- числены в порядке их выбора. На рабочем столе Windows щелкните на кнопке Start (Пуск) и в появившемся меню выберите пункты All Programs (Программы), Borland C++BuilderX, C++BuilderX. На экране появится IDE C++BuilderX. Сейчас IDE находит- ся в стандартной конфигурации, но ее можно изменить, в любой момент выбрав со- ответствующие пункты в меню Tools (Инструменты), расположенном вверху окна. На рис. 1.1 ниже строки меню можно заметить два ряда пиктограмм. Это панель инструментов. Когда курсор мыши окажется над пиктограммой, появится подсказка, в которой описано назначение кнопки. В первом ряду находится семь пиктограмм (слева направо): New File (Новый файл), здесь можно создать новый проект; Open File (Открыть файл), позволяет открыть уже существующий проект; Reopen a File (Открыть файл снова), позволяет вернуться к версии файла, сохраненной ранее на диске; Close a Source File (Закрыть файл исходного кода); Save Current File (Сохранить текущий файл); Save All Files (Сохранить все файлы) и Print (Печать). Пиктограмма с изображением папки и числом 10101 внизу принадлежит кнопке Make Project (Создать проект), которая позволяет откомпилировать код и создать исполняемый файл. Кноп- ка с зеленой стрелкой позволяет запустить откомпилированную программу на выпол- нение. Эти пиктограммы соответствуют одноименным пунктам меню. Рис. 1.1. Интегрированная среда разработки (IDE) C++BuilderX Обратите внимание: в названиях этих и некоторых других пиктограмм используется слово проект (project). Проект позволяет связать между собой несколько файлов. Файлы проектов C++BuilderX имеют расширение ,сЬх. Большинство проектов этой книги со- стоит из одиночных файлов. Создавать проект программы, состоящей из одного фай- ла исходного кода, возможно, и не стоит, но для достаточно сложной программы, ко- торая состоит из нескольких файлов (иногда — нескольких сот), это весьма удобно. По умолчанию IDE C++BuilderX настроен на отладку (поиск ошибок) программ. В книге эта возможность рассматривается позже, однако опробовать отладку можно прямо сейчас. IDE Borland предоставляет встроенный редактор кода. Щелкните на пиктограмме New File (Новый файл), перейдите на вкладку Source File (Файл исходного кода) и вы- берите тип создаваемого файла. Так добавляют одиночные файлы. Для работы с не- сколькими файлами и проектами щелкните на пиктограмме New File (Новый файл) снова и выберите во вкладке Project (Проект) пиктограмму New Console (Новое кон- сольное приложение). Чтобы продолжить, щелкните на кнопке ОК. Если позволить мастеру продолжить работу, он создаст среду выполнения для новой основной
26 Часть I. Введение в C++ программы, но если отказаться от его услуг, то будет получено пустое окно, в которое можно добавлять файлы исходного кода. Сохраните проект и все связанные с ним файлы в отдельном каталоге. Файл про- екта имеет расширение .сЬх, а файлы исходного кода C++ — расширение ,срр. Об- ратите внимание, что при сохранении редактор автоматически добавляет расширение файлов исходного кода — .срр. Чтобы сохранить текущий файл, достаточно щелкнуть на пиктограмме Save Current File (Сохранить текущий файл). Компиляция при помощи компилятора C++BuilderX Чтобы откомпилировать и скомпоновать проект или отдельный файл исходного кода, щелкните на пиктограмме Make Project (Создать проект). Некоторые компилято- ры осуществляют это в два этапа: сначала компиляция, затем — компоновка, но большинство интегрированных сред автоматически выполняют оба процесса, получив команду на компиляцию. Чтобы запустить программу на выполнение из IDE, щелк- ните на пиктограмме Run Project (Запустить проект), а чтобы отладить программу — на пиктограмме Debug Project (Отладить проект). При запуске программы в отладчике экран будет выглядеть как на рис. 1.2. В дан- ном случае автор установил контрольную точку на строке 7, и код был выполнен до этой точки. Рис. 1.2. Использование отладчика IDE C++BuilderX Как можно заметить, выделенная подсветкой строка на экране указывает, что вы- полнение программы возобновится со следующей строки, а красный круг — это кон- трольная точка. Контрольная точка (breakpoint) временно останавливает выполнение программы. При отладке программы контрольную точку имеет смысл установить снача- ла на первой строке исполняемого кода. Чтобы установить собственную контрольную точку, достаточно щелкнуть в поле слева от выбранной строки. Пиктограмма Step (Шаг) позволяет выполнить одну строку кода и остановиться на следующей. Пиктограмма Continue (Продолжить) запустит программу на выполнение до тех пор, пока она не за- вершится или не встретится следующая контрольная точка. Другие расположенные вни- зу окна пиктограммы (см. рис. 1.2) позволяют исследовать иные аспекты программы. Если в меню Help (Помощь) выбрать пункт About (О программе), то можно выяс- нить, что используется компилятор Borland C++BuilderX версии 1.0.0.1786 Personal. Если в меню Help (Помощь) выбрать пункт Help Topics (Темы), можно получить дос- туп к обширной библиотеке справочников языков С и C++. Этот весьма ценный ре- сурс заслуживает особого внимания.
Час 1. Первые шаги 27 Знаете ли 1Ы? Контекстная помощь Контекстная помощь — это одно из достоинств IDE. Достаточно выделить в коде любое ключевое слово и нажать на клавишу <F1>. В результате на экране появится окно, содержащее информацию об этой команде. Компилятор и редактор кода Компиляторы зачастую обладают собственным встроенным текстовым редактором. Для создания файлов исходного кода C++ можно использовать встроенный редактор, текстовый редактор стороннего производителя или даже текстовый процессор Microsoft Word, который также способен создавать текстовые файлы. Как уже было сказано, компилятор Borland обладает встроенным текстовым редактором. В принци- пе можно использовать любой редактор текста, однако предварительно следует убе- диться что он способен сохранять файлы в формате простого текста (plain-text) без встроенных команд и дескрипторов форматирования текста. Подходящими редакто- рами являются Notepad (Блокнот) операционной системы Microsoft Windows, команда Edit операционной системы DOS, Brief, Epsilon, EMACS и vi. Множество коммерче- ских текстовых процессоров, таких, как WordPerfect, Word и другие, также позволяют сохранять файлы в формате простого текста. ДИЫВ т осторожны: Будьте внимательны Используя текстовый процессор, будьте внимательны: не сохраняйте от- форматированный текст. Пользователь сможет прочитать такой текст, но компилятор его не поймет2. Файлы, создаваемые с помощью текстовых редакторов, называются исходными. Обыч- но они имеют расширение .срр, .ср или .с. В этой книге файлы, содержащие исходный код программ, имеют расширение .срр, поскольку именно оно принято по умолчанию для компилятора Borland. Между врмим Расширение исходного файла Для большинства компиляторов C++ неважно, какое расширение имеет файл, содержащий исходный текст программы, хотя многие из них по умолчанию используют расширение . срр. Рекомендуется Не рекомендуется Использовать для написания исходного текста программ простой текстовый редактор или редактор, встроенный в компилятор. Сохранять свои файлы с расширением . срр, . ср или . с (для компилятора Borland по умолчанию принято расширение . срр). Ознакомиться с документацией, прилагаемой к компилятору и компоновщику, чтобы быть уверенным в правильности их работы. Пренебрегать помощью, предостав- ляемой редактором с поддержкой языка программирования (LSE — Language Sensitive Editor) и IDE. Использовать текстовый процессор, который сохраняет форматирован- ный текст. Если им все же придется воспользоваться, сохраняйте файлы как текст ASCII. Фактически документ Microsoft Word является бинарным файлом, который ничего общего с обычным текстом не имеет. В этом легко можно убедиться, открыв любой файл с расшире- нием .doc в текстовом редакторе. — Прим. ред.
28 Часть I. Введение в C++ Компиляция и компоновка исходного кода Несмотря на то что текст исходного файла содержит код программы и мало поня- тен непосвященным в C++, программой он собственно не является и предназначен для чтения человеком, а не компьютером. Файл исходного кода не является испол- няемым и не может быть запущен как программа. йгжщ_______ прочли Компиляция в режиме командной строки Для “превращения” исходного текста в программу применяется ком- пилятор. Каким образом вызвать его и как сообщить ему о местонахож- дении исходного текста программы, зависит от конкретного компиля- тора; для этого необходимо вновь заглянуть в документацию. Если решено запустить компилятор из командной строки операцион- ной системы, а не использовать IDE, то, в зависимости от применяемо- го компилятора и операционной системы, необходимо будет ввести следующую команду: для компилятора Borland: Ьсс32 <имяфайла> -о <имя получаемого исполняемого файла> для бинарного GNU gxx <имяфайла> -о <имя получаемого исполняемого файла> для компилятора Cygwin: Я++ <имяфайла> -о <имя получаемого исполняемого файла> для большинства версий Linux: д++ <имяфайла> -о <имя получаемого исполняемого файла> для версий UNIX: C++ <имяфайла> -о <имя получаемого исполняемого файла> Компиляция в интегрированной среде разработки Большинство современных компиляторов предоставляет интегрированную среду разработки (IDE). В такой системе для компиляции, как правило, следует выбирать в меню пункт Build (Создать) или Compile (Компилировать). Кроме того, вполне мо- жет существовать комбинация клавиш (обычно <Ctrl+F5>), нажав которую, можно за- пустить компиляцию приложения. В IDE C++BuilderX можно также щелкнуть на пиктограмме Make Project (Создать проект). Компоновка программы После завершения компиляции исходного кода создается объектный файл. Обычно он имеет расширение .obj или .о. Но и он не является исполняемой программой. Для превращения его в исполняемый файл нужно запустить про- грамму компоновки. Программы на языке C++ обычно создаются в ходе компоновки одного или не- скольких объектных файлов (расширение .obj или .о) с одной или несколькими биб- лиотеками. Библиотекой называется набор компонуемых файлов, которые поставляются вместе с компилятором либо приобретаются отдельно, либо создаются и компилиру- ются самим программистом. Все компиляторы C++ укомплектованы библиотекой функций (или процедур) и классов, которые можно включить в программу. Функ- ция — это блок кода, который выполняет отдельную задачу, например, сложение двух чисел или вывод данных на экран. Класс — это определение нового типа. Класс реа-
Час 1. Первые шаги 29 лизован как набор данных и связанных с ними функций. Более подробная информа- ция о функциях и классах будет изложена несколько позже. Итак, чтобы создать исполняемый файл, необходимо предпринять следующие действия: 1. создать файл исходного кода (расширение .срр); 2. скомпилировать исходный код в объектный файл (расширение ,obj или .о); 3. скомпоновать объектный файл со всеми необходимыми библиотеками, получив исполняемый файл (с расширением .ехе). Щелчок на пиктограмме Make Project (Создать проект) в IDE Borland приводит к одновременной автоматической компиляции и компоновке программы. Для компи- ляции отдельных модулей достаточно щелкнуть правой кнопкой мыши на их назва- ниях в окне проекта, а затем в появившемся контекстном меню выбрать пункты Build Tools (Средства компиляции) и ВСС32 (собственно компилятор C++). Для компонов- ки вручную необходимо щелкнуть в окне проекта правой кнопкой мыши на имени самого проекта и в появившемся контекстном меню выбрать пункты Build Tools (Средства компоновки) и IL1NK32 (Встроенный компоновщик IDE Borland). Цикл разработки Если бы каждая программа заработала должным образом с первой попытки, то цикл разработки был бы таким; написание программы, компиляция исходного кода, компоновка программы и ее выполнение К сожалению, почти все программы (простые и не очень) содержат ошибки. Одни обнаружит компилятор, другие — ком- поновщик, а третьи проявятся только при запуске программы. Синтаксические ошибки (“syntax error”) обнаруживаются на фазе компиляции. Тогда же могут быть выданы предупреждения (warning), свидетельствующие о воз- можной ошибке, которая, тем не менее, не помешает компилятору создать бинарный объектный файл. Компоновщик обычно сообщает о таких ошибках, как отсутствие необходимых библиотек или неправильные имена функций. Ошибки, проявляющиеся при выполнении программы, называют логическими. Любая обнаруженная ошибка должна быть исправлена, а для этого необходимо от- редактировать исходный текст программы, перекомпилировать и перекомпоновать его, азатем выполнить снова. Цикл разработки представлен на рис. 1.3. Как можно заме- тить, это на самом деле цикл. Случаи, когда написанная программа оказывается отком- пилирована, скомпонована и запущена без ошибок с первой попытки, крайне редки. Hello. срр — первая программа на языке С++ Традиционно в книгах по программированию первые примеры программ начина- ются с вывода на экран слов “Hello World” или какой-нибудь вариации на эту тему. Не будем нарушать устоявшиеся традиции. Эта традиция заложена в первой книге по программированию на языке С, написанной авторами языка Керниганом (Kemighan) и Ритчи (Ritchie). Нежа__ ЦЮШ Создание новой программы Чтобы получить новый проект для примеров этой книги, создайте в IDE C++BuilderX новый файл исходного кода для консольного приложения. После создания проекта появится доступ к редактору кода.
30 Часть I. Введение в С++ Рис. 1.3. Этапы разработки программы на языке C++ Введите в редакторе код первой программы (листинг 1.1), в точности повторяя все нюансы. Завершив ввод, сохраните файл под именем hello.срр, скомпилируйте его, скомпонуйте (если используемый компилятор не делает этого автоматически) и запус- тите на выполнение. Пока не стоит задумываться о том, как работает эта программа. Просто получите удовлетворение от того, что прошли полный цикл разработки. Все аспекты программы подробнее рассматриваются на следующих занятиях.
Час 1. Первые шаги 31 Между ______ речам Несколько слов о листингах В приведенном ниже листинге слева расположены номера строк. Они установлены только для ссылок на соответствующие строки кода в тексте книги. В окне редактора их вводить не нужно. Например, пер- вая строка листинга 1.1 должна выглядеть так: #include <iostream> Номера строк применяются лишь при анализе и обсуждении. Исполь- зуемый редактор кода или IDE также могут нумеровать строки подобно компилятору Borland, но сами числа частью кода не являются. Нумерация строк листингов этой книги начинается с нуля, поскольку все массивы языка C++ начинаются с нулевого элемента. Компания Borland, напротив, предпочитает начинать нумерацию строк с единицы. Если читателю это не нравится, нумерацию можно отключить. Для этого в меню Tools (Инструменты) выберите пункт Editor Options (Параметры редактора), и в появившемся диалоговом окне сбросьте флажок Line Numbering (Нумерация строк). Коды всех листингов этой книги находятся на CD в файлах, имена кото- рых указаны в заголовке листинга, что позволяет существенно сэконо- мить время при вводе кода. Листинг 1.1. Файл hello. срр. Программа Hello World 0: ttinclude <iostream> 1: 2: int main() 3: { 4: std::cout << "Hello World!\n”; 5: return 0; 6: } №kv_______ рочмм Имена и пространства Команда std: :cout состоит из двух частей: std и cout. Часть cout обеспечивает вывод информации на консоль. Она принадлежит стан- дартному (standard) пакету. Однако поскольку функции и объекты для языка C++ создают очень много разработчиков, существует вероят- ность того, что для различных целей будут использованы одинаковые имена. Чтобы различать версии разных, но одноименных функций и объектов, следует указать необходимую версию или пространство имен (namespace), которому они принадлежат. В данном случае необ- ходимо использовать версию объекта cout из пространства имен std, поэтому именно оно и указано перед именем объекта. Убедитесь в том, что введенный текст программы совпадает с содержимым приве- денного здесь листинга. Обратите внимание на знаки препинания. Символ « в стро- ке 4 является оператором перенаправления потока данных. Эти символы на большин- стве клавиатур вводятся двойным нажатием клавиши <,> (запятая) при нажатой клавише <Shift>. В строке 4 между операторами std и cout находится двойное двое- точие Строки 4 и 5 завершаются точкой с запятой (,-); не забывайте о них! Кроме того, следует удостовериться в корректности работы компилятора Большинство компиляторов переходит к компоновке автоматически, но все-таки стоит свериться с до- кументацией. Если отображаются сообщения об ошибках, просмотрите внимательно текст программы и найдите отличия от текста, приведенного в книге.
32 Часть I. Введение в C++ При использовании компилятора, отличного от описываемого здесь, сообщения об ошибках могут оказаться иными. Если появится сообщение об ошибке со ссылкой на строку 0, уведомляющее о невозможности найти файл iostream.h (“cannot find file iostream"), обратитесь к документации за указаниями об установке пути для подклю- чаемых файлов или переменных окружения. Если сообщение об ошибке уведомляет об отсутствии прототипа для функции mainO. добавьте строку int main()непосредст- венно перед строкой 2. В этом случае придется добавлять эту строку перед каждой функ- цией main() во всех программах книги. Большинство компиляторов, включая Borland C++BuilderX, не требует прототипа для функции main (), но если попался именно такой, то в окончательном виде код программы будет выглядеть следующим образом: 0: ttinclude <iostream> 1: 2: int main(); 3: int main() 4: { 5: std::cout << "Hello World!\n"; 6: return 0; 7: } Попробуйте скомпилировать код файла hello.срр и запустить полученную про- грамму hello.exe на выполнение. Если все правильно, то на экране появится сле- дующее сообщение: Hello World! Итак, поздравляю! Только что была создана, скомпилирована и выполнена первая программа на языке C++. Она выглядит достаточно скромно, но почти все профес- сиональные программисты C++ начинали именно с этой программы. Ошибки компиляции Ошибки компиляции могут возникать по различным причинам. Обычно они яв- ляются результатом небрежного ввода исходного кода и других случайностей. При- личные компиляторы сообщат не только о том, что именно не в порядке, они укажут также точное местонахождение обнаруженной ошибки. Наиболее совершенные ком- пиляторы даже предложат вариант исправления! В этом можно убедиться, специально сделав ошибку в исходном коде программы. Давайте удалим в программе hello.срр закрывающую фигурную скобку в строке 6 листинга 1.1. Теперь программа будет выглядеть так, как показано в листинге 1.2. Листинг 1.2. Файл hellobad. срр. Демонстрация ошибки компиляции 0: ttinclude <iostream> 1: 2: int main!) 3: { 4: std::cout << "Hello World!\n"; 5: return 0; При повторной компиляции программы появится сообщение об ошибке, которое будет выглядеть следующим образом: "hellobad.срр": Е2134 Compound statement missing } in function main) ) at line 7 Это сообщение об ошибке предоставляет информацию о файле и номере строки, содержащей ошибку, а также о причине проблемы (хоть и несколько туманно). Обра-
Час 1. Первые шаги 33 тите внимание: в сообщении об ошибке речь идет о строке 7. В компиляторе Borland следующей была бы строка 7. Иногда сообщение об ошибке указывает только на об- ласть возникновения проблемы. Если бы компилятор мог точно выявить причину проблемы, он указал бы ее код явно. прочии Напоминание о листингах Просматривая код листингов этой книги, не забывайте, что номера строк предназначены лишь для ссылок в тексте. Нумерация строк кода в листингах книги начинается с нуля, как у некоторых компиляторов, однако в компиляторе Borland нумерация начинается с единицы. Вопросы и ответы В чем разница между текстовым редактором и текстовым процессором? Текстовый редактор создает и редактирует файлы, содержащие только текст. Дня написания текстов программ не нужно ни форматирование абзацев, ни специальные символы разметки, присущие текстовым процессорам. Исходным файлам программ не нужен ни автоматический перенос слов, ни выделение букв полужирным шрифтом, курсивом и т.д. Если компилятор имеет встроенный текстовый редактор, то обязательно ли ис- пользовать именно его? Почти все компиляторы будут компилировать программы, созданные в любом текстовом редакторе. Однако преимущество использования встроенного тексто- вого редактора заключается в том, что он может быстро переключаться между режимами редактирования и компиляции. Высокоорганизованные компилято- ры включают полностью интегрированную среду разработки, позволяя про- граммисту легко получать доступ к справочным файлам, редактировать, компи- лировать и сразу же исправлять ошибки компиляции и компоновки, не выходя из среды разработки. Можно ли игнорировать предупреждающие сообщения компилятора? Нельзя ни в коем случае. Возьмите за правило обязательно реагировать на пре- дупреждения компилятора как на сообщения об ошибках. Компилятор C++ передает предупреждения в тех случаях, когда, по его мне- нию, происходит нечто, не входящее в намерения программиста. Внимательно отнеситесь к этим предупреждениям и сделайте все, чтобы они исчезли. Если речь идет об ошибке, то программа просто не будет откомпилирована. Если речь идет только о предупреждении, то компилятор все-таки создаст исполняе- мую программу, однако она, возможно, будет работать не так, как ожидалось. Что такое время компиляции (compile time)? Это период времени, когда программа компилируется, в отличие от времени компоновки (link time), когда работает компоновщик, или времени выполнения (runtime) программы, когда она выполняется. Коллоквиум Научившись вводить код, компилировать, компоновать и запускать на выполнение программы, написанные на языке C++, имеет смысл ответить на несколько вопросов и выполнить ряд упражнений, чтобы закрепить полученные знания.
34 Часть I. Введение в С++ Контрольные вопросы 1. Зачем нужен проект программы? 2. Что такое отладка? 3. Почему имеет смысл использовать компилятор, который поддерживает нацио- нальные или международные стандарты? 4. Какие инструментальные средства можно использовать для редактирования ис- ходного кода? Упражнения Упражнения этого занятия предназначены лишь для самоконтроля. Для них нет стандартных решений или ответов. По мере изучения книги встретятся упражнения, решения которых находятся на прилагаемом CD. 1. Опробуйте кнопки и пункты меню IDE и компилятора Borland, поставляемого вместе с книгой. Что они делают? Облегчают они работу или усложняют? 2. Попробуйте откомпилировать, скомпоновать и запустить на выполнение при- меры, поставляемые с компилятором Borland. Изучите их код и попробуйте вы- яснить, как он работает и к каким результатам приводит. 3. Опробуйте версию компилятора Borland (или любого другого) для командной строки. На самом ли деле традиционные средства редактирования, компиляции и компоновки проще, чем IDE? Ответы на контрольные вопросы I. Он позволяет сообщить компилятору обо всех компонентах, необходимых для создания исполняемого файла. Кроме того, большинство приложений состоит из множества различных файлов, а проект упрощает их хранение. 2. Отладка — это процесс удаления ошибок из кода программы. Логической назы- вают такую ошибку, которую компилятор не может обнаружить, поскольку ему не известно о том, что именно задумал разработчик в действительности. 3. Простой ответ: потому что это соответствует общепринятым правилам. Должен ли компилятор вести себя предсказуемо в соответствии с тем, что описано в ру- ководствах? Что было бы без национальных и международных стандартов на бензин и автомобильные двигатели, например? Без стандартов каждый изгото- витель создал бы их по своему усмотрению, и бензин производства компании А нельзя было бы использовать для автомобилей компании В. 4. Использовать можно любой инструмент, который сохраняет код как простой текст ASCII. Можно использовать текстовые редакторы, поставляемые вместе с операционной системой (такие, как Edit DOS, vi и EMACS Unix/Linux, Notepad (Блокнот) Windows), можно купить коммерческий редактор (такой, как Brief), использовать IDE или даже текстовый процессор Microsoft Word. Глав- ное — не сохранять код в виде форматированного документа.
ЧАС 2 Структура программы на языке C++ На этом занятии вы узнаете: почему имеет смысл выбрать язык C++; какие бывают элементы программы C++; о взаимодействии элементов программы; что такое функция и как она работает. Почему язык C++ является правильным выбором Большинство профессиональных программистов используют для разработок язык C++ вовсе не случайно, поскольку он позволяет создавать надежные, быстродейст- вующие и небольшие по размеру программы. Современные инструментальные средст- ва C++ существенно упрощают и ускоряют создание сложнейших коммерческих при- ложений. Однако прежде чем переходить к изучению, имеет смысл сказать несколько слов о самом языке C++. По сравнению с некоторыми другими, язык C++ относительно новый. Безуслов- но, само программирование появилось лишь лет 60 назад, однако с тех пор машинные языки претерпели эволюцию. Язык C++ является дальнейшим развитием языка С, возраст которого составляет почти 30 лет. Вначале программисты работали с ограниченным набором примитивных команд, представлявшим собой машинный язык (machine language). Эти команды состояли из длинных строк нулей и единиц. Вскоре был изобретен язык ассемблер (assembler), способный преобразовывать в машинные команды более понятные для человека ин- струкции (например, add (Добавить) или mov (Переместить)). Со временем появились такие языки высокого уровня, как BASIC и COBOL. Бла- годаря им появилась возможность программировать, используя логические конструк- ции из слов и предложений, например, Let I = 100. Эти инструкции переводились
36 Часть I. Введение в С++ на машинный язык интерпретаторами и компиляторами. Интерпретатор (interpreter), такой, например, как BASIC, превращает инструкции программы в команды машин- ного кода последовательно, по мере чтения текста программы. Компилятор (compiler) преобразует исходный код программы в некоторую проме- жуточную форму — объектный код. Этот этап называется компиляцией (compiling). За- тем компилятор вызывает компоновщик (linker), который превращает объектный файл в исполняемый. Этот этап называется компоновкой (linking) Исполняемым файлом программы называется такой файл, который может быть запущен на установленной операционной системе. Обратите внимание: термин объектный код (object code) никакого отношения к кон- цепциям объектов и объектно-ориентированного программирования (описанного да- лее в этой книге) не имеет. С интерпретатором работать проще, поскольку команды программы выполняются в той последовательности, в какой они написаны. В настоящий момент интерпретируемые программы относят скорее к сценариям, а сам интерпретатор зачастую называют процес- сором сценариев. В процессе компиляции возникают дополнительные этапы преобразо- вания исходного кода (читаемого людьми) в объектный (который читается машинами). Наличие дополнительного этапа создает определенные неудобства, но скомпилирован- ные программы выполняются значительно быстрее, поскольку трансляция исходного кода в машинный язык, отнимающая достаточно много времени, выполняется только один раз (во время компиляции) и не производится во время выполнения программы. В течение многих лет основными достоинствами программы считались ее краткость и быстрота выполнения Программу стремились сделать как можно меньше, поскольку память стоила недешево, а заинтересованность в скорости выполнения объяснялась вы- сокой стоимостью процессорного времени. Однако по мере того, как компьютеры ста- новились меньше, дешевле (особенно ощутимо дешевела память) и быстрее, приоритеты менялись. Сегодня стоимость рабочего времени программиста намного превышает стоимость большинства компьютеров, используемых в бизнесе. Большим спросом поль- зуются профессионально написанные и легко эксплуатируемые программы, т.е. те, ко- торые при изменении требований, связанных с решением конкретных задач, легко пе- ренастраиваются без больших дополнительных затрат. Процедурное, структурное и объектно-ориентированное программирование При процедурном программировании (procedural programming) программа осуществ- ляет ряд действий с набором данных. Структурное программирование (structured programming) обеспечило систематический подход, позволяюший разделить сложную программу на ряд простых процедур и организовать с их помощью обработку больших объемов данных. Основная идея структурного программирования вполне соответствует принципу “разделяй и властвуй”. Компьютерную программу можно представить в виде набора за- дач. Любая задача, которая слишком сложна для простого описания, должна быть разде- лена на несколько более мелких составных задач, и это деление необходимо продолжать до тех пор, пока задачи не станут достаточно простыми для понимания. Для примера возьмем вычисление средней заработной платы всех служащих ком- пании. Это не такая уж простая задача, однако ее можно разделить на ряд подзадач. 1. Выяснить, сколько зарабатывает каждый служащий. 2. Подсчитать количество служащих компании. 3. Наити общую сумму всех зарплат. 4. Разделить суммарную зарплату на количество служащих компании.
Час 2. Структура программы на языке C++ 37 Подсчет суммарной зарплаты тоже можно разделить на несколько этапов. 1. Прочитать запись о каждом служащем. 2. Получить доступ к информации о зарплате. 3. Прибавить очередное значение зарплаты к накопительной сумме. 4. Прочитать запись о следующем служащем. В свою очередь операцию чтения записи о каждом служащем можно разделить на более мелкие. 1. Открыть файл служащих. 2. Перейти к нужной записи. 3. Считать данные с диска. Структурное программирование до сих пор остается довольно успешным способом решения сложных задач, однако оно обладает и недостатками. Разделение структуры дан- ных и манипулирующих ими функций усложняет понимание работы приложения по мере его усовершенствования, а, следовательно, затрудняет его сопровождение. Чем больше у программы возможностей по обработке данных, тем труднее разобраться в ее коде. Процедурное программирование подразумевает постоянный поиск новых решений для старых проблем. Это зачастую называемое “изобретением колеса” явление есть про- тивоположность многократному использованию (reusability) кода. Идея многократного ис- пользования заключается в создании решающих определенные задачи компонентов, ко- торые впоследствии при необходимости можно включать в программы. В материальном мире, когда инженеру необходим транзистор, он не изобретает его заново, а идет на склад (в магазин, к коллеге — возможны варианты) и в соответствии с нужными параметрами подбирает себе подходящий. Но до появления объектно-ориентированного программи- рования такой возможности при создании программного обеспечения не было. Суть объектно-ориентированного программирования (object-oriented programming) за- ключается в объединении данных и обрабатывающих их процедур в единый объект (object), обладающий уникальным идентификатором и вполне определенными воз- можностями. Язык C++ и объектно-ориентированное программирование Язык C++ полностью поддерживает принципы объектно-ориентированного про- граммирования, включая три кита, на которых оно стоит: инкапсуляцию, наследова- ние и полиморфизм. Инкапсуляция Когда инженер создает новое устройство, он связывает воедино набор компонен- тов. Это может быть резистор, конденсатор и транзистор. Транзистор обладает неко- торыми свойствами и может выполнять определенные действия. Поскольку инженеру известно, что транзистор делает, он может использовать его, не вникая в детали того, как именно он устроен. Для этого транзистор должен быть самозамкнутым объектом, должен решать одну- единственную задачу, но делать это самостоятельно. Это называется инкапсуляцией (encapsulation). Все свойства транзистора инкапсулируются в объекте Транзистор и не распростра- няются на другие элементы схемы. Таким образом, для использования транзистора вовсе не обязательно знать, как он работает в действительности.
38 Часть I. Введение в C++ В языке C++ инкапсуляция реализована за счет возможности создания нестан- дартных (пользовательских) типов данных, называемых классами. Грамотно опреде- ленный класс работает как полностью инкапсулированный объект, т.е. его можно ис- пользовать в качестве цельного программного модуля. Настоящая же внутренняя работа класса должна быть скрыта. Пользователям класса не нужно знать внутренне устройство класса, достаточно понимать, как его использовать. Более подробная ин- формация о создании классов приведена на занятии 7, “Простые классы”. Наследование и многократное использование В 1980-х годах автор работал в банке Citibank над созданием средств для обслужи- вания клиентов на дому. Разрабатывать систему с нуля не хотелось, условия рынка требовали сделать все как можно быстрее. Следовательно, пришлось начать с телефо- на и “расширить” его возможности. Новый усовершенствованный телефон все еще оставался телефоном, но уже обладал рядом дополнительных возможностей. Таким образом, сохраняя свойства старого простого телефона, новое устройство получило дополнительные возможности, позволяющие решить поставленную задачу. Язык C++ поддерживает наследование (inheritance). Это значит, что можно объя- вить новый тип данных (класс), который является расширением уже существующего. Об этом новом классе говорят, что он является наследником существующего класса, а называют его производным типом (derived type). Таким образом, усовершенствован- ный телефон является производным от простого старого телефона, а, следовательно, наследуя все его свойства, обладает дополнительными возможностями. О наследова- нии и его применении в языке C++ речь пойдет на занятии 16, “Наследование”. Полиморфизм Принимая вызов, усовершенствованный телефон (Enhanced Telephone — ЕТ) ведет себя не так, как обычный. Вместо обычного звонка включается экран, и голос гово- рит: “Поступил вызов”. Однако телефонная компания в этом никак не участвует. Ни- какой специальный сигнал на телефон усовершенствованного типа не посылается. Компания обеспечивает лишь передачу по проводам электрического импульса, в ответ на который обычный телефон звонит, электронный подает полифонический сигнал, а ЕТ говорит человеческим голосом. В ответ на переданный телефонной компанией сигнал каждый из телефонов “реагирует” по-своему. Благодаря так называемому полиморфизму функций (function polymorphism) и классов (class polymorphism) язык C++ позволяет каждому телефону “реагировать” на сигнал по-своему. Поли означает много, морфе — форма, следовательно, полиморфизм — это многообразие форм. Более подробная информация по этой теме приведена на занятиях 17, “Полиморфизм и производные классы”, и 18, “Расширенное наследование”. Элементы простой программы Даже простенькая программа hello.срр, приведенная на занятии 1, “Первые шаги”, состоит из нескольких небезынтересных элементов. В этом разделе упомяну- тая программа рассматривается более подробно. В листинге 2.1 она для удобства при- ведена еще раз. Листинг 2.1. Файл hello. срр. Элементы программы C++ 0: ttinclude <iostream> 1: 2: int main()
Час 2. Структура программы на языке С++ 39 3: { 4: std::cout << "Hello World!\n"; 5: return 0; 6: } Результат Hello World! Анализ В строке 0 к файлу программы подключается внешняя библиотека iostream. В ре- зультате при компиляции все содержимое файла iostream будет добавлено непосред- ственно в начало файла hello.срр. Директива ttinclude При запуске компилятора сначала запускается другая программа — препроцессор (preprocessor). Запускать препроцессор явно не нужно, его вызов происходит автома- тически при каждом запуске компилятора. Первым стоит символ #, который является инструкцией для препроцессора. Пре- процессор читает исходный текст программы, находит строки, которые начинаются с символа фунта (#), и обрабатывает их перед началом компиляции программы. Модифицированный в результате код передается компилятору. По существу препроцессор — это редактор кода, срабатывающий во время компи- ляции. Командами для этого редактора являются директивы препроцессора. Команда ttinclude (подключить) — это инструкция препроцессора, которая ука- зывает, что “далее следует имя файла, который необходимо найти и подключить именно здесь”. Угловые скобки, в которые заключено имя файла, означают, что этот файл нужно искать во всех папках, отведенных для хранения подобных файлов. Если ком- пилятор настроен правильно, угловые скобки укажут препроцессору на то, что файл iostream следует искать в папке, содержащей все подключаемые файлы компилятора. Такие файлы, подключаемые в файлы исходного кода, называются подключаемыми (include file) и традиционно имеют расширение .h. Между.... прочим Расширение подключаемого файла Новые, соответствующие стандарту ANSI подключаемые файлы зачас- тую не имеют никакого расширения вообще. Как правило, компиляторы комплектуют двумя версиями каждого из этих файлов. Например, если проверить каталог, в который установлен компилятор, можно обнару- жить и старый традиционный файл iostream.h, и новый, соответст- вующий стандарту ANSI iostream. Поскольку прежние файлы устарели, в этой книге используются новые. Если используется старый компилятор, следует применить прежний формат: ttinclude <iostream.h> Файл iostream (Input-Output-Stream — поток ввода-вывода) используется объектом cout, который обслуживает процесс вывода данных на экран. Обратите внимание: перед именем объекта cout указано пространство имен std::. Это заставит компилятор использовать стандартную библиотеку ввода и вывода. Более подробная информация по этой теме приведена далее, а пока рассматривайте
40 Часть I. Введение в C++ std::cout как имя объекта, который осуществляет вывод данных на экран. Ввод данных с консоли осуществляет объект std: :cin. Результатом выполнения строки 0 станет подключение файла iostream в код этой программы, как будто он был набран здесь изначально. Построчный анализ Основной код программы начинается в строке 2 с вызова функции main (). Каж- дая программа на языке C++ содержит функцию main(). Функция — это блок кода программы, который выполняет одно или несколько действий. Обычно функцию вы- зывает другая функция или оператор, но функция main () — особая: она вызывается автоматически при запуске программы. Функция main (), подобно всем остальным функциям, должна быть объявлена с указанием типа возвращаемого значения. В программе hello, срр функция main О возвращает целое число (значение типа int от слова integer — целый). Более подроб- ная информация о переменных приведена на занятии 3, “Переменные и константы”, а о возвращаемых значениях — на занятии 4, “Выражения и операторы”. Зипиш____ Ш! Возвращать ли значение? Некоторые операционные системы позволяют проверять значение, воз- вращаемое функцией main () (например, в MS DOS — ErrorLevel). Это общепринятая практика, используемая в пакетных системах (файлах .bat), сценариях и других инструментах, которые, запустив данную про- грамму, смогут выяснить причину ошибки. Все функции начинаются открывающей фигурной скобкой ({), а оканчиваются за- крывающей (}). Фигурные скобки функции main() помещены в строках 3 и 6. Все, что находится между ними, считается телом функции. В фигурные скобки заключают блок кода. В данном случае — внешний блок кода функции. Другие типы блоков кода рассматриваются несколько позже. “Мясо с картошкой” этой программы находится в строке 4. Объект cout использу- ется для вывода сообщений на экран. Об объектах речь пойдет на занятии 8, “Подробнее о классах”. Объекты cout и cin используются в языке C++ для организа- ции, соответственно, вывода и ввода данных с консоли (cin — Console Input — ввод с консоли (клавиатура) и cout — Console Output — вывод на консоль (экран)). Вот как используется объект cout: вводится слово cout, за которым располагается оператор перенаправления потока вывода («) (далее будем называть его оператором вывода). Для этого достаточно дважды нажать клавишу <,>, удерживая клавишу <Shift>. Все, что следует за этим оператором, будет выводиться на экран. Если необходимо вы- вести на экран строку текста, не забудьте заключить ее в двойные кавычки (-), как по- казано в строке 4. Текстовая строка (text string) — это набор отображаемых символов. Два заключительных символа текстовой строки (\п) означают, что после слов “Hello world!” необходимо выполнить переход на новую строку. В строке 5 оператор return возвращает операционной системе значение 0. В не- которых операционных системах возвращаемое программой значение используется для сообщений об успехе или отказе. Считается, что возвращение значения 0 свиде- тельствует о нормальном завершении программы, а любое другое число — об отказе. Операционная система Windows, устанавливаемая на большинстве современных ма- шин, почти никогда не использует такие значения, поэтому при завершении выпол- нения все программы этой книги возвращают значение 0. Закрывающая фигурная скобка в строке 6 завершает функцию main ().
Час 2. Структура программы на языке C++ 41 Комментарии Когда пишешь программу, все кажется ясным и очевидным Однако, вернувшись к ней всего через месяц, застаешь полный беспорядок, все запутано и непонятно. Со- вершенно неясно, как беспорядку удается влезть в программу, но делает он это всегда. Чтобы не казнить себя за провалы в памяти и помочь другим понять код програм- мы, используйте комментарии. Комментарии (comment) представляют собой текст, ко- торый игнорируется компилятором, но позволяет описать прямо в коде назначение отдельной строки или целого блока. Типы комментариев В языке C++ используются два типа комментариев: с двойной наклонной чертой (//) и сочетанием наклонной черты и звездочки (/*). Комментарий с двойной на- клонной чертой (его называют комментарием в стиле C++) велит компилятору игно- рировать все. что следует за этими символами, вплоть до конца текущей строки. Комментарий с двойной наклонной чертой и звездочкой (его называют коммен- тарием в стиле С) велит компилятору игнорировать все, что следует за символами /*, до того момента, пока не встретится символ завершения комментария: звез- дочки и наклонной черты. Каждой открывающей паре символов /* должна соот- ветствовать закрывающая пара символов */. Большинство программистов используют в основном комментарий стиля C++ (//), а комментарии стиля С (/* */) применяют для временного отключения больших уча- стков программы. Вполне допустимо использовать комментарии стиля C++ (//) внутри блока, “закомментированного” в соответствии со стилем С (/* */); поскольку весь текст между символами /* и * / будет проигнорирован, комментарии стиля C++ внутри него никак не повредят. Дине______г осторожны Комментирование кода Будьте внимательны, используя комментарии в стиле С для временного отключения фрагментов кода, уже содержащего комментарии в стиле С. Не исключена ситуация, когда закомментировано окажется меньшее ко- личество кода, чем ожидалось. Дело в том, что комментарии в стиле С не допускают вложения. Наличие двух комментариев / * вовсе не означает, что для его завершения также необходимы два символа * /. Таким обра- зом, комментарий начнется первым символом /* и завершится первым символом */. Хороший IDE выделяет закомментированный код другим цветом, чтобы сделать это заметным, а компилятор сообщит об ошибке, обнаружив лишний символ завершения комментария */. Использование комментариев Компилятор комментарии игнорирует, поэтому они никак не влияют ни на размер получаемой программы, ни на ее производительность. Примеры комментариев приве- дены в листинге 2.2. Листинг 2.2. Файл comments. срр. Примеры комментариев 0: #include <iostream> 1: 2: int main() 3: {
42 Часть I. Введение в C++ 4: /* это комментарий в стиле С, который продолжается до тех 5: пор, пока не встретится символ конца комментария 6: в виде звездочки и наклонной черты */ 7: std::cout << "Hello World!\n”; 8: // этот комментарий в стиле C++ оканчивается в конце строки 9: std::cout « "That comment ended!"; 10: 11: // строка может целиком состоять только из комментария 12: /* как с двойной наклонной чертой, так и со звездочкой */ 13: return 0; 14: } Результат Hello World! That comment ended' Анализ Комментарии в строках 4—6 полностью игнорируются компилятором, как и ком- ментарии в строках 8, 11 и 12. Комментарий в строке 8 завершается в конце текущей строки, но для завершения комментариев, начавшихся в строках 4 и 12, необходим символ окончания комментария (*/). Умение писать комментарии — это искусство программиста. При написании комментариев имейте в виду, что их будут читать те, кто знает язык C++, но не знает замысла разработчика. Что именно происходит, читателям со- общит исходный код, а комментарии позволят объяснить им, зачем это происходит. Функции Хоть main () и является функцией, но она нетипична. Функцию main () вызывает операционная система, и обратиться к ней из кода программы невозможно. Другие функции в ходе выполнения кода необходимо вызывать (или обращаться к ним) прог- раммно из функции main() или других. Функция main () всегда возвращает значение типа int. Как будет вскоре проде- монстрировано, другие функции могут возвращать значения других типов или не воз- вращать вообще ничего. Программа выполняется по строкам, в порядке их расположения в исходном коде, до тех пор пока не встретится вызов какой-нибудь функции. Тогда управление пере- дается строкам этой функции. После выполнения функции управление возвращается той строке программы, которая следует за вызовом функции. Обращение к функции Существует прекрасная аналогия взаимодействия программы и функции. Например, если во время рисования ломается карандаш, приходится прекратить рисование и заточить карандаш. После этого можно вернуться к тому месту рисунка, на котором произошла ос- тановка. Когда программа нуждается в выполнении некоторой вспомогательной операции, она обращается к функции, ответственной за выполнение этой операции, после чего про- должает свою работу с того места, где была вызвана функция. Эта концепция продемонст- рирована в листинге 2.3.
Час 2. Структура программы на языке С++ 43 Листинг 2.3. Файл callfunc. срр. Обращение к функции 0: ttinclude <iostream> 1: 2: // функция DemonstrationFunction() 3: // выводит на экран информационное сообщение 4: void DemonstrationFunction() 5: { 6: std::cout << "In Demonstration FunctionXn" ; 7: } 8: 9: // Функция main() выводит сообщение, 10: // вызывает функцию DemonstrationFunction(), а затем 11: // выводит на экран следующее сообщение: 12: int main() 13: { 14: std::cout << "In main\n"; 15: DemonstrationFunction(); 16: std::cout << "Back in mainXn"; 17: return 0; 18: } Результат In main In Demonstration Function Back in main Анализ Функция DemonstrationFunction () определена в строках 4—7. Она выводит на экран сообщение и возвращает управление программе Функция main () начинается в строке 12, а в строке 14 она выводит на экран со- общение, уведомляющее о том, что сейчас управление программой передано функции main(). Затем в строке 15 происходит вызов функции DemonstrationFunction(). В результате выполняется код строки 6, который выводит на экран другое сообщение. По завершении выполнения функции DemonstrationFunction () (строка 7) управ- ление программой возвращается туда, откуда эта функция была вызвана. В данном случае выполнение программы продолжается со строки 16, в которой функция main () выводит на экран заключительное сообщение. Использование функций Функции возвращают либо некоторое реальное значение, либо значение типа void, т.е. не возвращают ничего. Функцию, которая складывает два целых числа и возвращает значение суммы, следует определить как возвращающую целочисленное значение. Функции, которая только выводит сообщение, возвращать нечего, поэтому для нее задается возвращаемый тип void. Функции состоят из заголовка (строка 4) и тела (строки 5—7). Заголовок содержит объявление типа возвращаемого значения, имени и параметров функции. Параметры позволяют передавать в функцию данные. Следовательно, если функция предназначе- на для сложения двух чисел, то их необходимо передать в функцию как параметры. Вот как будет выглядеть заголовок возвращающей целочисленное значение функции Sum (), которой передают два целых числа: int Sum(int a, int Ь)
44 Часть I. Введение в C++ Параметр (parameter) — это объявление типа данных, передаваемых функции. Ре- альное значение, передаваемое при вызове функции, называется аргументом (argument). Некоторые программисты считают эти понятия синонимами. Другие счи- тают смешение этих терминов признаком непрофессионализма. Возможно, это и так, но в данной книге эти термины взаимозаменяемы Имя функции и перечень ее параметров (т.е. заголовок без возвращаемого значе- ния) называются сигнатурой (signature). Тело функции состоит из открывающей фи- гурной скобки, любого количества операторов и закрывающей фигурной скобки. На- значение функции определяется содержащимися в ней строками программного кода. Функция может возвращать значение в программу при помощи оператора return. Этот оператор означает также выход из функции. Если не поместить в нее оператор return, то по завершении она автоматически возвратит значение типа void. Значе- ние, возвращаемое функцией, должно иметь тип, объявленный в заголовке функции. Применение параметров функции В листинге 2.4 демонстрируется функция, получающая два целочисленных пара- метра и возвращающая целочисленное значение. Листинг 2.4. Файл func. срр. Пример простой функции 0: #include <iostream> 1 : 2 : int Add (int x, int y) 3 : { 4 : std::cout << "In Add(), received ” << x << " and " 5: << у << ”\n”; return (x+y); 6: } 7 : 8: int main() 9: { 10: std::cout << "I'm in main()!\n"; 11: std::cout << "\nCalling Add()\n"; 12 : std::cout << "The value returned is: " << Add(3,4); 13 : std::cout « "\nBack in main().\n"; 14 : Std::cout << ”XnExiting...\n\n"; 15: return 0; 16: } Результат I'm in main()! Calling Add() In Add(), received 3 and 4 The value returned is: 7 Back in main(). Exiting... Анализ Функция Add() определена в строке 2. Ей передают два целочисленных параметра, а возвращает она целочисленное значение. Сама же программа начинается в строке 10 — она выводит на экран сообщение.
г Час 2. Структура программы на языке C++ 45 В строке 12 функция main() выводит на экран сообщение, а затем отображает значение, получаемое в результате вызова функции Add(3,4). Обращение к этой функции передает управление строке 2. Переданные функции Add О значения пред- ставлены как параметры х и у. Они складываются, а результат возвращается в строке 5. Код строки 12 отображает полученное значение на экране (число 7). Затем в стро- ках 13 и 14 функция main () снова отображает сообщения, а по завершении своей ра- боты осуществляет выход (управление возвращается операционной системе). Вопросы и ответы Что делает оператор #include? Это директива препроцессора, который автоматически запускается при вызове компилятора. Данная директива служит для включения содержимого файла, имя которого стоит после директивы между символами “<>”, в выполняемый код программы В чем разница между символами комментариев // и /*? Комментарии, выделенные двойной наклонной чертой (//), распространяются до конца строки. Комментарии, начинающиеся косой чертой со звездочкой (/*), продолжаются до тех пор, пока не встретится символ завершения коммен- тария (*/). Помните, что даже конец функции не завершит действия коммен- тария, начавшегося с пары символов (/*). Если не установить символ заверше- ния комментария (*/), во время компиляции произойдет ошибка. В чем разница между хорошими и плохими комментариями? Хороший комментарий сообщит читателю, почему здесь используются именно эти операторы, или объяснит назначение данного блока программы. Плохой комментарий констатирует то, что делается в данной строке программы. Стро- ки программы должны быть написаны так, чтобы они говорили за себя сами, а логика выражений была бы проста и понятна без всяких комментариев. Коллоквиум Изучив конструкцию программы на языке C++, имеет смысл ответить на несколь- ко вопросов и выполнить ряд упражнений, чтобы закрепить полученные знания. Контрольные вопросы 1. Данные какого типа возвращает функция main () ? 2. Для чего предназначены фигурные скобки? 3. Чем компилятор отличается от интерпретатора? 4. Почему столь важно многократное использование кода? Упражнения 1. В коде листинга 2.1 строку 4 разделите на несколько строк (т е. “Hello” в од- ной строке, a “world” — в другой). Изменился ли отображаемый на экране ре- зультат? Как сделать так, чтобы оба слова отображались в разных строках?
46 Часть I. Введение в C++ 2. Используя компилятор, попробуйте откомпилировать код листинга 2.1, вос- пользовавшись обеими разновидностями директивы: ttinclude <iostream> и ttinclude <iostream.h>. Способен ли он распознать оба имени файла или только одно? Этот факт может пригодиться в будущем 3. Попробуйте откомпилировать и скомпоновать те же программы, используя компилятор командной строки и IDE. Были получены одинаковые исполняе- мые файлы программ или они отличающиеся друг от друга? Ответы на контрольные вопросы 1. Функция main () возвращает данные типа int. 2. Фигурные скобки отмечают начало и конец блока кода. В них заключают со- держимое функции или программы. 3. Компилятор преобразует исходный код программы в машинный язык заранее, до ее запуска на выполнение. Интерпретатор преобразовывает в исполняемый код каждую строку программы по мере ее выполнения. 4. Почему вне программирования одни и те же идеи используются многократно? Потому что так проще, быстрее и дешевле. Однако многие программисты пред- почитают оставить за собой плотный контроль над кодом и работой программы.
ЧАСЗ Переменные и константы На этом занятии вы узнаете: как объявить и определить переменные и константы; как присвоить переменной значение и манипулировать им; как вывести значение переменной на экран. Что такое переменная? С точки зрения программиста переменная (variable) — это место в памяти компью- тера, где можно размещать хранимое значение, а затем извлекать его оттуда. Чтобы лучше разобраться в вопросе хранения значений, имеет смысл сначала изу- чить, как работает память компьютера. Память компьютера можно представить как набор ячеек. Все ячейки выстроены в ряд и пронумерованы. Эти номера называют адресами памяти. Переменные имеют не только адреса, но и имена. Имя переменной (например. MyVariable) можно представить себе как надпись на ячейке, по которой ее можно найти, даже не зная настоящего номера (адреса в памяти). На рис. 3.1 эта концепция представлена схематически. Согласно схеме, перемен- ная MyVariable начинается в ячейке с адресом 103. Имя переменной ---------------► myVariable I Рис. 3.J. Схематическое представление памяти
48 Часть I. Введение в C++ Mesut_______ прочий Что такое оперативная память? Оперативная память (RAM — Random Access Memory) еще называет- ся памятью произвольного доступа. При выключении компьютера вся хранимая в оперативной памяти информация теряется Чтобы выпол- нить какую-либо программу, компьютер должен загрузить ее в оперативную память. Все переменные также создаются в оператив- ной памяти. Когда программисты говорят о памяти, они имеют в виду именно оперативную память. Резервирование памяти Определяя переменную в языке C++, необходимо предоставить компилятору ин- формацию о ее типе: int, chart и др. Это тип (type) переменной, называемый также типом данных (datatype). Тип переменной информирует компилятор о том, сколько мес- та в памяти необходимо зарезервировать для хранения значения (value) переменной. Каждая ячейка имеет размер один байт. Если для переменной указанного типа не- обходимы четыре байта, для нее будут выделены четыре ячейки, т.е. именно по типу переменной (например, int) компилятор судит о том, какой объем памяти (сколько ячеек) необходимо зарезервировать для нее. Поскольку для представления значений компьютеры оперируют битами и байтами, а также потому, что объемы памяти изме- ряются в байтах, рассмотрим эту концепцию подробнее. Размер целых чисел Переменная типа char (используемая для хранения символов), как правило, имеет размер один байт, переменная типа short int (на большинстве компьютеров) — два байта, переменная типа long int — четыре, переменная типа int (без ключевого слова short или long) — два или четыре байта При использовании современного компилятора на операционной системе Windows 95/98, Windows ХР или Windows NT/2000/2003 переменная типа int занимает четыре байта. Однако не следует рассчитывать на то, что определенный тип данных всегда зани- мает одинаковый объем памяти. Уверенным можно быть лишь в том, что размер пе- ременной типа short int всегда будет меньше или равен размеру переменной типа int, а размер переменной типа int будет равен или меньше размера переменной ти- па long. Типы данных для хранения чисел с плавающей запятой несколько отличают- ся; более подробная информация по этой теме приведена далее. Откомпилируйте и запустите на выполнение код листинга 3.1. Эта программа точ- но сообщит, какой именно размер имеют данные каждого из типов. Листинг 3.1. Файл sizer, срр. Определение размеров переменных разных типов на используемом компьютере 0: #include <iostream> 1: 2: int main() 3: { 4: std::cout << “The size of an int is:\t\t”; 5: std::cout << sizeof(int) << " bytes.\n“; 6: std::cout << "The size of a short int is:\t”; 7: std::cout << sizeof(short) << " bytes.\n"; 8: std::cout << "The size of a long int is:\t“; 9: std::cout << sizeof(long) << " bytes.\n”; 10: std::cout << "The size of a char is:\t\t“; 11: std::cout << sizeof(char) << " bytes.\n”;
Час 3. Переменные и константы 49 12: std: :cout << "The size of a bool is:\t\t"; 13: std: :cout << sizeof (bool) « c " bytes.\n"; 14: std: :cout << "The size of a float is:\t\t" 15: std: :cout « sizeof(float) « :< " bytes.\n”; 16: std: :cout « "The size of a double is:\t"; 17: std: :cout << sizeof(double) « " bytes.\n" 18: 19: return 0; 20: } Результат The S3 Lze of an int is 4 bytes. The size of a short int is 2 bytes. The size of a long int is 4 bytes. The size of a char is 1 bytes. The s 3 Lze of a bool is 1 bytes. The si Lze of a float is 4 bytes. The S3 Lze of a double is 8 bytes. Анализ Мввд_____ прими Результат может оказаться другим На другом компьютере размеры переменных могут быть иными! Большинство операторов листинга 3.1 читателю уже знакомо. Возможно, новым окажется использование функции sizeof О в строках 5—15. Функция sizeof О под- держивается каждым компилятором и возвращает размер объекта, переданного ей в качестве параметра. Например, в строке 5 функции sizeof () передается ключевое слово int. Используя функцию sizeof (), можно установить, что на компьютере ав- тора размеры переменных типа int и long int одинаковы и составляют четыре байта Типы signed и unsigned Все целочисленные типы существуют в двух вариантах: signed (знаковые) и unsigned (беззнаковые). Это связано с тем, что отрицательные числа иногда нужны, а иногда нет. Целые числа типа short int и long int без слова “unsigned” (без знака) счи- таются знаковыми. Знаковые типы целых чисел могут быть отрицательными либо по- ложительными. Беззнаковые типы всегда являются положительными. Поскольку для хранения знаковых и беззнаковых целочисленных переменных от- водится одинаковое количество байтов, максимальное число, которое может быть со- хранено в беззнаковом целом, вдвое превышает максимальное положительное число, которое можно хранить в знаковом целом. С помощью типа unsigned short int можно хранить числа в диапазоне от 0 до 65 535. Половина чисел, представляемых типом signed short int, отрицательные, следовательно, с помощью этого типа можно представить числа только в диапазоне от -32 768 до 32 767. каете ли_ иы? Бит знака у знаковых и беззнаковых целых чисел У целых чисел со знаком для хранения самого знака используется один бит, который у беззнаковых типов используется для записи больших чи- сел. Если перечислить все возможные числа, доступные для переменных знаковых и беззнаковых типов, то можно заметить, что они одинаковы. Различие заключается в их представлении!
50 Часть I. Введение в C++ Основные типы переменных В языке C++ предусмотрены и другие типы переменных. Кроме только что рас- смотренных целочисленных, имеются еще вещественные (с плавающей точкой) и символьные. Вещественные переменные содержат значения, которые могут выражаться в виде дробей. Символьные переменные занимают один байт и используются для хранения любого из 256-ти знаков ASCII, включая расширенный набор символов. Под символами ASCII понимают стандартный набор знаков, используемых в ком- пьютерных системах. ASCII — это American Standard Code for Information Interchange (Американский стандартный код для обмена информацией). Почти все компьютерные операционные системы поддерживают код ASCII, хотя многие поддерживают также национальные наборы символов. Основные типы переменных, используемых в программах C++, представлены в табл. 3.1. В ней также приведены ориентировочные размеры переменных указанных типов и предельные значения, допустимые для хранения в них. Кстати, с этой таблицей можно сравнить результаты работы программы, представленной в листинге 3.1. Таблица 3.1. Типы переменных Тип Размер Значение unsigned short int 2 байта от 0 до 65 535 short int 2 байта от-32 768 до 32 767 unsigned long int 4 байта от 0 до 4 294 967 295 long int 4 байта от -2 147 483 648 до 2 147 483 647 int 4 байта от -2 147 483 648 до 2 147 483 647 unsigned int 4 байта от Одо 4 294 967 295 char 1 байт 256 значений символов bool 1 байт true или false float 4 байта от 1.2е-38 до 3.4е38 double 8 байтов от2.2е-308до 1.8е308 Хотя многие и считают это недопустимым, но переменную типа char можно ис- пользовать для хранения очень маленьких целых чисел. Определение переменной Чтобы создать или определить (define) переменную, необходимо указать ее тип, за которым должны следовать ее имя и точка с запятой. Для имени переменной можно ис- пользовать практически любую комбинацию букв, но оно не должно содержать пробе- лов, например х, J23qrsnf и туАде. В следующем выражении определена целочислен- ная переменная по имени туАде. int туАде,- Не забывайте, что язык C++ чувствителен к регистру символов, поэтому туАде и МуАде — это разные переменные Старайтесь давать переменным осмысленные имена, которые свидетельствуют об их назначении и облегчают понимание работы программы в целом. Такие имена, как туАде (Мой возраст) и howMany (Сколько), намного понятнее, чем xJ4 и theint. Хорошо продуманные имена избавят от необ- ходимости подробно комментировать код, а также сделают его более понятным.
Час 3. Переменные и константы 51 Поставим эксперимент. На основании первых пяти строк программы попробуйте догадаться, д ля чего она предназначена. Пример 1 main() ( unsigned short х; unsigned short у; unsigned int z; z = x * у ; Пример 2 main () ( unsigned short Width; unsigned short Length; unsigned short Area; Area = Width * Length; Чувствительность к регистру Язык C++ чувствителен к регистру (case sensitive) букв. Иными словами, пропис- ные и строчные буквы считаются разными символами. Таким образом, переменные аде, Аде и AGE рассматриваются как три различных переменных. ЛйВД____ прочим Не отключайте чувствительность к регистру! Некоторые компиляторы позволяют отключать чувствительность к ре- гистру букв. Лучше этого не делать, поскольку такая программа не смо- жет работать с другими компиляторами, а остальные программисты не смогут разобраться в ее коде. Многие программисты предпочитают использовать в именах переменных исклю- чительно строчные буквы. Если для имени переменной необходимы два слова (например, my саг), то в соответствии с наиболее популярными соглашениями воз- можны два варианта: my_car и myCar. Последняя форма записи называется “верблюжьей” (camel notation), поскольку одна прописная буква посередине напоми- нает горб верблюда. Удобство этих нотаций можно рассматривать и с другой точки зрения, как соглашения Unix и Microsoft. Как правило, программисты, работающие под Unix предпочитают первую форму, а те, кто использует продукты корпорации Microsoft, находят более удобной вторую. Компилятору это совершенно безразлично, главное, чтобы имена не совпадали. Ключевые слова Некоторые слова изначально зарезервированы в языке C++, и поэтому их нельзя использовать в качестве имен переменных. Такие слова называются ключевыми и применяются компилятором для управления программой. В их число входят if, while, for, main и т.д. В состав имен переменных ключевое слово входить может, но не может совпадать с ним полностью. Такие имена переменных, как main_flag и forEver, вполне до- пустимы, а такие, как main и for — нет.
52 Часть I. Введение в С++ Рекомендуется Не рекомендуется Указывать тип перед именем переменной. Использовать для названия переменных осмысленные имена. Помнить, что в языке C++ различаются прописные и строчные буквы. Учитывать количество байтов, занимаемых в памяти переменной используемого типа, а также значения, которые могут быть сохранены в ней. Использовать ключевые слова в качестве имен переменных. Присваивать беззнаковым переменным значения в виде отрицательных чисел. Создание нескольких переменных одновременно Вполне допустимо создание нескольких переменных с помощью одного оператора. Для этого достаточно указать их тип и имена, разделив запятыми, например unsigned int myAge, myWeight; // две переменные типа unsigned int long area, width, length; // три переменные типа long int Обе переменные, myAge и myWeight, объявлены как беззнаковые целочисленные. Во второй строке объявлены три переменные area, width и length. Всем им присво- ен один и тот же тип (long), т.к. в одной строке определения переменных нельзя смешивать различные типы. Лкждх прочим Практика комментирования Переменные имеет смысл комментировать в строке их объявления. Это напомнит разработчику и уведомит других программистов о том, для чего предполагается использовать данную переменную, даже если ее имя такой информации не предоставляет. Присвоение значений переменным Для присвоения переменной значения используется оператор присвоения (=). Присвоить число 5 переменной width можно следующим образом: unsigned short Width; Width = 5; Эти две строки можно объединить в одну и инициализировать переменную width в процессе ее определения. unsigned short Width = 5; Инициализация напоминает присвоение, особенно в случае инициализации цело- численных переменных. Позже, при рассмотрении констант, будет описан случай, когда значение обязательно должно быть присвоено при инициализации, поскольку впоследствии этого сделать будет нельзя. В листинге 3.2 показана полностью готовая к компиляции программа, которая вы- числяет площадь прямоугольника и выводит результат на экран. Листинг 3.2. Файл usevar. срр. Демонстрация применения переменных 0: // Листинг 3.2. Демонстрация применения переменных 1: #include <iostream> 2 :
Час 3. Переменные и константы 53 3 : int main() 4 : 5: 6: 7 : 8: 9: 10: 11: 12: 13 : 14: 15: 16: { unsigned short int Width = 5, Length; Length = 10; // создать переменную типа unsigned short и присвоить // ей результат умножения Width и Length unsigned short int Area = Width * Length; std::cout « "Width: " << Width << std::endl; std::cout << “Length: " << Length << std::endl; std::cout << "Area: " << Area << std::endl; return 0; } Результат Width: 5 Length: 10 Area: 50 Анализ Строка 1 содержит директиву препроцессора #include, подключающую библиоте- ку iostream, которая обеспечивает работоспособность объекта cout. Собственно, программа начинает работу в строке 3. В строке 5 переменная Width определена как unsigned short int и сразу же ини- циализирована числом 5. Далее определена еше одна переменная Length такого же ти- па, но без инициализации. В строке 6 переменной Length присваивается значение 10. В строке 10 определена переменная Area типа unsigned short int, которая ини- циализируется значением, полученным в результате умножения значений переменных width и Length. В строках 12—14 значения всех переменных выводятся на экран. Об- ратите внимание: для переноса строк используется специальный оператор endl. Между____ ярочки Конец строки Оператор endl — это сокращение от end line (конец строки), читается как “энд-эл”, а не “энд-один” (иногда букву I принимают за единицу). Ключевое слово typedef Порой бывает утомительно и скучно многократно писать такие слова, как unsigned short int. В языке C++ для этого предусмотрена возможность создания псевдонима с помощью ключевого слова typedef, которое означает определение типа. Не следует путать это с созданием нового типа (об этом речь пойдет на занятии 7, “Простые классы”). Ключевое слово typedef применяется следующим образом: сна- чала идет ключевое слово typedef, затем соответствующий тип и имя псевдонима, завершающееся точкой с запятой. Например, строка typedef unsigned short int ushort; создает новый псевдоним ushort, который можно использовать везде, где нужно было бы написать фразу unsigned short int. Листинг 3.3 — это модификация листинга 3.2, где вместо слов unsigned short int используется псевдоним ushort.
54 Часть I. Введение в C++ Листинг 3.3. Файл typedef er. срр. Применение ключевого слова typedef 0: // ***************** 1: // Листинг 3.3. Применение ключевого слова typedef 2: #include <iostream> 3 : 4: typedef unsigned short int USHORT; // определение псевдонима 5 : 6: int main() 7: { 8: USHORT Width = 5; 9: USHORT Length; 10: Length = 10; 11: USHORT Area = Width * Length; 12: std::cout << "Width: " << Width << std::endl; 13: std::cout << “Length: " << Length << std::endl; 14: std::cout « "Area: " « Area << std::endl; 15: return 0; 16: } Результат Width: 5 Length: 10 Area: 50 JU ___________ рочим Избегайте потери зачащих цифр! Некоторые компиляторы выдают для такого кода предупреждение “conversion may lose significant digits"(преобразование может привести к потере значащих цифр) Это связано с тем, что результат умножения двух переменных типа ushort в строке 11 может оказаться больше допустимого для типа unsigned short, а, следовательно, при присвоении переменной Area оно может быть усечено Но в данном кон- кретном случае это предупреждение можно проигнорировать. Анализ В строке 4 идентификатор ushort с помощью ключевого слова typedef определен как псевдоним типа unsigned short int. В остальном эта программа аналогична предыдущей (см. листинг 3.2), да и результаты их работы совпадают. Таким образом, ключевое слово typedef можно рассматривать как средство заме- ны базового определения (unsigned short int) псевдонимом (ushort). В каких случаях использовать тип short, а в каких — long? Начинающим программистам иногда трудно решить, когда объявлять переменную типа long, а когда — типа short. Правило довольно простое: если существует хоть малейший шанс, что значение будет слишком большим для предполагаемого типа, используйте тип с большим размером. Как следует из табл. 3.1, переменные типа unsigned short int, как правило, имеют размер, равный двум байтам, и могут хранить значение, не превышающее 65 535. Диапа- зон типа signed short разделен между положительными и отрицательными числами, поэтому их максимальное значение вдвое меньше. Хотя переменные типа unsigned
Час 3. Переменные и константы 55 long int способны хранить очень большое (4 294 967 295), но все-таки конечное число. Если необходимо работать с еще большими числами, придется перейти к ис- пользованию типа float или double, но при этом возможен некоторый проигрыш в точности. Переменные типа float и double могут хранить чрезвычайно большие числа, но на большинстве компьютеров значимыми остаются лишь первые 7 или 9 цифр, т.е. после указанного количества цифр число округляется. Переполнение регистра беззнаковой переменной Как уже говорилось, переменные имеют максимально допустимое значение. Но что произойдет при превышении этого предела, какая именно проблема возникнет? Когда беззнаковое целое достигает своего максимального значения, при очередном добавлении оно сбрасывается в нуль, и отсчет начинается сначала, как на спидометре автомобиля. Код листинга 3.4 демонстрирует, что произойдет при попытке поместить слишком большое число в переменную типа unsigned short int. Листинг 3.4. Файл toobigu. срр. Переполнение регистра беззнаковой целочисленной переменной 0: #include <iostream> 1: 2: int main() 3: { 4: unsigned short int smallNumber; 5: smallNumber = 65535; 6: std::cout << "small number:” << smallNumber << std::endl; 7: smallNumber++; 8: std::cout << "small number:" << smallNumber << std::endl; 9: smallNumber++; 10: std::cout « "small number:" << smallNumber << std::endl; 11: return 0; 12: ) Результат small number: 65535 small number: 0 small number: 1 Анализ В строке 4 объявлена переменная smallNumber типа unsigned short int, которая на компьютере автора является двухбайтовой. Она способна хранить значения от 0 до 65 535. В строке 5 ей присваивается максимально допустимое значение, а в строке 10 оно выводится на экран. В строке 7 переменная smallNumber увеличивается на 1. Приращение осуществляет оператор инкремента, имеющий вид двух символов плюс (++). Следовательно, значени- ем переменной smallNumber должно стать 65536. Но переменная типа unsigned short int не может хранить число, большее 65535, поэтому ее значение сбрасывает- ся в 0, подобно спидометру автомобиля; значение выводится на экран в строке 8. В строке 9 переменная smallNumber вновь увеличивается на единицу, после чего ее новое значение — 1 — опять выводится на экран.
56 Часть I. Введение в C++ Переполнение регистра знаковой переменной Знаковые (signed) целые отличаются от беззнаковых (unsigned) тем, что половина диапазона этих значений — отрицательные числа. От аналогии со спидометром автомоби- ля перейдем к аналогии с циферблатом часов, на котором числа увеличиваются при дви- жении от полудня по часовой стрелке и уменьшаются при движении против часовой стрелки (от осталось до полудня до прошло после полудня). Один час от полудня яв- ляется 1 (по часовой стрелке) или -1 (против). Когда положительные числа будут исчерпа- ны, начнутся самые большие отрицательные числа, которые затем станут увеличиваться до нуля Листинг 3.5 демонстрирует, что происходит, если добавить единицу к максимально возможному положительному числу, хранящемуся в переменной типа signed short int. Листинг 3.5. Файл toobigs. срр. Переполнение регистра знаковой целочисленной переменной 0: #include <iostream> 1: 2: int main() 3: { 4: short int smallNumber; 5: smallNumber = 32767; 6: std;:cout << "small number:" << smallNumber << std::endl; 7: smallNumber++; 8: std::cout << “small number:" << smallNumber << std::endl; 9: smallNumber++; 10: std::cout << “small number:" << smallNumber << std::endl; 11: return 0; 12: } Результат small number: 32767 small number: -32768 small number: -32767 Анализ В строке 4 переменная smallNumber объявлена на этот раз как знаковая. Если в объявлении переменной отсутствует явное указание на то, что она является без- знаковой (ключевое слово unsigned), то по умолчанию она считается знаковой. В остальном эта программа выполняет те же действия, что и предыдущая, но на эк- ран выводятся совсем другие результаты. Чтобы до конца понять, почему получены именно такие результаты, нужно знать, как представляются числа со знаком в двух- байтовом целом значении. Этот пример показывает, что в случае приращения максимального положительного целого числа со знаком будет получено не нулевое значение (как в случае с беззнако- выми целыми), а минимальное отрицательное число. Константы Подобно переменным, константы (constant) предназначены для хранения данных. Но в отличие от переменных, значения констант нельзя изменить. Создаваемую константу необходимо инициализировать сразу, поскольку присвоить ей значение позже нс удастся. В языке C++ предусмотрено два типа констант: литеральные и символьные.
Час 3. Переменные и константы 57 Литеральные константы Литеральная константа (literal constant) — это значение, непосредственно вводи- мое в самой программе. Например, в выражении int myAge = 39; слово myAge яв- ляется переменной типа int, а число 39 — литеральной константой. Константе 39 никакого иного значения присвоить нельзя. Символьные константы Символьная константа (symbolic constant) — это константа, представленная именем (точно так же, как представлена любая переменная). Но в отличие от переменной, значение инициализированной константы изменить нельзя. Если в программе есть одна целочисленная переменная с именем students, а другая — с именем classes, то можно было бы вычислить общее количество учеников школы при условии, что известно, сколько классов в школе и сколько учеников в каждом классе (допустим, каждый класс насчитывает 15 учеников), students = classes * 15; Между_______ прочти Первый оператор Звездочка (*) означает умножение. Операторы рассматриваются позже. В этом примере число 15 является литеральной константой. Но если ее заменить символьной, программу будет легче читать и изменять код в будущем. students = classes * studentsPerClass Если впоследствии потребуется изменить количество учеников в каждом классе, то, сделав это один раз в той строке программы, где определена константа studentsPerClass, не придется вносить изменения во все строки программы, где используется это значение. Определение констант с помощью директивы #def ine Для определения константы устаревшим способом используется следующий синтаксис: #define studentsPerClass 15 Именно так определение осуществлялось в старых версиях языка С, а стандарт ANSI предоставил для этого ключевое слово const. Обратите внимание: константа studentsPerClass не имеет никакого конкретного типа (int, char и т.д.). Директи- ва #define просто выполняет текстовую подстановку. Каждый раз, когда препроцес- сор встречает слово studentsPerClass, он заменяет его литералом 15. Поскольку препроцессор запускается перед компилятором, последний никогда не увидит константу, а будет видеть только число 15. Определение констант с помощью ключевого слова const Хотя директива #define и справляется со своими обязанностями, в языке С++ сушествует новый, более удобный способ определения констант. const unsigned short int studentsPerClass = 15; В этом примере также объявлена константа studentsPerClass, но на сей раз для нее задан тип unsigned short int. Такой способ имеет неоспоримое преимущество, которое позволяет предотвращать появление ошибок. Оно заключается в том, что константа имеет тип, и компилятор может проследить за ее применением только по назначению (т.е. в соответствии с объявленным типом).
58 Часть I. Введение в C++ Перечисляемые константы Перечисляемые константы (enumerated constant) позволяют создать набор констант. Например, можно объявить константу COLOR как перечисляемую и определить для нее пять значений: red, blue, green, white и black. Для создания перечисляемой константы используется ключевое слово enum, за кото- рым следуют: название типа, открывающая фигурная скобка, список значений констан- ты, разделенных запятыми, закрывающая фигурная скобка и точка с запятой. Например: enum COLOR { RED, BLUE, GREEN, WHITE, BLACK }; Это выражение выполняет две задачи: 1. создает перечисляемую константу нового типа с именем color; 2. определяет символьные константы: red со значением 0, blue со значением 1, green со значением 2 и т.д. Каждому элементу перечисляемой константы соответствует шределенное целочис- ленное значение. По умолчанию первый элемент имеет значение 0, а каждый после- дующий — на единицу большее. Каждому элементу константы можно присвоить про- извольное значение; в этом случае остальные элементы (значения которых не задавались явно) инициализируются значением, на единицу больше предыдущего. Предположим, константа объявлена так: enum Color { RED=100, BLUE, GREEN=500, WHITE, BLACK=700 }; Здесь значение red равно 100, значение blue =101, значение green = 500, зна- чение white = 501, значение black = 700. Преимущество такого подхода заключается в том, что теперь вместо бессмысленного числа 1 или 700 можно использовать символьное имя, например, black или white. Для имен перечисляемых констант принято использовать заглавные буквы. В коде программы это наглядно свидетельствует о том, что используемое имя принадлежит перечисляемой константе, а не обычной переменной. Компилятору это безразлично, главное, чтобы имена не повторялись. Вопросы и ответы Если переменные типа short int могут переполнять регистр, то почему бы не ис- пользовать во всех случаях переменные типа long int? Как короткие (short), так и длинные (long) целочисленные переменные спо- собны исчерпать участок отведенной для них памяти, но для того чтобы это произошло с типом long int, необходимо значительно большее число. Однако применение для переменных типов больших, чем необходимо, может привести к замедлению выполнения процессором программы. Сейчас эта проблема не столь актуальна, поскольку быстродействие современ- ных машин достаточно велико. Что случится если присвоить число с десятичной запятой целочисленной перемен- ной, а не переменной типа float? Например: int aNumber = 5.4; Хороший компилятор выдаст по этому поводу предупреждение, но в принципе такое присвоение вполне правомочно. Присваиваемое число будет усечено до целого. Следовательно, если присвоить число 5.4 целочисленной переменной, она получит значение 5. Часть информации будет утеряна и, если затем храня- щееся в этой переменной значение снова присвоить переменной типа float, вещественной переменной достанется лишь значение 5.
Час 3. Переменные и константы 59 Почему использование символьных констант предпочтительнее литеральных? Если значение какой-либо константы используется во многих местах програм- мы, то применение символьной константы позволяет изменить все эти значе- ния, внеся изменение лишь в определение константы. Кроме того, смысл сим- вольной константы проще понять по ее названию. Ведь иногда трудно разобраться, почему некоторое значение умножается на число 360, а если это число будет заменено символьной константой degreeslnACircle (градусы ок- ружности), то сразу становится ясно, о чем идет речь. Коллоквиум Опробовав переменные и константы, имеет смысл ответить на несколько вопросов и выполнить ряд упражнений, чтобы закрепить полученные знания. Контрольные вопросы 1. Почему вместо знаковых чисел иногда используются беззнаковые? 2. В чем разница между инициализацией и определением переменной? 3. Принадлежат ли имена dog, dog, Dog и doG одной и той же переменной? 4. В чем разница между константами, объявленными при помощи директивы #def ine и ключевого слова const? Упражнения 1. Усовершенствуйте код листинга 3.1 так, чтобы отобразить информацию обо всех типах переменных, перечисленных в табл. 3.1. Полученные значения могут совпадать с приведенными в книге, но могут и отличаться. 2. Придумайте число и подберите для него подходящий тип и имя переменной, способной его содержать. 3. Если среди знакомых читателя есть профессиональные разработчики про- граммного обеспечения, выясните предпочитаемый ими формат именования переменных. Если таких знакомых несколько, то наверняка будут рекомендова- ны различные подходы. Спросите “почему?”, и будете удивлены разнообразием полученных ответов. Ответы на контрольные вопросы 1. Переменные типа “беззнаковое целое” позволяют хранить значительно большие числа, чем знаковые. Они не смогут также содержать никаких отрицательных значений. Иногда это бывает весьма полезным. 2. Определение — это объявление типа данных и имени переменной, а инициали- зация — это присвоение переменной исходного значения. Определение и ини- циализацию можно осуществить в одной команде. 3. Нет. Язык C++ чувствителен к регистру, и каждое из этих имен будет воспри- нято компилятором как индивидуальное. Какую форму имен следует выбрать — это уже вопрос персональных предпочтений или корпоративных соглашений. 4. Определенная в директиве препроцессора #define константа заменяется в коде ее значением (литеральной константой). Переменная, объявленная с использовани- ем ключевого слова const, занимает только одно место и обладает типом данных.
ЧАС 4 Выражения и операторы На этом занятии вы узнаете: что такое оператор; что такое выражение; как работать с операторами; что есть правда и ложь в языке C++. Операторы В языке C++ операторы (statement) управляют последовательностью выполнения выражений. Они возвращают результаты вычислений или не делают ничего (пустые операторы). Все выражения языка C++ оканчиваются точкой с запятой. Самый простой пример выражения — это операция присвоения значения: х = а + Ь ; В отличие от алгебры, это выражение не означает, что х равняется а+Ь. Данное выражение следует понимать так: присвоить результат суммирования значений пе- ременных а и Ь переменной х, или присвоить переменной х значение а+Ь. Несмот- ря на то что в этом выражении одновременно выполняются два действия (вычисление суммы и присвоение значения), после него устанавливается только один символ точки с запятой. Оператор (=) присваивает результаты операций, вы- полняемых над операндами, расположенными справа от знака равенства, операнду, находящемуся слева от него. Непечатаемые символы Непечатаемые символы (whitespace), такие, как пробелы, табуляция и символы но- вой строки, невидимы на экране и не отображаются при печати. Они используются только для наглядности кода, поскольку компилятор их игнорирует. Приведенное выше выражение можно записать так: х=а+Ь,-
Час 4. Выражения и операторы 61 или так: х =а + Ь Хотя последний вариант абсолютно правомочен, выглядит он довольно глупо. Непеча- таемые символы можно использовать для улучшения читабельности кода программы, что облегчает работу с ней. Однако при неумелом использовании эти непечатаемые символы могут совершенно запутать программный код. Создатели языка C++ предоставили много различных возможностей, а уж насколько эффективно они будут использованы, зависит от каждого пользователя. Не забывайте, что внутри имен переменных и функций, а также символьных строк непечатаемые символы учитываются. Компилятор ни в коем случае не воспримет литерал seconds Per Minute как имя переменной secondsPerMinute. Блоки кода и составные операторы В любом месте, где применим один оператор, может быть размещен также составной оператор, называемый еще блоком кода. Блок кода (compound statement) начинается от- крывающей фигурной скобкой ({) и заканчивается закрывающей фигурной скобкой (}). Хоть каждый оператор в блоке должен заканчиваться точкой с запятой, сам блок точкой с запятой заканчиваться не должен: { temp = а ,- a = Ь; b = temp; } Этот блок кода выполняется как одно выражение, осуществляющее обмен значе- ниями между переменными а и Ь. Рекомендуется Не рекомендуется Не забывать добавлять закрывающую фигурную скобку каждый раз, когда используется открывающая фигурная скобка. Завершать операторы точкой с запятой. Разумно использовать отступ и пробелы, чтобы сделать код более понятным. Создавать блоки, размер которых превышает два экрана, поскольку это снижет читабельность кода. Выражения Любой возвращающий значение оператор в языке C++ считается выражением (expression). Все очень просто и понятно. Если оператор возвращает значение, значит, это вы- ражение. Все выражения являются операторами. Большинство участков кода, как это ни удивительно, тоже являются выражениями. Вот три примера: 3.2 // возвращает значение 3,2 PI // вещественная константа, возвращающая значение 3,14 SecondsPerMinute // целочисл. константа, возвращающая значение 60 Учитывая, что PI — это константа, равная 3,14, a SecondsPerMinute — констан- та, равная 60, можно утверждать, что все эти операторы являются выражениями. А вот более сложное выражение: х а + Ь;
62 Часть I. Введение в С++ Здесь не только складываются значения переменных а и Ь, но и присваивается ре- зультат переменной х, т.е. переменной х возвращается результат суммирования. Таким образом, этот оператор (присвоение) также является выражением. Поскольку это выражение, оно может располагаться справа от оператора присвоения (=). у = х = а + Ь ; Оператор присвоения (=) (assignment operator) позволяет заменить значение операн- да, расположенного с левой стороны от знака равенства, значением, вычисляемым справа от него. Операнд (operand) — это математический термин, означающий часть выражения (левую или правую), используемую оператором. Приведенная выше строка обрабаты- вается в следующем порядке: сложение а и Ь; присвоение результата выражения а + Ь переменной х; присвоение результата выражения х = а + Ь переменной у. Если переменные а, Ь, х и у являются целочисленными, переменная а имеет зна- чение 2, а переменная Ь — значение 5, то переменным х и у будет присвоено значе- ние 7. Это проиллюстрировано в листинге 4.1. Листинг 4.1. Файл express. срр. Вычисление сложных выражений 0 : #include <iostream> 1: 2: 3: 4 : 5: 6: 7 : 8 : 9 : 10: 11: 12 : 13: int main() { int a=0, b=0, x=0, y=35; std::cout << "before a: " « a << ” b: ” « b; std:: cout << " x: " << x << “ у: " << у << std::endl; a = 9; b = 7; у = x = a+b; std:: cout « "after a: " << a « “ b: " << b; Std: : cout « ' x: " << X « ” y: “ << у << Std: : endl ; return 0; ) Результат before а: О Ь: 0 х: 0 у: 35 after а: 9 b: 7 х: 16 у: 16 Анализ В строке 4 объявляются и инициализируются четыре переменные. Их значения выводятся на экран в строках 5 и 6. В строке 7 переменной а присваивается значение 9. В строке 8 переменной b присваивается значение 7, в строке 9 значения перемен- ных а и Ь суммируются, а результат присваивается переменной х. Результат выраже- ния х = а+b в свою очередь присваивается переменной у. В строках 10 и 11 получен- ные результаты выводятся на экран. Операторы Оператор (operator) — это символ, который заставляет компилятор выполнить оп- ределенное действие.
Час 4. Выражения и операторы 63 Оператор присвоения Операнд, который находится слева от оператора присвоения, называется ад- ресным, или 1-значением (от англ, left, т.е. левый). Операнд, находящийся справа от оператора присвоения, называется операционным, или r-значением (от англ. right, т.е. правый). Яшкдц прочм Константы являются г-значениями Константы могут быть только г-значениями и никогда не 1-значениями, поскольку в ходе выполнения программы их значения изменять нельзя, х = 3 5/ / ок 35 = х; // ошибка, зто не 1-значение! Обратите внимание: все 1-значения могут быть г-значениями, но не все г-значения могут быть 1-значениями. Примером г-значения, которое не может быть 1-значением, служит литеральная константа. Так, можно написать х = 5;, но нельзя написать 5 = х; (х может быть 1- или r-значением а 5 может быть только г-значением). Иеы» И/ЮЧИН Зачем столько слов об г- и 1-значениях? Вполне резонный вопрос. Стоит ли изучать эту терминологию? Стоит, поскольку она используется в сообщениях об ошибках. Лучше разо- браться в этом сейчас, чем когда компилятор выдаст совершенно не- понятное сообщение. Это поможет также понять, почему некоторый код одним способом работать может, а другим — нет. Математические операторы Существуют пять математических операторов: сложения ( + ), вычитания (-), ум- ножения (*), целочисленного деления (/) и деления по модулю (%). Язык С++ (подобно языку С) — это один из немногих языков без встроенного оператора воз- ведения в степень. Сложение, вычитание и умножение осуществляются как обычно, но с делением дело обстоит иначе. Целочисленное деление несколько отличается от обычного. Целочисленное деле- ние — это деление без остатка. Т.е. в случае целочисленного деления числа 21 на число 4 (21/4) в ответе получается 5, а остаток 1 отбрасывается. Оператор деления по модулю (%) возвращает значение остатка целочисленного де- ления. Таким образом, в результате деления 21%4 получится 1, поскольку единица со- ставляет остаток деления 21 на 4. Операция деления по модулю иногда оказывается весьма полезной, например, ес- ли необходимо отобразить из ряда чисел каждое десятое. Любое число, результат де- ления которого по модулю на 10 равен нулю, является кратным десяти т.е. делится на 10 без остатка. Так, результат выражения 1%10 равен 1, 2%10 — 2 и т.д., а 10%10 равен 0. Результат деления 11%10 тоже равен 1, а 12%10, соответственно, 2, и так можно продолжать до следующего кратного 10 числа, которым окажется 2 0. Деление чисел с плавающей запятой осуществляется как обычно. Т.е. результат де- ления 21/4.0 составит 5.25. Если хотя бы один из операндов имеет тип float (литеральное значение или переменная), результат также будет иметь тип с плаваю- щей запятой.
64 Часть I. Введение в C++ Объединение операторов присвоения и математических операторов Нет ничего необычного в том, чтобы прибавить к переменной некоторое значение, а затем присвоить результат той же переменной. Если значение переменной туАде необходимо увеличить на два, можно написать следующее: int myAge = 5; int temp.- temp = myAge +2; // сложить 5 и 2, а результат поместить в temp myAge = temp; // поместить его обратно в myAge Такой способ довольно замысловат и расточителен. В языке C++ можно помес- тить одну и ту же переменную по обе стороны от оператора присвоения, тогда преды- дущий блок сведется лишь к одному выражению: myAge = myAge + 2 ; В алгебре это выражение было бы бессмысленно, но в языке C++ оно читается следующим образом: добавить два к значению переменной myAge и присвоить резуль- тат переменной myAge. Существует еще более простой вариант записи, хотя читать его труднее: myAge += 2; Оператор присвоения с суммой (+=) добавляет r-значение к 1-значению, а затем снова присваивает результат 1-значению. Этот оператор читается “плюс-равно”, а вы- ражение можно было бы прочесть как “myAge плюс-равно два”. Если бы до начала выполнения выражения переменная myAge имела значение 4, то после ее выполнения оно стало бы равным б. Помимо оператора присвоения с суммой, существуют также операторы присвоения с вычитанием (-=), делением (/=), умножением (*=) и делением по модулю (%=). Операторы инкремента и декремента Очень часто в программах к переменным добавляется (или вычитается) единица В языке C++ увеличение значения на 1 называется инкрементом (increment), а умень- шение на 1 — декрементом (decrement). Для этих действий предусмотрены специаль- ные операторы. Оператор инкремента (++) увеличивает значение переменной на I, а оператор дек- ремента (--) уменьшает его на 1. Так, если переменную с необходимо увеличить на единицу, для этого можно использовать следующее выражение: C++; // Увеличить значение С на единицу Этот оператор эквивалентен более подробному: С = Counter + 1; или С += 1; ММЦУ. -- Почему этот язык называется C++ прочкя Можно было бы подумать, что язык C++ получил свое имя после приме- нения оператора приращения к имени языка его предшественника С. Так и есть: C++ является инкрементным приращением языка С.
Час 4. Выражения и операторы 65 Префиксные и постфиксные операторы Как оператор инкремента (++), так и оператор декремента (—) существуют в двух вариантах: префиксном (prefix) и постфиксном (postfix). Префиксный (пре означает “перед”) вариант записывается перед именем переменной (++myAge), а постфиксный (пост означает “после”) — после него (туАде++). В простом выражении вариант использования не имеет большого значения, но в сложном, при выполнении приращения одной переменной с последующим при- своением результата другой переменной, это весьма существенно. Семантика префиксного оператора следующая: изменить значение, а затем при- своить его. Семантика постфиксного оператора иная: присвоить значение, а затем из- менить его оригинал. На первый взгляд это выглядит несколько запутанным, но примеры легко прояс- няют механизм действия таких операторов. Предположим, целочисленная переменная х имеет значение 5. Выражение int а = ++х,- сообщает компилятору, что перемен- ную х необходимо увеличить на единицу (сделав эту переменную равной б), а затем присвоить значение переменной а. Следовательно, значение переменной а теперь равно б и значение переменной х тоже равно б. Если затем написать int Ь = х++,-, компилятор получит команду сначала при- своить переменной Ь текущее значение переменной х (б), а затем увеличить значение переменной х на единицу. В этом случае значение переменной Ь будет равно б, а пе- ременной х — 7. В листинге 4.2 продемонстрировано использование обоих типов операторов инкремента и декремента. Листинг 4.2. Файл prepostfix. срр. Демонстрация префиксных и постфиксных операторов 0: 1: 2: 3 : 4: 5: б: 7: 8: 9: 10: 11: 12: 13 : 14: 15: 16: 17: 18: 19: 20: 21: 22: 23 : // Листинг 4.2. Демонстрация использования // префиксных и постфиксных операторов // инкремента и декремента #include <iostream> int main() { int myAge = 39; // инициализировать две переменные int yourAge = 39; std: ::cout <« : "I am:\t" « myAge << "\tyears old.\n"; std: ::cout <« : "You are:\t" << yourAge << "\tyears old\n"; myAge++; // постфиксный инкремент ++yourAge; // префиксный инкремент Std: ::cout <« : "One year passes ...\n"; Std: : : cout <« : "I am:\t” << myAge << ”\tyears old.\n“; Std: ::cout « : "You are:\t" << yourAge << ”\tyears old\n"; Std: ::cout <- : "Another year passes\n"; Std: ::COUt <« : "I am:\t" « myAge++ << "\tyears old.\n"; Std: ::cout <« : "You are:\t" << ++yourAge << "\tyears old\n“ Std: ::cout <« : "Let's print it again.\n”; Std: ::COUt <- : "I am:\t" << myAge << "\tyears old.\n"; Std: ::COUt <« : "You are:\t" << yourAge << "\tyears old\n"; return 0; )
66 Часть I. Введение в C++ Результат I ат 39 years old You аге 39 years old One year passes... I am 40 years old You are 40 years old Another year passes I am 40 years old You are 41 years old Let's print it again I am 41 years old You are 41 years old Анализ В строках 7 и 8 объявлены две целочисленные переменные и каждая из них инициали- зирована значением 39. Значения этих переменных выводятся на экран в строках 9 и 10. В строке 11 переменная myAge увеличивается на единицу с помощью постфиксного оператора инкремента, а в строке 12 переменная yourAge увеличивается с помощью префиксного оператора инкремента. Результаты этих операций выводятся на экран в строках 14 и 15; как можно заметить, они идентичны (обоим лицам по 40 лет). В строке 17 значение переменной myAge увеличивается в операторе вывода на экран также с помощью постфиксного оператора инкремента. Поскольку этот опе- ратор постфиксный, приращение происходит после вывода на экран, поэтому снова появляется значение 40. Затем (для сравнения с постфиксным вариантом) в стро- ке 18 увеличивается переменная yourAge с использованием префиксного операто- ра инкремента. Поскольку эта операция осуществляется перед выводом на экран, отображается значение 41. И, наконец, в строках 20 и 21 эти же значения выводятся снова. Поскольку приращения больше не выполнялись, значение переменной myAge сейчас равно 41, как и значение переменной yourAge. ЛкЖф— прочим Три способа решения одной задачи. Который выбрать? В языке C++ существуют три способа добавить 1 к переменной: а=а+1, а+=1 и А+ + . Спрашивается, который из них следует использовать и почему? В связи с особенностями архитектуры аппаратных средств процессора форма а+=1 эффективнее, нежели а=а+1, а А++ эффективнее, чем А+=1. Возможно, это справедливо и сейчас, но современные оптимизирую- щие компиляторы, как правило, очень хорошо справляются с созданием эффективного машинного кода. При добавлении 1 автор предпочитает использовать оператор + + , по- скольку это нагляднее, но если требуется добавить другое значение (2, 3 или переменную), то удобнее использовать оператор +=, а если сложить необходимо несколько переменных, то применяются обыч- ные операторы = и +. По мнению автора, различия в формате кода повышают его наглядность. Приоритет Приоритет (precedence) — это порядок выполнения операций. Какое действие — сложение или умножение — первым выполняется в таком слож- ном выражении, как представлено ниже? х 5+3*8;
Час 4. Выражения и операторы 67 Если первым выполняется сложение, то результат будет равен 8*8, или 64. Если же первым выполняется умножение, то 5 + 24 равно 29. В языке C++ операторы выполняются не случайным образом, каждый из них име- ет приоритет, полный список которых приведен на отрывной карточке, расположен- ной непосредственно под обложкой книги. Умножение имеет более высокий приори- тет, чем сложение, поэтому результатом этого выражения будет 29. Если два математических оператора имеют одинаковый приоритет, то они выпол- няются в порядке следования — слева направо. Таким образом, в выражении х = 5 + + 3 + 8*9 + 6*4,- сначала вычисляется умножение, причем слева направо: 8*9 = = 72 и 6*4 = 24. Теперь то же выражение выглядит проще: х = 5 + 3 + 72 + 24,-. Затем выполняется сложение, тоже слева направо: 5 + 3 = 8; 8 + 72 = 80; 80 + + 24 = 104. Но будьте осторожны — не все операторы придерживаются такого порядка выпол- нения. Например, операторы присвоения вычисляются справа налево! Что же делать, если установленный порядок приоритетов не отвечает реальным потребностям? Рас- смотрим следующее выражение: TotalSeconds = NumMinutesToThink + NumMinutesToType * 60 В этом выражении не нужно умножать значение переменной NumMinutesToType на 60, а затем складывать результат со значением переменной NumMinutesToThink. Нужно сначала сложить значения двух переменных, чтобы получить общее количество минут, а затем умножить это число на 60, получив тем самым общее количество секунд. В этом случае для изменения порядка действий, предписанного приоритетом опе- раторов, необходимо использовать круглые скобки. Элементы, заключенные в них, имеют более высокий приоритет, чем любые другие математические операторы. Та- ким образом, выражение будет выглядеть так: TotalSeconds = (NumMinutesToThink + NumMinutesToType) * 60 Знаете ли лыг Использование круглых скобок В сомнительных случаях, когда приоритет не ясен, всегда используйте круглые скобки. Выполнения программы они не замедлят, а время ком- пиляции увеличится на весьма незначительный период. Вложение круглых скобок в сложных выражениях При создании сложных выражений может возникнуть необходимость вложить круг- лые скобки друг в друга. Например, когда требуется вычислить общее количество се- кунд, а затем общее количество людей, а уж потом перемножить эти числа: TotalPersonSeconds = ( ( (NumMinutesToThink + NumMinutesToType) * 60) * (PeoplelnTheOffice + PeopleOnVacation) ) Это сложное выражение читается изнутри. Сначала значение переменной NumMinutesToThink складывается со значением переменной NumMinutesToType, по- скольку они заключены во внутренние круглые скобки. Затем полученная сумма ум- ножается на 60. После этого значение переменной PeoplelnTheOf f ice прибавляется к значению переменной PeopleOnVacation. Наконец, вычисленное количество лю- дей умножается на количество секунд. Следующий пример иллюстрирует не менее важную тему. Приведенное выше вы- ражение легко вычисляется компьютером, но нельзя сказать, что человеку его так же легко прочитать, понять и вычислить. Вот как можно переписать это выражение с помощью временных целочисленных переменных:
68 Часть I. Введение в C++ TotalMinutes = NumMinutesToThink + NumMinutesToType; TotalSeconds = TotalMinutes * 60; Totalpeople = PeoplelnTheOffice + PeopleOnVacation; TotalPersonSeconds = TotalPeople * TotalSeconds; Для записи этого варианта требуется больше сил и несколько временных перемен- ных, но он гораздо понятнее. Осталось лишь добавить комментарии, разъясняющие назначение программного кода, и заменить число 60 символьной константой. Такой программный фрагмент можно считать практически идеальным для чтения и даль- нейшей эксплуатации. Природа истины В предыдущих версиях языка C++ результаты логических выражений представлялись целочисленными значениями, но в стандарте ANSI применяется тип bool, имеющий только два возможных значения, true (истина) и false (ложь). Результатом любого логического выражения может быть либо истина, либо ложь. Математические выражения, возвращающие нуль, можно рассматривать как значе- ние false, а любой другой результат — как true. Раньше многие компиляторы внутренне представляли тип bool с помощью типа long int, поэтому он имел размер четыре байта Ныне ANSI-совместимые компи- ляторы поддерживают однобайтовый тип bool. Операторы отношения Операторы отношения (relational operator) используются для определения равенства или неравенства двух значений. Выражения сравнения всегда возвращают значение true (истина) или false (ложь). Операторы отношения представлены в табл. 4.1. Если одна целочисленная переменная myAge содержит значение 39, а другая, yourAge — значение 40, то, используя оператор равенства (==), можно выяснить, равны ли эти переменные. myAge == yourAge; // совпадает ли значение переменной myAge со // значением переменной yourAge? Это выражение возвращает значение false (ложь) или о, поскольку переменные не равны. myAge < yourAge; II значение переменной myAge меньше значения // yourAge? А это выражение возвращает значение true (истина), поскольку 39 меньше 40. Буд*те , осторожны! Присвоение — не равенство Многие начинающие программисты на языке C++ путают оператор при- своения (=) с оператором равенства (==). Это может привести к серьезной ошибке в программе. Результат операции сравнения можно присвоить переменной! Такие вы- ражения, как а=Ь>с и а=Ь==с, вполне допустимы. Но компилятор может выдать предупреждение, если при применении оператора присвоения более подошел бы оператор равенства, поскольку компилятор не будет уверен в том, что именно это имел в виду разработчик. Всего в языке C++ используются шесть операторов отношения; равно (==), мень- ше (<), больше (>), меньше или равно (<=), больше или равно (>=) и не равно (!=). В табл. 4.1 не только перечислены все операторы отношения, но и приведены приме- ры их применения.
Час 4. Выражения и операторы 69 Таблица 4.1. Операторы отношения Имя Оператор Пример Значение Равно -- 100 == 50; false 50 = = 50; true Не равно 1 = 100 != 50; true 50 ! = 50; false Больше > 100 > 50; true 50 > 50; false Больше или равно >= 100 >= 50; true 50 > = 50; true Меньше < 100 < 50; false 50 < : 50; false Меньше или равно < = 100 <= 50; false 50 < := 50; true Условный оператор if Обычно программа выполняет операторы строка за строкой в том порядке, в ка- ком они записаны в исходном коде. Оператор if позволяет проверить условие (например, равны ли две переменные) и, в зависимости от результата, выполнить тот или иной участок кода. Простейшая форма оператора if имеет вид: if (выражение) оператор; Выражение в круглых скобках может быть любым, но обычно оно содержит опера- торы отношения. Если это выражение возвращает значение false, то последующий оператор пропускается. Если же оно возвращает значение true, то оператор выпол- няется. Рассмотрим пример: if (bigNumber>smallNumber) bigNumber = smallNumber; Здесь сравниваются значения переменных bigNumber и smallNumber. Если зна- чение переменной bigNumber больше, то во второй строке этого фрагмента ее значе- ние устанавливается равным значению переменной smallNumber. Ключевое слово else Довольно часто в программах требуется, чтобы при выполнении условия (т.е. когда оно возвратит значение true) программа выполняла один блок кода, а при его невы- полнении (т.е. когда условие возвратит значение false) — другой. Способ, описанный выше, подразумевал последовательное использование несколь- ких операторов if для проверки ряда условий. Он прекрасно работает, но слишком громоздок. Ключевое слово else поможет существенно улучшить читабельность кода: if (выражение) оператор; else оператор; Применение ключевого слова else демонстрирует листинг 4.3.
70 Часть I. Введение в C++ Листинг 4.3. Файл i f else. срр. Пример применения ключевого слова else 0: // Листинг 4.3. Пример применения ключевого слова else 1 : 2: 3 : 4 : 5 : 6 : 7 : 8 : 9 : 10 : 11 : 12 : 13 : 14 : 15 : 16 : #include <iostream> int main() { int firstNumber, secondNumber; std::cout << "Please enter a big number: ”; std::cin >> firstNumber; std::cout << “\nPlease enter a smaller number: ; std::cin >> secondNumber; if (firstNumber>secondNumber) std::cout « "\nThanks!\n"; else std::cout « "\nOops. The second is bigger!"; return 0; ) Результат Please enter a big number: 10 Please enter a smaller number: 12 Oops. The second is bigger! Введя другие числа, можно получить иной результат: Please enter a big number: 12 Please enter a smaller number: 10 Thanks! Анализ Условие оператора if проверяется в строке 10. Если оно истинно, будет выполнен оператор в строке II, а затем управление перейдет к строке 13 (после оператора else). Если выражение в строке 10 вернет значение false, то управление перейдет к директиве else, и будет выполнен код строки 13. Если ключевое слово else в строке 16 удалить, то расположенный ниже оператор будет выполнен в любом случае, вне за- висимости от условия. В этом случае оператор if завершится после строки 11, и сле- дующей строкой программы станет строка 13. Не забывайте, что в конструкции if. .else можно использовать не только отдель- ные операторы, но и целые блоки, заключенные в фигурные скобки. Между---- Еще одна новая команда прочим В языке C++ объект std: :cin позволяет прочитать данные с консоли (клавиатуры) в переменную. Условный оператор if имеет следующий синтаксис: if (выражение) оператор; следующий_оператор; Если выражение возвращает значение true, то оператор выполняется, и про- грамма переходит к оператору следующий_оператор. Если выражение не возвращает
Час 4. Выражения и операторы 71 значение true, то оператор игнорируется, и программа сразу переходит к оператору следующий_оператор. Не забывайте, что оператор может быть как одиночным (завершающимся точкой с запятой), так и блоком операторов, заключенным в фигурные скобки. Усложнение оператора if В конструкции if. .else может находиться любой оператор, даже другая конструкция if. .else. Таким образом, можно создать комплекс операторов if в следующей форме: if (выражение!) { i f (выражение?) оператор!; else { i f (выражение?) оператор2; else операторЗ; } } else оператор4; Этот громоздкий условный оператор повествует: “Если выражение! истинно и ис- тинно выражение?, то следует выполнять оператор!. Если выражение! истинно, но выражение? ложно, то в случае истинности выражения? следует выполнять оператор2. Если выражение! истинно, но выражение? и выражение 3 ложны, то следует выпол- нить операторЗ. Если выражение! ложно, то следует выполнить оператор4." Как можно заметить, сложные условные операторы могут быть весьма запутанными! Пример такого сложного условного оператора if приведен в листинге 4.4. Листинг 4.4. Файл nestedif .срр. Сложный условный оператор if 0: // Листинг 4.4. Сложный условный оператор if 1: #include <iostream> 2 : 3: int main() 4: { 5: // Запросить два числа. 6: // Присвоить эти числа переменным bigNumber и littleNumber. 7: // Если значение bigNumber больше значения littleNumber, 8: // проверить, делится ли большее число на меньшее без 9: // остатка. Если да, то проверить, не равны ли они 10: 11: int firstNumber, secondNumber; 12: std::cout << "Enter two numbers.\nFirst: 13: std::cin >> firstNumber; 14: std::cout << “\nSecond: 15: std::cin » secondNumber; 16: std::cout << "\n\n"; 17: 18: if (firstNumber>=secondNumber) 19: { 20: if ( (firstNumber % secondNumber) == 0) // Делится нацело? 21: {
72 Часть I. Введение в C++ 22: if (firstNumber == secondNumber) 23: std::cout << "They are the same!\n"; 24: else 25: std::cout << "They are evenly divisible!\n"; 26: } 27: else 28: std::cout << "They are not evenly divisible!\n"; 29: } 30: else 31: std::cout << "Hey! The second one is larger!\n”; 32: return 0; 33 : } Результат Enter two numbers. First: 10 Second: 2 They are evenly divisible! Введя другие числа, можно получить иной результат: Enter two numbers. First: 2 Second: 2 They are the same! или даже Enter two numbers. First: 3 Second: 2 They are not evenly divisible! Анализ Сначала пользователю предлагается по очереди ввести два числа, которые затем сравниваются. Первый оператор if (в строке 18) проверяет: первое число больше или равно второму? Если оказывается обратное, то в строке 30 выполняется выражение после оператора else. Если первый оператор if возвращает значение true, то выполняется блок кода, начинающийся в строке 19, а также второй оператор if в строке 20, проверяющий предположение, что первое число делится на второе без остатка (т.е. с остатком, рав- ным нулю). Если это так, то первое число либо кратно второму, либо они вообще равны друг другу. Оператор if в строке 22 проверяет версию равенства чисел, а затем на экран выводится соответствующее сообщение. Если оператор if в строке 20 возвращает значение false, то выполняется опера- тор else в строке 27. Использование фигурных скобок для вложенных операторов If Для операторов if (которые являются унарными) можно не использовать фигур- ные скобки при вложении в другие операторы if, как показано ниже. if (х > у) // если х больше у if (х < z) //и если х меньше z, х = у; // то присвоить х значение у
Час 4. Выражения и операторы 73 Однако при создании большого числа вложенных операторов это может существенно усложнить код. Кстати, операторы с большим количеством уровней вложенности спо- собны запутать код. Помните: отступ и выравнивание способны сделать код программы нагляднее и удобнее, но они никак не повлияют на работу компилятора. Кроме того, можно перепутать, какому оператору if принадлежит оператор else, и допустить серьезную логическую ошибку. Все вышесказанное иллюстрирует листинг 4.5. Листинг 4.5. Файл bracesi f. срр. Применение фигурных скобок во вложенных операторах if.. else 0: // Листинг 4.5. Применение фигурных скобок 1: // во вложенных условных операторах 2: tfinclude <iostream> 3 : 4: int main() 5: { 6: int x; 7: std::cout << "Enter a number less than 10 or greater than 100: ”; 8: std::cin » x; 9: Std::cout << " \n"; 10: 11: if (x > 10) 12: if (x > 100) 13 : std::cout << "More than 100, Thanks!\n“; 14: else // чей это else? 15: std::cout << "Less than 10, Thanks!\n"; 16: 17: return 0; 18: ) Результат Enter a number less than 10 or greater than 100: 20 Less than 10, Thanks! Анализ Программа запрашивает числа меньше 10 или больше 100 и, проверив их значе- ния на соответствие этому требованию, выводит на экран сообщение. Однако задача заключается не в этом. Если оператор if в строке II возвращает значение true, то выполняется следую- щее выражение (строка 12) В данном примере строка 12 выполняется, если введено число, большее 10. Но строка 12 также содержит оператор if, который возвращает зна- чение true, если введенное число больше 100. В таком случае, выполняется строка 13. Если введенное число меньше 10, то оператор if в строке 11 возвратит значение false. В этом случае должен выполниться следующий оператор, находящийся за вы- ражением if (в данном случае — строка 16). Но если ввести число, меньшее 10, ре- зультат окажется следующим: Enter a number less than 10 or greater than 100: 9 Как можно заметить, никакое сообщение не отображается. Слово else в строке 14 явно относилось к оператору if в строке 11 и выровнено было соответственно. К сожалению, оператор else действительно принадлежал оператору if, но располо- женному в строке 12, таким образом в программу вкралась серьезная ошибка Причем достаточно сложная, поскольку компилятор ее не заметит. В результате получится
74 Часть I. Введение в C++ вполне допустимая программа C++, но делать она будет вовсе не то, что нужно. На отладку программы и поиск такой ошибки можно потратить очень много времени. Пока вводятся числа больше 100, программа работает нормально, но при вводе числа от 11 до 99 возникнет проблема. Мсждл цйчм Удобно должно быть разработчику, а не машине Не забывайте, что отступ и выравнивание предназначены для удобст- ва разработчика, а не компилятора! Ему безразлично, как выровнены операторы if. В листинге 4.6 описанная выше ошибка устранена с помощью фигурных скобок. Листинг 4.6. Файл properbraces. срр. Правильное применение фигурных скобок в конструкции if. .else 0 : 1: // Листинг 4.6. Демонстрирует правильное применение // фигурных скобок во вложенных условных операторах 3 : 4 : 5 : 6 : 7 : #include <iostream> int main() { int x; std::cout << "Enter a number less than 10 or greater than 100: '8: 9 : 10 : 11 : 12 : 13 : 14 : 15 : 16 : 17 : 18 : 19 : std::cin >> x; std::cout << "\n"; if (x > 10) { if (x > 100) std::cout << "More than 100, Thanks!\n"; } else // исправлено! std::cout << "Less than 10, Thanks!\n"; return 0; } Результат Enter a number less than 10 or greater than 100: 20 Анализ Фигурные скобки в строках 12 и 15 превращают все, что стоит между ними, в одно выражение, и теперь оператор else в строке 16 явно принадлежит оператору if, стоящему в строке 11, как и должно быть. Если пользователь теперь введет число 20, то оператор if в строке 11 возвратит значение true; но оператор if в строке 13 — false, поэтому сообщение и не было выведено на экран. Лучше было бы использовать еще один оператор else после стро- ки 14, который выводил бы сообщение об ошибке, если число не отвечает условиям. Лежи_____ рмм Защита от ошибок Приведенные в этой книге программы написаны для демонстрации рас- сматриваемых специфических вопросов. Они намеренно подаются как можно проще; при этом авторы не ставили себе цель предусмотреть все возможные ошибки, как это делается в профессиональных программах.
Час 4. Выражения и операторы 75 Подробнее о логических операторах Довольно часто возникает необходимость проверить несколько условных выраже- ний. Например, правда ли, что х больше у, а у больше z? Прежде чем выполнить со- ответствующее действие, программа должна установить, что оба эти условия истинны либо одно из них ложно. Представьте сложную сигнальную систему, обладающую следующей логикой. Если сработала сигнализация, И время после шести вечера, И сегодня НЕ праздник ИЛИ выходной, то нужно вызывать полицию. Для проверки всех этих условий можно ис- пользовать три логических оператора C++. Они перечислены в табл. 4.2. Таблица 4.2. Логические операторы Оператор Символ Пример AND (И) выражение! && выражение 2 OR (ИЛИ) II выражение 1 || выражение2 NOT (НЕ) 1 !выражение Логический оператор and Логический оператор and (И) оценивает два операнда, и если оба они истинны (true), то результатом оператора and также будет значение true. ЕСЛИ правда, что человек голоден, И правда, что у него есть деньги, ТО правда и то, что он пообедает, if ( (х == 5) && (у == 5) ) Это логическое выражение возвратит значение true, если обе переменные (х и у) равны 5, и — значение false, если хотя бы одна из них не равна 5. Обратите внимание: выражение возвращает значение true только в том случае, если истинны обе его части. Логический оператор and обозначается двумя символами &&. Одиночный символ & соответствует совсем другому оператору. Логический оператор or Логический оператор OR (ИЛИ) также оценивает два операнда. Если хотя бы один из них является истинным, то результатом оператора OR также будет true. ЕСЛИ у человека есть наличные ИЛИ кредитная карточка, ТО он может оплатить счет. При этом нет необходимости иметь одновременно и деньги, и кредитную карточку, доста- точно чего-либо одного (хотя и то, и другое — еще лучше), if ( (х == 5) || (у == 5) ) Это логическое выражение возвратит значение true, если значение либо перемен- ной х, либо переменной у, либо оба они равны 5. Обратите внимание: логический оператор OR обозначается двумя символами | [. Оператор, обозначаемый одиночным символом |, — это совсем другой оператор. Логический оператор not Логический оператор NOT (НЕ) оценивает только один операнд. Результат операто- ра not противоположен значению операнда. Истина — НЕ ложь, ложь — НЕ истина, if ( !(х == 5) )
76 Часть I. Введение в C++ Это логическое выражение возвратит значение true только в том случае, если х не равно 5. Это же выражение можно записать и по-другому: if (х != 5) Приоритет операторов отношения Операторы отношения и логические операторы, использующиеся в выражениях языка C++, возвращают значение true или false. Подобно всем другим операторам, они подчиняются приоритету, который определяет порядок их выполнения. Этот факт весьма существенен при вычислении значения, например, такого выражения: if (х > 5 && у > 5 || z > 5) Возможно, программист хотел, чтобы это выражение вернуло значение true, если и х и у больше 5 или если z больше 5, а, может быть, он хотел, чтобы это выражение воз- вращало значение true только в том случае, если х больше 5 и либо у, либо z больше 5. Если х равен 3, а у и z равны 10, то при первой интерпретации намерений про- граммиста это выражение возвратит значение true (z больше 5, поэтому значения х и у игнорируются), но при использовании второй интерпретации получится значение false (оно не может дать значение true, поскольку для этого требуется, чтобы зна- чение х было больше 5, а после установления этого факта результат вычисления вы- ражения справа от оператора && не важен, ведь для истинности всего выражения обе его части должны быть истинными). Хотя, согласно приоритету, отношение будет вычислено правильно, круглые скоб- ки могут полностью изменить его порядок и сделать оператор более ясным if ( (х > 5) && (у > 5 || z > 5) ) При использовании предложенных выше значений это выражение возвращает зна- чение false. Поскольку оказдлось, что х не больше 5, то выражение слева от оператора and возвращает значение false, а, следовательно, и все выражение целиком тоже воз- вратит значение false. Помнито, что оператор and возвращает значение true только в том случае, когда обе части выражения возвращают значение true. Между ярочлм Группировка Дополнительные круглые скобки стоит использовать и для того, чтобы четко определить, что именно необходимо сгруппировать. Помните: цель программиста — написать программу, которая будет прекрасно работать, легко читаться и быть понятной. Подробнее об истине и лжи В языке C++ нуль эквивалентен значению false, а все другие числовые значения эквивалентны значению true. Поскольку все выражения возвращают значение, мно- гие программисты используют это в выражениях условного оператора if. if (х) // если х не равен нулю, то условие истинно х = 0; Это выражение можно прочитать так: “Если х не равен нулю, то присвоить ему значе- ние о”. Чтобы смысл этого выражения стал более очевидным, можно записать его так: if (х != 0) // если х не нуль х = 0; Оба выражения одинаково правомочны, но последнее более понятно. Хорошим тоном программирования для проверки на нулевое значение считается использование именно этого способа, а предыдущего — для логических операторов.
Час 4. Выражения и операторы 77 Следующие два выражения также эквивалентны: if (!х) // истинно, если х равен нулю if (х == 0) // если х равен нулю Второе выражение проще для понимания и гораздо очевиднее, поскольку матема- тическое значение переменной х проверяется явно. Вопросы и ответы Зачем использовать круглые скобки, если последовательность выполнения операто- ров можно установить по приоритету? Действительно, как программисту, так и компилятору должны быть хорошо из- вестны приоритеты выполнения всех операторов. Но, несмотря на это, стоит использовать круглые скобки, если они облегчают понимание программы, а, значит, и дальнейшую работу с ней. Если операторы отношений всегда возвращают значение true или false, то поче- му любое ненулевое значение считается истинным (true)? Операторы отношений всегда возвращают значение true или false, но все ос- тальные выражения также всегда возвращают значения, и они могут быть ис- пользованы в операторе if, например: if ( (х = а + b) == 35 ) Это вполне законное выражение для языка C++. При его выполнении будет вычислено значение даже в том случае, если сумма переменных а и Ь не равна числу 35 Кроме того, переменной х значение будет присвоено в любом случае, вне зависимости от результата суммы переменных а и Ь. Как влияет на программу использование символов табуляции, пробелов и перехода на новую строку? Символы табуляции, пробелов и перехода на новую строку (зачастую их назы- вают отступом) никак не влияют на программу, хотя могут сделать ее более чи- табельной. Отрицательные числа эквивалентны значению true или false? Все числа, не равные нулю (как положительные, так и отрицательные), эквива- лентны значению true. Коллоквиум Изучив возможности выражений и операторов, имеет смысл ответить на несколько вопросов и выполнить ряд упражнений, чтобы закрепить полученные знания. Контрольные вопросы 1. В чем разница между выражениями х++ и ++х? 2. Какой оператор присутствует в большинстве языков программирования, но от- сутствует в языке C++? 3. В чем разница между г- и 1-значениями? 4. Что делает оператор деления по модулю?
78 Часть I. Введение в C++ Упражнения 1. Создайте три программы, где для инкремента значения переменной используются три формы (а=а+1, а+=1 и а++). Откомпилируйте, скомпонуйте и запустите по- лученные программы. Отличаются ли они по размеру и скорости выполнения? 2. Используя IDE Borland, введите математическое выражение в программу, по- местите курсор на один из математических операторов и нажмите клавишу <F1>. Насколько полезной оказалась полученная информация? 3. Попробуйте создать программу, которая в одном выражении применяет к пере- менной префиксный и постфиксный инкременты или декременты (например, ++Х++). Компилируется ли она? Имеет ли это выражение смысл? Что, если объединить операторы инкремента и декремента? Ответы на контрольные вопросы 1. Первый является постфиксным оператором инкремента, где результат возвра- щается прежде, чем происходит инкремент. Второй — это префиксный опера- тор инкремента, где инкремент происходит прежде возвращения значения. 2. Оператор возведения в степень. Для этого применяется специальная функция. 3. Слева от знака равенства (=) находится 1-значение, справа расположено г-значе- ние. Весе 1-значения могут также присутствовать справа от знака = (что позво- ляет им стать г-значениями), но большинство r-значений не смогут выступать в качестве 1-значений. 4. Он вычисляет остаток после целочисленного деления.
ЧАС 5 Функции На этом занятии вы узнаете: что такое функция и из чего она состоит; как объявлять и определять функции; как функции передать параметры; как функция возвращает значение. Что такое функция? По сути, функция (function) — это подпрограмма, которая может оперировать дан- ными и возвращать значение. Каждая программа на языке C++ содержит, по крайней мере, одну функцию — main(), вызов которой при запуске программы осуществляет- ся автоматически. Функция main () может вызывать другие функции, а те в свою оче- редь следующие, и т.д. Каждая функция имеет собственное имя. Когда оно встречается в коде программы, управление переходит к телу функции. Это называется вызовом, или обращением, к функции. По возвращении из функции выполнение программы возобновляется со строки, следующей за той, где функция была вызвана. Схема выполнения программы показана на рис. 5.1. Правильно разработанные функции должны выполнять конкретную и вполне по- нятую задачу. Сложные задачи следует разделить на несколько более простых, выпол- няемых по очереди. Это сделает код намного понятнее. Объявление и определение функций Использование функции в программе требует, чтобы сначала функция была объяв- лена, а затем определена. Объявление функции (function declaration) сообщает компилятору ее имя, тип воз- вращаемого значения и параметров. Объявление функции называется ее прототипом (prototype), оно не содержит никакого исполняемого кода. Определение функции (function definition) предоставляет компилятору исполняемый код функции. Ни одна функция не может вызвать другую, если та не была заранее объявлена. Именно здесь содержится исполняемый код.
80 Часть I. Введение в C++ Рис. 5.1. Когда программа вызывает функцию, управление переходит к ней, после чего выполнение программы возобновляется Объявление функции Прототипы встроенных функций уже записаны в файлы, включаемые в программу с помощью директивы «include. Прототип функции (prototype) представляет собой оканчивающееся точкой с запя- той выражение, состоящее из типа возвращаемого функцией значения, ее имени и списка параметров. Список параметров функции (function parameter list) представляет собой перечень всех параметров и их типов, разделенных запятыми. Составные части прототипа функции по- казаны на рис. 5.2. _ Имя параметра Тип параметра unsigned short int FindArea (int Length, int Width); Возвращаемый тип Имя Параметры Точка с запятой Рис. 5.2. Элементы прототипа функции И в прототипе, и в определении функции должны совпадать тип возвращаемого значения функции, ее имя и список параметров. Если такого соответствия нет, ком- пилятор выдаст сообщение об ошибке. Но прототип функции не обязан содержать имена параметров, может быть указан только их тип. Например, прототип, приведен- ный ниже, абсолютно правомочен. long FindArea(int, int); Этот прототип объявляет функцию по имени FindArea (), которая получает два цело- численных параметра и возвращает значение типа long. Хотя такая запись прототипа вполне допустима, она не является наилучшей. Добавление имен параметров сделает про- тотип более понятным. С именами параметров та же функция выглядит более понятно: long FindArea(int length, int width); Теперь сразу понятно назначение функции и смысл ее параметров.
Час 5. Функции 81 Обратите внимание: все функции возвращают значение определенного типа. Код программы, содержащей прототип функции FindAreaO, приведен в листинге 5.1. Листинг 5.1. Файл deciarefunction. срр. Объявление, определение и применение функции 0: 1: 2 : 3 : 4: 5: 6: 7 : 8: 9: 10: 11 : 12: 13 : 14 : 15: 16: 17: // Листинг 5.1. Применение прототипа функции #include <iostream> int FindArea(int length, int width); // прототип функции int main!) { int lengthOfYard; int widthOfYard; int areaOfYard; std::cout « "\nHow wide is your yard? std::cin >> widthOfYard; std::cout << "\nHow long is your yard? std::cin >> lengthOfYard; areaOfYard= FindArea(lengthOfYard,widthOfYard); 18: 19: 20: 21: 22 : 23 : 24: 25: 26: 27: std::cout << "\nYour yard is "; std::cout << areaOfYard; std::cout << " square feet\n\n"; return 0; } int FindArea(int 1, int w) II Определение функции { return 1 * w; } Результат How wide is your yard? 100 How long is your yard? 200 Your yard is 20000 square feet Анализ Прототип функции FindAreaO расположен в строке 3. Сравните прототип с оп- ределением функции в строке 24. Обратите внимание, что их имена, типы возвращае- мых значений и типы параметров полностью совпадают. В противном случае компи- лятор выдал бы сообщение об ошибке. Единственным отличием является то, что прототип функции оканчивается точкой с запятой и не имеет тела. Обратите внимание и на то, что имена параметров в прототипе (length и width) не совпадают с именами параметров в определении: 1 и w. Как уже говорилось, имена в прототипе не используются; они просто служат описательной информацией для программиста. Соответствие имен параметров прототипа именам параметров в разделе реализации функции считается хорошим тоном программирования, но это не обяза- тельное требование. Определение (строки 24—27) вполне можно поместить перед вызовом функ- ции, т.е. их можно переместить выше строки 16. В таком случае прототип больше
82 Часть I. Введение в С++ не понадобится. В небольших программах это является хорошим решением, но в серьез- ном проекте такое не приветствуется. По мере усложнения программы одни функции будут вызывать другие, и это существенно затруднит контроль над тем, правильно ли они расположены Поэтому рекомендуется объявлять все функции с прототипами, что избавит от необходимости соблюдать порядок их расположения Определение функции Определение функции состоит из заголовка функции и ее тела. Заголовок подобен прототипу функции, за исключением того, что параметры имеют имена и в конце заголовка отсутствует точка с запятой. Тело функции представляет собой набор операторов, заключенных в фигурные скобки. Заголовок и тело функции показаны на рис. 5.3. Возвращаемый тип Имя Параметры unsignecTshort int *FindArea *(int lengthT^int width) // Операторы Открывающая фигурная скобка return (Length * Width); Закрывающая фигурная скобка Возвращаемое значение Рис. 5.3. Заголовок и тело функции Коротко об определении функций Как уже было сказано, прототип сообщает компилятору тип возвращаемого функ- цией значения, ее имя и список параметров. возвращаемый_тип имя_функции ( [тип [имяПараметра]]...); Определение функции передает компилятору выполняемый код функции. возвращаемый_тип имя_функции ( [тип имяПараметра]...) { операторы; } Функция не обязана иметь параметры, и если она их не имеет, то прототип не обязателен. Прототип всегда заканчивается точкой с запятой Определение функции должно совпадать с прототипом по типу возвращаемого значения и списку параметров. Оно должно содержать имена всех параметров, а тело функции следует окружить фигурными скобками. Все операторы внутри тела функ- ции должны завершаться точкой с запятой, а сама функция завершается закрывающей фигурной скобкой. Если функция возвращает значение, она должна завершаться оператором return, хотя вполне законно он может присутствовать в теле функции в любом месте. Для каждой функции указывают тип возвращаемого значения. Если он не указан явно, то по умолчанию используется тип int. Однако программы становятся понятнее,
Час 5. Функции 83 если для каждой функции, включая main(), будет явно объявлен тип возвращаемого значения. Если функция фактическое значение не возвращает, ее объявляют как воз- вращающую тип void (т.е. ничто). Давайте рассмотрим несколько прототипов функций. long Areadong length, long width); void PrintMessage(int messageNumber); int GetChoice(); BadFunction(); и возвращает тип long. II обладает двумя параметрами II возвращает тип void. // обладает одним параметром // возвращает тип int, и параметрами не обладает II возвращает тип int, и параметрами не обладает Примеры определений функций приведены ниже, long Area(long 1, long w) { return 1 * w; ) void PrintMessage(int whichMsg) ( if (whichMsg == 0) std::cout << "Hello.Xn"; if (whichMsg == 1) std::cout « "Goodbye.\n"; if (whichMsg > 1) std::cout << "I'm confused.\n"; ) В теле функции может быть размещено любое количество операторов. С другой стороны, размер хорошо продуманной функции невелик. Подавляющее большинство функций содержит лишь несколько строк кода. Использование переменных в функциях Существуют несколько разных способов использования переменных внутри функ- ций. Как уже было сказано, при вызове функции передают параметры, значения ко- торых для нее локальны. Кроме того, в функции можно создать собственные пере- менные, которые будут локальными для ее области видимости. Функция может также обращаться к глобальным переменным программы. Локальные переменные В функции можно использовать не только переданные ей значения, но и объяв- лять собственные переменные внутри тела функции, которые будут называться ло- кальными (local variable), потому что существуют только внутри самой функции. Когда выполнение программы возвращается из функции к основному коду, локальные пе- ременные удаляются из памяти. Локальные переменные определяют подобно любым другим переменным. Пара- метры, переданные функции, тоже считаются локальными переменными, и их можно использовать подобно определенным внутри функции. Листинг 5.2 иллюст- рирует пример использования параметров функции и переменных, локально опре- деленных внутри нее. Ниже приведены результаты трех запусков программы с раз- личными значениями.
84 Часть I. Введение в С++ Листинг 5.2. Файл localvar. срр. Применение локальных переменных и параметров функции 0 : Ainclude <iostream> 1 : 2 : float Convert(float); 3 : 4 : int main() 5: ( 6 : float TempFer; 7 : float TempCel; 8: 9 : std::cout « "Please enter the temperature in Fahrenheit: ”; 10 : std::cin >> TempFer; 11 : TempCel = Convert(TempFer); 12: std::cout « "XnHere's temperature in Celsius:"; 13 : std::cout << TempCel << std::endl; 14 : return 0; 15: ) 16 : 17 : float Convert(float TempFer) 18 : ( 19 : float TempCel; 20 : TempCel = ((TempFer - 32) * 5) / 9; 21: return TempCel; 22 : ) Результат Please enter the temperature in Fahrenheit: 212 Here's the temperature in Celsius:100 Please enter the temperature in Fahrenheit: 32 Here's the temperature in Celsius:0 Please enter the temperature in Fahrenheit: 85 Here's the temperature in Celsius:29.4444 Анализ Результат отражает три запуска программы со значениями 212, 32 и 85 соответст- венно. В строках 6 и 7 объявлены две переменные типа float: одна (TempFer) — для хранения значения температуры в градусах по Фаренгейту, а другая (TempCel) — в градусах по Цельсию. В строке 9 пользователю предлагается ввести температуру по Фаренгейту, а в строке 11 это значение передается функции convert (). Управление переходит к первому оператору (строка 19) в теле функции Convert!), где объявляется локальная переменная, также названная TempCel. Обра- тите внимание: это не та переменная TempCel, которая была объявлена в строке 7. Эта переменная существует только внутри функции convert (). Значение, переданное в качестве параметра TempFer, также является лишь переданной из функции main() локальной копией одноименной переменной. В функции Convert () можно было бы задать параметр FerTemp и локальную пе- ременную celTemp, что не повлияло бы на работу программы. Чтобы убедиться в этом, можно ввести новые имена и перекомпилировать программу. Локальной переменной TempCel присваивается значение, которое получается в ре- зультате вычитания числа 32 из параметра TempFer и умножения этой разности на
Час 5. Функции 85 число 5 с последующим делением на число 9. Затем результат вычислений возвраща- ется в качестве значения функции, и в строке 11 оно присваивается переменной TempCel функции main(). В строке 13 это значение выводится на экран. В данном примере программа запускалась трижды. В первый раз было введено число 212, чтобы убедиться в том, что температура кипения воды по Фаренгейту (212) соответствует температуре кипения в градусах по Цельсию (100). Во второй раз проверялось значение точки замерзания воды, а в третий раз было выбрано случайное число для получения дробного результата. В качестве упражнения попробуйте переделать программу и изменить имена пере- менных так, как показано в листинге 5.3. Листинг 5.3. Файл localvar2 .срр. Применение локальных переменных и параметров функции (с разными именами в функциях main () и Convert ()) 0: #include <iostream> 1: 2: float Convert(float); 3: 4 : int main() 5: { 6: float TempFer; 7 : float TempCel; 8: 9: std::cout << “Please enter the temperature in Fahrenheit: 10: std::cin >> TempFer; 11: TempCel = Convert(TempFer); 12 : std::cout « "XnHere's the temperature in Celsius:"; 13 : std::cout « TempCel << std::endl; 14: return 0; 15: } 16: 17: float Convert(float Fer) 18: { 19: float Cel; 20: Cel = ((Fer - 32) * 5) / 9; 21: return Cel; 22: } Результат должен быть тем же. Каждая переменная имеет область видимости (scope), которая определяет, где именно переменная будет доступна в программе и где к ней можно обратиться. Пере- менные принадлежат тому блоку кода, внутри которого они объявлены. К ним можно обращаться только внутри этого блока, а по его завершении они “выходят из области видимости”. Глобальные переменные (global variable) имеют глобальную область види- мости и внутри программы доступны повсюду. Между врош Где угодно! Переменные можно объявлять внутри любого блока кода (например, в фигурных скобках конструкции if () {}). Не забывайте, что пере- менные существуют только внутри того блока, в котором объявлены.
86 Часть I. Введение в C++ Глобальные переменные Переменные, определенные вне тела какой-либо функции, имеют глобальную об- ласть видимости и доступны из любой функции в программе, включая main(). В языке C++ применения глобальных переменных избегают, поскольку они спо- собны запутать код и затруднить его поддержку. Ни в примерах кода этой книги, ни в большинстве программ автора глобальные переменные не применяются. Аргументы функций Аргументы функции могут иметь неодинаковый тип. Можно написать функцию, которой, например, передают в качестве аргументов одно значение типа int, два зна- чения типа long и один символьный аргумент. Любое допустимое выражение C++ может быть аргументом функции, включая константы, математические и логические выражения, а также другие функции, кото- рые возвращают значение. Рассмотрим следующую функцию: int MyFunction(int thelntegerParam, bool theBoolean); Ее можно вполне законно вызывать любым из следующих способов: int z,x=3,y=5; II объявление переменных z = MyFunction(х, у); II передача двух переменных типа int z = MyFunction(32,true); II передача двух констант z = MyFunction(23+9, 100>5); // выражения, возвращающие 32 и true Последний вызов функции идентичен по действию со вторым. Объявим еще две функции: int MylntFunction(int х, int у); bool MyBoolFunction(int x, int y); Теперь функцию MyFunction () можно вызвать так: z = MyFunction(MylntFunction(3,5), MyBoolFunction(2,4)) ; В последнем случае при вызове функции MyFunction () в качестве параметра передано целочисленное значение, возвращаемое при вызове функции MylntFunction(3,5), и логическое значение, возвращаемое при вызове функции MyBoolFunction (2,4). Использование функций как параметров Хотя вполне допустимо использовать возвращающую значение функцию как параметр при вызове другой функции, это существенно затрудняет чтение программы и ее отладку. Предположим, например, что существуют функции doublet), tripleO, square () и cube (), возвращающие некоторые значения. Вполне можно применить следующее выражение: Answer = (double(triple(square(cube(myValue))))); Это выражение получает переменную myValue и передает ее в качестве аргумента функции cube(), возвращаемое значение которой (куб числа) передается в качестве аргумента функции square (). После этого возвращаемое значение функции square () (квадрат числа) в свою очередь передается в качестве аргумента функции triple (). Затем результат выполнения функции triple () (утроенное число) переда- ется как аргумент функции double (). И, наконец, результат выполнения функции double () (удвоенное число) присваивается переменной Answer. Вряд ли можно с полной уверенностью говорить о том, какую задачу решает это выра- жение (было значение утроено до или после вычисления квадрата?); кроме того, в случае неверного результата выявить “виновную” функцию окажется весьма затруднительным.
Час 5. Функции 87 В качестве альтернативного варианта можно было бы каждый промежуточный ре- зультат вычисления присваивать промежуточной переменной. unsigned long myValue = 2; unsigned long cubed = cube(myValue); //2в кубе = 8 unsigned long squared = square(cubed); //8в квадрате = 64 unsigned long tripled = triple(squared); // 64 * 3 = 192 unsigned long Answer = myDouble(tripled); // Ответ = 384 Теперь можно легко проверить каждый промежуточный результат, при этом оче- виден порядок выполнения всех вычислений. Параметры — это также локальные переменные Аргументы, переданные функции, являются локальными переменными этой функции. Изменения, внесенные в аргументы во время выполнения функции, не влияют на пере- менные, значения которых передаются в функцию. Этот способ передачи параметров из- вестен как передача по значению (passing by value), т.е. в самой функции создается ло- кальная копия каждого аргумента. Такие локальные копии внешних переменных обрабатываются так же, как и любые другие локальные переменные функции. Данную концепцию иллюстрирует листинг 5.4. Листинг 5.4. Файл passbyvar. срр. Передача параметров по значению 0: // ' Листинг 5.4. Передача параметров по значению 1: #include <iostream> 2: 3: void swap(int x int у) ; 4: 5: int main!) 6: ( 7 : int x = 5, у = 10; 8: std::cout << “Main. Before swap, x: " << X 9: < < " у: " « у « "\n“ ; 10: swap(x,y); 11: std::cout << "Main. After swap, x: " « X 12: << " y: ” << у << "\n" ; 13 : return 0; 14: } 15: 16: void swap (int x, int y) 17: ( 18: int temp; 19: std::cout << “Swap. Before swap, x: “ << X 20: « " y: " << у << “\n"; 21: temp = x; 22: x = y; 23: у = temp; 24: std::cout « "Swap. After swap, x: “ « X 25: << ” y: " << у << "\n“; 26: } Результат Main. Before swap, х: 5 у: 10 Swap. Before swap, x: 5 у: 10 Swap. After swap, x: 10 у: 5 Main. After swap, x: 5 y: 10
88 Часть I. Введение в С++ Анализ Программа инициализирует две переменные в функции mainf), а затем передает их значения функции swap(), которая, казалось бы, должна поменять их местами. Однако после повторной проверки этих переменных в функции main () оказывается, что они не изменились. Переменные инициализируются в строке 7, а их значения отображаются на экране в строках 8 и 9. Затем в строке 10 осуществляется вызов функции swap (), и перемен- ные передаются ей в качестве аргументов. Выполнение программы переходит к функции swapo, где в строках 19 и 20 зна- чения переменных снова выводятся на экран. Как и ожидалось, их значения в функции main о от передачи в функцию swapf) не изменились. В строках 21—23 пе- ременные обмениваются значениями, что подтверждается очередной проверкой в строках 24 и 25. Но это положение сохраняется лишь до тех пор, пока программа не вышла из функции swap (). Затем в строке 11 управление возвращается функции main(), в которой продемон- стрировано, что переменным возвращены их исходные значения, а все изменения, произошедшие в функции, аннулированы! Итак, передача значений функции swap() показала, что фактически передаются лишь копии значений, которые используются как локальные переменные внутри функции. Эти локальные переменные менялись местами в строках 21—23, а перемен- ные функции main () затронуты не были. На следующих занятиях рассматриваются альтернативные способы передачи парамет- ров функциям, которые позволят изменять исходные переменные в функции main (). Возвращение значений из функций Функции возвращают либо реальное значение, либо значение типа void, кото- рое служит сигналом для компилятора, что никакое значение возвращено не будет. Для возврата значения из функции применяется ключевое слово return, за кото- рым следует подлежащее возвращению значение. В качестве результата можно зада- вать как сами значения, так и выражения, при вычислении которых они получаются, return 5; return (х > 5); return (MyFunction ()) ; Все приведенные выражения являются вполне допустимыми операторами выхода из функции, поскольку функция MyFunction () сама возвращает значение. Второе выражение, return (х > 5), будет возвращать значение false, если х не больше 5, или значение true в противном случае. Таким образом, возвращено будет логическое значение, а не значение переменной х. Встретив в коде функции ключевое слово return, программа выполнит следующее за ним выражение, а его результат вернет в качестве значения функции. При выполне- нии директивы return происходит мгновенный выход из функции, и управление пере- дается оператору, следующему непосредственно за вызовом функции. Функция вполне может иметь несколько операторов return, однако не забывайте, что после их выполнения происходит выход из функции. Эту концепцию иллюстриру- ет листинг 5.5.
Час 5. Функции 89 Листинг 5.5. Файл manyreturns. срр. Использование нескольких операторов return 0: // Листинг 5.5. Применение нескольких операторов 1: // return в теле функции 2: #include <iostream> 3 : 4: int Doubler(int AmountToDouble); 5: 6 : int main() 7 : ( 8: int result = 0; 9: int input; 10: 11: std::cout << "Enter a number between 0 and " 12 : << "10,000 to double: 13 : std::cin » input; 14: 15: std::cout << "\nBefore doubler is called..."; 16: std::cout << "\ninput: " << input 17: << " doubled: " « result « "\n“; 18: 19: result = Doubler(input); 20: 21: std::cout << "\nBack from Doubler..."; 22: std::cout << "\ninput: " << input 23: << " doubled: " << result « “\n\n“ 24: 25: return 0; 26: } 27 : 28: int Doubler(int original) 29: ( 30: if (original <= 10000) 31: return original * 2 ; 32 : else 33 : return -1; 34: std::cout << "You can't get here!\n "; 35: } Результат Enter a number between 0 and 10,000 to double: 9000 Before doubler is called... input: 9000 doubled: 0 Back from doubler... input: 9000 doubled: 18000 Enter a number between 0 and 10,000 to double: 11000 Before doubler is called... input: 11000 doubled: 0 Back from doubler... input: 11000 doubled: -1
90 Часть I. Введение в C++ Анализ Числа, введенные в строках 11, 12 и 13, выводятся на экран в строках 16 и 17 на- ряду с текущим значением локальной переменной result. Вызов функции Doubler () происходит в строке 19, а в качестве аргумента ей передается введенное значение. Результат выполнения функции присваивается локальной переменной result и в строках 21—23 снова выводится на экран. В строке 30 функции Doubler () значение переданного параметра сравнивается с числом 10 000. Если окажется, что оно меньше или равно, функция возвратит уд- военное значение исходного числа. Если же число больше 10 000, функция возвратит число -1 в качестве сообщения об ошибке. Оператор в строке 34 никогда не будет выполнен, поскольку при любом значении переданного параметра (больше или меньше 10 000) функция осуществит выход либо в строке 31, либо в строке 33, но в любом случае до строки 34. Большинство компиля- торов предупредит о том, что это выражение не может быть выполнено. Лежа лрочим Сообщения компилятора для файла manyreturns. срр При компиляции файла manyreturns. срр могут быть получены сле- дующие предупреждения: "manyreturns.срр": W8066 Unreachable code in function Doubler(int) at line 34 "manyreturns.cpp": W8070 Function should return a value in function Doubler(int) at line 35 Первое свидетельствует о том, что оператор std: :cout не будет вы- полнен никогда, поскольку все возможные пути выполнения заверша- ются операторами выхода прежде, чем управление перейдет к этой строке. Второе сообщает о том, что содержащая оператор std: :cout строка вызывает у компилятора сомнения, поскольку исполняемый код расположен непосредственно возле закрывающей фигурной скобки (т.е. без оператора return). Эти предупреждения свидетельствуют о том, что компилятор способен создать машинный код из исходного, и это код будет выполняться. Но результат может быть получен не та- кой, который ожидался! Безусловно, в данном случае будет получен именно тот результат, который и следовало, т.е. будет продемонстри- ровано, что некоторый код выполнен не будет. Значения параметров, используемые по умолчанию Для каждого параметра, объявляемого в прототипе и определении функции, при вы- зове должно быть передано соответствующее значение. Передаваемое значение должно иметь объявленный тип. Предположим, функция объявлена так: long myFunction(int); Ей следует передать целочисленное значение. Если тип объявленного параметра не совпадет с типом передаваемого аргумента, компилятор сообщит об ошибке. Из этого правила есть одно исключение, которое вступает в силу, если в прототипе функции для параметра объявляется значение по умолчанию. Оно используется в слу- чае, когда при вызове функции для параметра не задано значение. Несколько изме- ним предыдущее объявление: long myFunction (int х = 50);
Час 5. Функции 91 Этот прототип нужно понимать так: “Функция myFunction() возвращает значение типа long и получает параметр типа int, но если при ее вызове аргумент предоставлен не будет, используйте вместо него число 50”. Поскольку в прототипах функций имена пара- метров необязательны, то последний вариант объявления можно переписать по-другому: long myFunction (int = 50); При объявлении значения параметра по умолчанию определение функции не изменя- ется, поэтому заголовок определения этой функции будет выглядеть так, как и раньше: long myFunction (int х) Если при вызове этой функции аргумент не будет передан, компилятор присвоит пе- ременной х значение 50. Имя параметра, для которого в прототипе устанавливается зна- чение по умолчанию, может не совпадать с именем параметра, указываемого в заголовке функции: значение, заданное по умолчанию, присваивается по позиции, а не по имени. Значение по умолчанию можно назначить любому или всем параметрам функции. Но существует одно ограничение: если какой-либо параметр не имеет значения по умолчанию, то ни один из предыдущих по отношению к нему параметров также не может иметь значение по умолчанию. Предположим, прототип функции имеет вид: long myFunction (int Paraml, int Param2, int РагатЗ); Здесь параметру Param2 можно назначить значение по умолчанию только в том слу- чае, если значение по умолчанию назначено и параметру РагатЗ. Параметру Paraml можно назначить значение по умолчанию только в том случае, если назначены значения по умолчанию как параметру Param2, так и параметру РагатЗ. Использование значе- ний, задаваемых параметрам функций по умолчанию, показано в листинге 5.6. Листинг 5.6. Файл defaultparm. срр. Использование значений по умолчанию для параметров функций 0: 1: 2: // Листинг 5.6. Использование значений по умолчанию // для параметров функций #include <iostream> 3: 4: int AreaCube(int length, int width = 25, int height = 1); 5: 6: int main() 7 : { 8: int length = 100; 9: int width = 50; 10: int height = 2; 11: int area; 12 : 13: area = AreaCube(length, width, height); 14 : std::cout << "First time area equals " << area << "\n"; 15: 16: area = AreaCube(length, width); 17: std::cout << “Second time area equals " << area << "\n"; 18: 19: area = AreaCube(length); 20: std::cout << "Third time area equals " << area << "\n"; 21: return 0; 22: } 23: 24: int AreaCube(int length, int width, int height) 25: { 26: return (length * width * height); 27: }
92 Часть I. Введение в C++ Результат First area equals: 10000 Second time area equals: 5000 Third time area equals: 2500 Анализ В прототипе функции Areacube () (строка 4) объявлено, что она получает три па- раметра, причем последние два имеют значения по умолчанию. Эта функция вычис- ляет объем параллелепипеда на основании переданных размеров. Если значение ши- рины не передано, то по умолчанию оно принимается равным 25, а высота — 1. Если значение ширины передано, а значение высоты — нет, то по умолчанию принимается только значение высоты. Однако нельзя передать в функцию значение высоты, не пе- редав значения ширины. В строках 8—10 инициализируются переменные, предназначенные для хранения размеров параллелепипеда по длине, ширине и высоте. Эти значения передаются функции Areacube () в строке 13. После вычисления объема параллелепипеда резуль- тат выводится на экран в строке 14. В строке 16 вызов функции Areacube () осуществляется снова, но без передачи значения высоты. В этом случае используется заданное по умолчанию значение высо- ты, а полученный результат выводится на экран. При третьем вызове функции Areacube () (строка 19) не передается ни значение ширины, ни значение высоты. Поэтому вместо них используются значения, заданные по умолчанию. Управление возвращается в функцию main(), где последнее значение выводится на экран. Перегрузка функций Язык C++ позволяет создавать несколько различных функций с одинаковым име- нем. Это называется перегрузкой функций (function overloading). Функции должны от- личаться друг от друга списками параметров: типом или количеством параметров либо тем и другим одновременно. Рассмотрим такой пример: int myFunction (int, int); int myFunction (long, long); int myFunction (long); Здесь функция myFunction () перегружена тремя различными списками параметров. Первая и вторая версии отличаются типами параметров, а третья — их количеством. Версию фактически вызываемой функции определяют тип и количество параметров, переданных при обращении. Типы возвращаемых значений перегруженных функций могут быть одинаковыми или разными, равно как и списки их параметров. Однако пе- регружать функции лишь на основании типа возвращаемого значения нельзя. В результате перегрузки функций происходит явление, называемое полиморфизмом функций. Поли (гр. poly) означает много, морфе (гр. morph) — форма, т.е. полиморфная функция — это функция, отличающаяся многообразием форм. Под полиморфизмом функции понимают существование в программе нескольких перегруженных функций с различными назначениями. Изменяя количество или тип параметров, можно присвоить двум или нескольким функциям одно и то же имя. При этом никакой путаницы при вызове функций не будет, поскольку нужная оп- ределяется по совпадению используемых параметров. Это позволит создать функ- цию, которая сможет усреднять целочисленные значения (например, значения типа double и других), не создавая отдельных функций для каждого из типов — Averagelnts(), AverageDoubles() и т.Д.
Час 5. Функции 93 Предположим, существует функция, которая удваивает любое передаваемое ей значение. При этом необходимо иметь возможность передавать ей значения типа int, long, float или double. Без перегрузки функций пришлось бы создать четыре раз- личных функции, int Doublelnt(int); long DoubleLong(long); float DoubleFloat(float); double DoubleDouble(double); С помощью перегрузки функций достаточно использовать следующие объявления: int Double(int); long Double(long); float Double(float); double Double(double); Такую форму легче понять и еще легче использовать. Не нужно думать о том, ка- кую именно функцию следует вызвать; достаточно передать ей переменную, и нужная версия будет вызвана автоматически. Встраиваемые функции Обычно при определении функции компилятор создает в памяти только один набор ее операторов. После вызова функции управление программой передается этим операторам, а по возвращении из функции выполнение программы возобновляется со строки, следую- щей после вызова функции. Если ее вызывать 10 раз, то программа каждый раз будет по- слушно отрабатывать один и тот же набор команд. Это означает, что в памяти существует только одна копия функции, а не десять. Каждый переход к области памяти, содержащей операторы функции, замедляет выполнение программы. Оказывается, когда функция невелика (т.е. состоит лишь из одной или двух инструкций), то можно получить некоторый выигрыш в эффективно- сти, если вместо переходов от программы к функции и обратно просто дать компиля- тору команду встроить код функции непосредственно в программу по месту вызова. Когда программисты говорят об эффективности, они обычно подразумевают скорость выполнения программы, а программа выполняется быстрее, если удается избежать обращения к функции. Если функция объявлена с ключевым словом inline (т.е. встраиваемая), компиля- тор создает функцию не в памяти компьютера, а копирует ее строки непосредственно в код программы по месту вызова. Это равносильно вписыванию в программу соот- ветствующих блоков команд вместо вызовов функций. Однако использование встраиваемых функций чревато некоторыми издержками. Если вызов функции происходит 10 раз, то во время компиляции в код будет вставле- но 10 ее копий. За увеличение скорости выполнения программы придется расплатить- ся размерами ее кода, в результате чего ожидаемого повышения эффективности мо- жет и не произойти. Современные компиляторы способны сами (с разрешения программиста, конечно) оптимизировать объектный код под наименьший размер или наибольшую скорость выполнения, поэтому встраиваемой функцию можно и не объ- являть, если только в этом нет абсолютной необходимости. Неждх___ рмш Замена макрокоманды #def ine Встраиваемые функции предназначены для замены макрокоманды #defineязыка С.
94 Часть I. Введение в C++ Существует простое правило: если функция мала (один или два оператора), то она является кандидатом во встраиваемые, а когда возникают сомнения, лучше положить- ся на компилятор. Встраиваемая функция продемонстрирована в коде листинга 5.7. Листинг 5.7. Файл inliner. срр. Использование встраиваемых функций 0: // Листинг 5.7. Использование встраиваемых функций 1 : 2 : 3 : 4 : 5: 6 : 7 : 8 : #include <iostream> inline int Doubler(int); int tnainf) { int target; 9: 10: 11 : 12 : 13 : std::cout << "Enter a number to work with: "; std::cin >> target; std::cout << "\n"; target = Doubler(target); 14: 15: 16: std::cout << "Target: " << target = Doubler(target); target << std::endl; 17 : 18: 19: std::cout << "Target: " << target = Doubler(target); target << std::endl; 20: 21 : 22 : 23 : 24 : 25: 26: 27 : std::cout << "Target: " << return 0; } int Doubler(int target) { return 2‘target; } target << std::endl; Результат Enter a number to work with: 20 Target: 40 Target: 80 Target: 160 Анализ В строке 3 объявлена встраиваемая функция Double (), получающая параметр типа int и возвращающая значение типа int. Это объявление подобно любому другому прототипу, за исключением того, что перед типом возвращаемого значения стоит ключевое слово inline. Результат компиляции данного прототипа равносилен замене в программе вызова функции target = Double (target) ; строкой target = 2 * target;. К моменту выполнения программы копии функции уже расставлены по своим местам, и программа готова к выполнению без частых переходов к функции и обрат- но. Это позволяет сэкономить время на переходах в другой участок кода, но увеличи- вает размер программы.
Час 5. Функции 95 №ЖЛЛ_____ ярим Не более, чем рекомендация Ключевое слово inline служит для компилятора лишь рекомендацией. Компилятор волен игнорировать эти рекомендации и обращаться к функции, как обычно. Стек и функции При вызове функции ей передается управление программой. После передачи па- раметров начинается выполнение операторов, составляющих тело функции. По за- вершении выполнения функции возвращается значение (если функция не определена как возвращающая тип void), и управление передается вызывающей функции. Как же это реализовано? Когда программа начинает работу, операционная система выделяет для нее в па- мяти специальную область — стек. Стек (stack) — это специальная область оператив- ной памяти, выделенная для размещения данных программы, необходимых каждой вызываемой функции. Эта область называется так потому, что представляет собой очередь типа “последним пришел — первым ушел" и напоминает стопку тарелок в руках официанта (рис. 5.4). Принцип “последним пришел — первым ушел” означает, что элемент, добавленный в стек последним, будет извлечен из него первым. Большинство же очередей функцио- нирует подобно очереди в театр: первый, кто занял очередь, первым из нее и выйдет (и войдет в театр). Стек скорее напоминает стопку монет. Если сложить на столе моне- ты одну на другую, а затем взять несколько из них, то первыми читатель возьмет те, ко- торые он положил последними. При помещении (push) данных в стек он увеличивается, а при возвращении (pop) данных из стека — уменьшается. Невозможно достать из стопки тарелку, не взяв предварительно те, которые были помещены в стопку после нее. То же справедливо и для данных в стеке памяти. Аналогия со стопкой тарелок в этом случае приводится чаще всего. Такое сравне- ние довольно наглядно, но в принципе неверно. Более точным примером является этажерка с рядом полок, расположенных друг над другом. Вершиной стека будет лю- бая полка, на которую в данный момент указывает указатель вершины стека. Все данные имеют последовательные адреса, и один из них хранится в регистре ука- зателя вершины стека. Все, что находится ниже этого адреса (называющегося вершиной стека), относится к стеку. Все, что находится выше вершины, к стеку не относится и игнорируется, как показано на рис. 5.5.
96 Часть I. Введение в С++ Рис. 5.5. Указатель вершины стека Указатель вершины стека 102 При помещении элемента в стек он размещается на “полке”, расположенной над вершиной стека, после чего указатель вершины изменяется так, чтобы указывать на новое значение. При удалении значения из стека в действительности изменяется лишь адрес указателя вершины стека: так, чтобы он указывал на подлежащий удале- нию элемент стека. Принцип действия схематически показан на рис. 5.6. Ниже описаны события, происходящие при переходе программы к выполнению функ- ции. (Детали могут отличаться в зависимости от операционной системы и компилятора.) 1. В стек помещается обратный адрес функции. После выхода из нее выполнение продолжится с этого адреса. 2. В стеке выделяется участок памяти для возвращаемого значения объявленного типа. 3. В стек помещаются все аргументы функции. 4. Управление переходит к функции. 5. Локальные переменные помещаются в стек в порядке их определения. Когда работа функции завершается, возвращаемое значение помещается в область стека, зарезервированную на этапе 2. Вопросы и ответы Почему бы не сделать все переменные глобальными? Когда-то именно так и поступали. Но по мере усложнения программ стало очень трудно находить в них ошибки: поскольку значения глобальных пере-
Час 5. Функции 97 менных могли быть изменены любой из функций, было сложно определить, в какой именно блок программы вкралась ошибка. Многолетний опыт убедил программистов, что данные должны храниться локально (насколько это воз- можно) и доступ к ним на изменение должен быть по возможности ограничен. ) Вне стека Рис. 5.6. Перемещение указателя вершины стека В стеке Указатель вершины стека 108 Почему изменения, вносимые в теле функции в переменные, переданные как apiy- менты, не отражаются на значениях этих переменных в основном коде программы? Аргументы обычно передаются в функцию как значения, т.е. аргумент в функ- ции является на самом деле копией оригинального значения. Что произойдет, если объявить следующие функции: int Area (int width, int length = 1); int Area (int size);? Будут ли они перегруженными? Условие уникальности списков параметров соблю- дено, но в первом варианте для параметра определено значение, используемое по умолчанию. Эти объявления будут скомпилированы, но если вызвать функцию Area О с одним параметром, то произойдет ошибка компиляции, обусловленная неоп- ределенностью между функциями Area (int, int) и Area (int).
98 Часть I. Введение в C++ Коллоквиум Изучив возможности функций, имеет смысл ответить на несколько вопросов и выполнить ряд упражнений, чтобы закрепить полученные знания. Контрольные вопросы 1. Какой механизм передачи переменных при вызове функций принят по умолчанию? 2. Как при перегрузке выясняется, которую из версий функции следует использовать? 3. Зачем нужны функции? Почему бы не создавать большие монолитные программы? 4. Сколько значений способен вернуть оператор return? Упражнения 1. Закомментируйте в коде листинга 5.5 (файл manyreturns. срр) последний опе- ратор std: :cout. Что происходит при компиляции? Что будет, если поместить оператор return после оператора std: :cout? 2. Попробуйте усовершенствовать код листинга 5.2 (файл localvar. срр) так, чтобы он переводил температуру по Цельсию в температуру по Фаренгейту. 3. Используя перегрузку, усовершенствуйте код листинга 5.2 (файл localvar. срр) так, чтобы он был способен вычислять температуру, указанную как целым чис- лом (тип int), так и числом с плавающей запятой (тип float). Ответы на контрольные вопросы 1. Передача по значению. Передается только значение (копия исходной перемен- ной). Это предотвращает случайное изменение исходного значения в функции. 2. По аргументам. Сработает та версия, список параметров которой соответствует переданным аргументам. 3. Функция позволяет многократно использовать код, созданный самостоятельно или другими разработчиками, а также разделить большую задачу на более мел- кие. Функции упрощают поддержку и ускоряют разработку. 4. Только одно. Это единственное значение может быть результатом сложного вы- ражения, но только вызывающая функция получит лишь одно значение. Воз- вращать можно различные значения, а также использовать несколько разных операторов return, но возвращено будет только одно значение.
ЧАС 6 Ветвление процесса выполнения программ На этом занятии вы узнаете: что такое циклы и как их использовать; как организовать различные циклы; об альтернативах вложенному оператору if else. Циклы На предыдущем занятии рассматривалась первая форма изменения последователь- ности выполнения кода программы, операторы if и else. Не меньше проблем про- граммирования решается при помощи многократного повторения одинаковых действий. Итерация (iteration) — это повторение одних и тех же действий определенное ко- личество раз. Основным методом итерации является цикл (loop). Оператор goto На заре компьютерной эры программы были простыми, жесткими и короткими. Циклы состояли из метки, нескольких команд и оператора безусловного перехода. В C++ метка представляет собой имя, за которым следует символ двоеточия (:). Метка размещается слева от того оператора C++, к которому будет выполнен переход по оператору goto с соответствующим именем метки. Почему следует избегать использования оператора goto? Как правило, программисты избегают использовать оператор goto, и на это есть причины. Оператор goto позволяет перейти в любую точку программы — как вперед, так и назад. Беспорядочное использование операторов goto приводит к созданию за- пуганного, трудно читаемого кода, прозванного “кодом спагетти” (spaghetti code). Чтобы избежать использования оператора goto, применяют более сложные, управ- ляемые операторы цикла: for, while и do. .while.
100 Часть I. Введение в С++ Оператор цикла while Оператор while создает в программе цикл, который будет повторять последова- тельность операторов до тех пор, пока условие в начале цикла остается истинным. Его применение продемонстрировано в листинге 6 1. Листинг 6.1. Файл while. срр. Цикл оператора while 0: // Листинг 6.1. 1 : // Цикл оператора while 2 : #include <iostream> 3 : 4: int mainf) 5: { 6: int counter =0; // присвоить начальное значение 7 : 8: while (counter<5) // проверить, истинно ли еще условие 9: { 10: counter++; // тело цикла 11 : std::cout << "counter: " << counter << "\n" 12 : ) 13 : 14 : std::cout << "Complete, counter: " << counter << ".\n"; 15: return 0; 16 : ) Результат counter: 1 counter: 2 counter: 3 counter: 4 counter: 5 Complete, counter: 5. Анализ Эта несложная программа демонстрирует цикл оператора while. В строке 6 пере- менная counter (счетчик) инициализируется нулевым значением. Впоследствии она используется как часть условия. Затем проверяется условие, и если оно истинно, вы- полняется тело цикла. В данном случае условию продолжения цикла удовлетворяют все значения переменной counter, меньшие пяти (строка 8). Если условие истинно, то в строке 10 значение счетчика увеличивается на единицу, а в строке 11 выводится на экран. Как только значение счетчика достигает пяти и условие цикла в строке 8 перестает выполняться, все тело цикла (строки 9~ 12) пропускается, и программа пе- реходит к строке 14. Более сложный оператор while Условие, проверяемое в операторе while, может бьггь таким же сложным, как и любое другое выражение языка C++. Оно может состоять из нескольких выражений, объединенных логическими операторами && (AND — И), | | (OR — ИЛИ) и ! (NOT — НЕ). Листинг 6.2 демонстрирует более сложный оператор while.
Час 6. Ветвление процесса выполнения программ 101 Листинг 6.2. Файл complexwhile, срр. Сложный оператор while 0: // Листинг 6.2. 1: // Сложный оператор while 2: #include <iostream> 3: 4: int main() 5: { 6: unsigned short small; 7: unsigned long large; 8: const unsigned short MAXSMALL=65535; 9: 10: std::cout « "Enter a small number: "; 11: std::cin >> small; 12: std::cout << "Enter a large number: "; 13: std::cin >> large; 14: 15: std::cout << "small: " << small « 16: 17: // при каждой итерации проверять три условия 18: while (small<large && large>0 && small<MAXSMALL) 19: 20: { II после каждых 5000 строк выводить точку 21: if (small % 5000 == 0) 22: Std::cout << ” . “ ; 23: 24: small++; 25: 26: large-=2; 27: } 28: 29: std::cout << "\nSmall: " << small 30: << " Large: " << large << std::endl; 31: return 0; 32: } Результат Enter a small number: 2 Enter a large number: 100000 Small: 2.......... Small: 33335 Large: 33334 Анализ Это простая игра. Введите два числа — одно меньше (small), другое больше (large). Затем меньшее начнет увеличиваться на единицу, а большее — уменьшаться на два. Цель игры: угадать число, на котором значения “встретятся”. В строках 10—13 осуществляется ввод значений. В строке 18 проверяются три усло- вия продолжения цикла: 1. меньшее число меньше большего (small < large); 2. большее число больше нуля (large > 0); 3. меньшее число меньше максимально допустимого (small < maxsmall). В строке 21 вычисляется остаток от деления числа small на 5000, причем значе- ние переменной small не изменяется. Если small делится на 5000 без остатка
102 Часть I. Введение в C++ и в результате получается 0, на экран выводится точка. Затем в строке 24 значение пе- ременной small увеличивается на 1, а в строке 26 значение large уменьшается на 2. Цикл завершается, когда хотя бы одно из условий перестает выполняться. После этого управление передается в строку 29, следующую за телом цикла. Поскольку нуль является знаковой константой, компилятор может выдать следующее или подобное предупреждение: "complexwhile.срр": W8012 Comparing signed and unsigned values in function main() at line 18 Чтобы сделать числовую константу беззнаковой, можно добавить суффикс “и” (в данном случае — Ои). Операторы break и continue Иногда необходимо бывает перейти к началу цикла еще до завершения выполнения всех операторов тела цикла. Для этого используется оператор continue. Однако в ряде случаев требуется выйти из цикла еще до проверки условия про- должения цикла. Для этого служит оператор break. Пример использования этих операторов приведен в листинге 6.3. Это несколько ус- ложненный вариант уже знакомой игры. Теперь, кроме меньшего и большего значений, предлагается ввести шаг и конечное значение. Как и в предыдущем случае, за каждый цикл значение переменной small увеличивается на единицу, а значение переменной large уменьшается на два. Если меньшее число кратно значению переменной skip, то уменьшение на единицу (декремент) будет пропущено. Игра заканчивается, когда меньшее число превысит большее. Если значение переменной large совпадает с конеч- ным значением (target), игра прерывается, и выводится соответствующее сообщение. Цель игры: угадать конечное число (target). Листинг 6.3. Файл breaker. срр. Применение операторов break и continue 0: // Листинг 6.3. 1: // Использование операторов break и continue 2: #include <iostream> 3: using namespace std;// в этом файле используются std::cout, 4: // std::cin, std::endl и т.д. 5: 6: int main() 7: { 8: unsigned short small; 9: unsigned long large; 10: unsigned long skip; 11: unsigned long target; 12: const unsigned short MAXSMALL=65535; 13: 14: cout « "Enter a small number: "; 15: cin >> small; 16: cout << "Enter a large number: " ; 17,- cin >> large; 18: cout « "Enter a skip number: “; 19: cin >> skip; 20: cout << "Enter a target number: “; 21: cin >> target; 22 : 23: cout « ”\n”; 24 : 25: // проверить три условия выхода
Час 6. Ветвление процесса выполнения программ 103 26: while (smallclarge && large>0 && small<MAXSMALL) 27: { 28: small++; 29: 30: if (small % skip == 0) // пропустить декремент? 31: { 32: cout « "skipping on “ « small << endl; 33: continue; 34: } 35: 36: if (large == target) // результат угадан? 37: { 38: cout << "Target reached!"; 39: break; 40: } 41: 42: large-=2; 43: } // конец цикла while 44: 45: cout << "\nSmall: " << small << " Large: " << large << endl ; 46: return 0; 47: } Результат Enter a small number: 2 Enter a large number: 20 Enter a skip number: 4 Enter a target number: 6 skipping on 4 skipping on 8 Small: 10 Large: 8 Анализ Как можно заметить, игра закончилась поражением пользователя, поскольку меньшее значение превысило большее, а результат оказался 8, а не 6. В строке 26 проверяются условия выхода из цикла. Если значение переменной small меньше значения large, и large больше нуля, a small не превышает макси- мально допустимого значения, тело цикла выполняется. В строке 30 вычисляется остаток от деления значения переменной small на зна- чение переменной skip. Если значение переменной small кратно значению пере- менной skip, то оператор continue возвращает выполнение программы в начало цикла (строка 26), пропуская декремент переменной large и ее проверку на соответ- ствие ставке игрока. В строке 36 значение переменной target сравнивается со значением переменной large. Если игрок угадал, и эти значения равны, то игра заканчивается победой пользова- теля. В этом случае программа выводит сообщение о победе, работа цикла прерывается оператором break, и управление передается в строку 45. Операторы continue и break следует использовать осторожно. Программы, кото- рые внезапно меняют свое поведение, тяжело понять, а свободное применение опера- торов continue и break способно запутать даже маленький цикл while и сделать его непонятным.
104 Часть I. Введение в C++ Между______ арочяш Использование пространство имен В этом примере используется еще одно ключевое слово: using namespace. Оно указывает компилятору на то, что при вызове функции (как, например, cin и cout) следует подразумевать пространство имен std. Таким образом, его можно не указывать явно (префикс std: :). Поскольку эту команду поддерживают еще не все компиляторы, в большинстве примеров данной книги она не используется. Но компи- лятор Borland ее поддерживает. Цикл while (1) Проверяемым условием в операторе цикла while может быть любое допустимое выражение языка C++. Цикл выполняется до тех пор, пока выражение истинно. Если в качестве проверяемого условия задать значение 1, соответствующее логическому значению true (истина), то цикл не завершится никогда, став бесконечным (infinite loop). Листинг 6.4 демонстрирует пример бесконечного цикла, содержащего оператор безусловного выхода break. Листинг 6.4. Файл whileforever, срр. Цикл while (1) 0: // Листинг 6.4. 1: // Пример цикла while(true) 2: ♦include <iostream> 3 : 4: int main() 5: { 6 : int counter = 0; 7 : 8: while (1) 9: { 10: counter ++; 11: if (counter > 10) 12 : break; 13 : } 14 : std::cout << "counter: " << counter « "\n"; 15: return 0; 16: } Результат counter: 11 Анализ В строке 8 оператору while назначено условие, которое никогда не станет лож- ным. В строке 10 переменная counter увеличивается на единицу, а в строке 11 про- веряется, не превысило ли ее значение 10. Если этого не случилось, цикл while про- должается. Если счетчик counter стал больше 10, то в строке 12 цикл while прерывается оператором break, и выполнение программы переходит к строке 14, где результат выводится на экран. Несмотря на то что эта программа работоспособна, ее нельзя назвать корректной. Эго типичный пример безграмотного использования оператора while. Правильным решением была бы организация проверки значения счетчика counter в условии продолжения цикла.
Час 6. Ветвление процесса выполнения программ 105 -----г •тропы! Зависание Такие бесконечные циклы, как while (1), при которых условие выхода никогда не будет выполнено, приводят к зависанию компьютера. Исполь- зуйте их очень осторожно и тщательно проверяйте. Язык C++ располагает массой возможностей для решения одной и той же задачи. Поэтому важно научиться выбирать наилучшее средство в каждой конкретной ситуации. Рекомендуется Не рекомендуется Использовать оператор цикла while для выполнения блока операторов, пока проверяемое условие истинно. Внимательно использовать операторы continue и break. Удостовериться, что созданный цикл не является бесконечным. Ставить точку с запятой непосредственно после оператора while (), поскольку это завершит оператор, и он не будет иметь тела (т.е. не будет выполнять никаких действий). Оператор цикла do. .while Вполне возможен вариант, когда тело цикла while вовсе не будет выполнено. Оператор цикла while проверяет выражение условия цикла до того, как приступит к выполнению операторов тела цикла, и, если условие ложно с самого начала, тело цикла будет пропущено. Пример такой ситуации приведен в листинге 6.5. Листинг 6.5. Файл bodyskip. срр. Игнорирование тела цикла while 0: // Листинг 6.5. 1: // Пример игнорирования тела цикла while 2: // при заведомо ложном условии 3: #include <iostream> 4: 5: int main() 6: { 7: int counter; 8: std::cout « "How many hellos?: "; 9: std::cin >> counter; 10: while (counter>0) 11: { 12: std::cout << "Hello!\n"; 13: counter--; 14: } 15: std::cout << "counter is Output: " << counter; 16: return 0; 17: } Результат How many hellos?: 2 Hello! Hello! counter is Output:0
106 Часть I. Введение в C++ Запустите эту программу еще раз, но введите значение 0. Результат будет следующим: How many hellos?: 0 counter is OutPut:0 Анализ В строке 8 пользователю предлагается ввести начальное значение счетчика (counter). В строке 10 это значение проверяется (оно должно быть больше нуля), а затем в теле цикла уменьшается на единицу. При первом запуске программы на- чальное значение счетчика равнялось двум, поэтому тело цикла выполнялось дважды. Во втором случае было введено число 0. Выражение в строке 10 возвращает значение false, поскольку counter не больше нуля. Условие цикла не выполняется, поэтому все его тело пропускается, и на экран ничего не выводится. Так как же организовать вывод сообщения, по крайней мере, один раз? С помо- щью оператора while это сделать невозможно, т.к. условие проверяется еще до вы- полнения тела цикла. Эту проблему можно решить, использовав оператор if для кон- троля начального значения переменной counter: if (counter < 1) // коррекция минимального значения counter = 1; Именно такое решение — корявое и неэлегантное — программисты называют “клудж”. Цикл do. .while сначала выполняет тело цикла, а условие продолжения проверяет потом. Это гарантирует выполнение операторов цикла, по крайней мере, один раз. В листинге 6.6 приведен измененный вариант предыдущей программы, в котором вместо цикла while используется цикл do. .while. Листинг 6.6. Файл dowhile. срр. Пример цикла do. .while 0: // Листинг 6.6. 1: // Пример цикла do while 2: #include <iostream> 3 : 4: int main() 5: { 6: int counter; 7: std::cout << "How many hellos? " ; 8: std::cin >> counter; 9 : do 10: { 11: std::cout << "Hello\n“; 12: counter--; 13: } while (counter>0); 14: std::cout « "counter is: " « counter « std::endl; 15: return 0; 16: } Результат How many hellos? 2 Hello Hello counter is: 0 Запустите эту программу второй раз и введите значение 0. Результат будет таким: How many hellos?: О Hello counter is -1
Час 6. Ветвление процесса выполнения программ 107 Анализ В строке 7 пользователю предлагается ввести начальное значение счетчика (counter). В операторе do. .while тело цикла выполняется до проверки условия, что гарантирует выполнение операторов цикла, по меньшей мере, один раз. В строке 11 текст сообщения выводится на экран, в строке 12 значение счетчика counter умень- шается на единицу, а в строке 13 проверяется условие продолжения цикла. Если оно истинно, выполняется следующий цикл со строки 11, в противном же случае цикл за- вершается, и управление передается в строку 14. Операторы continue и break в цикле do. .while работают точно так же, как и в цикле while. Единственным различием циклов while и do. .while является рас- положение оператора проверки условий. Оператор цикла for Для организации цикла с помощью оператора while необходимо выполнить три обязательных действия: установить начальные значения переменных, а затем контро- лировать истинность условия продолжения и в каждом цикле изменять значение пе- ременной цикла (листинг 6.7). Листинг 6.7. Файл whileagain. срр. Еще один пример оператора while 0: // Листинг 6.7. 1: // Еще один пример цикла while 2: #include <iostream> 3: 4: int main)) 5: { 6: int counter = 0; 7: 8: while (counter<5) 9: { 10: counter++; 11: std::cout << "Looping! 12: } 13: 14: std::cout << "\nCounter: " « counter << ”.\n"; 15: return 0; 16: } Результат Looping! Looping! Looping! Looping! Looping! counter: 5. Анализ В строке 6 счетчик counter инициализируется нулевым значением. Затем в строке 8 проверяется условие продолжения цикла, а в строке 10 значение счетчика увеличи- вается на единицу. В строке 11 на экран выводится сообщение, наглядно иллюстри- рующее циклический процесс.
108 Часть I. Введение в C++ Инициализация, условие и приращение Оператор цикла for состоит из ключевого слова for и пары круглых скобок, со- держащих три оператора (инициализацию, условие и приращение), которые отделя- ются точками с запятой. Первое выражение цикла for — это инициализация. Оно инициализирует (устанавливает начальное значение) счетчик цикла. Счетчик, как правило, представля- ет собой целочисленную переменную, которая объявляется и инициализируется прямо в операторе for, хотя в языке C++ это может быть любым допустимым выражением, способным установить начальное значение счетчика. Второй параметр цикла for — это условие продолжения цикла, которое также может быть любым выражением, как и в операторе while. Третий параметр устанавливает приращение счетчика цикла (по умолчанию шаг приращения равен единице). В этой части также может использовать- ся любое корректное выражение или оператор C++. Нужно заметить, что, хотя пара- метры цикла for могут задаваться любыми корректными выражениями C++, для ус- тановки второго параметра должно использоваться выражение, возвращающее логическое значение. Пример применения цикла for приведен в листинге 6.8. Листинг 6.8. Файл forloop.cpp. Пример цикла for 0: // Листинг 6.8. 1: // Пример цикла for 2: ♦include <iostream> 3 : 4 : int main() 5 : { 6: int counter; 7 : for (counter=0; counter<5; counter**) 8: Std::cout << "Looping! "; 9 : 10: std::cout << "\nCounter: " << counter << ".\n"; 11 : return 0; 12 : } Результат Looping! Looping! Looping! Looping! Looping! counter: 5. Анализ Оператор for в строке 7 инициализирует счетчик counter, проверяет его текущее значение (counter < 5) и приращение. Тело оператора for находится в строке 8. Конечно, вместо него там может быть и блок операторов. , •старожш! Распространенные ошибки в цикле for Весьма распространенной ошибкой является использование внутри са- мого оператора for запятой (,) вместо точки с запятой (;). В этом случае компилятор сообщит об ошибке. Еще одной ошибкой является точка с запятой () после круглых скобок оператора. В результате получается пустой цикл. Поскольку иногда это имеет смысл, компилятор об ошибке не сообщит.
Час 6. Ветвление процесса выполнения программ 109 Более сложные операторы for Оператор for является мощным и гибким инструментом программирования. Три не- зависимых оператора (инициализация, проверка и условие) обеспечивают неограниченные возможности управления работой цикла. Цикл for работает следующим образом: 1. сначала осуществляется операция инициализации; 2. проверяется условие выхода; 3. если условие справедливо, то цикл повторяет выполняемые операторы. При каждой итерации этапы 2 и 3 повторяются. Инициализация нескольких счетчиков и их приращение В инициализации нескольких счетчиков нет ничего необычного, проверка не- скольких условий продолжения цикла также вполне допустима, как и несколько опе- раций над счетчиками цикла. Инициализацию и приращение можно заменить рядом операторов C++, разделенных запятой. Листинг 6.9 демонстрирует инициализацию и приращение двух счетчиков. Листинг 6.9. Файл formulti. срр. Использование нескольких счетчиков в цикле for 0: // Листинг 6.9. 1: // Использование нескольких счетчиков 2: //в цикле for 3: #include <iostream> 4: 5: int main() 6: { 7: for (int i=0, j=0; i<3; i++, j++) 8: std::cout « "i: " << i << " j: " « j « std::endl; 9: return 0; 10: } Результат i: 0 j: 0 i: 1 j: 1 i: 2 j: 2 Анализ В строке 7 переменные i и j инициализируются нулевыми значениями. Для разделе- ния двух выражений используется запятая, а от условия инициализации они отделяются точкой с запятой. Затем проверяется условие i<3, и т.к. оно справедливо, тело цикла вы- полняется. Значения счетчиков выводятся на экран. После этого выполняется третья часть оператора for, в которой значения счетчиков i и j увеличиваются на единицу. После выполнения строки 8 и изменения значений счетчиков условие проверяется снова. Если оно по-прежнему справедливо, тело цикла выполняется еще раз. Это происходит до тех пор, пока условие цикла не станет ложным. Тогда тело цикла пере- станет выполняться, а управление перейдет к следующему оператору после цикла. Пустые операторы цикла for Любой оператор цикла for может отсутствовать. Чтобы отметить позицию пропу- щенного оператора, нужно использовать точку с запятой (;). Чтобы создать цикл for, который работает так же, как цикл while, достаточно отказаться от первого и третьего операторов. Такой цикл демонстрирует листинг 6.10.
110 Часть I. Введение в C++ Листинг 6.10. Файл fornull. срр. Цикл for с пустыми операторами 0: // Листинг 6.10. 1 : // Цикл for с пустыми операторами 2 : ((include <iostream> 3 : 4 : int main() 5: { 6: int counter = 0; 7 : 8: for( ; counter<5; ) 9 : { 10: counters; 11 : Std::cout << "Looping! 12: } 13 : 14: std::cout << "\nCounter: " << counter << ".\n"; 15: return 0; 16: } Результат Looping! Looping! Looping! Looping! Looping! counter: 5. Анализ Очевидно, что результат выполнения такого цикла в точности совпадает с резуль- татом выполнения цикла while из листинга 6.7. В строке 6 инициализируется значе- ние переменной counter. Заданные в строке 8 параметры цикла for содержат только проверку условия продолжения цикла. Приращение переменной цикла отсутствует, поэтому он ведет себя точно так же, как и цикл while (counter < 5). Рассмотренный пример еще раз показывает, что возможности языка C++ позволяют решить задачу множеством способов. Ни один опытный программист не будет использо- вать цикл for подобным образом. Тем не менее, можно опустить даже все три параметра оператора for, а для управления циклом использовать операторы break и continue. Пример использования конструкции for без параметров приведен в листинге 6.11. Листинг 6.11. Файл forempty.cpp. Оператор for без параметров 0: // Листинг 6.11. 1: // Оператор for без параметров 2: ((include <iostream> 3 : 4: int main() 5: { 6: int counter=0; // инициализация 7 : int max; 8: std::cout << "How many hellos?"; 9 : std::cin >> max; 10 : for (;;) // бесконечный цикл for 11 : { 12 : if (counter < max) // проверка 13 : { 14: std::cout << "Hello!\n"; 15 : counter++; // приращение 16: )
Час 6. Ветвление процесса выполнения программ 111 17: else 18: break; 19: ) 20: return 0; 21: } Результат How many hellos? 3 Hello! Hello! Hello! Анализ Здесь операторы цикла for урезаны до нуля. Удалены все три оператора — ини- циализация, условие и приращение. Начальное значение счетчика инициализируется в строке 6 еще до начала работы цикла. Условие продолжения цикла также проверяет- ся в отдельном операторе if (строка 12), и если оно истинно, то после выполнения операций тела цикла в строке 15 увеличивается значение счетчика. Если условие не выполняется, оператор break в строке 18 прерывает выполнение цикла. Несмотря на то что рассмотренная программа совершенно абсурдна, встречаются ситуации, когда конструкции for(;;) и while (1) оказываются просто необходимы- ми Более полезный пример применения таких операторов будет приведен далее в этой главе при рассмотрении оператора switch. Пустой цикл for Поскольку синтаксис оператора for позволяет использовать его при описании цикла достаточно сложной конструкции, необходимость в теле цикла иногда вообще отпадает. Таким образом, тело цикла будет состоять из завершающего символа точки с запятой (,-). Его можно размещать в одной строке с оператором for, хотя назначение этого символа в данном случае не вполне очевидно. Пример пустого цикла приведен в листинге 6.12. Листинг 6.12. Файл f ornullbody. срр. Оператор for без тела цикла 0: II Листинг 6.12. 1: II Оператор for 2: // без тела цикла 3: #include <iostream> 4: 5: int main() 6: { 7: for (int i=0; i<5; std::cout << "i: " << i++ << std::endl) 8: 9: return 0; 10: } Результат i: 0 i: 1 i: 2 i: 3 i: 4
112 Часть I. Введение в C++ Анализ Оператор for в строке 7 содержит все три оператора. Инициализация в данном случае устанавливает счетчик i и присваивает ему значение 0. Затем проверяется ус- ловие i<5, и если оно истинно, то в третьей части оператора for значение перемен- ной выводится на экран и увеличивается на единицу. Поскольку все необходимые операции выполняются в самом операторе for, тело цикла можно оставить пустым. Такой вариант нельзя назвать оптимальным, т.к. за- пись в одной строке большого количества операций значительно усложняет воспри- ятие программы. Правильнее было бы записать этот цикл так: 8: for (int i=0; i<5; i++) 9: cout << "i: " « i << endl; Оба варианта записи равноценны, но второй вариант гораздо читабельнее и понятнее. Вложенные циклы Цикл, организованный в теле другого цикла, называют вложенным (nested). В этом слу- чае внутренний цикл полностью выполняется на каждой итерации внешнего. Заполнение элементов матрицы с помощью вложенного цикла for демонстрирует листинг 6.13. Листинг 6.13. Файл fornested. срр. Вложенный цикл for 0: // Листинг 6.13. 1: // Вложенный цикл for 2 : ((include <iostream> 3: 4: int main() 5: { 6: int rows, columns; 7: char theChar; 8: std::cout << “How many rows? “; 9: std::cin >> rows; 10: std::cout << "How many columns? "; 11: std::cin » columns; 12: std::cout << “What character? "; 13: std::cin >> theChar; 14: for (int i=0; icrows; i++) 15: { 16: for (int j=0; jccolumns; j++) 17: std::cout « theChar; 18: std::cout << "\n“; 19: } 20: return 0; 21: } Результат How many rows? 4 How many columns? 12 What character? x xxxxxxxxxxxx xxxxxxxxxxxx xxxxxxxxxxxx xxxxxxxxxxxx
Час 6. Ветвление процесса выполнения программ 113 Анализ В начале программы пользователю предлагается ввести количество строк и столб- цов, а также символ, которым будет заполнена матрица. Первый, внешний цикл for в строке 14, инициализирует счетчик (i) значением 0. Затем следует тело цикла for. В первой строке тела внешнего цикла (строка 16) инициализируется еше один цикл. Переменной j присваивается значение 0, и начинается выполнение тела внут- реннего цикла. В строке 17 на экран выводится символ, введенный в начале програм- мы. Заметьте, что внутренний цикл for содержит только один оператор (вывод сим- вола). Затем проверяется условие (j<columns), и если оно истинно, j увеличивается на единицу, а на экран выводится следующий символ. Так продолжается до тех пор, пока j не станет равно количеству столбцов. После вывода на экран двенадцати символов х условие внутреннего цикла ока- зывается ложным, и управление передается в строку 20, где на экран выводится символ новой строки. После этого проверяется условие внешнего цикла (i<rows), и если оно истинно, значение счетчика i увеличивается, а тело внеш- него цикла выполняется снова. При второй итерации внешнего цикла опять выполняется внутренний цикл. Пе- ременной j снова присваивается нулевое значение, и все операции внутреннего цикла повторяются сначала. Принцип вложенных циклов заключается в том, что при каждой итерации внеш- него цикла внутренний выполняется полностью. Таким образом, отображаемый сим- вол выводится в колонках для каждой строки. Оператор switch Операторы if и if. .else в сложных конструкциях с большим количеством вло- женных операторов могут существенно усложнить код. Язык C++ располагает альтер- нативным решением этой проблемы — оператором switch. В отличие от оператора if, он позволяет проверять сразу несколько условий, организуя ветвление программы более эффективно. Оператор switch имеет следующий синтаксис: switch (выражение) { case значениеОдин; оператор; break; case значениеДва; оператор; break; case значением; оператор; break; default: оператор; } Выражение в скобках оператора switch представляет собой любое допустимое вы- ражение языка C++, а оператор — это любой допустимый оператор или блок опера- торов. Выражение возвращает (или может быть однозначно преобразовано в) целочис- ленное значение. Поэтому использование логических операций или выражений сравнения здесь недопустимо. Переход осуществляется к той строке оператора switch, где после ключевого сло- ва case находится значение, соответствующее результату выражения. С этой строки выполнение операторов продолжится до тех пор, пока оператор switch не завершится либо пока не встретится оператор break. Если ни одно значение case не соответст- вует результату выражения, то выполняются операторы, следующие за ключевым сло- вом default, а в случае его отсутствия оператор switch завершается.
114 Часть I. Введение в C++ Нежа____ лрочш Раздел default Использование в операторах switch строки default считается хоро- шим стилем программирования. Даже если видимой необходимости в строке default нет, в ней можно разместить обработчик непредви- денной ситуации, а также сообщение о ее возникновении. Это окажет неоценимую помощь при отладке. Хоть это и не обязательно, но многие программисты помещают в раздел def ault и оператор break — исключительно для порядка. Следует заметить, что, если оператор break в конце оператора case отсутствует, то выполнение переходит к следующему оператору case. Иногда это необходимо, но, как правило, приводит к ошибкам. Если решено использовать такой прием, стоит описать его в комментарии, чтобы впоследствии это не воспринималось как случайная ошибка. Пример использования оператора switch приведен в листинге 6.14. Листинг 6.14. Файл switcher. срр. Пример оператора switch 0: II Листинг 6.14. 1: II Пример оператора switch 2: #include <iostream> 3: 4: int main() 5: { 6: unsigned short int number; 7: std::cout << "Enter a number between 1 and 5: 8: std::cin >> number; 9: switch (number) 10: { 11: case 0: 12 : std::cout << “Too small, sorry!”; 13 : break; 14: case 5: 15: std::cout << “Good job!\n“; // далее 16: case 4: 17 : std::cout << "Nice Pick!\n"; // далее 18: case 3: 19: std::cout << “Excellent!\n"; // далее 20: case 2: 21: std::cout << "Masterful!\n"; // далее 22 : case 1: 23: std::cout << "Incredible!\n"; 24 : break; 25: default: 26: std::cout << "Too large!\n”; 27 : break; 28: } 29: Std::cout << "\n\n"; 30: return 0; 31: 1 Результат Enter a number between 1 and 5: 3 Excellent! Masterful!
Час 6. Ветвление процесса выполнения программ 115 Incredible! Enter a number between 1 and 5: 8 Too large! Анализ Программа предлагает ввести число, которое затем передается оператору switch. Если введен 0, что соответствует оператору case в строке 11, то на экране отобража- ется сообщение “Too small, sorry!”, после чего оператор break завершает выпол- нение оператора switch Если введено число 5, управление передается в строку 14, и на экран выводится соответствующее сообщение. Затем выполняется код строки 17, ко- торый также выводит сообщение, и так до строки 23. При вводе чисел от 1 до 5 на экран выводятся соответствующие сообщения. Если число не лежит в диапазоне от 0 до 5, управление переходит к оператору default в строке 25, код которой выводит на экран сообщение “Too large! ”. Вопросы и ответы Какую из конструкций — if.. else или switch — лучше использовать в конкрет- ной ситуации? Если придется применять более одного оператора else при проверке одного и того же значения, лучше воспользоваться оператором switch. Как выбрать между операторами while и do. .while? Если тело цикла должно выполняться хотя бы один раз, используйте цикл do. .while, в противном случае используйте оператор while. Как выбрать между операторами while и for? Если необходимо инициализировать счетчик, проверять его значение и изме- нять после каждого цикла на постоянную величину, используйте оператор for. Если счетчик инициализирован ранее или не должен изменяться при каждом цикле, предпочтительнее оператор while. Что лучше: while (1) или for (;;) ? Существенного различия между ними нет. Коллоквиум Изучив возможности ветвления процесса выполнения программы, имеет смысл от- ветить на несколько вопросов и выполнить ряд упражнений, чтобы закрепить полу- ченные знания. Контрольные вопросы 1. Какой тип данных следует использовать в цикле for? 2. В чем разница между операторами break и continue? 3. Пример вложенного цикла for приведен в листинге 6.13. Могут ли циклы while и do. .while также быть вложенными? 4. Что команда break делает внутри оператора switch?
116 Часть I. Введение в C++ Упражнения 1. Измените код файла forloop.cpp так, чтобы для счетчика внутри цикла ис- пользовалось значение типа float. Попробуйте увеличивать значение счетчика не на 1, а на 0.1. 2. Измените код файла fornested.cpp так, чтобы вместо цикла for использовал- ся цикл while. Замените первый цикл for и запустите программу. Затем заме- ните второй. Это наглядно демонстрирует возможность вложения различных видов циклов. 3. Измените код файла switcher .срр так, чтобы включить цикл в один из опера- торов case. Можно ли теперь откомпилировать и выполнить программу? Ответы на контрольные вопросы 1. Большинство программистов используют в операторе for только целые числа. Но это не ограничение языка; использовать можно и числа с плавающей запя- той, и строки, и данные любого другого типа. 2. Команда break завершает цикл, и управление переходит к оператору, следующе- му за телом цикла. Команда continue позволяет пропустить остальные операто- ры тела цикла, но не покинуть его, а перейти к проверке условия его завершения. 3. Вполне могут! Язык C++ самодостаточен. Цикл for можно поместить внутрь цикла while, в котором находится цикл do. .while, и так до бесконечности! 4. Команда break внутри оператора switch прерывает его выполнение и передает управление за пределы тела оператора switch. Без него будут выполнены все команды, начиная с той, где удовлетворяется условие поиска, и до конца тела оператора switch.
ЧАСТЬ II Классы В этой части... Час 7. Простые классы Час 8. Подробнее о классах
ЧАС 7 Простые классы На этом занятии вы узнаете: что такое тип; что такое класс и объект; как определить новый класс и создать его объект. Что такое тип Тип (type) — это категория Способность категоризации отличает людей от остальных живых существ. Люди видят сотни разнообразных форм на поверхности саванны, жи- вотных и деревья. Причем не просто животных, а газелей, жирафов, слонов, буйволов и т.д. Для их категоризации люди придумали царства, типы, классы, отряды, семейства, род и вид. Короче говоря, люди воспринимают вещи в терминах абстрактных типов. Апельсин относится к типу цитрусовых, цитрусовые — к типу плодовых, плодо- вые — к типу растений, а растения — к типу живых существ. К общеизвестным типам относятся автомобиль, дом, человек и Земля. В языке C++ тип — это некий объект, обладающий размером, состоянием и набором возможностей. Программист на языке C++ может создавать любой необходимый тип, и каждый из этих новых типов способен обладать всеми функциональными возможностями и преимуществами встроенных, предопределенных типов языка C++, таких, как int, long И double. Программы, как правило, создают для решения реальных проблем, таких, напри- мер, как контроль записей о служащих или моделирование работы системы обогрева. Хотя решить сложную проблему можно и при помощи программы, использующей только целые числа и символы, гораздо проще сделать это, когда есть возможность создавать достаточно сложные представления объектов, о которых идет речь. Иными словами, моделируя работу системы обогрева, необходимо иметь возможность созда- вать переменные такого типа, который соответствует помещениям, термодатчикам, термостатам и бойлерам. Чем точнее свойства этих переменных соответствуют дейст- вительности, тем проще создать программу.
Час 7. Простые классы 119 Создание новых типов Встроенные типы переменных языка C++, включая беззнаковые целые и символы, были рассмотрены на предыдущих занятиях. По типу переменной о ней можно узнать немало. Например, если переменные Height и width объявлены как беззнаковые короткие целые (unsigned short int), то сразу понятно, что каждая из них сможет хранить целое число в диапазоне от нуля до 65 535 (если тип unsigned short int за- нимает только два байта). Кроме размера, тип переменной свидетельствует о ее возможностях. Например, короткие целые числа (тип short int) можно перемножить. Таким образом, лишь объявив переменные Height и Width как unsigned short int, их можно будет сложить или присвоить одной из них значение другой. Итак, тип переменной определяет: ее размер в памяти; тип информации, которую она может хранить; операции, которые могут выполняться с ее участием. Язык C++ позволяет программисту самостоятельно расширить возможности язы- ка, создавая собственный тип данных, необходимый для решения текущей задачи. Механизм для объявления нового типа позволяет создавать классы. Класс — это оп- ределение нового типа. Классы и их члены В языке C++ новый тип данных создается в результате объявления класса. Класс (class) — это набор переменных, зачастую различных типов, объединенный с набором функций, предназначенных для работы с ними. Автомобиль, например, можно представить как набор колес, дверей, синений, окон и тл. Или как удобное средство передвижения, способное ехать, разгоняться, тормозить, ос- танавливаться, парковаться и т.д. Класс позволяет инкапсулировать различные запчасти автомобиля и его разнообразные функции в один комплект, называемый объектом. Инкапсуляция (encapsulation) позволяет объединить все, что известно об автомоби- ле, в единый класс, обеспечивая разработчику ряд преимуществ. Когда все сведения собраны в одном объекте, к ним легче обращаться, копировать и манипулировать ими. При использовании класса можно не заботиться о том, как он устроен и какие процессы происходят внутри него. Клиент (client) класса — это другой класс или функция, которая использует дан- ный класс. Инкапсуляция позволяет клиентам класса использовать его, ничего не зная о его внутреннем устройстве и о том, как именно он работает. Управлять авто- мобилем можно и не зная принципа действия двигателя внутреннего сгорания. Кли- ент класса также может использовать его, ничего не зная о том, как именно данный класс работает. Достаточно знать только то, что этот класс делает, а не как. Класс может состоять из комбинации переменных любых типов, а также других классов. Переменные в классе называют переменными-членами, или данными- членами. Класс Саг (автомобиль) может иметь переменные-члены, представляющие сидения, радиоприемник, шины и т.д. Переменные-члены (member variable), известные также как данные-члены (data member), являются переменными этого класса. Переменные-члены — такие же со- ставные части класса, как колеса и мотор — составные части автомобиля. Функции в классе обычно выполняют действия над переменными-членами. Они на- зываются функциями-членами (member function), или методами (method) класса. К мето-
120 Часть II. Классы дам класса Саг можно отнести start () (разгоняться) и Break () (тормозить). Класс Cat (кот) может иметь такие данные-члены, как Аде и weight (возраст и вес), а его метода- ми могут быть sleep (), Meow () и ChaseMice () (спать, мяукать и ловить мышей). Подобно переменным-членам, функции-члены являются составной частью класса. Именно они и определяют, что данный класс может делать. Объявление класса Для объявления класса используется ключевое слово class, за которым следуют открывающая фигурная скобка, список данных-членов и методов класса. Объявление завершается закрывающей фигурной скобкой и точкой с запятой. Вот как выглядит объявление класса Cat: class Cat { public: unsigned int itsAge; unsigned int itsWeight; Meow() ; }; При объявлении класса cat память не резервируется. Это объявление просто со- общает компилятору о существовании класса Cat, о том, какие данные он содержит (переменные itsAge и itsWeight), а также о том, что он умеет делать (метод MeowO). Кроме того, объявление сообщает компилятору о размере класса Cat, т.е. сколько места должен зарезервировать компилятор для каждого объекта класса Cat. Поскольку в приведенном примере для целого значения требуются четыре байта, раз- мер объекта Cat составит восемь байтов (четыре байта для переменной itsAge и че- тыре — для itsWeight). Метод MeowO не требует выделения памяти, поскольку для функций-членов (методов) объекта пространство в памяти не резервируется. Соглашения об именовании классов Имена переменным-членам, функциям-членам и классам дает программист. Как уже говорилось на занятии 3, “Переменные и константы”, всегда следует создавать понятные и осмысленные имена. Например, Cat (кот), Rectangle (прямоугольник) и Employee (служащий)— вполне подходящие имена для классов, a MeowO (мяукать), ChaseMice () (ловитьМышей) и StopEngine () (остановитьДвигатель) — прекрасные названия для методов, поскольку из названий понятно, что они делают. Многие программисты сопровождают имена своих переменных-членов префиксом its (его) (например, itsAge, itsWeight, itsSpeed — егоВозраст, егоВес, егоСкорость). Это помогает отличить переменные-члены от переменных, не являю- щихся членами класса. Язык C++ чувствителен к регистру букв, а все имена классов должны быть одного образца. Поэтому всегда нужно точно помнить, как именно пишется название каж- дого класса: Rectangle, rectangle или rectangle. Некоторые программисты пред- почитают добавлять к имени каждого класса однобуквенный префикс “с” (от слова class), например, cCat и ePerson, в то время как другие используют для написания имени только прописные или только строчные буквы. Автор предпочитает писать имена классов с прописной буквы, например, Cat или Person. Очень многие программисты пишут имена функций с прописной буквы, а имена остальных переменных — со строчной. Слова, являющиеся составными частями имен, разделяют обычно символом подчеркивания (например, Chase_Mice) или просто пи- шут каждое слово с прописной буквы (например, ChaseMice и Drawcircle).
Час 7. Простые классы 121 Важно придерживаться одного стиля во всей программе. По мере приобретения опыта и культуры программирования вырабатывается собственный стиль написания кода, включающий в себя соглашения не только по присвоению имен, но и по отсту- пам, выравниванию фигурных скобок и оформлению комментариев. Обычно солид- ные компании, занимающиеся разработкой программного продукта, имеют внутрен- ние стандарты для всех стилей оформления кода программ. Это гарантирует, что все разработчики смогут легко читать программы, созданные их коллегами. Определение объекта Объект нового типа определяется точно так же, как и любая целочисленная пере- менная: unsigned int Grossweight; // определение беззнакового целого Cat Frisky; // определение объекта Cat В этом коде определяется переменная Grossweight, которая имеет тип unsigned int, а также определяется объект Frisky класса (или типа) Cat. Классы и объекты Кот — это разновидность домашнего животного, но никто не заводит в доме раз- новидность, обычно покупают конкретного живого котенка. Существует различие ме- жду абстрактным котом как понятием и конкретным котенком Фриски, который сей- час разгуливает по гостиной автора. Точно так же в языке C++ существует различие между классом Cat, который является концепцией кота, и каждым конкретным объек- том класса Cat, который гуляет сам по себе. Таким образом, Frisky — это объект ти- па Cat, точно так же, как Grossweight является переменной типа unsigned int. Объект — это конкретный экземпляр абстрактного класса. Отдельный объект оп- ределенного класса называют экземпляром (instance) класса, а процесс его создания — созданием экземпляра (instantiation). Доступ к членам класса После определения объекта класса Cat, например, Cat Frisky,-, возникает необ- ходимость получить доступ к членам данного объекта. Для этого используется точеч- ный оператор (.), который позволяет обратиться к элементам объекта непосредствен- но. Следовательно, чтобы присвоить число 50 переменной-члену Weight объекта Frisky, можно написать: Frisky.itsWeight = 50; Аналогично для вызова метода Meow О достаточно использовать следующую запись: Frisky.Meow(); В языке C++ нельзя присвоить значение типу данных, оно присваивается только переменной. Например, нельзя написать: int =5; // неверно Компилятор пометит эту строку как ошибочную, поскольку нельзя присваивать число 5 типу int. Вместо этого нужно определить целочисленную переменную и при- своить число 5 ей. Например: int х; // определить х как переменную типа int х = 5; // присвоить переменной х значение 5
122 Часть II. Классы Таким образом, число 5 присваивается переменной х, которая имеет тип int. Точно так же недопустима следующая строка: Cat.itsAge =5; // неверно Компилятор снова пометит ее как ошибочную, поскольку сначала необходимо создать объект класса Cat, а потом его переменной itsAge присвоить значение 5, например: Cat Frisky; // то же, что и int х; Frisky.itsAge = 5; // то же, что и х = 5; Закрытые и открытые члены класса В объявлении класса используются и другие ключевые слова. Двумя самыми важ- ными из них являются public (открытый) и private (закрытый). По умолчанию все данные-члены и функции класса являются закрытыми. К за- крытым членам могут обращаться только те методы, которые принадлежат этому классу. Открытые члены доступны для всех других функций программы. Это отличие крайне важно, но не совсем понятно. Чтобы прояснить ситуацию, рассмотрим при- мер, который уже приводился в этой главе. class Cat { unsigned int itsAge; unsigned int itsWeight; Meow () ; ); Здесь объявлены закрытые переменные itsAge и itsWeight, а также закрытый метод Meow(), поскольку по умолчанию все члены класса являются закрытыми. Это значит, что если член класса не указан как открытый явно, то он считается закрытым. Например, если в функции main() написать нижеследующее, то компилятор по- метит эту строку как ошибочную: int main() { Cat Boots; Boots.itsAge =5; II Ошибка! Нельзя обращаться к закрытым данным! Фактически компилятору было сказано: “Все обращения к itsAge, itsWeight и Meow () осуществляются только функциями-членами класса Cat”. Но здесь проис- ходит обращение к переменной itsAge (члену объекта Boots) вне объекта, принад- лежащего классу Cat. Только то, что Boots является объектом класса Cat, еще не оз- начает, что ко всем его элементам можно обратиться свободно. Чтобы разрешить доступ извне к переменным-членам объектов, созданных на базе класса cat, необходимо сделать их открытыми. class Cat { public: unsigned int itsAge; unsigned int itsWeight; Meow () ; }; Теперь все члены itsAge, itsWeight и Meow() стали открытыми, а строка Boots.itsAge = 5 больше не вызывает проблем. Согласно общей стратегии использования классов, их переменные-члены следует оставлять закрытыми. Следовательно, чтобы передавать и возвращать значения закры-
Час 7. Простые классы 123 тых переменных, необходимо создать открытые функции, известные как методы дос- тупа (accessor method). Применение методов доступа позволяет скрыть от пользователя подробности хране- ния данных в объектах, предоставляя в то же время методы их использования. В результа- те можно модернизировать способы хранения и обработки данных внутри класса, не пе- реписывая при этом методы доступа и вызовы их во внешнем программном коде. В объявлении ключевое слово степени доступа (public, private и т.д.) применя- ется ко всем расположенным ниже членам до тех пор, пока не встретится следующее ключевое слово степени доступа. Это позволяет создавать в объявлении класса разде- лы для открытых, закрытых и тому подобных членов. Объявление класса заканчивает- ся закрывающей фигурной скобкой и точкой с запятой. Рассмотрим следующий пример: class Cat ( public: unsigned int Age; unsigned int Weight; void Meow(); }; Cat Frisky; Frisky.Age = 8; Frisky.Weight = 18; Frisky.Meow(); Теперь рассмотрим такой пример: class Car { public: // ниже следуют пять открытых членов void Start(); void Accelerate(); void Brake(); void SetYear(int year); int GetYear(); private: // два следующих закрыты int Year; Char Model [255]; }; // конец объявления класса Car OldFaithful; // создание экземпляра класса Car int bought; // локальная переменная типа int OldFaithful.SetYear(84); // присвоить год 84 bought = OldFaithful-GetYear(); // присвоить bought значение 84 OldFaithful.Start(); // вызов метода start Оставив данные-члены закрытыми и ограничив таким образом доступ к ним, можно усилить контроль над изменением их значений. Реализация методов класса Каждый объявленный метод класса должен быть реализован. Реализация (implementation) — это описание действий функции. Код листинга 7.1 содержит полное объявление простого класса Cat, в котором находится реализация объявленных ранее методов доступа к данным и одна обычная функция-член.
124 Часть II. Классы врочм Методы класса Определение метода класса (или функции-члена) начинается с имени класса, сопровождаемого двумя двоеточиями, имени функции и списка ее параметров. rfs rfs GJ GJ GJ GJ GJ GJ GJ GJ GJ GJ NJ NJ NJ NJ NJ NJ NJ NJ NJ NJ H4 H4 H4 H* 1 I—1 H4 H4 H4 H4 H4 kOC©'JCnLn^bJNJH‘O<0OO‘'JO>U'^bJNJF‘O<DO5<O>Ln^bJNJ»-‘O<DCO<O>Ln^bJNJH‘OU5a5<O>Un^bJNJ Листинг 7.1. Файл simpleclass. срр. Реализация методов простого класса 0: // Листинг 7.1. Пример объявления класса и 1: // реализации его методов : #include <iostream> // для std::cout : class Cat // начало объявления класса : { : public: // начало раздела public : int GetAge(); // функция доступа : void SetAge (int age); // функция доступа : void Meow(); // обычная функция : private: // начало раздела private : int itsAge; // переменные-члены : }; : // Реализация открытой функции доступа GetAgeO, : // возвращающей значение элемента itsAge : int Cat::GetAge() : { : return itsAge; : ) : // Реализация открытой функции доступа SetAge(), : // устанавливающей значение : // элемента itsAge : void Cat::SetAge(int age) : { : // присвоить переменной-члену itsAge значение, : // переданное через параметр age : itsAge = age; : ) : // Определение метода Meow() : // Возвращает: ничего (void) : // Параметры : нет : // Действия : выводит на экран "Мяу" ("Meow") : void Cat::Meow() : { : std::cout « "Meow.\n"; : } : // Создать кота, установить его возраст, мяукнуть, : // сообщить его возраст, затем мяукнуть снова. : int main() : { : Cat Frisky; : Frisky.SetAge(5); : Frisky.Meow(); : std::cout « "Frisky is a cat who is "; : std::cout « Frisky.GetAge() << " years old.\n";
Час 7. Простые классы 125 50: Frisky.Meow(); 51: return 0; 52: } Результат Meow. Frisky is a cat who is 5 years old. Meow. Анализ В строках 5—13 содержится определение класса Cat. Строка 7 содержит ключевое слово public, которое сообщает компилятору о том, что за ним следует набор открытых членов класса. В строке 8 содержится объявление открытого метода GetAge (), который возвращает значение закрытой перемен ной-члена itsAge, объявленной в строке 12. В строке 9 объявлена открытая функция доступа SetAge (), которая получает в качестве аргумента целочисленное значение и присваивает его переменной itsAge. В строке 11 начинается раздел private, который содержит в строке 12 объявление закрытой переменной-члена itsAge. Объявление класса завершается закрывающей фигурной скобкой и точкой с запятой в строке 13. Строки 17—20 содержат реализацию функции-члена GetAgef). Этот метод не по- лучает никаких аргументов и возвращает целочисленное значение. Обратите внима- ние, что при определении методов класса используется имя класса, за которым сле- дуют два двоеточия и имя функции (строка 17). Это указывает компилятору на то, что реализуемая здесь функция GetAge () объявлена в классе Cat. За исключением стро- ки заголовка, метод GetAge () создается точно так же, как и другие функции. Реализация функции GetAge () занимает только одну строку, в которой она воз- вращает значение переменной-члена itsAge. Обратите внимание: функция main() не может обратиться к этой переменной, поскольку она объявлена закрытой (только для класса Cat). Но можно обратиться к открытому методу GetAge (), который возвраща- ет значение переменной itsAge в функцию main(). В строке 25 начинается реализация функции-члена SetAgeO. Она получает цело- численный параметр и присваивает его переменной itsAge в строке 29. Являясь членом класса Cat, функция SetAge () имеет прямой доступ к переменной-члену itsAge. В строке 36 начинается реализация метода Meow () класса Cat. Этот метод также зани- мает одну строку, в которой на экран выводится слово “Meow” с последующим переходом на новую строку. Помните: для перехода на новую строку используется символ \п. В строке 44 начинается тело функции main (). В строке 45 функции main () созда- ется объект класса Cat по имени Frisky. Иными словами, функция main!) объявля- ет о создании объекта Frisky класса Cat. В строке 46 переменной-члену itsAge с помощью метода доступа SetAge () присваивается значение 5. Обратите внимание: при вызове метода указывается имя объекта (Frisky), за которым следует оператор прямого доступа (.) и имя самого метода (SetAge()). Таким же образом можно вы- зывать и другие методы класса. В строке 47 осуществляется вызов функции-члена Meow(), а в строках 48—49 на экран выводится сообщение с использованием функции доступа GetAge (). В строке 50 также расположен вызов функции Meow().
126 Часть II. Классы Конструкторы и деструкторы, или создание и удаление объектов Существуют два способа определить целочисленную переменную. Можно сначала определить переменную, а затем (несколько ниже в программе) присвоить ей значе- ние, например: int Weight; // определить переменную ... // здесь расположен другой код Weight = 7; // присвоить переменной значение Или, определив переменную, сразу же инициализировать ее, например: int Weight = 7; // определить и инициализировать значением 7 Инициализация объединяет определение переменной с присвоением ей исходного значения, что, однако, ничуть не запрещает изменить это значение впоследствии. Инициализация гарантирует, что переменная никогда не останется без значения. Как же инициализировать переменные-члены класса? Для этого в классе исполь- зуется специальная функция-член, называемая конструктором (constructor). Задача конструктора заключается в создании готового к применению экземпляра класса, что зачастую подразумевает инициализацию его данных-членов. Конструктор — это метод класса, имя которого совпадает с именем самого класса. При необходимости конст- руктор может получать параметры, но не может возвращать значения, даже типа void. Объявив конструктор, необходимо объявить и деструктор (destructor). Если конст- рукторы служат для создания и инициализации объектов класса, то деструкторы уда- ляют из памяти отработавшие объекты и освобождают выделенную для них память. Деструктору всегда присваивается имя класса с символом тильды (~) в начале. Дест- рукторы не получают аргументов и не возвращают значений. Объявление деструктора класса cat будет выглядеть следующим образом: -Cat(); Стандартные конструкторы Следующий код приводит к вызову конструктора класса Cat, которому передается один параметр (в данном случае — значение 5): Cat Frisky(5); Однако следующий код, без круглых скобок, заставит компилятор вызвать стан- дартный конструктор (default constructor): Cat Frisky; Стандартным называют конструктор без параметров. Конструктор, предоставляемый компилятором Если конструктор или деструктор не созданы явно, то компилятор создаст их сам. Созданный компилятором конструктор будет стандартным, т.е. без аргументов. Созданные компилятором конструктор и деструктор не только не имеют аргумен- тов, но и ничего не делают! Здесь очень важно учитывать следующее: стандартным является любой конструктор, который не получает никаких пара- метров. Можно определять свой собственный стандартный конструктор или использовать предоставляемый компилятором;
Час 7. Простые классы 127 если определить любой конструктор (с параметрами или без), то компилятор стандартный конструктор уже не предоставит. В этом случае, если стандартный конструктор необходим, его придется определить явно. Если не определить деструктор, то компилятор предоставит его автоматически. Этот деструктор также окажется пуст и не будет делать ничего. Чтобы придать классу законченность, при объявлении конструктора не забудьте объявить и деструктор, даже если ему нечего делать. Хотя и стандартный деструктор будет работать корректно, отнюдь не повредит объявить собственный. Это сделает программу более ясной. В листинге 7.2 класс Cat дополнен конструктором и деструктором. Конструктор используется для инициализации объекта Cat и установки его возраста равным пре- доставляемому значению. Обратите внимание на место расположения вызова деструк- тора в коде программы. Листинг 7.2. Файл constructive. срр. Применение конструкторов и деструкторов 0: // Листинг 7.2. Пример объявления стандартных конструктора 1: //и деструктора для класса Cat 2: #include <iostream> // для std::cout 3: 4: using std::cout; // здесь используется std::cout 5: 6: class Cat // начало объявления класса { 7: public: // начало раздела public 8: Cat(int initialAge); // конструктор 9: -Cat(); // деструктор 10: int GetAge(); // функция доступа 11: 12: void SetAge(int age); // функция доступа void Meow(); 13: private: // начало раздела private 14: 15: 16: 17: 18: 19: 20: 21: 22: int itsAge; // переменные-члены }; // Конструктор класса Cat Cat::Cat(int initialAge) { itsAge = initialAge; } 23: // Деструктор. Не делает ничего 24: Cat::-Cat() 25: { 26: 27: } 28: 29: // GetAge(), открытая функция доступа, 30: // возвращающая значение элемента itsAge 31: int Cat::GetAge() 32: { 33: return itsAge; 34: } 35: 36: // Реализация открытой функции доступа SetAgeО, 37: // устанавливающей значение элемента itsAge 38: void Cat::SetAge(int age) 39: {
128 Часть II. Классы 40: // присвоить переменной-члену itsAge значение. 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: // переданное через параметр аде itsAge = аде; } // Реализация метода Meow() // Возвращает: ничего (void) // Параметры : нет // Действия : выводит на экран "Мяу" ("Meow") void Cat::Meow() { cout « "Meow.Xn"; } // Создать кота, установить его возраст, мяукнуть, // сообщить его возраст, затем мяукнуть снова. int main() { Cat Frisky(5); Frisky.Meow() ,- cout « "Frisky is a cat who is cout « Frisky.GetAge() « ’ years old.Xn"; Frisky.Meow(); Frisky.SetAge(7); cout « "Now Frisky is "; cout « Frisky.GetAge() « " years old.Xn"; return 0; } Результат Meow. Frisky is a cat who is 5 years old. Meow. Now Frisky is 7 years old. Анализ Листинг 7.2 подобен листингу 7.1, за исключением строки 8, в которой добавлен конструктор, получающий в качестве аргумента целочисленное значение. В строке 9 объявлен деструктор, который не получает никаких аргументов. Деструкторы никогда не получают параметров; кроме того, ни конструкторы, ни деструкторы не возвраща- ют значений, даже типа void. В строках 18—21 содержится реализация конструктора, аналогичная реализации функции доступа SetAge (), которая также не возвращает никакого значения. В строках 24—27 находится реализация деструктора -Cat (). Сейчас эта функция не делает ничего, но если она объявлена, то в код следует включить и ее реализацию. рочм Подробнее о ключевом слове using В этом примере используется ключевое слово using. Как было описано на прошлом занятии, оно сообщает компилятору о том, что любой ис- пользованный без модификатора пространства имен объект cout при- надлежит стандартной библиотеке. Если используются другие стан- дартные функции (например, cin), то, подобно коду строки 3, их следует определить (std: :cin).
Час 7. Простые классы 129 В строке 58 создается объект Frisky класса Cat. В конструктор объекта Frisky передается значение 5. В данном случае нет необходимости вызывать функцию-член SetAgeO, поскольку объект Frisky сразу создается с использованием значения 5, присвоенного переменной-члену itsAge, как показано в строке 61. В строке 63 пере- менной itsAge объекта Frisky присваивается значение 7. Новое значение выводится на экран в строке 65. Вопросы и ответы Чем определяется размер объекта класса? Размер объекта класса в памяти определяется суммой размеров его перемен- ных-членов. Методы класса не занимают место в области памяти, выделен- ной для объекта. Некоторые компиляторы располагают переменные в памяти таким образом, что двухбайтовые переменные занимают в памяти несколько больше • места. При желании это можно уточнить в документации компилятора. Почему не следует объявлять все данные-члены открытыми? Объявление данных-членов закрытыми позволяет клиенту класса использо- вать данные, не волнуясь о том, как они хранятся или вычисляются. Например, если класс Cat имеет метод GetAge (), клиенты класса Cat могут возвратить шачение возраста кота (объекта класса Cat), не заботясь о том, в какой пере- менной-члене оно хранится или вычисляется. Следовательно, разработчик сможет в будущем изменить содержимое класса Cat без всякой необходимости корректировки всех программ, которые его используют. Коллоквиум Изучив возможности классов и объектов, имеет смысл ответить на несколько во- просов и выполнить ряд упражнений, чтобы закрепить полученные знания. Контрольные вопросы 1. Чем класс отличается от объекта? 2. В чем разница между открытыми (public) и закрытыми (private) данными- членами? 3. Что делает конструктор? 4. Что делает деструктор? Упражнения 1. Измените код файла simpleclass .срр так, чтобы он создавал второго кота (по имени spot (Пушок)). Может ли Spot мяукать (обращаться к методу Meow())? 2. Что случается, если в файле simpleclass.срр попытаться изменить значение переменной itsAge внутри функции main!)? Сработает ли такое выражение, как itsAge++ или Frisky. itsAge++? Как здесь проявилось различие между открытыми и закрытыми переменными-членами? 3. Измените код файла simpleclass .срр так, чтобы переменная itsAge стала открытой (public). Попробуйте выполнить упражнение 2 теперь. Возможность
130 Часть II. Классы изменять данные в объекте Frisky из функции main() затрудняет поиск при- чин ошибок, если они возникнут, поскольку сложно будет выяснить, где и по- чему переменная получила свое текущее значение. Ответы на контрольные вопросы 1. Класс — это описание объекта. Объект — это расположенный в памяти экземп- ляр класса. 2. К закрытым данным и методам можно обращаться только из самого класса. К открытым данным и методам можно обращаться извне. Как правило, все данные-члены класса оставляют закрытыми, а методы — открытыми (чтобы к ним можно было обращаться). 3. Конструктор создает объект на основании определения класса. Если объект достаточно прост, то можно позволить компилятору самостоятельно создать для него стандартный конструктор, но если объект относительно сложен, то в соб- ственном конструкторе можно разместить код его инициализации. 4. Деструктор удаляет из памяти данные внутренних элементов объекта, когда он оказывается уже не нужен. Можете позволить компилятору создавать стандарт- ный деструктор, если объект достаточно прост, или использовать собственный, если он сложен.
= ЧАС 8 Подробнее о классах На этом занятии вы узнаете: что такое постоянная функция-член; как отделить интерфейс класса от его реализации; как манипулировать классами и застави ь компилятор искать ошибки. Постоянные функции-члены Если объявить метод класса как const (константа), то он не сможет изменить значе- ние ни одного из членов класса. Чтобы объявлять метод класса как постоянный, помес- тите ключевое слово const после круглых скобок, но перед точкой с запятой. Напри- мер, объявление постоянной функции-члена (constant member function) SomeFunction(), не получающей никаких аргументов и ничего не возвращающей, выглядит так: void SomeFunction() const; Функции доступа (accessor function и getter), которые лишь получают значение, за- частую объявляют как постоянные (const). Класс Cat имеет две функции доступа: void SetAge(int anAge); int GetAge(); Функция SetAge () не может быть объявлена постоянной, поскольку она изменяет значение переменной-члена itsAge. А вот функция GetAge () вполне может и даже должна быть объявлена постоянной, поскольку она ничего не изменяет в классе. Функция GetAge () просто возвращает текущее значение переменной-члена itsAge. Следовательно, объявление этих функций необходимо записать так: void SetAge(int anAge); int GetAge() const; Если объявить функцию постоянной, а затем в ее реализации каким-либо образом изменить объект, поменяв значение любого из его членов, то компилятор пометит эту строку как ошибочную. Например, если реализовать функцию GetAge () таким образом, чтобы она подсчитывала количество обращений к ней, это приведет к ошибке во время компиляции. Эго связано с тем, что объект Cat изменился бы при вызове данного метода. Использовать ключевое слово const в объявлениях методов, не изменяющих объект, считается хорошим тоном в программировании. Это позволит компилятору обнаружить ошибки до того, как они станут причиной проблем. Постоянные методы используют по тем же причинам, по которым применяют константы.
132 Часть II. Классы Между_______ прочим Постоянство Используйте ключевое слово const везде, где это возможно. Если функция-член не должна изменять объект, объявляйте ее постоянной, это поможет компилятору при поиске ошибок. Интерфейс и реализация Как уже было сказано, клиент (client) — это та часть программы, которая создает и ис- пользует объекты класса. Интерфейс класса (или объявление класса) можно рассматривать как соглашение с этим клиентом, объявляющее ему о том, как класс будет себя вести. В объявлении класса Cat, например, заключено соглашение, что возраст каждого кота может быть инициализирован в его конструкторе и изменен с помощью функ- ции доступа SetAge () или возвращен с помощью метода доступа GetAge (). Обещано также, что каждый кот сможет мяукать (Meow ()). Объявив функцию GetAge () постоянной, как и положено, в соглашение вносится пункт, где оговаривается, что метод GetAge () не будет изменять объект Cat, из кото- рого он вызван. Зачем использовать компилятор для поиска ошибок? Было бы просто замечательно написать код без ошибок сразу, однако случается это крайне редко. Поэтому программисты разработали пра- вила, способные минимизировать количество ошибок и выявлять их на ранних этапах процесса разработки, а не после сдачи проекта. Ошибки компиляции (compile-time error) — это ошибки, обнаруженные во время компиляции. Они намного безопаснее, чем ошибки выполне- ния (runtime error). Это связано с тем, что ошибка компиляции будет об- наружена в любом месте кода, а ошибка выполнения — только при по- пытке выполнить участок кода, содержащий ее. Возможно, программа успешно сработает много раз, прежде чем создадутся такие условия, когда будет выполнен именно этот участок кода. Таким образом, ошиб- ка выполнения программы может скрываться долгое время, а ошибки компиляции обнаруживаются каждый раз при запуске компилятора. Та- ким образом, их проще найти и устранить. Именно гарантия отсутствия ошибок процесса выполнения в любых условиях является показателем качественного программирования. Одна из проверенных методик для достижения такого результата — использование компилятора для по- иска ошибок в процессе разработки. Безусловно, созданный код может быть абсолютно корректным, но ра- ботающим вовсе не так, как нужно Именно для этого и необходима квалифицированная группа поддержки. Где объявлять класс и располагать реализацию методов Каждая объявленная в классе функция должна иметь определение. Определение функции называется также реализацией (implementation). Подобно определению дру- гих функций, реализация метода класса имеет заголовок и тело. Определение должно находиться в файле, доступном компилятору. Для большин- ства компиляторов C++ такой файл должен иметь расширение .с или .срр. В этой книге используется расширение .срр.
Час 8. Подробнее о классах 133 Между ______. прочим Расширения файлов исходного кода Многие компиляторы рассматривают файлы с расширением . с как ис- ходный код программы на языке С, а файлы с расширением . срр — как исходный код на языке C++. Можно воспользоваться любым расширени- ем, но . срр сведет к минимуму возможные недоразумения. Файлы .срр необходимо добавить в проект. Как именно это сделать, зависит от применяемого компилятора Если используется интегрированная среда разработки, следует найти команду добавления файлов в проект (add files to project). Чтобы отком- пилировать и скомпоновать программу в исполняемый файл, в проект необходимо добавить все ее файлы . срр. Объявления классов можно поместить в один файл с программой, но это не считается хорошим тоном. В соглашении, которого придерживаются многие программисты, принято помещать объявления в файл заголовка (header file), имя которого обычно совпадает с именем файла программы; файл имеет расширение ,h, .hp или .hpp. В этой книге для имен файлов заголовков используется расширение . hpp. Например, можно поместить объявление класса Cat в файл cat.hpp, а определе- ния методов класса — в файл cat.срр. Затем файл заголовка необходимо подключить в файл кода с расширением .срр. Для этого в файле cat.срр перед началом про- граммного кода используется уже известная директива #include. flinclude "Cat.hpp" Это заставит компилятор внести содержимое файла cat .hpp в указанное место исход- ного кода так, как будто оно было там набрано. Как показывает практика, клиентов класса обычно не очень волнуют подробности его реализации. Небольшой файл заго- ловка содержит всю необходимую информацию, а файл с подробностями реализации класса можно до поры игнорировать. Между вроты Объявление класса Объявление класса сообщает компилятору, что представляет собой этот класс, какие данные он содержит и какими функциями располага- ет. Объявление класса называется его интерфейсом (interface), по- скольку оно сообщает пользователю, как взаимодействовать с классом. Интерфейс обычно хранится в файле с расширением . hpp, который на- зывается файлом заголовка. Определение функции сообщает компилятору, как она работает Такое определение называется реализацией (implementation) метода класса и хранится в файле с расширением . срр. Подробности реализации клас- са касаются только программиста, создавшего этот класс. Клиентам же класса, т.е. тем частям программы, которые его используют, вовсе не нужно знать подробностей реализации самой функции. Основным преимуществом такого подхода является возможность совме- стного использования классов. Это позволяет другим программистам создавать объекты классов разработчика, а ему — использовать классы, созданные другими (коллегами или коммерческими производителями). Встраиваемая реализация Подобно тому, как можно указать компилятору сделать обычную функцию встраи- ваемой, методы класса также можно сделать встраиваемыми. Для этого перед типом возвращаемого значения необходимо разместить ключевое слово inline. Встраивае- мая реализация функции GetWeight (), например, выглядит так.
134 Часть II. Классы inline int Cat::GetWeight() { return itsWeight; // возвратить значение переменной-члена Weight } Можно также поместить реализацию функции в объявление класса, что автомати- чески сделает функцию встраиваемой, например: class Cat { public: int GetWeight() { return itsWeight; } // встраиваемая void SetWeight(int aWeight); }; Обратите внимание на синтаксис определения функции GetWeight(). Тело встраиваемой функции начинается сразу же после объявления метода класса, причем после круглых скобок нет точки с запятой. Подобно определению обычной функции, реализация метода начинается с открывающей фигурной скобки и оканчивается за- крывающей фигурной скобкой. Как обычно, отступы значения не имеют, поэтому то же объявление можно записать несколько иначе: class Cat { public: int GetWeight() const { return itsWeight; } // встраиваемая void SetWeight(int aWeight); }; В листингах 8.1 и 8.2 вновь создается класс Cat, но теперь объявление класса содер- жится в файле cat.hpp, а реализация — в файле cat.срр. Кроме того, в листинге 8.1 методы доступа к данным класса и метод Meow() сделаны встраиваемыми. Листинг 8.1. Файл cat. hpp. Объявление класса Cat 0: #include <iostream> 1 : 2 : class Cat 3 : { 4: public: 5: Cat (int initialAge); 6 : -Cat(); 7 : int GetAge() { return itsAge; } // Встроен! 8 : void SetAge (int age) { itsAge = age; } // Встроен! 9 : void Meow() { std::cout << "Meow.Xn"; } // Встроен! 10: private: 11: int itsAge; 12 : }; Листинг 8.2. Файл cat. срр. Реализация класса Cat 0: // Листинг 8.2. Пример использования встраиваемых 1: // функций и подключения файлов заголовка. Убедитесь в 2: ((include "cat.hpp'' // подключении файла заголовка! 3 : 4: Cat::Cat(int initialAge) // конструктор
Час 8. Подробнее о классах 135 5: { 6: itsAge = initialAge; 7: 8: } 9: Cat::~Cat() // деструктор, не делает ничего 10: { 11: } 12: 13: // Создать кота, установить его возраст, мяукнуть. 14: // сообщить его возраст, затем мяукнуть снова. 15: int main() 16: { 17: Cat Frisky(5); 18: Frisky.Meow(); 19: std::cout << "Frisky is a cat who is 20: std::cout << Frisky.GetAge() << " years old.Xn"; 21: Frisky.Meow(); 22: Frisky.SetAge(7); 23: std::cout << "Now Frisky is 24: std::cout << Frisky.GetAge() << " years old.Xn”; 25: return 0; 26: } Результат Meow. Frisky is a cat who is 5 years old. Meow. Now Frisky is 7 years old. Анализ В строке 7 файла Cat. hpp объявлена функция GetAge (), тут же расположена ее встраиваемая реализация. Строки 8 и 9 содержат еще две встраиваемые функции, реа- лизации которых не изменились “с прошлого раза”. В строке 2 листинга 8.2 находится директива #include "Cat.hpp", подключаю- щая к коду программы содержимое файла cat.hpp. Библиотека iostream, необходи- мая для объекта cout, подключается в строке 0. Классы, содержащие другие классы как данные-члены При создании сложных классов нередко приходится объявлять ряд простых клас- сов, которые впоследствии включаются в состав других, более сложных Например, можно объявить класс колесо, класс мотор и класс коробка передач, а затем объеди- нить их в класс автомобиль. Таким образом, создается взаимосвязь классов имеет (has-a): автомобиль имеет мотор, колеса и коробку передач. Рассмотрим второй пример. Прямоугольник состоит из линий. Линия определяется двумя точками. Каждая точка определяется координатами X и у. В листинге 8.3 показа- но объявление класса Rectangle, содержащегося в файле rectangle.hpp. Поскольку прямоугольник определяется четырьмя линиями, соединяющими четыре точки, а каждая точка определяется координатами на плоскости, сначала будет объявлен класс Point для хранения координат х и у каждой точки. Листинг 8.4 содержит реализацию обоих классов.
136 Часть II. Классы Листинг 8.3. Файл rect .hpp. Объявления классов (заголовок) 0: // Листинг 8.3. Начало rect.hpp 1: ((include <iostream> 2 : 3: class Point // содержит координаты x и у 4: { 5: // конструктора нет, использовать стандартный 6 : public: 7 : void SetXdnt x) { itsX = x; } 8 : void SetY(int y) { itsY = y; } 9 : int GetX() const { return itsX;} 10: int GetY() const { return itsY;} 11 : private: 12 : int itsX; 13 : int itsY; 14 : }; // конец объявления класса Point 15: 16: 17: class Rectangle 18: { 19: public: 20 : Rectangle (int top, int left. int bottom, int right); 21 : -Rectangle () () 22 : 23 : int GetTop() const { return itsTop; } 24 : int GetLeft() const { return itsLeft; } 25: int GetBottomj ) const { return itsBottom; } 26: int GetRight() const { return itsRight; } 27 : 28: Point GetUpperLeft() const { return itsUpperLeft; } 29: Point GetLowerLeft() const { return itsLowerLeft; } 30: Point GetUpperRight() const { return itsUpperRight; } 31: Point GetLowerRight() const { return itsLowerRight; } 32: 33: void SetUpperLeft(Point Location); 34: void SetLowerLeft(Point Location); 35: void SetUpperRight(Point Location); 36: void SetLowerRight(Point Location); 37: 38: void SetTop(int top); 39: void SetLeftlint left); 40: void SetBottom(int bottom); 41: void SetRightlint right) ,- 42: 43: int GetAreaO const; 44 : 45: private: 46 : Point itsUpperLeft; 47 : Point itsUpperRight 48: Point itsLowerLeft; 49: Point itsLowerRight 50: int itsTop; 51: int itsLeft; 52: int itsBottom; 53: int itsRight; 54 : }; 55: // конец Rect.hpp
Час 8. Подробнее о классах 137 Листинг 8.4. Файл rect. срр. Объявления классов 0: // Листинг 8.4. Начало rect.срр 1: #include “rect.hpp" 2: 3: Rectangle::Rectangle(int top, int left, int bottom, • int right) 4: { 5: itsTop = top; 6: itSLeft = left; 7: itsBottom = bottom; 8: itsRight = right; 9: 10: itsUpperLeft.SetX(left); 11: itsUpperLeft.SetY(top); 12: 13: itsUpperRight.SetX(right); 14: itsUpperRight.SetY(top); 15: 16: itsLowerLeft.SetX(left); 17: itsLowerLeft.SetY(bottom); 18: 19: itsLowerRight.SetX(right); 20: itsLowerRight.SetY(bottom); 21: } 22: 23: void Rectangle::SetUpperLeft(Point Location) 24: { 25: itsUpperLeft = Location; 26: itsUpperRight.SetY(Location.GetY()); 27: itsLowerLeft.SetX(Location.GetX()); 28: itsTop = Location.GetY(); 29: itsLeft = Location.GetX(); 30: } 31: 32: void Rectangle::SetLowerLeft(Point Location) 33: { 34: itsLowerLeft = Location; 35: itsLowerRight.SetY(Location.GetY()); 36: itsUpperLeft.SetX(Location.GetX()); 37: itsBottom = Location.GetY(); 38: itsLeft = Location.GetX(); 39: } 40: 41: void Rectangle::SetLowerRight(Point Location) 42: { 43: itsLowerRight = Location; 44 : itsLowerLeft.SetY(Location.GetY()); 45: itsUpperRight.SetX(Location.GetX()); 46: itsBottom = Location.GetY(); 47: itsRight = Location.GetX(); 48: } 49: 50: void Rectangle::SetUpperRight(Point Location) 51: { 52: itsUpperRight = Location; 53 : itsUpperLeft.SetY(Location.GetY()); 54 : itsLowerRight.SetX(Location.GetX()) ; 55: itsTop = Location.GetY();
138 Часть II. Классы 56: itsRight = Location.GetX(); 57 : 58 : 59 : 60: 61 : 62: 63: 64: 65: 66: 67 : 68: 69: 70: 71: 72: 73 : 74 : 75: 76 : 77 : 78: 79: 80 : 81 : 82 : 83: 84: 85: 86: 87: 88 : 89: 90: 91: 92: 93 : 94: 95: 96: 97 : 98: 99: 100: 101: 102 : 103 : 104: 105: 106: 107: } void Rectangle::SetTop(int top) { itsTop = top; itsUpperLeft.SetY(top); itsUpperRight.SetY(top); } void Rectangle::SetLeft(int left) { itsLeft = left; itsUpperLeft.SetX(left); itsLowerLeft.SetX(left); ) void Rectangle::SetBottom(int bottom) { itsBottom = bottom; itsLowerLeft.SetY(bottom); itsLowerRight.SetY(bottom); } void Rectangle::SetRight(int right) { itsRight = right; itsUpperRight.SetX(right); itsLowerRight.SetX(right); } int Rectangle::GetArea() const ( int Width = itsRight-itsLeft; int Height = itsTop - itsBottom; return (Width * Height); } // Найти по точкам ширину и высоту, а затем, // перемножив их, вычислить площадь прямоугольника int main() ( // Инициализировать локальную переменную Rectangle Rectangle MyRectangle (100, 20, 50, 80); int Area = MyRectangle.GetArea(); std:;cout << "Area: " << Area << “\n"; std::cout << "Upper Left X Coordinate: "; std::cout << MyRectangle.GetUpperLeft().GetX(); return 0; } Результат Area: 3000 Upper Left X Coordinate: 20
Час 8. Подробнее о классах 139 Анализ В строках 3—14 листинга 8.3 объявлен класс Point (точка), который используется для хранения координат х и Y на плоскости. В этой программе класс Point почти не используется. Но в других графических методах он пригодится. Внутри класса Point (строки 12 и 13) объявлены две переменные-члена (itsx и itsY). Эти переменные содержат значения координат точки. При увеличении координаты х точка на плоскости перемещается вправо. При увеличении координаты Y точка переме- щается вверх. В других случаях могут использоваться иные системы координат. Некото- рые оконные программы увеличивают значение координаты Y при перемещении вниз. Класс Point использует для чтения и записи координат точек X и Y встраиваемые функции доступа, объявленные в строках 7—10. Объекты класса Point используют стандартные конструктор и деструктор, предоставляемые компилятором. Следователь- но, координаты точек придется задавать явно. В строке 17 начинается объявление класса Rectangle, который содержит четыре точки, представляющие углы прямоугольника. Конструктор класса Rectangle (строка 20) получает четыре целочисленных пара- метра: top (верхний), left (левый), bottom (нижний) и right (правый). Их значения присваиваются четырем переменным-членам, после чего устанавливаются значения четырех точек (4 объекта класса Point). Помимо обычных функций доступа к данным-членам класса, в классе Rectangle предусмотрена функция GetArea (), объявленная в строке 43. Вместо хранения зна- чения площади в виде переменной эта функция вычисляет ее в строках 89—91 лис- тинга 8.4. Для этого она сначала вычисляет значение длины и ширины прямоуголь- ника, а затем перемножает полученные результаты. Чтобы получить координаты верхнего левого угла прямоугольника, необходимо вер- нуть значение х точки UpperLef t. Поскольку функция GetUpperLef t () является мето- дом класса Rectangle, она может получить доступ ко всем закрытым данным этого класса непосредственно, включая и доступ к переменной itsUpperLef t. Поскольку пе- ременная itsUpperLeft является объектом класса Point, а переменная itsx этого объекта закрыта, функция GetUpperLeft () не может обратиться к ней непосредствен- но. Чтобы получить значение переменной itsx, она вынуждена использовать открытую функцию доступа Getx (). В строке 96 листинга 8.4 начинается тело основной части программы. Вплоть до строки 99 память не выделяется, и ничего, по сути, не происходит. Все сделанное до сих пор служило одной цели — сообщить компилятору, как создаются точка и прямоуголь- ник (на случай, если в этом появится необходимость). В строке 99 при передаче реальных значений параметров тор, Left, Bottom и Right создается прямоугольник (объект MyRectangle класса Rectangle). В строке 101 создается локальная переменная Area типа int. Она предназначена для хранения площади созданного прямоугольника. Переменная Area инициализиру- ется значением, возвращаемым функцией-членом GetArea () класса Rectangle. Кли- ент класса Rectangle может создать его объекты и возвратить значение их площади, не заботясь о нюансах выполнения функции GetArea (). Просмотрев файл заголовка rect.hpp, содержащий объявление класса Rectangle, можно узнать о том, что функция GetArea () возвращает значение типа int. Тех, кто использует класс Rectangle, не волнует, как именно действует функция GetAreaO, а разработчик класса может изменить ее реализацию, не опасаясь, что это повлияет на программы, использующие класс Rectangle.
140 Часть II. Классы Вопросы и ответы Если объявление функции постоянной приводит к ошибке компиляции при измене- нии ею объекта, то почему бы не отказаться от использования ключевого слова const, избежав сообщений об ошибках? Если функция-член логически не должна изменять класс, то использование клю- чевого слова const — прекрасный способ привлечь компилятор для поиска случай- ных ошибок. Например, у функции GetAge () нет видимых причин для изменения класса Cat, но в реализации класса может присутствовать следующая строка: if (itsAge = 100) cout << "Неу! You're 100 years old\n"; Объявление функции GetAge () постоянной позволило бы компилятору обна- ружить ошибку. Ведь имелось в виду сравнение значения переменной itsAge с числом 100, а вместо этого случайно произошло присвоение числа 100 пере- менной itsAge. Поскольку такое присвоение изменяет класс, а с помошью ключевого слова const было заявлено, что этот метод не будет его изменять, то компилятор смог обнаружить ошибку. Ошибки такого рода очень трудно найти даже при просмотре текста программы. Обычно ведь видят то, что хотя увидеть. Гораздо опаснее, если на первый взгляд покажется, что программа работает правильно (даже после установки такого странного значения), но рано или поздно эта ошибка превратится в проблему. Есть ли смысл использовать структуры в программах на языке C++? Многие программисты приберегают слово struct для классов, которые не имеют функций. Можно расценивать это как ностальгию по устаревшим струк- турам языка С, которые не могли иметь функций. Автор считает это сомни- тельным и даже плохим тоном в программировании. Хотя сегодня структуре методы не нужны, не исключено, что они могут понадобиться завтра. И тогда ее либо придется заменить классом, либо, нарушив свое же правило, работать со структурой, которая “не брезгует” присутствием в ней методов Коллоквиум Изучив классы подробнее, имеет смысл ответить на несколько вопросов и выпол- нить ряд упражнений, чтобы закрепить полученные знания. Контрольные вопросы 1. Почему определения классов помещают в другой файл? 2. Что делает конструктор в файле cat. срр? 3. Что случится, если класс Point не будет определен ни в файле rect.срр, ни в файле rect. hpp? 4. Как компилятор узнает, где искать подключаемые файлы? Упражнения 1. Вынесите класс Point из файла rect.hpp в новый файл rect.hpp и подклю- чите его. Изменило ли это процесс компиляции файла rect.срр? Изменился ли результат выполнения программы?
Час 8. Подробнее о классах 141 2. Измените файл cat.hpp так, чтобы метод GetAge() стал постоянным Нужно ли изменить что-нибудь еще? 3. Измените файл cat. срр так, чтобы он создавал второго кота (по имени Spot). Может ли Spot стать старше и способен ли он мяукать? Ответы на контрольные вопросы 1. Чтобы разработчики могли их использовать совместно (т.е. заимствовать объек- ты друг у друга). 2. При создании экземпляра класса он инициализирует переменную itsAge пере- данным значением, что позволяет сэкономить на вызове функции SetAgeO, присваивающей значение возраста. 3. Если класс Point не был определен где-нибудь еще, то компилятор выдаст со- общение об ошибке, свидетельствующее о наличии неопределенной ссылки (undefined reference). Если он определен где-нибудь еще, следует подключить дополнительный файл (например, point.hpp). Классы довольно часто исполь- зуют другие классы. 4. Когда имя подключаемого файла расположено в двойных кавычках ("), компиля- тор ищет его в том же каталоге, где расположен текущий файл исходного кода. Но можно задать компилятору поиск и в других каталогах. Если путь указать в двойных кавычках, компилятор будет искать только в указанном каталоге. Если имя подключаемого файла заключено в угловые скобки (<>), поиск будет осуще- ствляться только в системном каталоге для подключаемых файлов.

ЧАСТЬ III Управление памятью В этой части... Час 9. Указатели Час 10. Подробнее об указателях Час 11. Ссылки Час 12. Подробнее о ссылках и указателях
ЧАС 9 Указатели На этом занятии вы узнаете: что такое указатели; как объявлять и использовать указатели; что такое распределяемая память и как с ней работать. Указатели и их назначение Одной из наиболее мощных возможностей языка C++ является непосредственный доступ к памяти при помощи указателей. Этим язык C++ превосходит некоторые другие языки. Следовательно, очень важно понять, что представляют собой указатели в действительности. Здесь указатели рассматриваются поэтапно. Подробное описание поможет понять преимущества их использования, а последующие занятия докажут их абсолютную не- обходимость. Указатель (pointer) — это переменная, содержащая адрес области в памяти компь- ютера. Если удастся осознать смысл этого выражения, то это все, что необходимо знать об указателях. Целочисленная переменная хранит число, символьная перемен- ная содержит символ, а указатель — это переменная, которая содержит адрес области в оперативной памяти. Х орошо Но что же такое адрес области в памяти (memory address)9 Чтобы лучше разобраться в этом вопросе, давайте рассмотрим устройство компьютерной памяти. Оперативная память компьютера — это хранилище значений переменных. Она разделена на последовательно пронумерованные ячейки. Каждая переменная разме- щается в одной или нескольких последовательно расположенных отдельных ячейках памяти, адрес первой из них называется адресом переменной в памяти. На рис 9.1 схематически представлено расположение переменной theAge типа unsigned long integer в оперативной памяти компьютера. Различные виды компьютеров используют различные схемы памяти. Обычно про- граммисту не нужно знать реальный адрес каждой переменной, поскольку компиля- тор способен сам позаботиться о таких подробностях. Но если необходимость в этой информации все же возникнет, то получить ее можно с помощью оператора обраще- ния к адресу (&), применение которого проиллюстрировано в листинге 9.1.
Час 9. Указатели 145 Оперативная Переменная theAge память л 10110101 01110110 11110110 11101110 100 101 102 103 104 105 106 107 108 Адреса ZX Каждая ячейка в оперативной памяти Адрес переменной занимает один байт или 8 бит theAge Переменная theAge типа unsigned long занимает 4 байта или 32 бита Адресом переменной является адрес ее первого байта. Таким образом, адресом переменной theAge будет 102. Рис. 9.1. Схематическое представление переменной theAge Листинг 9.1. Файл addressdemo. срр. Оператор обращения к адресу 0: // Листинг 9.1. Применение оператора обращения к 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: // адресу и адреса локальной ttinclude <iostream> int main() { unsigned short shortVar=5; unsigned long longVar=65535 long sVar = -65535; std::cout << "shortVar:\t" переменной << shortVar; 11: 12: std::cout std::cout "\tAddress of ' longVar : \ t ' shortVar:\t" << longVar, << ishortVar « "\n"; 13: 14: std::cout std::cout << "XtAddress of "sVar:\t\t" longVar:\t” << sVar; << slongVar « "\n"; 15: 16: 17: 18: } std::cout return 0; "XtAddress of sVar:Xt" << bsVar << "\n"; Результат shortVar: longvar: sVar: 5 65535 -65535 Address of shortVar: 1245066 Address of longVar: 1245060 Address of sVar: 1245056 Полученный результат может выглядеть иначе, поскольку каждый компьютер со- храняет переменные по разным адресам, в зависимости от того, что еще находится в памяти и сколько ее доступно. Это может выглядеть и следующим образом:
146 Часть III. Управление памятью shortVar: 5 Address of shortVar: 0x8fc9:fff4 longVar: 65535 Address of longVar: 0x8fc9:fff2 sVar: -65535 Address of sVar: 0x8fc9:ffee Между_______ прочим Специальные символы языка С++ \t применяется в коде листинга 9.1 для вставки символа табуляции. Это довольно простой, но далеко не совершенный способ создания столбцов. \п перемещает курсор на следующую строку. То же делает функция std::endl. \\ отображает символ косой черты (внутри строки). \" отображает символ двойных кавычек (внутри строки). \' отображает символ одиночной кавычки (внутри строки или как от- дельный символ). В данном случае символ обратной косой черты представляет собой управляющий символ (escape character), поскольку он указывает на то, что следующий за ним символ является не обычным, а специальным (например, \п — это символ новой строки, а не буква п). Анализ В начале программы объявляются и инициализируются три переменные: в строке 6 — переменная типа unsigned short, в строке 8 — типа unsigned long. Затем в строках 10—15 на экран выводятся их значения и адреса, полученные с помощью оператора обращения к адресу (&)- Компилятор Borland расположил равное 5 значение переменной shortVar на ком- пьютере автора (Intel х86) по адресу 1245066. Этот адрес специфичен для каждого ком- пьютера и может незначительно меняться при каждом запуске программы. Поэтому на другом компьютере результат, вероятнее всего, будет иным. Но что не изменяется, так это разница в четыре байта между первыми двумя адресами, если в используемом ком- пьютере короткие целые числа занимают по четыре байта. Разница между вторым и третьим адресами также составит четыре байта, если в используемом компьютере длин- ные целые числа являются четырехбайтовыми. Размещение переменных этой програм- мы в памяти иллюстрирует рис. 9.2. (Обратите внимание: в зависимости от настройки компилятора на некоторых компьютерах переменные могут иметь иной размер.) shortVar longVar sVar “I I I I I I I I I I I I I I I I I Г 0000 0000 65.535 -65.535 0000 0101 -A.-------- k—V—^0000 0000 1111 1111 1000 00001111 1111 5 0000 00001111 1111 0000 0000 1111 1111 fff4 fff3 fff2 fff1 fffO ffef ffee ffed ffec ffeb ffea ffe9 ffe8 ffe7 ffe6 ffe5 ffe4 ffe3 Puc. 9.2. Схема расположения переменных в памяти Фактически нет никаких причин, по которым имело бы смысл узнать реальное чи- словое значение адреса переменной. Главное, что каждая переменная имеет адрес и занимает в памяти соответствующий объем.
Час 9. Указатели 147 Но откуда же компилятор узнает, в каком именно объеме памяти нуждается ка- ждая переменная? Об этом разработчик сообщает ему, указывая тип переменной при объявлении. Следовательно, когда разработчик объявляет переменную типа unsigned long, он сообщает компилятору о необходимости зарезервировать для нее в памяти четыре байта, поскольку любое беззнаковое длинное целое число занимает четыре байта. Заботу о том, по какому именно адресу будет размещена переменная, берет на себя компилятор'. Когда создается указатель, компилятор выделяет для него в памяти объем, доста- точный для хранения адреса, размер которого соответствует используемым аппарат- ным средствам и операционной системе. Размер указателя может и не совпадать с размером целого числа, кроме того, поскольку размер указателя зависит от конкрет- ной системы, бессмысленно делать предположения о его размере. Сохранение адреса в указателе Каждая переменная имеет адрес. Даже не зная сам адрес (номер ячейки) значения, его можно сохранить в другой переменной. Такая переменная, хранящая адрес дру- гого объекта, называется указателем (pointer). Предположим, существует целочисленная переменная howold. Для объявления указателя на нее по имени рАде применяется следующая форма записи: int *pAge = NULL; Этой строкой переменная рАде объявляется указателем на тип int. В результате бу- дет получена переменная рАде, предназначенная для хранения адреса значения типа int. Следует заметить, что рАде — это обычная переменная. При объявлении переменной целочисленного типа (например, int) указывается, что она будет хранить целое число. Когда переменная объявляется указателем на какой-либо тип, это значит, что она будет хранить адрес переменной определенного типа (в данном случае — типа int). При объявлении указателя задают тип переменной, на которую он должен указы- вать. Это позволяет уведомить компилятор о том, как именно воспринимать область памяти, на которую он указывает. Сам указатель содержит лишь адрес. В данном примере переменная рАде инициализируется нулевым значением (null). Указатели, значения которых равны null, называют пустыми (null pointer). После объявления указателю обязательно нужно присвоить какое-либо значение. Если предназначенный для хранения в указателе адрес заранее не известен, ему следует присвоить значение null. Неинициализированные указатели называются дикими (wild pointer). Они очень опасны. Инициализировать указатель нулевым значением можно и так: int *pAge = 0; Результат будет тем же, что и при инициализации значением null, однако чисто технически 0 — это целочисленная константа, a null — это адрес константы 0. Между l|)04NM Инициализируйте указатели! Не шутите с этим: ВСЕГДА инициализируйте указатели! Если указатель инициализируется значением 0 или null, то впоследствии ему не- обходимо присвоить адрес соответствующей переменной. Например, указателю рАде можно присвоить адрес переменной howold следующим образом: int howOld =50; // объявить int * рАде =0; // объявить рАде = bhowOld; // присвоить переменную указатель адрес переменной howOld указателю рАде 1 Скорее операционная система. — Прим. ред.
148 Часть III. Управление памятью В первой строке объявлена переменная howOld типа int, которая сразу инициализи- руется значением 50. Во второй строке объявлен указатель рАде на переменную типа int, который инициализируется значением 0. Символ “звездочка” (*), расположенный межау объявляемым типом и именем переменной, свидетельствует о том, что это указатель. В последней строке указателю рАде присваивается адрес переменной howOld. На это указывает оператор взятия адреса (&) перед именем переменной howOld. Если бы этого оператора не было, присваивался бы не адрес, а значение переменной, которое также может быть корректным адресом. Между протии Указатели и неуказатели Присвоение указателю простой переменной является довольно распространенной ошибкой. Но компилятор обычно предупреждает об этом. В данном случае значением указателя рАде будет адрес переменной howOld, зна- чение которой равно 50. Две последние строки можно объединить в одну. unsigned short int howOld =50; // объявить переменную unsigned short int * pAge = bhowOld; // объявить указатель на нее Теперь указатель рАде содержит адрес переменной howOld. С помощью указате- ля можно получить и значение переменной, на которую он указывает (в случае с указателем рАде — это значение 50). Доступ к значению переменной по указате- лю на нее называется косвенным, поскольку осуществляется не совсем по правилам. Более подробно косвенное обращение к значениям переменных мы рассмотрим да- лее в этой главе. Косвенное обращение (indirection) подразумевает получение доступа к значению пе- ременной по адресу, содержащемуся в указателе. Имена указателей Поскольку указатели — это не более, чем обычные переменные, им можно при- сваивать любые имена, допустимые для других переменных. Но большинство про- граммистов, следуя соглашению об именовании, пишут имена всех указателей с ма- ленькой буквы р (от pointer — указатель), например, рАде и pNumber. Оператор косвенного доступа Оператор косвенного доступа (*) называется также оператором взятия значения, разыменования или ссылкой (dereference). При извлечении значения из указателя бу- дет возвращено то значение, которое хранится по адресу, содержащемуся в указателе. Обычные переменные обладают прямым доступом к собственным значениям. Что- бы создать новую переменную yourAge типа unsigned short int и присвоить ей значение другой переменной, howOld, можно применить следующий код. unsigned short int howOld = 50; unsigned short int yourAge; yourAge = howOld; Указатель обеспечивает лишь косвенный доступ к значению переменной, адрес ко- торой он хранит. Чтобы присвоить переменной yourAge значение переменной howOld с помощью указателя рАде, применяется следующая запись: unsigned short int howOld =50; II объявить переменную unsigned short int * pAge = bhowOld; // присвоить адрес переменной
Час 9. Указатели 149 // howOld указателю pAge unsigned short int yourAge; // объявить еше одну // переменную yourAge = *pAge; // присвоить значение переменной, на которую II указывает pAge (50) переменной yourAge Оператор косвенного доступа (*) перед переменной pAge означает, что “само зна- чение находится по адресу...”. Иными словами, “возьмите значение, хранимое по ад- ресу, который находится в переменной pAge, и присвойте его переменной yourAge”. own Операции косвенного доступа Оператор косвенного доступа (*) используется двумя способами: при объявлении и при косвенном доступе. При объявлении он свидетельст- вует о том, что это указатель, а не обычная переменная. Например. unsigned short * pAge = NULL; // объявить указатель При косвенном доступе звездочка означает, что речь идет о значе- нии в памяти, находящемся по адресу в данной переменной, а не о самом адресе. *рАде = 5; II разместить значение 5 по адресу, находящемуся в pAge Заметьте, что тот же символ (*) используется как оператор умножения. Что именно имел в виду программист, поставив звездочку, компилятор определяет, исходя из контекста. В повседневной жизни косвенное обращение встречается регулярно. Если необхо- димо заказать пиццу или обед на дом, но номер телефона службы доставки неизвес- тен, его можно найти в телефонной книге (или в интернете). Этот источник инфор- мации не содержит пиццерию, а только ее “адрес” (номер телефона). Если позвонить по нему, произойдет косвенное обращение! Указатели, адреса и переменные Очень важно понимать разницу между указателем, хранимым в нем адресом и зна- чением, расположенным по адресу, который содержится в этом указателе. Обычно понимание этого и составляет основную сложность при изучении указателей. Рассмотрим следующий фрагмент кода: int theVariable = 5; int * pPointer = SctheVariable; В первой строке объявляется целочисленная переменная theVariable, которой при- сваивается значение 5. В следующей строке объявлен указатель pPointer на тип int, которому присвоен адрес переменной theVariable. Переменная pPointer является указателем и содержит адрес переменной theVariable. Значение, хранящееся по адре- су, записанному в указателе pPointer, равно 5. Переменная theVariable и содержа- щий ее адрес указатель pPointer предстанлены на рис. 9.3. Здесь значение 5 (двоичное представление 0000 0000 0000 0101) хранится в ячейке по адресу 101 (двоичное представление 0000 0000 0000 0000 0000 0000 ОНО 0101). В двоичном представлении десятичному числу 5 типа int соот- ветствуют два байта, или 16 битов (по восемь битов в байте). Далее следует двоич- ное представление числа 101, которое является адресом переменной theVariable, содержащей значение 5.
150 Часть 111. Управление памятью Оперативная память f 00000000 00000101 00000000 00000000 00000000 01100101 100 101 102 103 Адреса Z Л Переменная theVariable оперативной | | памяти Адрес переменной theVariable 104 105 106 107 108 Указатель pPointer Рис. 9.3. Схема размещения в памяти переменной и указателя на нее Манипулирование данными при помощи указателей После того как указателю присвоен адрес какой-либо переменной, его можно ис- пользовать для работы со значением этой переменной. В листинге 9.2 приведен при- мер обращения к значению локальной переменной через указатель на нее. Листинг 9.2. Файл pointeruse. срр. Манипулирование данными при помощи указателей 0: // Листинг 9.2. Использование указателей 1: 2 : ♦include <iostream> using std::cout; // здесь используется std::cout 3 : 4: 5 : 6: 7 : 8: 9 : 10: int main() I int myAge; // переменная int * pAge = NULL; // указатель myAge = 5; pAge = &myAge; // присвоить адрес myAge указателю pAge 11: 12: 13 : 14: 15 : cout << "myAge: " << myAge << "\n"; cout << "‘pAge: " << ‘pAge << "\n\n"; cout << "‘pAge = 7\n“; ‘pAge =7; // присвоить myAge значение 7 16: 17: 18: 19 : 20: 21: 22: 23 : 24: 25: cout << "‘pAge: " << ‘pAge << "\n"; cout << "myAge: " << myAge << "\n\n"; cout « "myAge = 9\n"; myAge = 9; cout << "myAge: " « myAge << "\n“; cout « "‘pAge: " << ‘pAge « "\n"; return 0; ) Результат myAge: 5 *pAge: 5
Час 9. Указатели 151 ‘pAge = 7 ‘pAge: 7 myAge: 7 myAge =9 myAge: 9 ‘pAge: 9 Анализ В программе объявлены две переменные: myAge типа int и рАде, являющаяся указателем на этот тип. В строке 9 переменной рАде присваивается значение 5, а в строке 11 это значение выводится на экран. Затем в строке 10 указателю рАде присваивается адрес переменной myAge. В стро- ке 12 при помощи оператора * извлекается значение, адрес которого хранит указатель рАде, и выводится на экран, демонстрируя, что по этому адресу расположено значе- ние переменной myAge, равное 5. В строке 15 по адресу в указателе рАде размещается число 7, в результате чего оно оказывается присвоенным переменной myAge. Убедиться в этом можно после вывода этих значений на экран в строках 16—17. В строке 20 переменной myAge присваивается значение 9. Затем происходит обра- щение к ее значению непосредственно через переменную (в строке 21) и косвенно, через указатель на нее (строка 22). Адреса Указатели позволяют манипулировать адресами, не принимая во внимание их ре- альные значения Ранее уже говорилось, что когда адрес переменной присваивается указателю, то содержать он будет реальный адрес именно этой переменной. Лис- тинг 9.3 позволит проверить это на практике. Листинг 9.3. Файл pointerstore. срр. Что хранится в указателях? 0: // Листинг 9.3. Что же хранится в указателях? 1: #include <iostream> 2: 3: 4: int main() { 5: unsigned short int myAge = 5, yourAge = 10; 6: unsigned short int * pAge = &myAge; // указатель 7: 8: std::cout « “myAge:\t" « myAge; 9: std::cout << "\t\tyourAge:\t“ « yourAge << “\n"; 10: std::cout << "&myAge:\t“ << &myAge; 11: std::cout << "\t&yourAge:\t“ << SyourAge << “\n"; 12: 13: std::cout << "pAge:\t" << pAge << "\n”; 14: std::cout << "*pAge:\t" << *pAge << “\n\n"; 15: 16: pAge = &yourAge; // переприсвоить указатель 17: 18: std::cout << "myAge:\t" << myAge; 19: std::cout << "\t\tyourAge:\t" << yourAge << "\n"; 20: std: .-cout << “&myAge:\t" « &myAge; 21: std::cout << "\t&yourAge:\t" « SyourAge << "\n“; 22:
152 Часть III. Управление памятью 23 : Std: : cout << "pAge:\t“ < < pAge << "\n" ; 24 : std: : cout << “*pAge:\t" << ‘pAge < :< "\n\n" 25 : 26: std: : cout << "&pAge:\t" << &pAge < :< "\n“; 27: return 0; 28: } Результат myAge: 5 yourAge: 10 &myAge: 1245066 &yourAge: 1245064 pAge: 1245066 ‘pAge: 5 myAge: 5 yourAge: 10 &myAge: 1245066 kyourAge: 1245064 pAge: 1245064 ‘pAge: 10 &pAge: 1245060 Здесь полученный результат также может выглядеть иначе, поскольку каждый компьютер сохраняет переменные по разным адресам, в зависимости от того, что еще находится в памяти и сколько ее доступно. Это может выглядеть так: myAge: ЬтуАде: рАде: *рАде: 5 0х355С 0х355С 5 yourAge: kyourAge: 10 0x355E туАде: ЬтуАде: рАде: *рАде: 5 0х355С 0х355Е 10 yourAge: &yourAge: 10 0x355E ЬрАде: 0х355А Анализ В строке 5 объявлены две переменные, myAge и yourAge, типа unsigned short. Далее в строке объявлен указатель на этот тип (рАде), который инициализируется ад- ресом переменной myAge. В строках 14—18 значения и адреса переменных pAge и туАде выводятся на экран. В строке 13 на экран выводится содержимое переменной pAge, которое является ис- тинным адресом переменной myAge. В строке 14 на экран выводится результат ссылки (взятия значения) на адрес, содержащийся в указателе pAge, который представляет собой значение переменной myAge, а именно — число 5. В этом и заключается сущность указателей. Строка 13 демонстрирует, что указа- тель pAge хранит адрес переменной myAge, а в строке 14 продемонстрировано, как получить значение, хранимое в переменной myAge, сославшись на указатель, в кото- ром содержится адрес этой переменной. Прежде чем продолжить изучение книги, удостоверьтесь, что четко понимаете, что такое указатель и для чего он нужен. В строке 16 указателю pAge присваивается адрес переменной yourAge, а затем их зна- чения и адреса выводятся на экран. Результат выполнения программы свидетельствует, что теперь указатель pAge содержит адрес переменной yourAge, и ссылка на него возвращает, соответственно, значение переменной yourAge.
Час 9. Указатели 153 Строка 26 выводит на экран адрес самого указателя рАде. Как и любая другая пере- менная, указатель имеет адрес, значение которого может храниться в другом указателе. О хранении в указателе адреса другого указателя речь пойдет несколько позже. Рекомендуется Не рекомендуется Использовать оператор косвенного доступа (*), чтобы получить доступ к данным, хранящимся по адресу, который содержится в указателе. Инициализировать указатели всегда. При отсутствии необходимого адреса инициализируйте указатель значением null или 0. Помнить о различии между адресом в указателе и значением, доступным по этому адресу. Сохранять адрес в переменной, не являющейся указателем (например, в целочисленной переменной). Для чего нужны указатели? В предыдущих разделах подробно рассматривалась процедура присвоения указате- лю адреса другой переменной. Но на практике такое использование указателей встре- чается достаточно редко. К тому же зачем задействовать еще и указатель, если значе- ние уже хранится в переменной? Теперь, разобравшись в синтаксисе применения указателей, можно приступить к их реальному применению. Чаще всего указатели применяются в таких случаях: манипулирование данными в динамически распределяемой памяти; доступ к переменным-членам и функциям класса; передача данных между функциями по ссыпке. Остальная часть этой главы посвящена динамическому управлению данными и опе- рациям с переменными и функциями-членами классов в динамически распределяемой памяти. Передача переменных по ссылке рассматривается на занятии 11, “Ссылки”. Стек и динамически распределяемая память Программы взаимодействуют с пятью следующими областями памяти: с областью глобальных переменных; с динамически распределяемой памятью; с регистрами; с сегментами программного кода; со стеком. Локальные переменные и параметры функций размещаются в стеке. Объектный код программ размещается в сегментах, а глобальные переменные — в области гло- бальных переменных. Регистры используются для внутренних целей процессора, на- пример, для контроля над вершиной стека и указателем команд. Остальная часть па- мяти составляет так называемую динамически распределяемую память (heap).
154 Часть III. Управление памятью Особенностью локальных переменных является то, что после выхода из функции, в которой они были объявлены, память, выделенная для их хранения, освобождается, а значения переменных уничтожаются. Глобальные переменные позволяют частично решить эту проблему ценой неограниченного доступа к ним из любой точки про- граммы, что значительно усложняет восприятие текста программы Использование динамической памяти полностью решает обе проблемы. Динамически распределяемую память можно представить как огромный массив последовательно пронумерованных ячеек, предназначенных для хранения данных. В от- личие от стека, ячейкам свободной памяти нельзя присвоить имя. Но можно, зарезер- вировав определенное количество ячеек, запомнить адрес первой из них в указателе. Чтобы лучше понять изложенное выше, рассмотрим пример. Допустим, существует номер телефона службы заказов товара по почте. Этот номер можно поместить в память телефона, а листок бумаги, на котором он был записан, выбросить. Нажимая на соот- ветствующую кнопку телефона, можно соединяться со службой заказов, не имея поня- тия ни о номере, ни об адресе этой службы, поскольку для доступа достаточно помнить, какую именно кнопку следует нажать. Служба заказов в этом случае представляет собой данные в динамической памяти. Нс обязательно знать, где именно находятся данные, достаточно знать, как их найти. Для обращения к данным используется их адрес, рань которого играет номер телефона службы доставки. Причем помнить адрес (или номер) необязательно — достаточно лишь записать его значение в указатель (или телефон). Указатель позволяет обращаться к данным, забыв о подробностях. Когда функция возвращает значение, стек очищается автоматически. Когда об- ласть видимости локальных переменных заканчивается, они также удаляются из стека Но динамическая память не очищается до завершения самой программы. Поэтому от- ветственность за освобождение всей памяти, зарезервированной под все использован- ные данные, ложится на программиста. Важным преимуществом динамической памяти является то, что выделенная в ней область не может использоваться в других целях до тех пор, пока не будет освобожде- на явно. Поэтому, если во время работы функции в динамической памяти выделяется область, ее можно использовать даже по завершении работы функции. Недостатком динамической памяти является то, что выделенные в ней участки ос- таются зарезервированными до тех пор, пока они не будут освобождены явно. Если этого не сделать, области памяти так и останутся занятыми, что через какое-то время способно исчерпать ресурсы системы. Еще одним преимуществом динамического выделения памяти по сравнению с глобальными переменными является то, что доступ к данным можно получить толь- ко из тех функций, которые обладают доступом к указателю, хранящему нужный ад- рес. Это позволяет жестко контролировать манипулирование данными, а также избе- гать нежелательного или случайного их изменения. Для этого необходимо создать указатель на распределяемую область динамической памяти и передать его соответствующей функции. В следующих разделах описано, как это сделать. Ключевое слово new Для выделения участка в динамически распределяемой области памяти использу- ется ключевое слово new, после которого указывают тип размещаемого в памяти объ- екта. Так, компилятор определяет размер необходимой для размещения объекта в об- ласти памяти. Следовательно, выражение new unsigned short int выделит два байта динамической памяти, а выражение new long — четыре. В качестве результата оператор new возвращает адрес выделенного фрагмента памяти, который должен быть присвоен указателю. Например, чтобы создать в ди-
Час 9. Указатели 155 намически распределяемой памяти переменную типа unsigned short, необходимо написать следующее: unsigned short int * pPointer; pPointer = new unsigned short int; Безусловно, инициализировать указатель можно сразу в момент его создания, unsigned short int * pPointer = new unsigned short int; В любом случае указатель pPointer указывает теперь на переменную типа unsigned short int, размещенную в динамически распределяемой памяти. Его можно использовать как любой другой указатель на переменную и передавать с его помощью значения в эту область памяти, например: ‘pPointer = 72; Эту строку можно прочитать так: “Присвоить число 72 значению указателя pPointer” или “Разместить число 72 в той области динамически распределяемой па- мяти, на которую указывает pPointer”. Если оператор new не сможет выделить место в динамически распределяемой па- мяти (в конце концов, память — ограниченный ресурс), то он передаст исключение. ИО0_ ifvm Передача исключения Исключения — это объекты, позволяющие отреагировать на ошибки. Более подробная информация о них приводится на занятии 24, “Исключения, об- работка ошибок и другое”. Некоторые устаревшие компиляторы возвращают нулевой указатель. Если исполь- зуется именно такой компилятор, то после попытки зарезервировать память операто- ром new имеет смысл проверять, не является ли полученный указатель нулевым. Все современные компиляторы способны передавать исключения. Ключевое слово delete По завершении работы с выделенной областью памяти ее следует освободить. Для этого применяется оператор delete, после которого следует имя указателя. Оператор delete освобождает область памяти, на которую указывает указатель. Помните, что сам указатель, в отличие от области памяти, на которую он указыва- ет, является локальной переменной. Поэтому, когда объявившая указатель функция завершает работу, он выходит из области видимости, а содержащийся в нем адрес те- ряется. Поскольку распределенная с помощью оператора new память автоматически не освобождается, в случае потери адреса ее не удастся ни удалить, ни использовать. Такой участок памяти становится абсолютно недоступным, а подобная ситуация на- зывается утечкой памяти (memory leak). Для освобождения выделенной памяти используется ключевое слово delete: delete pPointer; При удалении указателя с помощью оператора delete происходит реальное осво- бождение участка памяти, адрес которого содержится в указателе. Иными словами, отдается команда: “Вернуть в динамически распределяемую память участок, на кото- рый указывает этот указатель” Но сам указатель остается (ведь это обычная перемен- ная), и ему может быть передан на хранение другой адрес. Листинг 9.4 демонстрирует размещение переменной в динамической памяти, ее использование и удаление.
156 Часть III. Управление памятью Sjiaut______т осторожны! Удаление указателей Когда оператор delete применяется к указателю, освобождается об- ласть динамической памяти, на которую он указывает. Повторное приме- нение оператора delete к этому же указателю приведет к зависанию программы. При освобождении области динамической памяти рекомен- дуется присваивать связанному с ней указателю нулевое значение (о). Вызов оператора delete для нулевого указателя пройдет совершенно безболезненно для программы. Animal *pDog = new Animal; delete pDog; // освободить память pDog =0; // присвоить указателю нулевое значение II... delete pDog; // вполне безопасно Не расстраивайтесь, если приведенный выше код кажется малопонят- ным Размещение объектов в распределяемой памяти рассматривается на следующем занятии. Этот подход применим и для таких простых типов данных, как int, например: int *pNumber = new int; delete pNumber; // освободить память pNumber =0; // присвоить указателю нулевое значение //. . . delete pNumber; // вполне безопасно Листинг 9.4. Файл pointerheap. срр. Создание, использование и удаление указателей 0: // Листинг 9.4. 1: // Создание, использование и удаление указателей 2: Hinclude <iostream> 3 : 4: int main() 5: ( 6: int localvariable = 5; 7: int * pLocal= &localVariable; 8: int * pHeap = new int; 9: if (pHeap == NULL) 10: ( 11: std::cout << "Error! No memory for pHeap!’"; 12: return 1; 13: } 14: ‘pHeap = 7; 15: std::cout << "localvariable: " << localvariable << "\n"; 16: Std::cout << "‘pLocal: " << ‘pLocal << "\n”; 17: std::cout << "‘pHeap: “ << *pHeap << "\n"; 18: delete pHeap; 19: pHeap = new int; 20: if (pHeap == NULL) 21: { 22: std::cout << “Error! No memory for pHeap!!"; 23: return 1; 24: } 25: ‘pHeap = 9; 26: std::cout << "‘pHeap: " << ‘pHeap << ”\n”; 27: delete pHeap; 28: return 0; 29: }
Час 9. Указатели 157 Результат localvariable: 5 ‘pLocal: 5 ‘рНеар: 7 ‘рНеар: 9 Анализ В строке 6 объявлена и инициализирована локальная переменная localvariable. Затем объявлен указатель, которому присваивается адрес этой переменной (строка 7). В строке 8 объявлен другой указатель (рНеар), который инициализируется результа- том операции new int. Таким образом, в динамически распределяемой памяти выде- ляется пространство для переменной типа int. В строке 14 этому участку памяти присваивается значение 7. Строка I5 выводит на экран значение локальной перемен- ной, а строка 16 — значение, на которое указывает pLocal. Как и ожидалось, они идентичны. Строка 17 выводит на экран значение, на которое указывает рНеар. Это доказывает, что значение, присвоенное в строке I4, вполне доступно. В строке 18 оператор delete освобождает участок динамически распределяемой памяти, занятый в строке 8. Это не только освобождает память, но и ликвидирует связь указателя с этим участком. Теперь указатель рНеар пуст и пригоден для записи адреса другого участка памяти, что осуществляется в строках 19—25. Строка 26 демон- стрирует результат. Строка 27 вновь освобождает этот участок памяти. Несмотря на то что строка 27 является избыточной (завершение программы само по себе освободило бы эту область памяти), правила хорошего тона требуют освобож- дать все распределенные участки памяти явно. Если в программу будут внесены изме- нения, или она окажется модифицирована, или этот участок кода будет использован в другом месте, то забытые указатели станут причиной серьезных проблем. Утечка памяти Другим наилучшим способом нажить себе неприятностей является переприсвоение указателя без предварительного освобождения участка памяти, на который он указы- вает. Это приводит к утечке памяти. Рассмотрим следующий фрагмент кода: 1: unsigned short int * pPointer = new unsigned short int; 2: *pPointer = 72; 3: pPointer = new unsigned short int; 4: *pPointer = 84; В строке 1 объявляется указатель pPointer и выделяется память для хранения пе- ременной типа unsigned short int. В следующей строке в выделенную область за- писывается значение 72. Затем в строке 3 указателю присваивается адрес другой об- ласти памяти, в которую записывается число 84 (строка 4). Теперь исходный участок памяти, содержащий значение 72, оказывается недоступен, поскольку указателю на эту область было присвоено новое значение. В результате невозможно ни использо- вать, ни освободить зарезервированную память до завершения программы. Правильно этот фрагмент выглядел бы так: 1: unsigned short int * pPointer = new unsigned short int; 2: ‘pPointer = 72; 3: delete pPointer; 4: pPointer = new unsigned short int; 5: ‘pPointer = 84; Теперь выделенная под переменную память освобождается корректно (строка 3).
158 Часть III. Управление памятью Зкатм, Пи Как избежать утечек памяти Каждый раз, когда в программе используется оператор new, за ним должен следовать оператор delete. Очень важно отслеживать указате- ли, ссылающиеся на выделенные области динамической памяти, и вовремя освобождать ее. Лашг рмш Функции malloc О и free () В некоторых старых программах можно встретить функции mallocO и free (). Эти аналогичные операторам new и delete функции очень долго использовались в языке С для распределения и освобождения распределяемой памяти. Принципиальным отличием языка С от C++ является то, что функции malloc () следует точно указать количество подлежащей распределению памяти. Более подробная информация по этой теме должна быть приведена в документации, прилагаемой к компилятору, но в этой книге подразумевается использование новых возможностей языка C++. Вопросы и ответы Почему указатели так важны? Как было продемонстрировано на сегодняшнем занятии, указатели важны по- тому, что они способны содержать адрес расположенного в распределяемой па- мяти объекта, а также передавать аргументы по ссылке. На занятии I3, “Дополнительные возможности функций”, будет также проде- монстрировано применение указателей при полиморфизме классов. Зачем вообще размещать что-либо в распределяемой памяти? Расположенные в распределяемой памяти объекты сохраняются после выхода из функции. Кроме того, возможность хранить объекты в распределяемой па- мяти позволяет принять решение о количестве используемых объектов во время выполнения программы, а не заранее при разработке. Более подробная инфор- мация по этой теме приведена на занятии 10, “Подробнее об указателях”. Коллоквиум Изучив возможности указателей, имеет смысл ответить на несколько вопросов и выполнить ряд упражнений, чтобы закрепить полученные знания. Контрольные вопросы 1. В чем разница между инициализацией указателя значением 0 и null? 2. Какой объем памяти занимает указатель на целое число, а какой — на число с пла- вающей запятой? 3. Что такое утечка памяти? 4. Как освободить память, распределенную при помощи оператора new?
Час 9. Указатели 159 Упражнения 1. Измените файл pointeruse.срр (листинг 9.2) так, чтобы инициализировать указатель рАде значением 0, а не null. Изменился ли результат выполнения программы? 2. Измените файл pointerstore.срр (листинг 9.3) так, чтобы перемножить yourAge и *рАде и сохранить результат в новой переменной. Отобразите со- держимое этой переменной на экране. Удостоверьтесь на практике в том, что компилятор выясняет, где оператор * применяется для умножения, а где — для ссылки по указателю рАде. 3. Измените файл pointerstore.срр (листинг 9.3) так, чтобы ссылка на указа- тель *рАде использовалась в нем для изменения содержимого переменной myAge или yourAge. Изменится ли адрес, хранимый в указателе рАде? Ответы на контрольные вопросы 1. Оба значения инициализируют указатель нулевым адресом. Различие в том, что NULL — это нулевой указатель, а 0 — число, подобное типу int. Но это не бо- лее, чем вопрос стиля. 2. В большинстве систем объем обоих типов одинаков. Но о размере указателя программист может не заботиться, он будет достаточным для хранения адреса, используемого на каждой конкретной машине! 3. Утечка памяти происходит тогда, когда не освобождается пространство, заре- зервированное в распределяемой памяти. Если программа продолжает резерви- ровать новые участки, не освобождая уже не нужные, то объем доступной па- мяти может быть исчерпан. 4. При помощи ключевого слова delete. Зарезервированную память имеет смысл освобождать сразу, как только она будет уже не нужна.
ЧАС 10 Подробнее об указателях На этом занятии вы узнаете: как создавать объекты в распределяемой памяти; как эффективно использовать указатели; как при помощи указателей предотвратить проблемы памяти Создание объектов в динамической памяти Одним из наиболее мощных средств, доступных программисту на языке C++, яв- ляется возможность непосредственно манипулировать памятью компьютера при по- мощи указателей. Аналогично созданию указателя на переменную целочисленного типа, в динамиче- ской памяти можно размещать объекты любых классов. Например, если объявить объект класса Cat, то для манипулирования им можно создать указатель, хранящий адрес области памяти, которую занимает его объект. Эта ситуация абсолютно анало- гична размещению переменных в стеке. Синтаксис операции такой же, как и для це- лочисленных типов. Cat *pCat = new Cat; B данном случае в операторе new использован стандартный конструктор класса, т.е. конструктор, применяющийся без параметров. Следует напомнить, что при созда- нии объекта класса конструктор вызывается всегда, независимо от того, размещается объект в стеке или в области динамической памяти. Удаление объектов При использовании оператора delete для указателя на объект перед освобождени- ем занимаемой им памяти происходит вызов деструктора соответствующего класса. Такой подход позволяет объекту класса корректно завершить свое существование, по- добно тому, как он это делает, находясь в стеке. Листинг ЮЛ иллюстрирует создание и удаление объектов в динамически распределяемой памяти.
Час 10. Подробнее об указателях 161 Листинг 10.1. Файл heapcreate. срр. Размещение и удаление объектов в динамической памяти 0: // Листинг 10.1. Размещение и удаление объектов 1: // в динамической памяти 2: #include <iostream> 3: 4: class SimpleCat 5: { 6: public: 7: SimpleCat(); 8: -SimpleCat(); 9: private: 10: int itsAge; 11: 1; 12: 13: SimpleCat: :SimpleCat () 14: { 15: std::cout << "Constructor called." << std::endl; 16: itsAge = 1; 17: } 18: 19: SimpleCat::-SimpleCat() 20: { 21: std::cout << “Destructor called." << std::endl; 22: } 23: 24: int main() 25: { 26: std::cout << "SimpleCat Frisky..." << std::endl; 27: SimpleCat Frisky; 28: 29: std::cout << "SimpleCat *pRags = new SimpleCat...” << std::endl; 30: SimpleCat * pRags = new SimpleCat; 31: 32: std::cout << "delete pRags..." << std::endl; 33: delete pRags; 34: 35: std::cout << "Exiting, watch Frisky go..." << std::endl; 36: return 0; 37: } Результат SimpleCat Frisky. . . Constructor called. SimpleCat * pRags = new SimpleCat... Constructor called. delete pRags... Destructor called. Exiting, watch Frisky go... Destructor called. Анализ В строках 4—11 объявлен упрощенный класс SimpleCat. В строке 27 при помощи стандартного конструктора объект Frisky класса SimpleCat создается в стеке. В строке 30 экземпляр класса SimpleCat создается в динамической памяти. Его адрес
162 Часть III. Управление памятью сохраняется в указателе pRags. В данном случае также используется стандартный кон- структор. В строке 33 указатель pRags освобождается с помощью оператора delete, при этом происходит вызов деструктора класса. Когда объект Frisky выходит из об- ласти видимости, автоматически происходит вызов его деструктора. Доступ к переменным-членам при помощи указателя Для доступа к переменным-членам и функциям объекта Cat, созданного обычным способом, используется точечный оператор (.). Для доступа к переменным-членам и функциям объекта cat, созданного в динамической памяти, необходимо использо- вать ссылку на указатель и точечный оператор. Следовательно, для доступа к функ- ции-члену GetAge () необходим следующий код: (*pRags).GetAge(); Круглые скобки используются для того, чтобы сначала обратиться по адресу в ука- зателе к объекту, а только затем — к его функции GetAge (). Такой синтаксис слишком громоздок, поэтому язык C++ укомплектован специ- альным сокращенным оператором косвенного доступа: “указатель на” (->), который состоит из символов минус (-) и больше (>). Компилятор C++ воспринимает его как единый оператор. В листинге 10.2 приведен пример обращения к переменным-членам и функциям объектов, размещенных в динамической памяти. Листинг 10.2. Файл heapaccess. срр. Обращение к данным-членам объектов, размещенных в динамической памяти 0: // Листинг 10.2. Обращение к данным-членам 1: // объектов, размещенных В динамической памяти 2: 3 : 4 : 5 : 6 : 7 : #include <iostream> using std::endl; class SimpleCat { public: SimpleCat() { itsAge 2; } 8: 9: -SimpleCat () {} int GetAge() const { return itsAge; } 10: void SetAge(int age) { itsAge = age; } 11: private: 12: int itsAge; 13 : } ; 14 : 15: int main() 16: { 17 : SimpleCat * Frisky = new SimpleCat; 18: std::cout << “Frisky is “ « Frisky->GetAge() 19: << “ years old" << endl ; 20: 21: Frisky->SetAge(5) ; 22 : std::cout << "Frisky is " << Frisky->GetAge() 23 : << " years old" << endl; 24: 25: delete Frisky; 26: return 0; 27: }
Час 10. Подробнее об указателях 163 Результат Frisky is 2 years old Frisky is 5 years old Анализ Код строки 17 создает объект класса SimpleCat в динамической памяти. Стан- дартный конструктор устанавливает его возраст равным 2, а в строке 18 осуществляет- ся вызов метода GetAge (). Поскольку речь идет об указателе, для обращения к его данным-членам и функциям используется оператор косвенного доступа (->). В строке 21 метод SetAge () устанавливает новое значение возраста, а метод GetAge () в строке 22 еще раз выводит его на экран. Данные-члены в динамической памяти Указателем на объект, размещенный в динамической памяти, может быть одна или несколько переменных-членов класса. Память может выделяться в конструкторе клас- са или в одном из его методов, а освобождаться — в его деструкторе. Листинг 10.3 де- монстрирует, как это происходит. Листинг 10.3. Файл datamemberptr. срр. Указатели как члены класса 0: // Листинг 10.3. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: // Указатели как члены класса ♦include <iostream> class SimpleCat { public: SimpleCat(); -SimpleCat(); int GetAge() const { return *itsAge; ) void SetAge(int age) { ‘itsAge = age; } int GetWeight!) const { return ‘itsWeight; } void setweight (int weight) { ‘itsWeight = weight; } private: int * itsAge; int * itsWeight; ); SimpleCat::SimpleCat() { itsAge = new int(2); itsWeight = new int(5); ) SimpleCat::-SimpleCat() { delete itsAge; delete itsWeight; ) int main() {
164 Часть III. Управление памятью 34: SimpleCat ‘Frisky = new SimpleCat; 35: 36: 37: std::cout << "Frisky is " << Frisky->GetAge() << " years old\n"; 38: 39: 40: 41: 42 : 43 : 44 : ) Frisky->SetAge(5); std::cout << "Frisky is " << Frisky->GetAge() << " years old\n"; delete Frisky; return 0; Результат Frisky is 2 years old Frisky is 5 years old Анализ В составе объявленного класса SimpleCat находятся две переменные-члена, яв- ляющиеся указателями на тип int. Конструктор (строки 20—24) создает в динамиче- ской памяти обе переменные и инициализирует их начальными значениями. Деструктор (строки 26—30) освобождает выделенную память. Поскольку это дест- руктор, нет смысла назначать указателям нулевые значения, т.к. теперь они станут недоступны. Это именно тот случай, когда из правила, запрещающего оставлять неоп- ределенные указатели, существует безопасное исключение. Вызывающей функции (в данном случае — main ()) абсолютно безразлично, что переменные itsAge и itsWeight являются указателями на области в динамической па- мяти. Функция main () просто вызывает методы GetAge () и SetAge (), а подробности управления памятью инкапсулированы в реализации класса, как и должно быть. Для уничтожения объекта Frisky (строка 42) применяется деструктор. Деструктор удаляет каждый указатель, принадлежащий объекту. Если этот указатель указывает на объект другого класса, то будет вызван также и их деструктор. Это наглядно показывает, почему имеет смысл создавать свой собственный дест- руктор, а не использовать стандартный, предоставляемый компилятором. В предос- тавляемом стандартном деструкторе операторы delete строк 28 и 29 отсутствуют. По- этому, когда код доходит до строки 42 и удаляет объект, эти значения останутся в памяти, что приведет к ее утечке. Указатель this Каждый метод класса имеет скрытый параметр — указатель this. Этот указатель содержит адрес текущего объекта. Следовательно, при каждом обращении к функциям GetAge () и SetAge () указатель объекта this используется как скрытый параметр. Указатель this указывает на тот объект, метод которого вызван. Нужда в нем возни- кает не часто, как правило, хватает вызова методов и установки значений переменных- членов. Но иногда нужно получить доступ к самому объекту (возможно, понадобится вернуть указатель на текущий объект). Именно здесь и пригодится указатель this. Обычно указатель this не применяется для доступа к переменным-членам объекта из методов самого объекта. Но при желании для явного обращения можно применить и указатель this. В листинге 10.4 это сделано в демонстрационных целях.
Час 10. Подробнее об указателях 165 Листинг 10.4. Файл usingthis. срр. Применение указателя this 0: // Листинг 10.4. 1: // Применение указателя this 2: #include <iostream> 3: using namespace std; 4: class Rectangle 5: { 6: public: 7: Rectangle(); 8: -Rectangle(); 9: void SetLength(int length) { this->itsLength = length; } 10: int GetLength() const { return this->itsLength; } 11: void SetWidth(int width) { itsWidth = width; } 12: 13: int GetWidth() const { return itsWidth; } 14: private: 15: int itsLength; 16: int itsWidth; 17: 18: J; 19: Rectangle::Rectangle() 20: { 21: itsWidth = 5; 22: itsLength = 10; 23: 24: } 25: Rectangle::-Rectangle() 26: 27: {} 28: int main!) 29: { 30: Rectangle theRect; 31: cout << "theRect is " << theRect.GetLength() 32: << " feet long." << endl; 33: cout << "theRect is " << theRect.GetWidth() 34: 35: << " feet wide." << endl; 36: theRect.SetLength(20); 37: theRect.Setwidth(10); 38: cout << "theRect is " << theRect.GetLength() 39: << " feet long." << endl; 40: cout << "theRect is " << theRect.GetWidth() 41: 42: << ” feet wide." << endl; 43: return 0; 44: ) Результат theRect is 10 feet long theRect is 5 feet 1 wide theRect is 20 feet long theRect is 10 feet wide
166 Часть III. Управление памятью Анализ Функции доступа SetLength () и GetLength () используют указатель this для доступа к переменным-членам объекта Rectangle, а методы доступа Setwidth () и Getwidth () организованы традиционным способом. Как можно заметить, они ве- дут себя одинаково, хотя синтаксис последних проще. Лишяь 1Ы? Для чего нужен указатель this Если бы возможности указателя this этим исчерпывались, то о нем не стоило бы и говорить. Но указатель this содержит адрес текущего объекта, и в этой роли может оказаться достаточно мощным инструментом. Пример практического применения указателя this рассматривается на занятии 14, “Перегрузка операторов”. Речь идет о перегрузке функций. Сейчас же необходимо просто усвоить, что каждый объект обладает скрытым указателем this, содержащим адрес самого объекта. О создании и удалении указателя this беспокоиться не нужно, об этом позаботится компилятор. Паразитные, дикие и зависшие указатели Источником ошибок, которые сложнее всего обнаружить, являются паразитные ука- затели. Паразитный указатель (stray pointer) образуется в тех случаях, когда после опера- тора delete, освободившего участок памяти, не следует переприсвоение указателя. Вернемся к примеру со службой доставки. Предположим, служба переехала в дру- гое место, а заказчик продолжает нажимать ту же кнопку телефона, требуя доставить товар на дом. Возможно, ничего страшного и не случится, если на другом конце про- вода телефон будет звонить на пустом складе, но представьте, что будет, если там разместился склад боеприпасов. Не исключено, что вместо пиццы на дом доставят противотанковую мину! Одним словом, будьте осторожны при использовании указателей, для которых был вызван оператор delete. Указатель по-прежнему содержит адрес области памяти, но по этому адресу уже могут находиться другие данные. В этом случае обращение по указанному адресу может привести к аварийному завершению программы. Или, что еще хуже, программа может продолжить “нормально” работать, а фатальные послед- ствия наступят несколько позже и в совершенно другом месте. Такая ситуация назы- вается “миной замедленного действия” и является достаточно серьезной проблемой. Поэтому во избежание неприятностей после освобождения указателя присваивайте ему значение 0. Это обезопасит указатель. йежад______ В/ЮЧЯИ Паразитный указатель Паразитный указатель называют еще диким (wild), зависшим (dangling) и потерянным. Постоянные указатели Ключевое слово const при объявлении указателей можно использовать как перед указанием типа, после него и в обоих местах сразу. Например, все приведенные ниже объявления вполне допустимы: const int * pOne; int * const pTwo; const int * const pThree; однако означают они совершенно разное.
Час 10. Подробнее об указателях 167 Указатель рОпе объявлен как указатель на константу типа int. Следовательно, значение, на которое он указывает, не может быть изменено. Это можно записать так: *р0пе = 5 Если попытаться изменить значение, компилятор пометит такой объект как оши- бочный. Указатель pTwo является постоянным указателем на тип int. Само целое число может быть изменено, но адрес в указателе pTwo — нет. Постоянный указатель не- возможно переназначить. Это можно записать так: pTwo = &х И, наконец, pThree объявлен как постоянный указатель на константу типа int. Это значит, что он всегда указывает на одну и ту же область памяти, и значение, на- ходящееся по этому адресу, также не может изменяться. Вся тонкость заключается во взаиморасположении ключевого слова const и того, что объявляется константой. Если справа от ключевого слова const находится тип, то константой будет объявляемое значение. Если справа от ключевого слова const нахо- дится имя, то константой будет сам указатель. const int * pl; // указатель на константу типа int int * const р2; // р2 - константа и не может указывать ни на что иное Константы как указатели и как функции-члены На занятии 7, “Простые классы”, использование ключевого слова const при объявле- нии функций-членов уже рассматривалось. Если объявить метод класса как const (константа), он не сможет изменить значение ни одного из членов класса, а при по- пытке сделать это компилятор выдаст сообщение об ошибке. Если объявлен указатель на постоянный объект, то те методы, которые можно вы- звать с его помощью, также должны быть постоянными. Пример такой ситуации при- веден в листинге 10, “Подробнее об указателях”. Листинг 10.5. Файл constptr.срр. Использование указателя на константный объект 0: // Листинг 10.5. 1: // Указатель на константный объект 2: #include <iostream> 3: 4: class Rectangle 5: { 6: public: 7 : Rectangle(); 8: -Rectangle (); 9: void SetLength(int length) { itsLength = length; } 10: int GetLength() const { return itsLength; } 11: 12: void SetWidth(int width) { itsWrdth = width; } 13: int GetWidth() const { return itsWidth; } 14: 15: private: 16: int itsLength; 17: int itsWidth; 18: }; 19: 20: Rectangle::Rectangle(): 21: itsWidth(5), 22: itsLength(10)
168 Часть III. Управление памятью 23 : {} 24 : 25 : Rectangle::-Rectangle() 26 : О 27: 28: int main() 29 : ( 30: Rectangle* pRect = new Rectangle; 31: const Rectangle * pConstRect = new Rectangle; 32: Rectangle * const pConstPtr = new Rectangle; 33 : 34: std::cout << "pRect width: " 35: << pRect->GetWidth() << " feet” << std::endl; 36: std::cout << "pConstRect width: " 37: << pConstRect->GetWidth() « " feet" << std::endl; 38: std::cout << "pConstPtr width: " 39: << pConstPtr->GetWidth() << " feet" << std::endl; 40: 41: pRect->SetWidth(10); 42 : // pConstRect->SetWidth(10); 43 : pConstPtr->SetWidth(10); 44 : 45: std::cout << "pRect width: " 46: << pRect->GetWidth() << " feet" << std::endl; 47 : std::cout << "pConstRect width: “ 48: << pConstRect->GetWidth() << " feet” << std::endl; 49 : std::cout << "pConstPtr width: " 50: << pConstPtr->GetWidth() << " feet" << std::endl; 51: return 0; 52 : 1 Результат pRect width: pConstRect width: pConstPtr width: pRect width: pConstRect width: pConstPtr width: 5 feet 5 feet 5 feet 10 feet 5 feet 10 feet Анализ Строки 4—18 содержат объявление класса Rectangle, а строка 13 — объявление постоянного метода-члена Getwidth(). В строке 30 объявлен указатель pRect на эк- земпляр класса Rectangle, в строке 31 — указатель pConstRect на постоянный объ- ект этого же класса, а в строке 32 — постоянный указатель pConstPtr на экземпляр класса Rectangle. В строках 34—39 значения переменных класса выводятся на экран. В строке 41 указатель pRect используется для присвоения ширине прямо- угольника значения 10. В строке 42 предпринимается попытка использовать ука- затель pConstRect, но он был объявлен как указатель на константу, в связи с чем наличие функций-членов, не являющихся постоянными, здесь недопустимо; поэтому данную строку пришлось закомментировать. Указатель pConstPtr объявлен как по- стоянный на объект, т.е. он не может указывать ни на что, кроме объекта класса Rectangle, который постоянным не является.
Час 10. Подробнее об указателях 169 Постоянный указатель this При объявлении постоянного объекта указатель this автоматически оказывается указателем на постоянный объект, а, следовательно, может использоваться только с постоянными функциями-членами. Подробно постоянные объекты и указатели рассматриваются на занятии 11, “Ссылки”. Вопросы и ответы Зачем объявлять объект константным, ведь это ограничивает его возможности? Каждый программист желает привлечь компилятор для поиска ошибок. Одной из серьезнейших ошибок, крайне трудных для обнаружения, является функция, изменяющая объект нетрадиционными способами, которые неочевидны ни для вызывающей функции, ни для самого программиста. Объявление объекта по- стоянным предотвращает такие действия. Чем удобно размещение объектов в динамической памяти? Объекты, находящиеся в динамической памяти, не уничтожаются после выхода их из той области, в которой они были объявлены. Кроме того, появляется возможность уже в процессе выполнения программы решать, какое количество объектов требуется объявить. Более подробно эта тема обсуждается на занятии 19, “Связанные списки”. Коллоквиум Изучив указатели подробнее, имеет смысл ответить на несколько вопросов и вы- полнить ряд упражнений, чтобы закрепить полученные знания. Контрольные вопросы 1. Какие ключевые слова используются для резервирования и освобождения памяти? 2. Когда удаляется объект (т.е. происходит вызов его деструктора), если оператор delete не применен явно? 3. Каковы два способа доступа к переменной-члену с использованием указателя? Который из них лучше? 4. Что такое паразитный указатель? Упражнения 1. Раскомментируйте строку 42 в коде листинга 10.5 (файл constptr.cpp). Что произошло? О чем сообщил компилятор? 2. Обратите внимание, что в коде одних листингов используется управляющий сим- вол \п, а в коде других — функция std: :endl. Одни программисты используют такую форму записи, как std: :cout и std: :endl, а другие используют оператор using и такую форму записи, как cout и endl. Попробуйте в коде некоторых программ изменить применяемый подход. Что лучше: \п или endl? Какой под- ход проще: использование префикса std:: или оператора using? 3. Измените файл heapaccess.срр (листинг 10.2) так, чтобы в нем не использо- вался оператор ->. Какой код является более читабельным: первоначальный или модифицированный?
170 Часть 111. Управление памятью Ответы на контрольные вопросы 1. Ключевое слово new используется для резервирования, а ключевое слово delete — для освобождения памяти. 2. Когда объект покидает область видимости, он удаляется автоматически. Если объект создан в функции main () и не удален явно, его деструктор будет вызван при завершении функции main (). Этот случай демонстрирует результат работы программы из листинга 10.1. 3. Один способ — (*pRags) .GetAgeO, второй — pRags->GetAge(). Способ с ис- пользованием оператора -> лучше, поскольку он очевиднее. 4. Паразитный указатель проявляется при попытке использовать указатель, для которого был выполнен оператор delete. В данном случае неизвестно, что именно находится по этому адресу.
ЧАС 11 Ссылки На этом занятии вы узнаете: что такое ссылки; чем ссылки отличаются от указателей; как создавать и использовать ссылки; каковы ограничения ссылок; как передавать по ссылке значения и объекты из функций и в функции. Что такое ссылка? На двух предыдущих занятиях рассматривалось применение указателей для мани- пулирования объектами в динамической памяти, а также способы косвенного доступа к ним. Рассматриваемые сегодня ссылки обладают почти теми же возможностями, что и указатели, но синтаксис их несколько проще. Ссылка (reference) — это псевдоним, который при создании инициализируется именем другого объекта, адресата (target). С этого момента ссылка выступает в роли альтернативного имени адресата, а, следовательно, все, что делается со ссылкой, про- исходит и с объектом В некоторых первоисточниках можно прочитать, что ссылки являются указателя- ми, но это не так. Хотя внутренне ссылки зачастую реализуют при помощи указателей (это уже забота создателя компилятора), программисту достаточно знать, что это два разных понятия. Указатели — это переменные, которые содержат адрес другого объекта, а ссылки — это псевдонимы объектов. Создание ссылок При объявлении ссылки вначале указывают тип объекта адресата, за которым следуют оператор ссылки (&) и имя ссылки. Для ссылок можно использовать любое допустимое имя переменной, но многие программисты используют перед именами ссылок префикс “г”. Таким образом, для целочисленной переменной somelnt ссылку можно создать так: int &rSomeRef = somelnt;
172 Часть III. Управление памятью Это читается следующим образом: “rSomeRef является ссылкой на целочисленное значение, инициализированное адресом переменной someint”. Создание и использо- вание ссылок продемонстрировано в листинге 11.1. Между цю*пш Оператор ссылки Обратите внимание на то, что оператор ссылки (&) выглядит так же, как и оператор возвращения адреса, который используется при работе с указателями. Но здесь он используется в объявлении. Не забывайте: звездочка (*) в объявлении означает, что эта перемен- ная — указатель. Когда звездочка используется при операциях с указателями, она является оператором косвенного доступа, а в мате- матическом выражении — оператором умножения. Листинг 11.1. Файл createreference. срр. Создание и использование ссылок 0: // Листинг 11.1. 1: // Создание и использование ссылок 2: #include <iostream> 3 : 4: int main!) 5: { 6: int intOne; 7: int irSomeRef = intOne; 8: 9: intOne = 5; 10: std::cout << "intOne: " << intOne << std::endl; 11: std::cout << "rSomeRef: " << rSomeRef << std::endl; 12 : 13: rSomeRef = 7; 14: std::cout << "intOne: " << intOne << std::endl; 15: std::cout << “rSomeRef: " << rSomeRef << std::endl; 16: return 0; 17: } Результат intOne: 5 rSomeRef: 5 intOne: 7 rSomeRef: 7 Анализ В строке 6 объявлена локальная целочисленная переменная intone, а в строке 7 — ссылка rSomeRef на целочисленное значение, инициализируемая адресом переменной intone. Если объявить, но не инициализировать ссылку, произойдет ошибка компиля- ции. Ссылки, в отличие от указателя, необходимо инициализировать при объявлении. В строке 9 переменной intone присваивается значение 5, а в строках 10—11 на эк- ран выводятся значения переменной intone и ссылки rSomeRef. Безусловно, они ока- зываются одинаковыми, поскольку rSomeRef — это не более чем псевдоним intone. В строке 13 ссылке rSomeRef присваивается значение 7. Поскольку это ссылка, и она является псевдонимом переменной intone, число 7 в действительности присваивается пе- ременной intone, что и подтверждается выводом значения на экран в строках 14—15.
Час 11. Ссылки 173 Использование в ссылках оператора обращения к адресу (&) Если запросить ссылку об адресе, она вернет адрес своего адресата. В этом и со- стоит природа ссылок Они являются псевдонимами для своих адресатов. Это свойст- во демонстрирует листинг 11.2. Листинг 11.2. Файл addressreference. срр. Обращение к адресу ссылки 0: // Листинг 11.2. 1: // Пример использования ссылок 2: #include <iostream> 3: 4: int main!) 5: { 6: int intOne; 71 int trSomeRef = intOne; 8: 9: intOne = 5; 10: std::cout << "intOne: " << intOne << std::endl; 11: std::cout << "rSomeRef: " << rSomeRef << std::endl; 12: 13: std::cout << "bintOne: " << &intOne << std::endl; 14: std::cout << "brSomeRef: “ << 4rSomeRef << std::endl; 15: 16: return 0; 17: } Результат intone: 5 rSomeRef: 5 SintOne: 1245064 SrSomeRef : 1245064 Полученный результат может выглядеть иначе, поскольку каждый компьютер со- храняет переменные по разным адресам, в зависимости от того, что еще находится в памяти и сколько ее доступно. Если используется компилятор, отличный от компи- лятора Borland, результат может выглядеть так: intOne: 5 rSomeRef: 5 bintOne: 0x0012FF7C &rSomeRef: 0x0012FF7C Анализ И вновь ссылка rSomeRef инициализируется адресом переменной intone. На этот раз выводятся адреса двух переменных, которые оказываются идентичными. В языке C++ не предусмотрено предоставление доступа к адресу самой ссылки, поскольку в этом нет смысла. С таким же успехом для этого можно использовать указатель или другую пере- менную. Ссылки инициализируются при создании и всегда действуют как синонимы для своих адресатов, даже в том случае, когда применяется оператор взятия адреса. Например, если существует класс по имени President, то его объект можно было бы объявить следующим образом: President George_W_Bush;
174 Часть III. Управление памятью Затем можно было бы объявить ссылку на объект класса President и инициали- зировать ее, использовав конкретный объект. President &Dubya = George_W_Bush; Существует только один класс President; оба идентификатора ссылаются на один и тот же объект одного и того же класса. Любое действие, предпринятое для ссылки Dubya, будет выполнено также и для объекта George_w_Bush. Обратите внимание на различие между символом & в строке 7 листинга П.2, который объявляет ссылку rSomeRef на значение типа int, и символами & в строках 13 и 14, которые возвращают адреса целочисленной переменной intone и ссылки rSomeRef. Обычно для ссылки оператор взятия адреса не используется. Просто ссылка ис- пользуется точно так же, как и переменная, вместо которой она применяется (см. строку 11). Даже опытных программистов, которым хорошо известно, что ссылки нельзя пе- реназначать и что они являются псевдонимами своих адресатов, может ввести в за- блуждение явление, происходящее при попытке переназначить ссылку. То, что кажет- ся переназначением, оказывается присвоением нового значения адресату. Это продемонстрировано в листинге 11.3. Листинг 11.3. Файл assignreference. срр. Переприсвоение значения ссылке 0: // Листинг 11.3. 1: // Переприсвоение значения ссылке 2: #include <iostream> 3: using namespace std; // здесь используется объекты std:: 4 : 5: int main() 6 : { 7: int intone; 8: int &rSomeRef = intone; 9: 10: intone = 5; 11: cout << "intone:\t" « intone << endl; 12: cout << "rSomeRef:\t" << rSomeRef << endl; 13 : cout << "&intOne:\t" << &intOne << endl; 14: cout « “&rSomeRef:\t" << brSomeRef << endl; 15: 16: int intTwo = 8; 17: rSomeRef = intTwo; // Это не то, что кажется! 18: cout << "\nintOne:\t" << intone << endl; 19: cout << "intTwo:\t" << intTwo « endl; 20: cout << "rSomeRef:\t" << rSomeRef << endl; 21: cout << "&intOne:\t" << bintOne << endl; 22 : cout « "&intTwo:\t“ « bintTwo << endl; 23 : cout << "4rSomeRef:\t" << &rSomeRef << endl; 24 : return 0; 25: } Результат intone: 5 rSomeRef: 5 SintOne: 1245064 fcrSomeRef: 1245064
Час 11. Ссылки 175 intOne: 8 intTwo: 8 rSomeRe f: 8 &intOne: 1245064 SintTwo: 1245056 brSomeRef: 1245064 Полученный результат может выглядеть иначе, поскольку каждый компьютер со- храняет переменные по разным адресам, в зависимости от того, что еще находится в памяти и сколько ее доступно. Если используется компилятор, отличный от компи- лятора Borland, результат может выглядеть так: intOne: 5 rSomeRef: 5 bintOne: 0x0012FF7C brSomeRef: 0x0012FF7C intOne: 8 intTwo: 8 rSomeRef: 8 &intOne: 0x0012FF7C bintTwo: 0x0012FF74 brSomeRef: 0x0012FF7C Анализ И вновь в строках 7—8 объявляются целочисленная переменная и ссылка на ее значение. В строке 10 целочисленной переменной присваивается значение 5, а в строках 11—14 значения переменной и ссылки, а также их адреса выводятся на экран. В строке 16 создается новая переменная intTwo, которая тут же инициализируется значением 8. В строке 17 предпринимается попытка переназначить ссылку rSomeRef так, чтобы она стала псевдонимом переменной intTwo, но этого не происходит. На самом же деле ссылка rSomeRef остается псевдонимом переменной intone, поэтому такое присвоение эквивалентно следующей операции: intOne = intTwo; Это кажется достаточно убедительным, особенно при выводе на экран значений пе- ременной intone и ссылки rSomeRef (строки 18—20): их значения совпадают со значе- нием переменной intTwo. На самом деле при выводе на экран адресов в строках 21—23 становится очевидным, что ссылка rSomeRef ссылается на переменную intone, а не на переменную intTwo. Рекомендуется Не рекомендуется Использовать ссылки для создания псевдонимов объектов. Инициализировать ссылки при объявлении. Пытаться переназначить ссылку. Путать оператор обращения к адресу с оператором ссылки. На что можно ссылаться? Ссылаться можно на любой объект, включая определенный пользователем. Обра- тите внимание, что ссылка создается на объект, а не на класс. Нельзя объявить ссыл- ку таким образом: int & rlntRef = int; // неверно
176 Часть III. Управление памятью Ссылку rintRef необходимо инициализировать, используя конкретную целочис- ленную переменную, например: int howBig = 2 00; int & rintRef = howBig; Точно так же нельзя инициализировать ссылку на класс Cat: Cat & rCatRef = Cat; // неверно Ссылку rCatRef необходимо инициализировать, используя конкретный объект класса Cat: Cat Frisky; Cat & rCatRef = Frisky; Ссылки на объекты используются точно так же, как сами объекты. Доступ к дан- ным-членам и методам осуществляется с помощью обычного оператора доступа к чле- нам класса (.). Подобно встроенным типам, ссылка действует как псевдоним объекта. Нулевые указатели и ссылки Когда указатели освобождены или не инициализированы, им следует присваивать нулевое значение (0). Ссылок это не касается. Фактически ссылка не может быть ну- левой, и программа, содержащая ссылку на нулевой объект, считается некорректной. Во время ее работы может случиться все, что угодно. Компиляторы могут поддерживать нулевой объект, ничего не сообщая об этом до тех пор, пока объект не попытаются каким-то образом использовать. Но пользоваться поблажками компилятора опасно, поскольку они могут дорого обойтись во время вы- полнения программы. Передача функции аргументов по ссылке На занятии 5, “Функции”, уже говорилось, что функции имеют два ограничения: аргументы передаются как значения и теряют связь с исходными данными, а возвра- щать функция может только одно значение. Передавая функции аргументы по ссылке, можно преодолеть оба ограничения. В языке C++ передача данных по ссылке осуществляется двумя способами: с помо- щью указателей и с помощью ссылок. Не запутайтесь в терминах: передать аргумент по ссылке можно, используя либо указатель, либо ссылку. Передача объекта по ссылке позволяет функции изменить сам объект. Код листинга 11.4 содержит функцию swap(), параметры которой передаются по значению. Листинг 11.4. Файл passbyvalue. срр. Передача параметров по значению 0: // Листинг 11.4. Передача параметров по значению 1: #include <iostream> 2 : 3: void swap(int x, int y); 4 : 5: int main() 6: { 7: int x = 5, у = 10 ; 8: 9: std::cout << "Main. Before swap, x: " << x 10: << " у: " << у << "\n";
Час 11. Ссылки 177 11: swap(х, у); 12: std::cout « "Main. After swap, x: “ << x 13: << " у: " << у « ”\n"; 14: return 0; 15: } 16: 17: void swap (int x, int y) 18: { 19: int temp; 20: 21: std::cout << "Swap. Before swap, x: " << x 22: << " y: " << у << "\n" 23: 24: temp = x; 25: x = y; 26: у = temp; 27: 28: std::cout << "Swap. After swap, x: " << x 29: << ” у: " << у << "\n"; 30: 31: } Результат Main. Before swap, х: 5 у: 10 Swap. Before swap, x: 5 у: 10 Swap. After swap, x: 10 у: 5 Main. After swap, x: 5 y: 10 Анализ Эта программа инициализирует в функции main () две переменные, а затем пере- дает их функции swap (), которая, казалось бы, должна поменять их значения. Но по- сле проверки значений этих переменных оказывается, что они не изменились! Проблема в том, что переменные х и у, переданные функции swap() по значе- нию, являются локальными копиями этих переменных, созданными непосредственно в функции. Чтобы решить проблему, необходимо передать значения переменных х и у по ссылке. В языке C++ существуют два способа ее решения: можно сделать па- раметры функции swap () указателями на исходные значения или передать функции ссылки на их исходные значения. Вариант функции swap () для работы с указателями При передаче указателя на самом деле передается лишь адрес объекта, а, следо- вательно, функция получает возможность манипулировать значением, находящимся по этому адресу. Чтобы заставить функцию swap () изменить реальные значения с помощью указателей, ее нужно объявить так, чтобы она получала два указателя на целочисленные значения. Затем, ссылаясь на указатели, соответствующие перемен- ным х и у, можно фактически поменять местами их значения. Листинг 11.5 демон- стрирует этот подход.
178 Часть III. Управление памятью Листинг 11.5. Файл passbyptr. срр. Передача аргументов по ссылке с помощью указателей 0: // Листинг 11.5. Передача параметров по ссылке 1: ttinclude <iostream> 2 : 3: void swap(int *x, int *y); 4: // "*" свидетельствует, что функции передают указатель 5: int main() 6: { 7: int x = 5, у = 10; 8 : 9: std::cout << "Main. Before swap, x: " << x 10: << " y: " << у « "\n"; 11: swap(&x,&y); 12: std::cout << "Main. After swap, x: " << x 13: << " у: " « у << "\n" ; 14: return 0; 15: } 16: 17: void swap (int *px, int *py) 18: ( 19: int temp; 20: 21: std::cout << "Swap. Before swap, *px: " << *px 22: << " *py: " << *py << "\n"; 23 : 24: temp = *px; 25: *px = *py; 26: *py = temp; 27 : 28: std::cout << "Swap. After swap, *px: " << *px 29: « " *py: " << *py « "\n"; 30: } Результат Main. Before swap, x: 5 y: 10 Swap. Before swap. *px: 5 *py: 10 Swap. After swap. *px: 10 *py: 5 Main. After swap, x: 10 у: 5 Анализ Получилось! В строке 3 изменен прототип функции swap (), где в качестве пара- метров объявлены указатели на значения типа int, а не сами переменные типа int. При вызове в строке 11 функции swap () в качестве аргументов передаются адреса пе- ременных х и у. В строке 19 объявлена локальная для функции swap() переменная temp, которой вовсе не обязательно быть указателем: она будет просто хранить значение *рх (т.е. зна- чение переменной х вызывающей функции) во время действия функции. После окон- чания работы функции переменная temp больше не нужна. В строке 24 переменной temp присваивается значение, хранящееся по адресу рх. В строке 25 значение, хранящееся по адресу рх, записывается в участок памяти по ад- ресу ру. В строке 26 значение, оставленное на время в переменной temp (т.е. исход- ное значение, хранящееся по адресу рх), помещается в участок памяти по адресу ру. В результате значения переменных вызывающей функции, адреса которых были переданы функции swap (), успешно поменялись местами.
Час 11. Ссылки 179 Реализация функции swap () для работы со ссылками Приведенная выше программа работоспособна, но синтаксис функции swap () не- сколько громоздок. Во-первых, внутри нее необходимо многократно возвращать зна- чения указателей, что усложняет код и повышает вероятность возникновения ошибок. Во-вторых, необходимость передавать адреса переменных из вызывающей функции нарушает принцип инкапсуляции функции swap (). Язык C++ позволяет скрыть от пользователей функции подробности ее реализации. Передача параметров с помощью указателей перекладывает ответственность за получение адресов переменных на вызывающую функцию, вместо того чтобы сделать это в теле вы- зываемой функции. Однако вызывающая функция должна знать, адреса каких именно объектов были ей переданы для обмена. Бремя понимания семантики ссылок полностью лежит на функции, реализующей обмен. Для этого можно использовать ссылки. В листинге 11.6 функция swap() пере- писана так, что обмен значениями осуществляется с использованием ссылок. Теперь вызывающая функция только передает объект, а поскольку параметры объявлены как ссылки, семантически передача осуществляется по ссылке. Таким образом, вызываю- щая функция ничего специфического делать не должна. Листинг 11.6. Файл passbyref. срр. Функция swap () с использованием ссылок 0: 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: // Листинг 11.6. Пример передачи параметорв // с помощью ссылок ♦include <iostream> void swap(int &x, int &y); // свидетельствует, что функции передают ссылку int main() ( int x = 5, у = 10; std::cout << "Main. Before swap, x: ” << x << " у: " << у << ”\n"; swap(x,y); std::cout << "Main. After swap, x: " << x << " у: " << у << ”\n"; return 0; } void swap (int &rx, int &ry) { int temp; std::cout << "Swap. Before swap, rx: " << rx << “ ry: " << ry « "\n"; temp = rx; rx = ry; ry = temp; std::cout << "Swap. After swap, rx: << " ry: 1 << rx << ry << “\n";
180 Часть III. Управление памятью Результат Main. Before swap, х:5 у: 10 Swap. Before swap, rx:5 ry:10 Swap. After swap, rx:10 ry:5 Main. After swap, x:10, y:5 Анализ Подобно примеру с указателями, в строке 8 объявлены две переменные, а их зна- чения выводятся на экран в строках 10—11. В строке 12 расположен вызов функции swap (), но ей передаются именно значения х и у, а не их адреса. Вызывающая функ- ция просто передает ей свои переменные. После вызова функции swap () выполнение программы переходит к строке 18, в которой эти переменные идентифицируются как ссылки. Их значения выводятся на экран в строках 22—23, но, как видите, для этого не требуется никаких специальных операторов, поскольку речь идет о псевдонимах исходных значений, используемых по прямому назначению. В строках 25—27 происходит обмен значений, после чего они выводятся на экран в строках 29—30. Управление программой вновь возвращается в вызывающую функ- цию, а затем в строках 13—14 эти значения опять выводятся на экран, но уже в функции main(). Поскольку параметры для функции swap() объявлены как ссылки, то и пе- ременные из функции main О передаются как ссылки, следовательно, они также из- меняются и в функции main!). Таким образом, благодаря использованию ссылок функция приобретает новую возможность изменять исходные данные в вызывающей функции, хотя при этом сам вызов функции ничем не отличается от обычного. Понятие заголовка и прототипа функции Использовать функцию, которая получает в качестве параметров ссылки, легче, да и в коде программы она выглядит проще. Но как вызывающей функции определить, каким способом переданы параметры — по ссылке или по значению? Будучи клиен- том (или пользователем) функции swap (), программист должен быть уверен в том, что она на самом деле изменит параметры. Самое время вспомнить о прототипе функции. В данном случае для него нашлось еще одно применение. Изучив параметры, объявленные в прототипе (который обычно находится в файле заголовка вместе с другими прототипами), программист будет точ- но знать, что получаемые функцией swap () значения передаются по ссылке, а, следо- вательно, обмен значениями произойдет должным образом. Если бы функция swap () была методом класса, то его объявление, также располо- женное в файле заголовка, обязательно содержало бы эту информацию. В языке C++ клиенты классов и функций всю необходимую информацию черпают из файла заголовка. Этот файл выполняет роль интерфейса для класса или функции, действительная реализация которых скрыта от клиента, что позволяет программисту сосредоточиться на собственных проблемах и использовать класс или функцию, не вникая в подробности их внутреннего устройства. Возвращение нескольких значений Как уже говорилось, функции могут возвращать только одно значение. Но что же де- лать, если в результате работы функции необходимо получить сразу несколько значе- ний? Решением этой проблемы будет передача функции нескольких объектов по ссылкам.
Час 11. Ссылки 181 В ходе выполнения функция присвоит объектам нужные значения. Передача объектов по ссылке, позволяющая функции изменять исходные объекты, равносильна разреше- нию функции возвращать несколько значений. В этом случае можно обойтись вовсе без возвращаемого значения, которое (зачем же добру пропадать?) можно использовать для сообщения об ошибках. Здесь также помогут ссылки и указатели. В листинге 11.7 показана функция, кото- рая возвращает три значения: два — в виде параметров-указателей и одно — в виде возвращаемого значения функции. Листинг 11.7. Файл returnwithptr. срр Возвращение значений с помощью указателей 0: // Листинг 11.7. Возвращение функцией 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: // нескольких значений ((include <iostream> short Factor(int, int*, int*); int main!) { int number, squared, cubed; short error; std::cout << "Enter a number (0 - 20): "; std::cin >> number; error = Factor (number, ^squared, (.cubed); if (!error) ( std::cout << "number: " << number << "\n"; std::cout « "square: " << squared << "\n"; std::couc << "cubed: " << cubed << ”\n"; ) else std::cout << “Error encountered!!\n"; return 0; ) short Factor(int n, int ‘pSquared, int *pCubed) { short Value = 0; if (n > 20) Value = 1; else ( *pSquared = n*n; ‘pCubed = n*n*n; Value = 0; } return Value; ) Результат Enter a number (0-20): 3 number: 3 square: 9 cubed: 2 7
182 Часть III. Управление памятью Анализ В строке 8 определены переменные number, squared и cubed типа int. Перемен- ной number присваивается значение, введенное пользователем. Это значение, а также адреса переменных squared и cubed передаются функции Factor () в виде параметров. Функция Factor () анализирует первый параметр, который был передан как значение. Если его значение больше 20 (максимальное, которое может обработать эта функция), то возврашаемое значение Value устанавливается равным единице, а это — признак ошибки. Функция Factor() возвращает либо значение 1, либо значение 0, что свидетельствует об успешном завершении работы. Обратите внима- ние: функция возвращает это значение только в строке 38. Таким образом, искомые значения (квадрат и куб заданного числа) возвращаются в вы- зывающую функцию не с помощью стандартного механизма возврата значений, а за счет изменения значений переменных, указатели на которые были переданы в функцию. Результирующие значения присваиваются с помощью указателей в строках 34—35 В строке 36 переменной value присваивается возвращаемое значение, свидетельст- вующее об успешном завершении работы, а в строке 38 значение Value возвращается вызывающей функции. Эту программу можно усовершенствовать, объявив следующее перечисление, enum ERROR_VALUE { SUCCESS, FAILURE }; Теперь вместо значений 0 и 1 программа сможет возвращать более осмысленные значения success и failure. Первое значение перечисления (success) будет соот- ветствовать значению 0, второе — 1. Возвращение значений по ссылке Хотя код листинга 11.7 вполне работоспособен, применение ссылок вместо указателей сделает его более простым и читабельным. В листинге 11.8 приведена та же программа, но вместо указателей использованы ссылки и добавлено перечисление err_code. Листинг 11.8. Файл returnwithref. срр. Листинг 11.7, переделанный для использования ссылок 0: // Листинг 11.8. 1: // Возвращение функций нескольких значений 2: // при помощи ссылок 3: #include <iostream> 4: 5: enum ERR_CODE { SUCCESS, ERROR }; 6 : 7: ERR_CODE Factor(int, int&, int&); 8: 9: int main() 10: { 11 : int number, squared, cubed; 12 : ERR_CODE result; 13: 14 : std::cout « "Enter a number (0 - 20): "; 15: std::cin >> number; 16: 17 : result = Factor(number, squared, cubed); 18: 19: if (result == SUCCESS) 20: I 21: std::cout << "number: " << number << "\n"
Час 11. Ссылки 183 22: std::cout << "square: " << squared << "\n"; 23: std::cout « "cubed: " << cubed << "\n"; 24: } 25: else 26: std::cout « "Error encountered!!\n"; 27: return 0; 28: } 29: 30: ERR_CODE Factor(int n, int irSquared, int &rCubed) 31: { 32: if (n > 20) 33: return ERROR; // код ошибки 34: else 35: { 36: rSquared = n*n; 37: rCubed = n*n*n; 38: return SUCCESS; 39: } 40: } Результат Enter a number (0-20): 3 number: 3 square: 9 cubed: 2 7 Анализ Листинг 11.8 идентичен листингу 11.7, за двумя исключениями. Перечисление err_code делает сообщение об ошибке более осмысленным (см. строки 33 и 38), как, впрочем, и его обработку (строка 19). Однако наиболее существенно изменилась функция Factor!). Теперь она объявлена как получаю- щая не указатели, а ссылки на переменные squared и cubed, что упрощает работу с этими параметрами. Вопросы и ответы Зачем использовать ссылки, если указатели могут делать то же самое? Ссылки легче использовать и они проще для понимания. Косвенный характер обращения при этом скрывается, кроме того, устраняется необходимость мно- гократно брать значения переменной. Зачем нужны указатели, если со ссылками легче работать? Ссылки не могут быть нулевыми и их нельзя переназначать. Указатели предла- гают большую гибкость, но их сложнее использовать. Коллоквиум Изучив возможности ссылок в действии, имеет смысл ответить на несколько во- просов и выполнить ряд упражнений, чтобы закрепить полученные знания.
184 Часть III. Управление памятью Контрольные вопросы I. Что такое ссылка? 2. Какой оператор используется для создания ссылки? 3. Каков адрес ссылки? 4. Каков стандартный механизм передачи переменных вызываемой функции в языке C++? Какие еше способы передачи переменных существуют? Упражнения 1. Объедините механизмы, представленные в листингах 11.5 (файл passbyptr. срр) и 11.6 (файл passbyref. срр), так, чтобы одна переменная была передана при помощи указателя, а другая — при помощи ссылки. Это продемонстрирует их взаимозаменяемость. 2. Измените файл returnwithptr.срр (листинг 11.7) так, чтобы вместо указате- лей использовались ссылки. 3. Разделите файл returnwithptr.срр (листинг 11.7) на три части: основная программа (файл returnwithptr.срр), функция factor!) (файл factor.срр) и прототип функции factor () (файл factor.hpp). Добавьте файл factor.срр в проект и откомпилируйте его. Это продемонстрирует возможность много- кратного использования библиотек функций и классов. Возможность откомпи- лировать функцию один раз и многократно использовать ее машинный код (наряду с содержащим прототип функции файлом заголовка) позволяет сущест- венно повысить эффективность разработки программ Ответы на контрольные вопросы I. Ссылка — псевдоним или синоним другой переменной или объекта. 2. При объявлении ссылки используется символ амперсанда (&). Ссылки следует инициализировать при объявлении. В отличие от указателя, получить “нулевую” ссылку нельзя. 3. Адрес ссылки совпадает с адресом переменной или объекта, псевдонимом кото- рого она является. 4. Стандартный механизм: передача значений, когда внутри функции использует- ся не исходная переменная, а ее копия, что предотвращает изменение ее значе- ния внутри функции. Передача указателя (первый способ) позволяет обойти это ограничение, поскольку переданный адрес принадлежит исходному значению. Передача ссылки (второй способ) также позволяет обратиться к исходному зна- чению переменной.
ЧАС 12 Подробнее о ссылках и указателях На этом занятии вы узнаете: как использовать передачу по ссылке для повышения эффективности программы; когда использовать ссылки, а когда — указатели; как избежать проблем памяти при использовании указателей; как избежать проблем при использовании ссылок. Передача ссылок как средство повышения эффективности При каждой передаче объекта в функцию по значению создается копия этого объ- екта. При каждом возвращении объекта из функции создается еще одна копия. Для больших пользовательских объектов расход ресурсов на копирование существенно возрастает. В результате программа не только использует больше памяти, чем необхо- димо, но и работает медленнее. Размер такого объекта в стеке представляет собой сумму всех его переменных-членов. Причем каждая переменная-член в свою очередь может быть пользовательским объектом, поэтому передача такой массивной структуры в стек может оказаться весьма дорогим удо- вольствием как по времени, так и по занимаемой памяти. Кроме того, существуют и другие затраты. При создании каждой из этих времен- ных копий компилятор вызывает специальный конструктор — конструктор копий. Бо- лее подробная информация о создании и работе конструктора копий приведена на заня- тии 13, “Дополнительные возможности функций”. Пока достаточно знать лишь то, что вызов конструктора копий происходит каждый раз при помещении в стек временной копии объекта. При выходе из функции временный объект удаляется. Для этого ис- пользуется деструктор объекта. Если функция возвращает объект как значение, то ко- пия этого объекта должна быть сначала создана, а затем уничтожена.
186 Часть III. Управление памятью При работе с большими объектами постоянные вызовы конструктора и деструкто- ра могут оказать ощутимое влияние на скорость работы программы и использование памяти компьютера. Для подтверждения этого в листинге 12.1 создается упрощенный пользовательский объект simpleCat. Реальный объект имел бы большие размеры и занял бы больше памяти, но и этого примера вполне достаточно, чтобы показать насколько часто происходят вызовы конструктора и деструктора копий. Сначала код листинга 12.1 создает объект simpleCat, а затем вызывает две функ- ции. Первой функции передают объект класса Cat как значение, и возвращает она его как значение. Второй функции передают не сам объект, а лишь указатель на него, и возвращает она также указатель. Передача по ссылке позволяет избежать создания копий (а, следовательно, и вызова конструктора копий), повышая таким образом об- щую производительность программы. С другой стороны, это предоставляет вызывае- мой функции сам объект, что позволяет ей изменят его. Листинг 12.1. Файл passobjectsbyref. срр. Передача объекта по ссылке 0: // Листинг 12.1. 1: // Передача указателя на объект 2: ttinclude <iostream> 3 : 4: class SimpleCat 5: { 6: public: 7: SimpleCat (); // конструктор 8: SimpleCat(SimpleCat&); // конструктор копий 9: -SimpleCat(); // деструктор 10: }; 11: 12: SimpleCat::SimpleCat() 13: { 14: std::cout « "Simple Cat Constructor...\n"; 15: } 16: 17: SimpleCat::SimpleCat(SimpleCat&) 18: ( 19: std::cout << "Simple Cat Copy Constructor...\n" ; 20: } 21: 22: SimpleCat::-SimpleCat() 23: { 24: std::cout « "Simple Cat Destructor...\n"; 25: } 26 : 27: SimpleCat FunctionOne (SimpleCat theCat); 28: SimpleCat* FunctionTwo (SimpleCat *theCat); 29: 30: int main!) 31: { 32: std::cout << "Making a cat...\n"; 33: SimpleCat Frisky; 34: std::cout << "Calling FunctionOne...\n"; 35: FunctionOne(Frisky); 36: std::cout << "Calling FunctionTwo...\n”; 37: FunctionTwo(&Frisky); 38: return 0; 39: } 40:
Час 12. Подробнее о ссылках и указателях 187 41: // FunctionOne, передача по значению 42: SimpleCat FunctionOne(SimpleCat theCat) 43: { 44: std::cout << "Function One. Returning ...\n"; 45: return theCat; 46: } 47: 48: Il FunctionTwo, передача по ссылке 49: SimpleCat* FunctionTwo (SimpleCat ‘theCat) 50: { 51: std::cout << "Function Two. Returning ...\n"; 52: return theCat; 53: } Результат 1: Making a cat.. . 2: Simple Cat Constructor... 3: Calling FunctionOne... 4: Simple Cat Copy Constructor... 5: Function One. Returning... 6: Simple Cat Copy Constructor... 7: Simple Cat Destructor... 8: Simple Cat Destructor... 9: Calling FunctionTwo... 10: Function Two. Returning... 11: Simple Cat Destructor... Между ЦЮШ1 Нумерация строк результата Приведенные выше номера строк использованы лишь для удобства анализа; при выполнении программы их не будет. Анализ В строках 4—10 объявлен весьма упрощенный класс SimpleCat. Конструктор, кон- структор копий, деструктор и все компоненты класса выводят на экран свои инфор- мативные сообщения, чтобы было точно известно, когда происходит их вызов. В строке 32 функция main() выводит на экран первое сообщение (строка 1 ре- зультата работы программы). В строке 33 создается экземпляр класса SimpleCat. Это приводит к вызову конструктора, что подтверждает сообщение, отображаемое на эк- ране этим конструктором (строка 2 результата работы программы). В строке 34 функция main () “докладывает” о вызове функции FunctionOne (), которая также выводит свое сообщение (строка 3 результата работы программы). По- скольку при вызове функции FunctionOne () передача объекта класса SimpleCat осуществляется по значению, в стек помещается копия объекта SimpleCat как ло- кального для вызываемой функции. Это приводит к вызову конструктора копий, ко- торый “вносит свою лепту” в результаты работы программы (сообщение в строке 4) Выполнение программы переходит к строке 44, которая принадлежит телу вызванной функции, выводящей свое информационное сообщение (строка 5 результата работы программы). Затем эта функция возвращает управление программой вызывающей функции, и объект класса SimpleCat вновь возвращается как значение. При этом соз- дается еще одна копия объекта за счет вызова конструктора копий, и на экран выводит- ся очередное сообщение (строка 6 результата работы программы).
188 Часть III. Управление памятью Значение, возвращаемое функцией FunctionOne (), не присваивается ни одному объекту, поэтому ресурсы, затраченные на создание временного объекта при реализации механизма возврата, можно сказать, выброшены на ветер, как и ресурсы, затраченные на его удаление с помощью деструктора, который заявил о себе в строке 7 результата работы программы. Поскольку функция FunctionOne () завершена, локальная копия объекта выходит из области видимости и уничтожается, вызывая деструктор и ото- бражая на экране сообщение, показанное в строке 8 результатов работы программы. Управление программой возвращается функции main(), после чего следует вызов функции FunctionTwoC), параметр которой передается по ссылке. Никакая копия объек- та при этом не создается, поэтому отсутствует и сообщение конструктора копий. Код функции FunctionTwo () отображает сообщение, занимающее строку Ю результатов рабо- ты программы, а затем возвращает объект класса SimpleCat (снова по ссылке), поэтому и нет обращений к конструктору и деструктору. Наконец, программа завершается, и объект Frisky выходит из области видимости, создавая последнее обращение к деструктору, отображающему на экране свое сообще- ние (строка 11 результата работы программы). Проанализировав работу этой программы, можно сделать вывод, что при вызове функции FunctionOne () происходят два обращения к конструктору копий и два об- ращения — к деструктору (поскольку объект в эту функцию передается как значение), в то время как при вызове функции FunctionTwo () подобных обращений нет. Передача постоянного указателя Несмотря на то что передача указателя функции FunctionTwo () эффективнее переда- чи значения, она таит в себе немалую опасность. При вызове функции FunctionTwo() совершенно не имелось в виду, что ей разрешается изменять объект класса SimpleCat, передаваемый в виде адреса. Такой способ передачи открывает объект для изменений и аннулирует защиту, обеспечиваемую при передаче объекта по значению. Передачу объектов по значению можно сравнить с передачей музею фотографии шедевра вместо самого шедевра. Если какой-нибудь хулиган испортит фотографию, оригинал при этом не пострадает. А передачу объекта по ссылке можно сравнить с передачей музею ключей от квартиры, где лежит шедевр, с приглашением всем же- лающим посетить ее и полюбоваться. Решить проблему можно, передав в функцию указатель на постоянный объект класса SimpleCat. В этом случае к объекту могут применяться только постоянные методы, не имеющие права изменять объект SimpleCat. Этот подход продемонстри- рован в листинге 12.2. Листинг 12.2. Файл passconstptr. срр. Передача постоянных указателей 0: // Листинг 12.2. 1: // Передача указателей на объекты 2: #include <iostream> 3 : 4: class SimpleCat 5: { 6: public: 7: SimpleCat(); 8: SimpleCat(SimpleCat&); 9: -SimpleCat(); 10: 11: int GetAge() const { return itsAge; } 12: void SetAge(int age) { itsAge = age; }
Час 12. Подробнее о ссылках и указателях 189 13: 14: private: 15: int itsAge; 16: }; 17: 18: SimpleCat::SimpleCat() 19: { 20: std::cout « "Simple Cat Constructor...\n"; 21: itsAge = 1; 22: } 23: 24: SimpleCat::SimpleCat(SimpleCat&) 25: { 26: std::cout << "Simple Cat Copy Constructor...\n"; 27: } 28: 29: SimpleCat::-SimpleCat() 30: { 31: std::cout << "Simple Cat Destructor...\n"; 32: } 33: 34: const SimpleCat * const 35: FunctionTwo (const SimpleCat * const theCat); 36: 37: int main() 38: { 39: std::cout « "Making a cat...\n"; 40: SimpleCat Frisky; 41: std::cout << "Frisky is 42: std::cout << Frisky.GetAge() << " years old\n“; 43: int age = 5; 44: Frisky.SetAge(age); 45: std::cout << "Frisky is "; 46: std::cout << Frisky.GetAge() << " years old\n"; 47: std::cout « "Calling FunctionTwo...\n"; 48: FunctionTwo(&Frisky); 49: std::cout << "Frisky is "; 50: std::cout << Frisky.GetAge() « " years old\n"; 51: return 0; 52: } 53: 54: // FunctionTwo, передача постоянного указателя 55: const SimpleCat * const 56: FunctionTwo (const SimpleCat * const theCat) 57: { 58: std::cout << "Function Two. Returning...\n" ; 59: std::cout << "Frisky is now " « theCat->GetAge(); 60: std::cout << “ years old \n"; 61: // theCat->SetAge(8); постоянная! 62: return theCat; 63: } Результат Making a cat. . . Simple Cat Constructor... Frisky is 1 years old
190 Часть III. Управление памятью Frisky is 5 years old Calling FunctionTwo... Function Two. Returning... Frisky is now 5 years old Frisky is 5 years old Simple Cat Destructor... Анализ В класс SimpleCat были добавлены две функции доступа к данным: метод GetAge () (строка 11), который является постоянной функцией, и метод SetAge () (строка 12), который не является постоянным. В этот класс была также добавлена пе- ременная-член itsAge (строка 15). Конструктор, конструктор копий и деструктор по-прежнему выводят на экран свои сообщения. Конструктор копий не вызывался ни разу, поскольку объект был передан по ссылке, и копии объекта не создавались. В строке 40 был создан объект со значением возраста, заданным по умолчанию. Это значение выводится на экран в строках 41—42. В строке 44 с помощью метода доступа SetAge () устанавливается значение пере- менной itsAge, а результат выводится на экран в строках 45—46. В этой программе функция FunctionOne() не используется, но применяется функция FunctionTwof), которая несколько изменена. На этот раз и параметр, и возвращаемое значение объ- явлены как постоянные указатели на постоянные объекты. Поскольку и параметр, и возвращаемое значение передаются по ссылке, никакие ко- пии не создаются и конструктор копий не вызывается. Но указатель в функции FunctionTwo () теперь является постоянным, а, следовательно, к нему не может приме- няться непостоянный метод SetAge (). Если бы обращение к методу SetAge () в строке 61 не было закомментировано, программа не прошла бы этап компиляции. Обратите внимание, что создаваемый в функции main() объект не является по- стоянным, и объект Frisky может вызвать метод SetAgeO. Адрес этого обычного объекта передается функции FunctionTwo!), но поскольку в объявлении функции FunctionTwo!) заявлено, что передаваемый указатель должен быть постоянным ука- зателем на постоянный объект, то с этим объектом функция обращается так, как если бы он был постоянным! Ссылки как альтернатива указателям В листинге 12.2 проблема создания излишних временных копий решена. Сокра- щается количество обращений к конструктору и деструктору класса, в результате чего программа работает более эффективно. Здесь использовался постоянный указа- тель на постоянный объект, что предотвращало возможность изменения объекта собственной функцией. Но определенная громоздкость синтаксиса, свойственная передаче в функции указателей, по-прежнему сохраняется. Поскольку известно, что объект никогда не бывает нулевым, внутреннее содержа- ние функции можно упростить, если вместо указателя передавать ссылку. Это под- тверждает листинг 12.3, являющейся модификацией листинга 12.2. Листинг 12.3. Файл passref toobj . срр. Передача ссылок объектам 0: // Листинг 12.3. 1: // Передача ссылок объектам 2 : #include <iostream> 3 : 4: class SimpleCat 5: {
Час 12. Подробнее о ссылках и указателях 191 6: public: 7: SimpleCat(); 8: SimpleCat(SimpleCatb); 9: -SimpleCat(); 10: 11: int GetAge() const { return itsAge; } 12: void SetAge(int age) { itsAge = age; } 13: 14: private: 15: int itsAge; 16: }; 17: 18: SimpleCat::SimpleCat() 19: ( 20: std::cout << "Simple Cat Constructor...\n"; 21: itsAge = 1; 22: ) 23: 24: SimpleCat::SimpleCat(SimpleCatb) 25: { 26: std::cout << "Simple Cat Copy Constructor...\n"; 27: } 28: 29: SimpleCat::-SimpleCat() 30: { 31: std::cout « "Simple Cat Destructor...\n"; 32: } 33: 34: const SimpleCat & FunctionTwo (const SimpleCat & theCat 35: 36: int main)) 37: { 38: std::cout << "Making a cat...\n"; 39: SimpleCat Frisky; 40: std::cout << "Frisky is " << Frisky.GetAge() 41: << " years old\n" 42: 43: int age = 5; 44: Frisky.SetAge(age); 45: std::cout « "Frisky is " << Frisky.GetAge() 46: « " years old\n"; 47: 48: std::cout << "Calling FunctionTwo...\n"; 49: FunctionTwo(Frisky); 50: std::cout << "Frisky is " « Frisky.GetAge() 51: << " years old\n"; 52: return 0; 53: } 54: 55: // FunctionTwo(), передача ссылки постоянному объекту 56: const SimpleCat & FunctionTwo (const SimpleCat & theCat 57: { 58: std::cout << "Function Two. Returning...\n"; 59: std::cout << "Frisky is now “ << theCat.GetAge() 60: « " years old \n"; 61: // theCat.SetAge(8); постоянная! 62: return theCat; 63: ) ) ; ) ’
192 Часть III. Управление памятью Результат Making a cat... Simple Cat constructor... Frisky is 1 years old Frisky is 5 years old Calling FunctionTwo FunctionTwo. Returning... Frisky is now 5 years old Frisky is 5 years old Simple Cat Destructor... Анализ Результат работы этой программы идентичен результату программы, представленной в листинге 12.2. Единственным существенным отличием является функция FunctionTwo!), которая теперь получает и возвращает ссылки на постоянный объект. И вновь работа со ссылками оказывается несколько проще, чем с указателями, хотя при этом достигается та же экономия средств и эффективность выполнения, а, кроме того, обеспечивается надежность за счет привлечения компилятора к поиску возможных ошибок. Когда использовать ссылки, а когда — указатели Опытные программисты безоговорочно отдают предпочтение ссылкам перед указа- телями. Ссылки проще использовать и они позволяют скрыть информацию, как было продемонстрировано в предыдущем примере. Однако ссылки нельзя переназначить. Поэтому, если необходимо сначала указывать на один объект, а затем — на другой, придется использовать указатель. Ссылки не могут быть нулевыми, поэтому, если су- ществует вероятность того, что рассматриваемый объект может стать нулевым, ис- пользовать ссылку нельзя. В этом случае необходим указатель. Кроме того если необ- ходимо зарезервировать область в динамической памяти, следует также использовать указатели, как было продемонстрировано на предыдущих занятиях. Не возвращайте ссылку на объект, находящийся вне области видимости! Научившись передавать ссылки на объекты как аргументы, программисты порой за- бывают об осторожности. Все хорошо в меру. Не забывайте, что ссылка всегда служит псевдонимом некоторого объекта. При передаче ссылки в функцию или из нее нелиш- ним будет задаться вопросом: “Что представляет собой объект, псевдонимом которого предстоит воспользоваться, и будет ли он существовать в момент его применения?” В листинге 12.4 приведен пример возможной ошибки, когда функция возвращает ссылку на объект, который уже не существует. Листинг 12.4. Файл returnref. срр. Возвращение ссылки на несуществующий объект 0: // Листинг 12.4. 1: // Возвращение ссылки на 2: // несуществующий объект 3: #include <iostream> 4:
Час 12. Подробнее о ссылках и указателях 193 5: class SimpleCat 6: ( 7: public: 8: SimpleCat (int age, int weight); 9: -SimpleCat() {} 10: int GetAge() { return itsAge; } 11: int GetWeight() { return itsWeight; } 12: private: 13: int itsAge; 14: int itsWeight; 15: }; 16: 17: SimpleCat::SimpleCat(int age, int weight): 18: itsAge(age), itsWeight(weight) {} 19: 20: SimpleCat ^TheFunction)) ; 21: 22: int main() 23: { 24: SimpleCat &rCat = TheFunction(); 25: int age = rCat.GetAge(); 26: std::cout << "rCat is " << age << " years old!\n”; 27: return 0; 28: } 29: 30: SimpleCat &TheFunction() 31: { 32: SimpleCat Frisky(5,9); 33: return Frisky; 34: } Результат При попытке компиляции кода листинга 12.4 будет передано следующее сообще- ние об ошибке: "returnref.срр”: Е2363 Attempting to return a reference to local variable 'Frisky' in function TheFunction() at line 33 W т кпрожиы.' Интеллектуальные компиляторы Некоторые компиляторы достаточно интеллектуальны, чтобы распознать ссылку на нулевой объект и сообщить об этом (как показано выше). Дру- гие компиляторы нормально компилируют и даже запускают такой код на выполнение. Однако не следует радоваться отсутствию сообщения об ошибке — это “медвежья" услуга. Как свидетельствует приведенное вы- ше сообщение об ошибке, компилятор Borland достаточно интеллектуа- лен, чтобы ее распознать. Анализ В строках 5—15 объявлен класс SimpleCat. В строке 24 инициализируется ссылка на объект класса SimpleCat с использованием результатов вызова функции TheFunction!), объявленной в строке 20. Согласно объявлению, функция возвращает ссылку на объект класса simpleCat. В теле функции TheFunction!) объявлен локальный объект типа SimpleCat и инициа-лизирован значениями возраста и веса. Затем он возвращается по ссылке.
194 Часть III. Управление памятью Некоторые компиляторы обладают достаточным интеллектом, чтобы распознать эту ошибку, и не позволят запустить программу на выполнение. Другие же (сразу видно, кто настоящий друг!) разрешат выполнить ее, что приведет к непредска- зуемым последствиями. По возвращении из функции TheFunction() локальный объект Frisky будет разрушен (надеюсь, безболезненно для него самого), и воз- вращаемая этой функцией ссылка останется псевдонимом несуществующего объек- та, а это очень нехорошо. Возвращение ссылки на объект в динамической памяти Можно было бы попытаться решить проблему, описанную в листинге 12.4, пере- писав функцию TheFunction () так, чтобы она создавала объект Frisky в области динамической памяти. В этом случае после выхода из функции TheFunction () объект Frisky останется “жив”. Новый подход порождает новую проблему: что делать с памятью, выделенной для объекта Frisky, когда он становится ненужным? Решение этой проблемы проиллюст- рировано в листинге 12.5. Листинг 12.5. Файл leaky, срр. Утечка памяти 0: // Листинг 12.5. 1 : 2 : 3: 4 : 5: 6: 7 : 8: 9: 10: 11 : 12: 13: 14 : 15: 16: 17 : 18: 19: 20: 21: 22: 23 : 24: 25 : 26: 27 : 28: 29: 30: 31: // Устранение утечки памяти #include <iostream> class SimpleCat ( public: SimpleCat (int age, int weight); -SimpleCat() {} int GetAge() { return itsAge; } int GetWeight() { return itsWeight; } private: int itsAge; int itsWeight; } ; SimpleCat::SimpleCat(int age, int weight): itsAge(age), itsWeight(weight) {} SimpleCat & TheFunction(); int main!) { SimpleCat & rCat = TheFunction(); int age = rCat,GetAge(); std::cout << "rCat is “ << age << " years old!\n"; std::cout << "&rCat: " << &rCat << std::endl; II Как бы избавиться от этой памяти? SimpleCat * pCat = &rCat; delete pCat; // Ой-ой, на что же теперь ссылается rCat?
Час 12. Подробнее о ссылках и указателях 195 32: return 0; 33: 34: 35: 36: 37: 38: 39: 40: } SimpleCat &TheFunction() { SimpleCat * pFrisky = new SimpleCat(5,9); std::cout << "pFrisky: " << pFrisky << std::endl; return *pFrisky; } Результат pFrisky: 8861880 rCat is 5 years old! brCat: 8861880 Полученный результат может выглядеть иначе, поскольку каждый компьютер со- храняет переменные по разным адресам, в зависимости от того, что еще находится в памяти и сколько ее доступно. pFrisky: 0х00431СА0 rCat is 5 years old! brCat: 0x00431CA0 Ше------T встарожш! Будьте внимательны, удаляя объекты Эта программа компилируется, компонуется и начинает работать. Однако она содержит мину замед ленного действия, которая ожидает своего часа. Анализ Функция TheFunction!) была изменена таким образом, чтобы больше не возвра- щать ссылку на локальную переменную. В строке 37 выделяется необходимая область динамически распределяемой памяти, и ее адрес присваивается указателю. Этот адрес выводится на экран, после чего ссылка на указатель объекта pFrisky класса SimpleCat возвращается по ссылке. В строке 24 возвращаемое функцией TheFunction!) значение присваивается ссылке на объект класса SimpleCat. Затем этот объект используется для получения значения возраста кота, которое выводится на экран в строке 26. Для доказательства того, что объявленная в функции main () ссылка указывает на объект, размещенный в области динамической памяти, выделенной для него в теле функции TheFunction (), к ссылке rCat применяется оператор взятия адреса (&). Вполне закономерно, что адрес объекта, на который ссылается rCat, совпадает с ад- ресом объекта, расположенного в области распределяемой памяти. До сих пор все было нормально. Но как освободить область памяти, которая больше не нужна? Ведь нельзя же выполнить операцию удаления для ссылки. На ум приходит одно решение: создать указатель и инициализировать его адресом, полученным из ссыл- ки rCat. При этом память будет освобождена, а ее утечка предотвращена. Но остается одна небольшая проблема: на что ссылается переменная rCat теперь, после выполнения кода строки 31? Как уже говорилось, ссылка всегда должна оставаться псевдонимом ре- ального объекта, если же она ссылается на нулевой объект (как в данном случае), то о корректности программы говорить не приходится.
196 Часть III. Управление памятью Будие______, ктврожвы! Нулевые объекты Иногда содержащая ссылку на нулевой объект программа может быть от- компилирована. Но она все равно останется некорректной, а результаты ее работы будут непредсказуемы. Есть два пути решения этой проблемы. Первый заключается в возвращении указателя на область памяти, зарезервированную в строке 37. Теперь вызывающая функция сможет удалить указатель, когда он окажется не нужен. Для этого функцию TheFunction)) необходимо изменить так, чтобы она возвращала не ссылку, а указатель. SimpleCat * TheFunction)) { SimpleCat * pFrisky = new SimpleCat(5,9); std::cout << "pFrisky: " << pFrisky << std::endl; return pFrisky; // возврат указателя } Второе решение (возможно, самое правильное) — объявить объект в вызывающей функции, а затем передать в функцию TheFunction)) ссылку на него. Преимущест- вом такого подхода является то, что ответственность за освобождение памяти несет та же функция (вызывающая), которая ее и зарезервировала. Как будет продемонстриро- вано в следующем разделе, это весьма желательно. Кто владеет указателем? При выделении программой области в динамической памяти возвращается указа- тель, который очень важно сохранить, поскольку в случае его утраты эту область па- мяти нельзя будет освободить, что приведет к ее утечке. При передаче этого участка памяти между функциями кто-то будет “обладать” ука- зателем. Как правило, значение, содержащееся в участке, передается в виде ссылки, а освобождать его должна та функция, которая его создала. Но это не догма, а лишь рекомендация для программистов. Весьма небезопасно, если объект в динамической памяти создаст одна функция, а удаляет другая. Если не знаешь точно, кто именно в данный момент владеет указате- лем, то можно забыть освободить память или попытаться сделать это дважды. Любой из двух случаев становится причиной больших неприятностей. Именно поэтому целе- сообразно придерживаться правил и освобождать память там, где она и выделялась. Если пришлось создавать функцию, которая требует выделения области в динами- ческой памяти, а затем возвращает ее вызывающей функции, то следует пересмотреть концепцию программы. Лучше сделать так, чтобы память выделялась вызывающей функцией, а затем освобождалась после возвращения результата из вызываемой функции. Это переносит все операции по обслуживанию памяти в одну функцию, снимая вопрос о принадлежности указателя. Рекомендуется Не рекомендуется Передавать параметры по значению только при необходимости. Возвращать из функций результат по значению только при необходимости. Передавать ссылки на объекты, которые могут выйти из области видимости. Использовать ссылки на нулевые объекты.
Час 12. Подробнее о ссылках и указателях 197 Вопросы и ответы Зачем возвращать результат функции как значение? Если возвращается объект, который является локальным членом данной функ- ции, то необходимо организовать его возвращение именно как значения, в про- тивном же случае возможно появление ссылки на несуществующий объект. Если возвращение объекта по ссылке так опасно, то почему бы не возвращать ре- зультат по значению всегда? Возвращение по ссылке гораздо более эффективно: и экономит память, и про- грамма выполняется быстрее. Коллоквиум Изучив ссылки и указатели подробнее, имеет смысл ответить на несколько вопро- сов и выполнить ряд упражнений, чтобы закрепить полученные знания. Контрольные вопросы 1. Почему передача по ссылке лучше, когда речь идет о больших объемах данных? 2. Почему при передаче по ссылке имеет смысл использовать постоянный указатель? 3. Почему при передаче по ссылке имеет смысл использовать указатель вместо ссылки? 4, Можно ли создать ссылку для переменной-указателя? Упражнения I. Разделите файл passobjectsbyref.срр (листинг 12.1) на три части: основная программа и функции (файл passobjectsbyref.срр), методы класса (файл SimpleCat.срр) и объявление класса (файл SimpleCat.hpp). Добавьте файл SimpleCat.срр в проект и откомпилируйте его. Это продемонстрирует воз- можность разделения и многократного использования библиотек классов. Воз- можность откомпилировать функцию один раз и многократно использовать ее машинный код (наряду с содержащим прототип функции файлом заголовка) позволяет существенно повысить эффективность разработки программ. 2. Измените файл leaky.срр (листинг 12.5) так, чтобы при обращении к функ- ции TheFunction () использовались указатели. Для предотвращения утечки памяти используйте оператор delete. 3. Измените файлы passobjectsbyref .срр и passreftoobj .срр (листинги 12.1 и 12.3 соответственно) так, чтобы адреса переменных отображались на экране до и после вызова функции. Это позволит лучше понять используемый механизм. Ответы на контрольные вопросы 1. Стандартный механизм передачи по значению подразумевает создание копий передаваемых переменных. Для больших объектов это может потребовать зна- чительного времени и объема памяти. 2. Ключевое слово const уведомит компилятор о том, что вызываемая функция не должна изменять значение указателя. Так можно и передаваемое значение защитить, и копий не создавать.
198 Часть III. Управление памятью 3. Когда вызываемая функция резервирует область в распределяемой памяти, ее адрес может быть возвращен вызывающей функции при помощи указателя. Чтобы предотвратить утечку памяти, очень важно не забыть освободить заре- зервированную область. 4. Безусловно. Почему бы и нет? Однако при этом следует соблюдать осторож- ность, поскольку при работе с указателями она необходима всегда.
ЧАСТЬ IV Дополнительные средства Ватой части... Час 13. Дополнительные возможности функций Час 14. Перегрузка операторов Час 15. Массивы
ЧАС 13 Дополнительные возможности функций На этом занятии вы узнаете: как перегружать функции-члены; как создавать функции для поддержки классов с переменными в динамической памяти; как инициализировать объекты; как создавать конструкторы копий. Перегрузка функций-членов На занятии 5, “Функции”, было дано лишь общее представление о полиморфизме и перегрузке функций. Речь шла об объявлении нескольких функций под одним именем, но с разными параметрами. Точно так же можно перегружать функции-члены класса. В классе Rectangle (листинг 13.1) объявлены две функции Drawshape!). Первая, не требующая параметров, рисует прямоугольник на основании текущих значений класса. Вторая получает два значения (ширины и длины) и отображает на экране прямоугольник в соответствии с ними, игнорируя текущие значения. Листинг 13.1. Файл overloadfunctions .срр. Перегрузка функций-членов класса 0: // Листинг 13.1. Перегрузка функций-членов класса 1: #include <iostream> 2 : 3: // Объявление класса Rectangle 4: class Rectangle 5: ( 6: public: 7: // конструкторы 8: Rectangle (int width, int height) 9: -Rectangle!) {} 10: 11: // перегрузка функции-члена класса Drawshape
Час 13. Дополнительные возможности функций 201 12: void DrawShape() const; 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: void DrawShape(int aWidth, int aHeight) const; private: int itsWidth; int itsHeight; ) ; // Реализация конструктора Rectangle::Rectangle(int width, int height) { itsWidth = width; itsHeight = height; } // Перегружаемая функция DrawShape(), без параметров // Рисунок на основании текущих значений элементов класса void Rectangle::DrawShape() const { DrawShape( itsWidth, itsHeight); ) // Перегружаемая функция DrawShape() - два параметра // Рисунок на основании полученных параметров void Rectangle::DrawShape(int width, int height) const ( for (int i=0; i<height; i++) { for (int j=0; j<width; j++) { std::cout << "*"; } std::cout << "\n"; ) } 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: II Основная программа для демонстрации перегрузки функций int main() { // инициализация прямоугольника 30 на 5 Rectangle theRect(30,5); std::cout « "DrawShape(): \n”; theRect.DrawShape(); std::cout << ''XnDrawShape (40,2 ) : \n"; theRect.DrawShape(40,2); return 0; } Результат DrawShape () :
202 Часть IV. Дополнительные средства DrawShape(40,2) : Анализ Наиболее важный код листинга расположен в строках 12—13, где происходит пе- регрузка функции DrawShape (). Реализация перегружаемых методов класса находится в строках 28—48. Обратите внимание: версия функции Drawshape () без параметров об- ращается к тому варианту функции, который получает два параметра, и передает ей те- кущие значения переменных-членов. В программировании всегда следует избегать дублирования одинаковых участков кода. В противном случае придется запоминать созданные копии всех функций, чтобы при изменении кода в одной из них внести соответствующие изменения во все остальные. Основная программа в строках 50—60 создает объект прямоугольника, а затем вы- зывает его метод DrawShape (): первый раз без параметров, а во второй раз передавая ему два целых числа. На основании количества и типа передаваемых параметров компилятор сам решит, какой именно из методов применять. Можно было бы создать третий вариант функ- ции Drawshape (), которой передают один целочисленный параметр и один параметр перечисляемого типа, указывающий на то, чем именно является первое из передан- ных значений — шириной или высотой. Использование значений по умолчанию Функции-члены класса, подобно обычным функциям, способны использовать зна- чения параметров, заданные по умолчанию. При объявлении таких функций-членов используется уже знакомый синтаксис (листинг 13.2). Листинг 13.2. Файл usingdefaults. срр. Использование значений по умолчанию 0: II Листинг 13.2. Значения по умолчанию для функций-членов 1: #include <iostream> 2 : 3: II Объявление класса Rectangle 4: class Rectangle 5: { 6: public: 7 : II Конструкторы 8: Rectangle(int width, int height); 9: -Rectangle() {} 10: void DrawShape(int aWidth, int aHeight, 11: bool UseCurrentVals = false) const; 12 : private: 13: int itsWidth; 14: int itsHeight; 15: }; 16: 17: II Реализация конструктора 18: Rectangle::Rectangle(int width, int height): 19: itsWidth(width), // Инициализация 20: itsHeight(height) 21: {} // Пустое тело 22 :
Час 13. Дополнительные возможности функций 203 23: // Для третьего параметра используется значение по умолчанию 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: void Rectangle::DrawShape( int width, int height, bool UseCurrentValue ) const { int printwidth; int printHeight; if (UseCurrentValue == true) { printwidth = itsWidth; // Значение текущего класса printHeight = itsHeight; ) else ( printwidth = width; // Значение параметра printHeight = height; } for (int i=0; i<printHeight; i++) { for (int j=0; j<printWidth; j++) { Std::cout << } std::cout « "\n"; } } // Основная программа для демонстрации перегрузки функций int main() ( II Инициализация прямоугольника 30 на 5 Rectangle theRect(30,5); std::cout << "DrawShape(0,0,true)...\n”; theRect.DrawShape(0,0,true); std::cout << "DrawShape(40,2)...\n"; theRec t.DrawShape(40,2) ; return 0; } Результат DrawShape(0,0,true)... DrawShape(40,2)...
204 Часть IV. Дополнительные средства Анализ В листинге 10.2 перегруженная функция DrawShape () заменена простой функци- ей с параметрами, задаваемыми по умолчанию. В строках 10—11 она объявлена с тре- мя параметрами Первые два, aWidth и aHeigth, имеют тип int, а третий пред- ставляет собой логическую переменную UseCurrentVals, которой по умолчанию присваивается значение false. Реализация этой несколько “неуклюжей” функции начинается в строке 29. Пара- метр UseCurrentVals проверяется, и если он содержит значение true, то для при- своения значений локальным переменным printwidth и printHeigth используются, соответственно, переменные-члены itsWidth и itsHeigth. Если оказывается, что параметр UseCurrentVals содержит значение false (установ- ленное либо пользователем, либо по умолчанию), то переменным printwidth и printHeigth присваиваются значения параметров функции, заданные по умолчанию. Обратите внимание: если значение параметра UseCurrentVals истинно, то значе- ния двух остальных параметров функции просто игнорируются. Выбор между значениями по умолчанию и перегруженными функциями Коды листингов 13.1 и 13.2 решают одинаковые задачи, но применение перегрузки функций в листинге 13.1 делает программу более естественной и читабельной. Кроме того, если в программе потребуется третий вариант функции, например, для того что- бы пользователь мог задать только один размер геометрической фигуры, а другой ос- тавить по умолчанию, то добавить новую перегруженную функцию не составит боль- шого труда. Кстати, при добавлении новых вариантов функции значения по умолчанию станут полностью непригодными. Какое же решение предпочтительнее: перегрузка функций или применение значе- ний по умолчанию? Существует эмпирическое правило: используйте перегрузку функций, если: нет приемлемого значения по умолчанию; необходимы различные алгоритмы; требуется обеспечить замену типов в списке параметров. Перегрузка конструкторов Конструкторы, как и все остальные функции, можно перегружать. Перегрузка конструкторов является мощным и очень гибким средством повышения эффективно- сти программ. Например, рассматриваемый объект Rectangle может иметь два конструктора. Первому передают значения длины и ширины, на основании которых он создает прямоугольник. Второму никаких значений не передают, и он создает прямоугольник, размеры которого заданы по умолчанию. Как и у всех перегружаемых функций, ком- пилятор выбирает нужный конструктор на основании количества и типа параметров. В отличие от конструкторов, деструкторы перегружать нельзя. Деструкторы, по оп- ределению, всегда имеют одинаковую сигнатуру: знак тильды (~), имя класса — и ни- каких параметров.
Час 13. Дополнительные возможности функций 205 Инициализация объектов До сих пор значения переменных-членов объектов задавались непосредственно в теле конструктора. Между тем, вызов конструктора осуществляется в два этапа: инициализация, а затем тело конструктора. Большинство переменных может быть задано на любом из этих этапов: как во время инициализации, так и во время выполнения тела конструктора. Но логически правильнее, а зачастую и эффективнее, устанавливать значения переменных-членов на этапе инициализации конструктора. В следующем примере показана инициализа- ция переменных-членов. Cat(): II Имя конструктора и его параметры itsAge(5), // Список инициализации itsWeight(8) {} // Тело конструктора После скобки, закрывающей список параметров конструктора, ставится двоеточие. Затем перечисляются имена переменных-членов. Внутри круглых скобок содержится выражение, результат которого будет использован для инициализации этой перемен- ной. Если инициализируются сразу несколько переменных, то они должны быть отде- лены запятыми. Константы не изменяются! Не забудьте, что ссылки и константы должны быть инициализированы, и присвоить им новое значение нельзя. Если ссылки или константы явля- ются данными-членами, их следует инициализировать в списке инициа- лизации, как показано выше. Как уже было сказано, инициализировать переменные-члены намного эффективнее, чем присваивать им значения. Чтобы уяснить, почему, рассмотрим конструктор копий. Конструктор копий Помимо стандартных конструктора и деструктора, компилятор способен предоста- вить конструктор копий, вызов которого осуществляется всякий раз, когда необходи- мо создать копию объекта. При возвращении и передаче объекта в функцию по значению всегда создается его временная копия. Если объект является пользовательским, то вызывается конструктор копий его класса. Все конструкторы копий получают один параметр — ссылку на объект своего клас- са. Эту ссылку имеет смысл сделать постоянной, поскольку конструктор не должен изменять передаваемый ему объект, например: Cat(const Cat & theCat); В данном случае конструктор Cat получает постоянную ссылку на объект класса Cat, ведь задачей конструктора копий является создание копии объекта theCat. Стандартный конструктор копий (предоставляемый компилятором по умолчанию) просто копирует каждую переменную-член переданного ему объекта в переменные- члены нового объекта. Такое копирование называется поверхностным Хоть оно и подходит для большинства случаев, могут возникнуть серьезные проблемы, если пе- ременные-члены являются указателями на объекты в динамической памяти. В результате поверхностного копирования (shallow сору) создаются точные копии значений всех переменных-членов одного объекта в другом. Указатели в обоих объек- тах будут указывать на одну и ту же область памяти. Глубокое копирование (deep сору) переносит значения, находящиеся в динамической памяти, во вновь созданные участки.
206 Часть IV. Дополнительные средства Если класс Cat содержит переменную-член itsAge, которая указывает на целое чис- ло в динамической памяти, то стандартный конструктор копий скопирует “исходную” переменную-член itsAge объекта Cat в переменную-член itsAge нового объекта Cat. Теперь оба объекта указывают на один и тот же участок памяти (рис. 13.1). Динамическая Рис. 13.1. Использование стандартного конструктора копий Катастрофа случится тогда, когда любой из объектов класса Cat прекратит сущест- вование. Если деструктор первого объекта класса Cat освободит эту память, а второй объект класса Cat будет указывать на нее, то возникнет паразитный указатель, и программа окажется в опасности (рис. 13.2). Рис. 13.2. Создание паразитного указателя Чтобы избежать подобных проблем, необходимо вместо стандартного конструктора копий использовать собственный, который будет осуществлять глубокое копирование с перемещением значений переменных-членов, расположенных в динамической па- мяти. Это продемонстрировано в листинге 13.3. Листинг 13.3. Файл copyconstructors. срр. Конструктор копий 0: 1 : 2 : 3 : 4 : 5 : // Листинг 13.3. // Конструктор копий #include <iostream> class Cat { public: Cat(); // Стандартный конструктор
Час 13. Дополнительные возможности функций 207 8: Cat (const Cat &); // Конструктор копий 9: -Cat(); // Деструктор 10: int GetAge() const { return ‘itsAge; } 11: int GetWeight() const { return *itsWeight; } 12: void SetAge(int age) ( ‘itsAge = age; } 13: 14: private: 15: int *itsAge; 16: int *itsWeight; 17: } ; 18: 19 : Cat::Cat() 20: { 21: itsAge = new int; 22: itsWeight = new int; 23: *itsAge = 5; 24: ‘itsWeight = 9; 25: } 26: 27: Cat::Cat(const Cat & rhs) 28: { 29: itsAge = new int; 30: itsWeight = new int; 31: ‘itsAge = rhs.GetAge(); 32: *itsWeight = rhs.GetWeight (); 33: } 34: 35: Cat::~Cat() 36: { 37: delete itsAge; 38: itsAge = 0; 39: delete itsWeight; 40: itsWeight = 0; 41: } 42: 43: int main!) 44: { 45: Cat Frisky; 46: std::cout << "Frisky's age: " << Frisky.GetAge() << "\n"; 47: std::cout << “Setting Frisky to 6...\n"; 48 : Frisky.SetAge(6); 49: std::cout << "Creating Boots from FriskyXn"; 50: Cat Boots(Frisky); 51: std::cout << "Frisky's age: " << Frisky.GetAge() << "\n“; 52: std::cout « “Boots' age: " << Boots.GetAge() << "\n"; 53: std::cout << "Setting Frisky to 7...\n”; 54: Frisky.SetAge(7); 55: std::cout << "Frisky's age: " << Frisky.GetAge() << ”\n"; 56: std::cout << “Boots' age: " << Boots.GetAge() << "\n”; 57: return 0; 58: } Результат Frisky's age: 5 Setting Frisky to 6... Creating Boots from Frisky Frisky's age: 6
208 Часть IV. Дополнительные средства Boots' age: 6 Setting Frisky to 7 . . . Frisky's age: 7 Boots’ age: 6 Анализ В строках 4—17 объявлен класс Cat. Обратите внимание: в строке 7 объявлен стан- дартный конструктор, а в строке 8 — конструктор копий. В строках 15—16 объявлены две переменные-члена, представляющие собой указа- тели на целочисленные значения. Обычно так не поступают. Хранить в переменных- членах класса указатели на тип int абсолютно бессмысленно, здесь это сделано толь- ко для того, чтобы привести пример манипулирования переменными-членами в ди- намической памяти. Стандартный конструктор в строках 19—25 выделяет в динамической памяти об- ласть для двух переменных типа int и инициализирует их. Конструктор копий начинается в строке 27. Обратите внимание на параметр rhs. В качестве параметра конструктора копий принято использовать объект rhs, название которого является сокращением от right-hand side (стоящий справа). Если рассмотреть операторы в строках 31 —32, то сразу станет заметно, что объект, передающийся в виде параметра, находится справа от знака “=”. Вот как работает конструктор копий: в строках 29—30 резервируются области в динамической памяти. Затем в стро- ках 31—32 новым областям присваиваются значения существующего объекта класса Cat; параметр rhs, передаваемый в конструктор копий в качестве постоянной ссыл- ки, соответствует объекту классу Cat. Функция-член rhs.GetAge() возвращает хранимое в памяти значение, на которое указывает переменная-член itsAge. Являясь объектом класса Cat, параметр rhs содержит все переменные-члены, присущие любому другому объекту класса Cat; при вызове конструктора копий, создающего новый объект класса Cat, сущест- вующий объект класса Cat передается ему в виде параметра. На рис 13.3 схематически показан этот процесс. Значения, на которые указывают переменные-члены существующего объекта класса Cat, копируются в другую область памяти, предназначенную для нового объекта класса Cat. Динамическая Рис. 13.3. Пример глубокого копирования В строке 45 создается объект класса Cat по имени Frisky. Исходный возраст Frisky выводится на экран, а затем в строке 48 устанавливается равным 6. В строке 50 с помощью конструктора копий создается новый объект класса Cat по имени
Час 13. Дополнительные возможности функций 209 Boots, которому в виде параметра передается объект Frisky. Если объект Frisky пе- редается функции по значению (а не по ссылке), то обращение происходит к конст- руктору копий компилятора. В строках 51—52 возраст обоих котов выводится на экран. Обратите внимание: и Boots, и Frisky имеют одинаковый возраст, равный 6, а не заданный по умолча- нию 5. В строке 54 возраст Frisky установлен равным 7, а затем возраст обоих котов снова выводится на экран. На сей раз возраст Frisky равен 7; возраст Boots все еще равен 6. Это свидетельствует о том, что они хранятся в разных областях памяти. Когда объекты класса Cat выйдут из области видимости, их деструкторы будут вы- званы автоматически. Реализация деструктора класса Cat представлена в строках 37—43. Для обоих указателей, itsAge и itsWeight, выполняется оператор delete, освобож- дая выделенную для них память. Кроме того, в целях безопасности обоим указателям присвоено значение null. Вопросы и ответы Зачем использовать значения по умолчанию, если можно перегрузить функцию? Проще иметь дело с одной функцией, чем с двумя. Легче понять работу функ- ции, использующей значения по умолчанию, чем каждый раз внимательно изу- чать тело функции, чтобы выяснить ее назначение. Кроме того, обновление од- ной версии функции без обновления другой зачастую бывает причиной ошибок в работе программы. Почему бы не использовать в таком случае только значения по умолчанию? Перегрузка функций предоставляет ряд возможностей, которые нельзя реализо- вать, используя только значения по умолчанию. Например, изменять не только количество параметров в списке, но и их типы. Какие переменные-члены следует инициализировать одновременно с конструкто- ром, а какие оставлять для тела конструктора? Придерживайтесь простого правила: одновременно с конструктором инициали- зируйте как можно больше переменных-членов. Только некоторые из них, та- кие, как переменные для текущих вычислений и управления выводом на пе- чать, следует инициализировать в теле конструктора. Может ли перегруженная функция содержать параметры, заданные по умолчанию? Конечно. Нет никакой причины, по которой не следовало бы использовать это мощное средство. Одна или несколько версий перегруженных функций могут иметь собственные значения, заданные по умолчанию. При установке значений по умолчанию для перегруженных функций нужно следовать тем же общим правилам, что и при установке значений по умолчанию для обычных функций. Коллоквиум Изучив функции подробнее, имеет смысл ответить на несколько вопросов и вы- полнить ряд упражнений, чтобы закрепить полученные знания.
210 Часть IV. Дополнительные средства Контрольные вопросы 1. Как компилятор узнает, которую из версий перегруженной функции-члена сле- дует вызвать? 2. Можно ли использовать перегруженные функции-члены со значениями по умолчанию? 3. Зачем перегружать конструктор? 4. Что делает конструктор копий? Упражнения 1. Измените файл overloadfunctions.срр (листинг 13.1) так, чтобы получить еще одну версию функции DrawShape (), но с двумя целочисленными парамет- рами, обладающими значениями по умолчанию. Компилируется и выполняется ли код? Что нового стало известно об объединении перегруженных функций и значений по умолчанию? 2. Модифицируйте файл copyconstructors.срр (листинг 13.3) так, чтобы воз- раст кота Boots изменялся после изменения возраста кота Frisky. Влияет ли изменение возраста кота Boots на возраст кота Frisky? То, что изменения в объекте Frisky не влияют на объект Boots, уже было продемонстрировано. 3. Что будет, если заменить деструктор, используемый в файле copyconstruc- tors, срр (листинг 13.3), стандартным? Что при этом случится или, еще важ- нее, не случится? Ответы на контрольные вопросы 1. По количеству и типу аргументов. 2. Безусловно, поскольку количество и типы аргументов остаются уникальными для каждой версии функции. 3. Перегрузка конструктора повышает гибкость программы. Таким образом можно получить создающий объект конструктор, который не получает никаких пара- метров (по существу, создающий пустой объект), а также один или несколько других конструкторов, обладающих разными наборами параметров. 4. Вызов конструктора копий происходит при создании копии объекта, когда его передают функции по значению, а также при создании объекта из другого, уже существующего. Компилятор предоставляет стандартный конструктор копий, но для достаточно сложных объектов он не всегда ведет себя адекватно.
ЧАС 14 Перегрузка операторов На этом занятии вы узнаете: как перегрузить оператор присвоения; как использовать операторы преобразования. Перегрузка операторов Язык C++ располагает рядом встроенных типов данных, включая int, real, char и т.д. Для работы с данными этих типов используются встроенные операторы, напри- мер, сложение (+) и умножение (*). Кроме того, язык C++ позволяет добавлять и пе- регружать подобные операторы для пользовательских классов. Чтобы в деталях рассмотреть процедуру перегрузки операторов, в листинге 14.1 создается новый класс Counter (счетчик). Объект класса Counter будет использован в качестве счетчика (сюрприз!) в циклах и других приложениях, где необходимо при- ращение (инкремент или декремент) числа. Листинг 14.1. Файл counter. срр. Класс Counter 0: // Листинг 14.1. 1: // Класс Counter 2: #include <iostream> 3: 4: class Counter 5: { 6: public: 7 : Counter(); 8: -Counter() {} 9: int GetItsVal() const { return itsVal; } 10: void SetltsVal(int x) { itsVal = x; } 11: 12: private: 13: int itsVal; 14: } ; 15: 16: Counter::Counter(): 17: itsVal(0) 18: {}
212 Часть IV. Дополнительные средства 19: 20: int main() 21: { 22: Counter i; 23: std::cout << "The value of i is " << i.GetltsVal() 24: << std::endl; 25: return 0; 26: 1 Результат The value of i is 0 Анализ Как можно заметить, это совершенно бесполезный класс. Класс Counter опреде- лен в строках 4—14. Он обладает единственной переменной-членом типа int. Стан- дартный конструктор, объявленный в строке 7 и реализованный в строке 18, инициа- лизирует единственную переменную-член itsVal значением 0. В отличие от настоящей переменной типа int, объект класса Counter нельзя уве- личить, уменьшить, добавить, присвоить или использовать иначе. Вывод на экран его значения также затруднен. Создание функции инкремента С помощью перегрузки операторов можно восстановить утраченные функциональ- ные возможности класса Counter. При реализации для класса оператора считается, что оператор перегружен. Код листинга 14.2 демонстрирует перегрузку оператора инкремента. Листинг 14.2. Файл overloadincrement. срр. Перегрузка оператора инкремента 0: // Листинг 14.2. 1: // Перегрузка оператора инкремента 2: ^include <iostream> 3 : 4: class Counter 5: { 6: public: 7: Counter(); 8: -Counter!) {} 9: int GetltsVal!) const { return itsVal; } 10: void SetltsVal(int x) { itsVal = x; } 11: void Increment!) { ++itsVal; } 12: const Counters operator++ (); 13 : 14: private: 15: int itsVal; 16: 17: 18: Counter: -.Counter!) : 19: itsVal(0) 20: {} 21: 22: const Counters Counter::operator++() 23: { 24: ++itsVal; 25: return *this;
Час 14. Перегрузка операторов 213 26: } 27: 28: int main() 29: ( 30: Counter i; 31: std::cout << "The value of i is " << i.GetltsVal() 32: << std::endl; 33: i.Increment(); 34: std::cout << "The value of i is ” << i.GetltsVal() 35: << std::endl; 36: ++i; 37: std::cout << “The value of i is " << i.GetltsVal() 38: << std::endl; 39: Counter a = ++i; 40: std::cout << "The value of a: " « a.GetltsVal(); 41: std::cout << " and i: " << i.GetltsVal() << std::endl; 42: return 0; 43: } Результат The value of i is 0 The value of i is 1 The value of i is 2 The value of a: 3 and i: 3 Анализ Вызов оператора инкремента (++i;) расположен в строке 36. Компилятор интерпретирует это как обращение к реализации представленного в стро- ках 22—26 метода operator++(), который увеличивает переменную-член itsValue, азатем обращается к указателю this, чтобы вернуть текущий объект. Это позволяет присвоить переменной а объект типа Counter. Если бы объект класса Counter резер- вировал память, необходимо было бы переопределить и конструктор копий, но в дан- ном случае прекрасно сработает и стандартный. Обратите внимание: возвращаемое значение представляет собой ссылку класса Counter, благодаря чему отпадает необходимость в создании каких-либо дополни- тельных временных объектов. Ссылка задана как const, поскольку ее значение не должно изменяться при использовании в функции. Перегрузка постфиксных операторов Что, если необходимо перегрузить оператор постфиксного инкремента? Перед компилятором встанет проблема: как различить операторы постфиксного и префикс- ного инкремента? По соглашению, в объявлении оператора постфиксного инкремента используется целочисленный тип. Значение параметра неважно, поскольку это лишь символ того, что речь идет об операторе постфиксного инкремента. Различие между префиксом и постфиксом Прежде чем приступить к перегрузке оператора постфиксного инкремента, следует четко понять, чем он отличается от оператора префиксного инкремента. Напомним, что префикс — “сначала приращение, затем возвращение”, а постфикс — “сначала возвращение, затем приращение”.
214 Часть IV. Дополнительные средства Следовательно, если оператор префиксного инкремента сначала осуществляет при- ращение значения, а затем возвращает объект, то оператор постфиксного инкремента воз- вращает объект с исходным значением, а затем осуществляет приращение. Для решения этой задачи необходимо создать временный объект, сохранив в нем исходное значение, выполнить приращение в исходном объекте и вновь вернуть его во временный объект. Повторим все еще раз Рассмотрим следующий фрагмент кода: а = х++; Если изначально переменная х была равна 5, то в этом выражении пе- ременной а будет присвоено значение 5, а переменная х станет равной 6 Если х — не просто переменная, а объект, то его оператор постфиксно- го инкремента должен сохранить исходное значение 5 во временном объекте, прирастить значение объекта х до 6, а затем возвратить зна- чение временного объекта и присвоить его объекту а. Обратите внимание: поскольку речь идет о временном объекте, его следует воз- вращать по значению, а не по ссылке, так как временный объект выйдет из области видимости после возвращения функцией своего значения. В листинге 14.3 продемонстрировано использование операторов обоих типов. Листинг 14.3. Файл preandpostfix.cpp. Использование префиксных и постфиксных операторов 0: // Листинг 14.3. 1: // Возвращение ссылки на указатель this 2 : ♦include <iostream> 3 : 4: class Counter 5: { 6: public: 7: Counter(); 8 : -Counter!) {} 9: int GetItsVal() const { return itsVal; } 10: void SetltsVal(int x) { itsVal = x; } 11: const Counters, operator++ (); // Префикс 12 : const Counter operator++ (int); // Постфикс 13 : 14: private: 15: int itsVal; 16: }; 17: 18: Counter::Counter)): 19: itsVal(0) 20: {} 21: 22 : const Counters, Counter:: operator++() // Префикс 23: { 24: ++itsVal; 25: return *this; 26: } 27: 28: const Counter Counter::operator++(int) // Постфикс 29 : { 30: Counter temp(*this); 31: ++itsVal;
Час 14. Перегрузка операторов 215 32: return temp; 33: } 34: 35: int main)) 36: { 37: Counter i; 38: std::cout << "The value of i is " << i.GetltsVal() 39: << std::endl; 40: i + +; 41: std::cout << "The value of i is • << i.GetltsVal() 42: << std::endl; 43: + +1 ; 44: std::cout << "The value of i is " << i.GetltsVal() 45: << std::endl; 46: Counter a = ++i; 47: std::cout << "The value of a: " << a.GetltsVal(); 48: std::cout << " and i: " << i. GetltsVal () << std::endl; 49: a = i++; 50: std::cout << "The value of a: " << a.GetltsVal(); 51: std::cout << " and i: " << i.GetltsVal() << std::endl; 52: return 0; 53: } Результат The value of i is 0 The value of i is 1 The value of i is 2 The value of a: 3 and i : 3 The value of a: 3 and i : 4 Анализ Постфиксный оператор объявлен в строке 12 и реализован в строках 28—33. Обратите внимание: переданный в постфиксный оператор флаг (строка 43) должен лишь сооб- щить о том, что это постфиксный оператор, но само значение никак не используется. Оператор суммы Инкремент является унарным оператором (unary operator), т.е. работает только с одним объектом. Оператор суммы (+) — это парный оператор (binary operator), рабо- тающий с двумя объектами. Большинство математических операторов являются пар- ными, им передают два объекта (например, а+Ь). Количество операндов, которыми может манипулировать оператор, является его важнейшей характеристикой. В языке C++ существуют унарные операторы, использующие только один операнд (myValue++), парные операторы, использующие два операнда (а+Ь), и всего один тройственный оператор (ternary operator), использующий три операнда (?). Этот опе- ратор иногда называют троичным, поскольку он единственный в языке C++, который использует три операнда (а > b? X: у). Так как же реализовать перегрузку оператора + для объекта Counter? Задача за- ключается в том, чтобы объявить две переменные класса Counter, а затем сложить их следующим образом: Counter varOne, varTwo, varThree; VarThree = VarOne + VarTwo;
216 Часть IV. Дополнительные средства Создадим функцию Add (), в которой объект Counter будет аргументом. Эта функция должна сложить два значения, а затем возвратить объект Counter с полу- ченным результатом. Такой подход продемонстрирован в листинге 14.4. Листинг 14.4. Файл addfunction. срр. Функция Add () 0: // Листинг 14.4. 1: // Функция Add() 2: #include <iostream> 3 : 4: class Counter 5: { 6: public: 7: Counter(); 8: Counterfint initialvalue); 9 : -Counter() {} 10: int GetltsVal() const { return itsVal; } 11: void SetltsVal(int x) { itsVal = x; } 12: Counter Add(const Counter &); 13 : 14: private: 15: int itsVal; 16: 17: }; 18: 19: Counter::Counter(int initialvalue): 20: itsVal(initialvalue) 21: {} 22 : 23: Counter::Counter(): 24: itsVal(0) 25: {} 26: 27: Counter Counter::Add(const Counter & rhs) 28: { 29: return Counter(itsVal+ rhs.GetltsVal()); 30: } 31: 32: int main() 33: { 34: Counter varOne(2), varTwo(4), varThree; 35: varThree = varOne.Add(varTwo); 36: std: : cout << “varOne: " << varOne.GetltsVal() << std: :endl; 37: Std: : cout << “varTwo: “ << varTwo.GetltsVal() << std: :endl; 38: Std: : cout << “varThree: “ << varThree.GetltsVal() 39: << std::endl; 40: return 0; 41: } Результат varOne: 2 varTwo: 4 varThree: 6
Час 14. Перегрузка операторов 217 Анализ Функция Add О объявлена в строке 12. В качестве аргумента она получает постоян- ную ссылку на объект класса Counter, представляющий собой второе число, которое нужно добавить к текущему объекту. Функция возвращает объект класса Counter, пред- ставляющий собой результат суммирования, который присваивается операнду слева от оператора присвоения (=), как показано в строке 35. Здесь переменная varOne является объектом, varTwo — параметром функции Add(), а результатом будет объект varThree. Чтобы создать объект varThree без исходной инициализации каким-либо значе- нием, используется стандартный конструктор, который инициализирует переменную itsval значением 0, как показано в строках 23—25. Поскольку переменные varOne и varTwo должны инициализироваться не нулевым значением, в строках 19—21 создан специальный конструктор. Другим решением этой проблемы является инициализация нулевым значением в стандартном конструкторе, объявленном в строке 8. Перегрузка оператора суммы operator+ Сама функция Add() находится в строках 27—30 листинга 14.4. Она вполне работо- способна, но изяществом не отличается. Перегрузка оператора суммы (+) сделала бы ра- боту класса Counter более естественной. Этот подход иллюстрирует листинг 14.5. Листинг 14.5. Файл plusoperator. срр. Перегрузка оператора суммы 0: // Листинг 14.5. 1: // Перегрузка оператора плюс (+) 2: #include <iostream> 3: 4: class Counter 5: { 6: public: 7 : Counter ( ) ; 8: Countertint initialvalue); 9: -Counter() {} 10: int GetltsVal() const { return itsVal; } 11: void SetltsVal(int x) { itsVal = x; } 12: Counter operator* (const Counter &); 13: private: 14: int itsVal; 15: }; 16: 17: Counter::Counter(int initialvalue): 18: itsval(initialvalue) 19: {} 20: 21: Counter::Counter(): 22 : itsVal(0) 23: {} 24: 25: Counter Counter::operator* (const Counter & rhs) 26: { 27: return Counter(itsVal + rhs.GetltsVal()); 28: } 29: 30: int mainl) 31: { 32: Counter varOne(2), varTwo(4), varThree; 33: varThree = varOne + varTwo;
218 Часть IV. Дополнительные средства 34: Std: : cout << "varOne: " << varOne.GetltsVal() << std: :endl ; 35: Std: : cout << "varTwo: ” << varTwo.GetltsVal() << std: :endl; 36: Std: : cout << "varThree: ' << varThree . GetltsVal () 37: << std::endl; 38: return 0; 39 : } Результат varOne: 2 varTwo: 4 varThree: 6 Анализ В строке 33 вызов оператора сложения осуществляется следующим образом: varThree = varOne + varTwo; Компилятор интерпретирует ее так: varThree = varOne.operator*(varTwo); Оператор суммы (operator*) объявлен в строке 12 и реализован в строках 25-28. Сравните его с объявлением и реализацией функции Add () в предыдущем листинге. Они почти идентичны. Но синтаксис их применения совершенно различен. Более ес- тественно написать так: varThree = varOne + varTwo; чем так: varThree = varOne.Add(varTwo); Небольшое изменение, но программа стала более читабельной. Итак, операторы вполне можно перегружать и заменять ими явные вызовы функций. Ограничения на перегрузку операторов Операторы для встроенных типов (например, int) не могут быть перегружены. Не может быть изменен ни приоритет, ни количество операндов (arity) оператора. Т.е. унарный оператор не будет работать с двумя операндами. Нельзя создать новый опе- ратор, поэтому не удастся объявить, что ** будет оператором “возведения в степень”. Что можно перегружать? Возможность перегрузки операторов — это одно из наиболее широко используе- мых средств языка C++, которым иногда злоупотребляют начинающие программи- сты. Новичков захватывает азарт при присвоении новых интересных функций обыч- ным операторам. В результате код программы зачастую оказывается непонятным даже для создателя, не говоря уже о других программистах. Безусловно, если в программе оператор + осуществляет вычитание, а оператор * — суммирование, то это может тешить самолюбие начинающего программиста. Но про- фессионал никогда такого не допустит. Вполне естественно желание использовать оператор + для конкатенации строк и символов, а оператор / — для разделения строк. Возможно, было бы неплохо уделить больше внимания особенностям использования перегруженных операторов, однако начнем с предостережений. Прежде всего следует помнить, что главная цель перегрузки операторов — повышение эффективности программы и упрощение ее кода.
Час 14. Перегрузка операторов 219 Оператор присвоения Помните: компилятор предоставляет стандартный конструктор, деструктор и кон- структор копий. Четвертой предоставляемой компилятором функцией для работы с объектами, если не заданы дополнительные функции, является оператор присвоения (operator= ()). Он применяется всякий раз, когда объекту необходимо присвоить но- вое значение, например: Cat catone(5,7); Cat catTwo(3,4); // ... здесь будет остальной код catTwo = catone; Здесь создан объект catone, а его переменные itsAge и itsWeight инициализи- рованы при помощи конструктора, получившего значения 5 и 7. Затем создан объект CatTwo и инициализирован значениями 3 и 4. Потом при помощи оператора при- своения (operator =) объекту catTwo присваивается значение объекта catone. catTwo = catOne; В результате переменным itsAge и itsWeight объекта catTwo будут присвоены значения одноименных переменных объекта catone. Таким образом, после выполне- ния этого оператора значением переменной catTwo.itsAge будет 5, а переменной catTwo. itsWeight — 7. Обратите внимание, что в данном случае конструктор копий не применяется. Объект catTwo уже существует, и нет никакой необходимости создавать его заново. Компи- лятор достаточно “умен”, чтобы применить оператор присвоения. Как уже было сказано на занятии 13, “Дополнительные возможности функций”, в языке C++ различают поверхностное и глубокое копирование данных. При поверхно- стном копировании значения одной переменной передаются другой, включая адреса в указателях. В результате оба объекта указывают на одни и те же области памяти. При глубоком копировании значения переменных переносятся из одной области памяти в другую. Различия между этими методами копирования показаны на рис. 13.1. Как можно заметить, оператор присвоения имеет ту же проблему, что и конструк- тор копий, а, следовательно, все сказанное ранее справедливо и для оператора при- своения. Объект catTwo уже существует и занимает определенные области памяти. Во избежание утечки памяти эти области должны быть впоследствии освобождены. Но что произойдет, если присвоить объект catTwo ему самому? catTwo = catTwo; Специально так никто не поступит, но случайно это может произойти, например, при косвенном обращении к значениям по ссылкам, содержащимся в указателях на один и тот же объект. Если не предусмотреть решения такой проблемы, оператор присвоения сначала освободит ячейки памяти объекта catTwo, а затем попытается присвоить объекту catTwo свои собственные значения, которых уже не будет и в помине! Чтобы предотвратить подобную ситуацию, создаваемый оператор присвоения пре- жде всего должен проверить, не совпадают ли объекты по обе стороны от него. Это можно реализовать при помощи указателя this, как показано в листинге 14.6. Листинг 14.6. Файл assignoperator.срр. Оператор присвоения 0: // Листинг 14.6. 1: // Конструкторы копий 2: #include <iostream> 3: 4: class Cat 5: {
220 Часть IV. Дополнительные средства 6: public: 7: Cat(); // Стандартный конструктор 8: // конструктор копий и деструктор отсутствуют! 9: int GetAge() const { return ‘itsAge; } 10: int GetWeight!) const { return *itsWeight; } 11: void SetAge(int age) { *itsAge = age; } 12: Cat operator=(const Cat &); 13 : 14: private: 15: int ‘itsAge) 16: int "itsWeight; 17: }; 18 : 19: Cat::Cat() 20: { 21: itsAge = new int; 22: itsWeight = new int; 23: ‘itsAge = 5; 24: "itsWeight = 9; 25: } 26: 27 : 28: Cat Cat::operator=(const Cat & rhs) 29: { 30: if (this == &rhs) 31: return "this; 32: delete itsAge; 33: delete itsWeight; 34: itsAge = new int; 35: itsWeight = new int; 36: "itsAge = rhs.GetAge(); 37: "itsWeight = rhs.GetWeight(); 38: return "this; 39: } 40: 41: 42: int main() 43: { 44: Cat Frisky; 45: std::cout << "Frisky's age: " << Frisky.GetAge() 46: << std::endl; 47: std::cout << "Setting Frisky to 6...\n"; 48: Frisky.SetAge(6); 49: Cat Whiskers; 50: std::cout << "Whiskers' age: " << Whiskers.GetAge() 51: << std::endl; 52: std::cout << "copying Frisky to Whiskers ...\n"; 53: Whiskers = Frisky; 54: std::cout << "Whiskers' age: " « Whiskers.GetAge() 55: << std::endl; 56: return 0; 57: } Результат Frisky's age:5 Setting Frisky to 6...
Час 14. Перегрузка операторов 221 Whiskers' age:5 copying Frisky to Whiskers... Whiskers' age:6 Анализ Листинг 14.6 вновь обращается к классу cat, но теперь без конструктора копий и деструктора во избежание лишнего расхода памяти. Оператор присвоения объявлен в строке 12, а реализован в строках 28—39. В строке 30 текущий (присваиваемый) объект cat проверяется на идентичность результирующему объекту cat. Для этого проверяется совпадение адресов в указате- лях rhs и this. Безусловно, оператор равенства (==) также может быть перегружен, что позволяет применить свой собственный механизм проверки идентичности объектов. HUV_____ 4 Удаление объектов из памяти В строках 32-35 переменные-члены удаляются, а затем снова созда- ются в распределяемой памяти. В данном случае это не обязательно, но такой прием, являющийся распространенной практикой програм- мирования, позволит избежать вероятной утечки памяти при работе с объектами переменной длины, у которых не перегружен оператор присвоения. Преобразование типов данных Что происходит при попытке присвоить значение переменой одного из базовых типов, таких, как int и unsigned short, объекту пользовательского класса? В лис- тинге 14.7 вновь возвращаемся к классу counter и пытаемся присвоить его объекту значение переменной типа int. Ш-----т ктарожш! Не удивляйтесь! Не компилируйте листинг 14.7! Полученное сообщение об ошибке и его причина рассматриваются в разделе “Анализ”. Листинг 14.7. Файл counterconvert. срр. Попытка присвоить объекту класса Counter значение переменной типа int 0: 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: И: 12: 13: 14: 15: 16: // Листинг 14.7. // Этот код нельзя компилировать! #include <iostream> class Counter { public: Counter(); -Counter!) {} int GetltsVal!) const { return itsVal; } void SetltsVal(int x) { itsVal = x; } private: int itsVal; }; Counter::Counter(): itsVal(0)
222 Часть IV. Дополнительные средства 17: {} 18: 19: int main() 20: { 21: int theShort = 5; 22: Counter theCtr = theShort; 23: std::cout << “theCtr: " << theCtr.GetltsVal() 24: << std::endl; 25: return 0; 26: } Результат "counterconvert.срр": E2034 Cannot convert 'int' to 'Counter' in function main() at line 22 Анализ Класс counter объявлен в строках 4—13. Он содержит лишь стандартный конст- руктор. В объявлении класса отсутствуют методы преобразования данных типа int в тип counter, поэтому строка 22 приводит к ошибке компиляции. Компилятор ни- чего не сможет сделать, пока не получит четких инструкций о присвоении перемен- ной-члену itsVal значения типа int. В листинге 14.8 эта ошибка исправлена в результате создания оператора преобразо- вания типов. Здесь добавлен еще один конструктор, который получает значение типа int и присваивает его переменной-члену itsVal создаваемого объекта класса counter. Листинг 14.8. Файл counterconvert2 . срр. Преобразование типа int в Counter 0: // Листинг 14.8. 1: // Конструктор как оператор преобразования 2: #include <iostream> 3 : 4: class Counter 5: { 6: public: 7: Counter(); 8: Counter(int val); 9: -Counter() {} 10: int GetltsVal() const { return itsVal; } 11: void SetltsVal(int x) { itsVal = x; } 12: private: 13: int itsVal; 14 : } ; 15: 16: Counter::Counter(): 17: itsVal(0) 18: {) 19: 20: Counter::Counter(int val): // Перегрузка конструктора 21: itsVal(val) 22: {} 23 : 24: int main() 25: { 26: int theShort = 5; 27: Counter theCtr = theShort;
Час 14. Перегрузка операторов 223 28: std::cout << "theCtr: " << theCtr.GetltsVal() << std::endl; 29: return 0; 30: } Результат theCtr: 5 Анализ Важнейшие изменения произошли в строке 8, где конструктор перегружен так, что получает значения типа int, а также в строках 20—22, где этот конструктор реализо- ван. В результате он сможет создать из переменной типа int объект класса Counter. В итоге компилятор получит необходимый конструктор, способный получить в ка- честве аргумента переменную типа int. Но что произойдет, если изменить порядок присвоения, как в следующем примере? 1: Counter theCtr(5); 2: int thelnt = theCtr; 3: cout << "thelnt : " << thelnt << endl; И вновь компилятор сообщает об ошибке. Хотя сейчас ему известно, как создать временный объект типа counter для значения типа int, ему не известно, как осуще- ствить обратный процесс. Оператор int () Для решения этой и подобных проблем язык C++ обладает операторами преобра- зования типов, которые можно добавить в пользовательский класс. Это позволит объек- там создаваемого класса осуществлять неявные преобразования встроенных типов. Листинг 14.9 демонстрирует такой подход. Кстати, операторы преобразования не ус- танавливают тип возвращаемого значения, несмотря на то что фактически возвраща- ют преобразованное значение. Листинг 14.9. Файл counterconvertint. срр. Преобразование типа counter в int 0: 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: И: 12: 13: 14: 15: 16: 17: 18: 19: 20: // Листинг 14.9. // Операторы преобразования #include <iostream> class Counter { public: Counter(); Counter(int val); -Counter!) {} int GetltsVal!) const { return itsVal; } void SetltsVal(int x) { itsVal = x; } operator unsigned short!); private: int itsVal; }; Counter::Counter(): itsVal(0) {}
224 Часть IV. Дополнительные средства 21: Counter::Counter(int val): 22: itsVal(val) 23: {} 24: 25: Counter::operator unsigned short () 26: { 27: return ( int (itsVal) ); 28: } 29: 30: int main() 31: { 32: Counter ctr(5); 33: int theShort = ctr; 34: std::cout << "theShort: " << theShort << std::endl; 35: return 0; 36: } Результат theShort: 5 Анализ В строке 12 объявлен оператор преобразования. Обратите внимание: он не имеет возвращаемого значения. Реализация этой функции находится в строках 25—28. Код строки 27 возвращает значение itsVal, преобразованное в тип int. Теперь компилятору известно, как присвоить объекту класса значение типа int, как возвратить из объекта класса текущее значение и как присвоить его внешней пе- ременной типа int. Вопросы и ответы Зачем перегружать оператор, если достаточно создать функцию-член? Перегруженные операторы проще использовать, поскольку их поведение хоро- шо известно. Это позволяет пользовательскому классу подражать возможностям встроенных типов. Чем конструктор копий отличается от оператора присвоения? Конструктор копий создает новый объект с теми же значениями, что и у суще- ствующего. Оператор присвоения изменяет существующий объект так, чтобы он получил значения другого объекта. Что происходит при использовании ключевого слова int в постфиксных операторах? Ничего. При перегрузке постфиксных и префиксных операторов ключевое сло- во int используется только как флаг. Коллоквиум Изучив возможности перегрузки операторов, имеет смысл ответить на несколько вопросов и выполнить ряд упражнений, чтобы закрепить полученные знания.
Час 14. Перегрузка операторов 225 Контрольные вопросы 1. Почему нельзя создать совершенно новый оператор, например, ** для возведе- ния в степень? 2. Почему бы полностью не переопределить поведение уже существующего опера- тора и не использовать для возведения в степень символ Л? 3. Почему синтаксис перегрузки префиксного инкремента и декремента отличает- ся от синтаксиса постфиксного? 4. Что делают операторы преобразования? Упражнения 1. Измените файл assignoperator.срр (листинг 14.6) следующим образом: пе- регрузите оператор равенства (==), а затем, используя его сравните возраст ко- тов Frisky и Whiskers. 2. Измените файл preandpostfix.cpp (листинг 14.3) так, чтобы реализовать дек- ремент переменной itsVal, с учетом того, что оператор инкремента перегружен. Завершив и опробовав код, отложите его на день или несколько часов. Просмот- рите программу снова. Действительно ли работа всех операторов вполне очевид- на? Или работа программы уже не столь понятна? Это наглядно демонстрирует, почему при перегрузке операторов не следует создавать причудливый код. 3. Измените файл plusoperator. срр (листинг 14.5) так, чтобы перегрузить опе- ратор “минус”. Создайте несколько примеров применения перегруженного оператора вычитания. Влияет ли порядок объектов в операторе на результат? Ответы на контрольные вопросы 1. Поскольку это потребовало бы внесения изменений в компилятор. Знак ** не является частью языка, поэтому компилятор не будет знать, что с ним делать. 2. Это вполне возможно и в некоторых случаях имеет смысл. Однако других про- граммистов это может ввести в заблуждение (и даже автора, если он будет чи- тать свой код года через два). Таким образом, язык не обладает механизмом, который мог бы предотвратить попытки программиста усложнить себе жизнь. 3. Поскольку они ведут себя совершенно по-разному. В префиксной форме зна- чение возвращается после выполнения оператора. Постфиксная форма возвра- щает исходное значение, а затем осуществляет соответствующую операцию. Этому поведению и должен подражать код. На самом деле подражать ему вовсе не обязательно, особенно если хотите заслужить ненависть коллег! 4. Они применяются для преобразования объекта пользовательского типа в объект базового типа. На этом занятии рассматривался объект счетчика и способ ото- бражения хранимого им значения как простого целого числа. Для этого исполь- зовались операторы преобразования.
ЧАС 15 Массивы На этом занятии вы узнаете: что такое массив и как его объявить; что такое строка и как использовать массив символов для ее создания; что такое взаимосвязь между массивами и указателями; как использовать арифметические операции над указателями с применением массивов. Что такое массив? Массив (array) — это набор элементов, спо- собных хранить данные одинакового типа. Мас- сив можно представить в виде ряда ячеек для хранения данных. Каждая из таких ячеек назы- вается элементом массива. Объявляя массив, необходимо сначала ука- зать тип хранимых данных, имя массива и его размер в квадратных скобках. Размером массива (subscript) называется количество его элементов, long LongArray[25]; Здесь, например, объявлен массив LongArray из 25-ти элементов типа long. Обнаружив это объявление, компилятор зарезервирует область памяти, достаточную для хранения 25 пе- ременных типа long. Поскольку каждой переменной типа long необходимы четыре байта, весь объявлений набор займет непрерывную область памяти размером 100 бай- тов, как показано на рис. 15.1. Доступ к элементам массива К каждому из элементов можно обратиться по его номеру, расположенному в квадратных скобках после имени массива. Номера элементов начинаются с нуля. Следовательно, первым элементом массива LongArray будет LongArray [ 0 ], вто- рым — LongArray [ 1 ], и т.д.
Час 15. Массивы 227 Например, массив SomeArray[3] состоит из трех элементов: SomeArray[0], SomeArray[1] и SomeArray [2]. В общем случае массив SomeArray[п], состоящий из п элементов, содержит элементы от SomeArray [0] до SomeArray [п-1]. Следовательно, массив LongArray[25] пронумерован от LongArray[0] до LongArray[24]. Листинг 15.1 демонстрирует, как объявить массив из пяти целых чисел и заполнить его значениями. Листинг 15.1. Файл intarray. срр. Использование массива целых чисел 0: и Листинг 15.1. Массивы 1: ♦include <iostream> 2: 3: int main() 4: { 5: int myArray[5] 6: for (int i=0; i<5; i++) // 0-4 7: { 8: std::cout << : "Value for myArray[" << i << "]: 9: std::cin >> myArray[i]; 10: } 11: for (int i = 0; i < 5; i + +) 12: std::cout << : i << ": “ << myArray[i] << "\n"; 13: return 0; 14: } Результат Value for myArray[0]: 3 Value for myArray[1]: 6 Value for myArray[2]: 9 Value for myArray[3]: 12 Value for myArray[4]: 15 0: 3 1: 6 2: 9 3: 12 4: 15 Анализ В строке 5 объявлен массив myArray из пяти целочисленных переменных. В строке 6 начинается цикл от 0 до 4, перебирающий все пять элементов. Здесь пользователю предлагается ввести значение, которое сохраняется в соответствующем элементе массива. Первое значение сохраняется в элементе массива myArray [0], второе — в элемен- те myArray [ 1 ], и т.д. Второй цикл for выводит каждое значение на экран. JkiV_____ РИМ В языке C++ массивы начинаются с нулевого элемента Массивы начинаются с 0, а не с 1. Это обычная причина ошибок в про- граммах, написанных новичками. Используя массивы, помните, что мас- сив из десяти элементов начинается с элемента ArrayName[O] и заканчивается ArrayName [ 9 ], а элемент ArrayName [10] не использу- ется. Если бы в строках 6 и 11 предыдущей программы вместо оператора <5 был оператор <=5, то цикл вышел бы за пределы массива.
228 Часть IV. Дополнительные средства Запись данных за пределами массива При записи значения в элемент массива компилятор вычисляет необходимую об- ласть памяти на основании размера массива и размера типа элемента. Предположим, необходимо записать значение в переменную LongArray[5 ], являющуюся шестым элементом массива. Компилятор умножает индекс (5) на размер переменной (в дан- ном случае — 4). Затем текущий указатель смещается на 20 байтов от начального ад- реса массива, и записывается новое значение. Если попробовать записать значение в элемент LongArray [50], то компилятор, проигнорировав тот факт, что такого элемента не существует, вычислит смещение от начала массива (200 байтов), а затем запишет значение по этому адресу. Здесь могут оказаться другие данные, и запись нового значения будет иметь непредсказуемые по- следствия. Если повезет, программа зависнет сразу. Если нет, результаты проявятся намного позже и совершенно в другом месте. Обнаружить такую ошибку крайне сложно, даже если возникли подозрения, что что-то пошло не так. Подобно слепому, которого попросили отнести предмет в дом номер шесть, ком- пилятор двинется “вдоль стенки от дома к дому”, решив: “Необходимо пройти от первого дома (MainStreet [0]) пять зданий. Каждый дом — это четыре больших ша- га. Всего следует сделать 20 шагов”. Но если послать его на MainStreet [100], размер которой всего 25 зданий, то, сделав 400 шагов, несчастный может угодить под грузо- вик. Так что будьте внимательны, отправляя его. Ошибка последнего столба Ошибка записи данных за пределами массива встречается так часто, что для нее придумали собственный термин — ошибка последнего столба (fence post error). Сколько столбов необходимо для десятиметровой изгороди, если ставить их через каждый метр? Большинство людей, не задумываясь, ответят: “Десять”, но на самом деле необ- Рис. 15.2. Ошибка последнего столба Подобный тип подсчета (“минус один”) отравляет жизнь любому начинающему программисту. Но со временем к этому можно привыкнуть и запомнить, что массив из 25-ти элементов заканчивается двадцать четвертым номером, а начинается нулевым. Инициализация массивов Небольшой массив переменных встроенных типов (например, int или char) можно инициализировать при его объявлении. Для этого после имени массива ста- вят знак равенства (=) и заключенный в фигурные скобки список значений, отде- ляемых запятой, например: int IntegerArray[5] = { 10, 20, 30, 40, 50 }; Здесь объявлен массив IntegerArray из пяти целочисленных элементов, которым присвоены значения IntegerArray [ 0 ] — 10, IntegerArray [1] — 20, и т.д.
Час 15. Массивы 229 Если размер массива не указан, а список значений присутствует, то будет создан и инициализирован массив достаточного размера, чтобы содержать все перечисленные значения. Таким образом, эта строка аналогична предыдущей: int IntegerArray[] = { 10, 20, 30, 40, 50 }; Если размер массива все же необходим, можно позволить компилятору вычислить его самостоятельно, например: const int IntArrayLength = sizeof(IntArray)/sizeof(IntArrayfOJ); Здесь целочисленная константа IntArrayLength инициализируется результатом вычисления количества элементов массива, полученного при делении размера всего массива на размер одного элемента. Нельзя инициализировать количество элементов, превосходящее объявленный размер массива. int IntegerArray[5] = { 10, 20, 30, 40, 50, 60}; Такая строка приведет к ошибке во время компиляции, поскольку объявлен мас- сив для пяти элементов, а инициализировать пытались шесть. Но следующая запись вполне допустима. ' int IntegerArray[5] = {10, 20}; Массивы объектов Массив может хранить любые объекты: как встроенные, так и созданные пользова- телем. При объявлении массива компилятору сообщают тип и количество хранимых объектов, после чего он выделяет участок памяти необходимого размера. Компилятор определяет размер памяти, которая требуется для размещения одного элемента масси- ва (объекта), на основании объявления класса. Чтобы объекты могли быть созданы при определении массива, класс должен обладать стандартным конструктором, кото- рому не передают никаких аргументов. KtXff_______ Стандартный конструктор Не забывайте, что стандартный конструктор — это не тот конструк- тор, который предоставляет компилятор, а конструктор без парамет- ров, который может быть создан либо компилятором, либо програм- мистом. Доступ к данным-членам объектов, находящихся в массиве, осуществляется в два эта- па. Сначала, используя оператор индекса ([ ]), идентифицируют элемент массива, а затем с помощью точечного оператора (.) получают доступ к определенной переменной- члену. Листинг 15.2 демонстрирует создание массива из пяти объектов класса cat. Листинг 15.2. Файл obj array, срр. Создание массива объектов 0: // Листинг 15.2. Создание массива объектов 1: Ainclude <iostream> 2: 3: class Cat 4: { 5: public: 6: Cat() { itsAge = 1; itsWeight=5; } // Стандартный // конструктор 7: ~Cat() {} // Деструктор 8: int GetAge() const { return itsAge; } 9: int GetWeight() const { return itsWeight; }
230 Часть IV. Дополнительные средства 10 : void SetAge(int age) { itsAge = age; } 11: 12 : 13 : 14: 15: 16: 17 : 18: 19: 20: 21: 22: 23: 24: 25: 26: 27 : 28: private: int itsAge; int itsWeight; } ; int main() { Cat Litter[5]; II создать массив из пяти объектов int i ; for (i=0; i<5; i++) Litter[i].SetAge(2*i +1); for (i=0; i<5; i++) std::cout « “Cat #" << i+l<< ": " << Litter[i]-GetAge() << std::endl; return 0; } Результат cat #1: 1 cat #2: 3 cat #3: 5 cat #4: 7 cat #5: 9 Анализ Класс cat объявлен в строках 3—15. Для создания массива объектов класс Cat должен иметь стандартный конструктор. В данном случае стандартный конструктор объявлен и определен в строке 6. Не забывайте, что при наличии любого собствен- ного конструктора стандартный конструктор компилятором не создается. Поэтому не- обходимо создать собственный стандартный конструктор. В строке 19 создается мас- сив Litter из пяти объектов класса cat. Первый цикл for (строки 21—22) устанавливает возраст каждого из пяти объектов класса Cat в массиве. Второй цикл for (строки 24—26) обращается к каждому из эле- ментов и вызывает функцию GetAge (). Для этого при обращении к элементу массива Litter [i] необходимо использовать точечный оператор (.) и имя функции-члена. Многомерные массивы Массив можно представить как ряд или последовательность данных, а можно как таблицу, данные которой распределены по рядам и столбцам. Это двумерный массив, где одна размерность соответствует рядам, а вторая — столбцам. Трехмерный массив аналогичен кубу, где одна размерность представляет ширину,, вторая — высоту, тре- тья — глубину. Можно создавать массивы и более трех размерностей, хотя вообразить такой объект в пространстве очень тяжело При объявлении массива каждая размерность представляет собой дополнительный индекс. Следовательно, двумерный массив имеет два индекса, трехмерный — три, и т.д. Массив может иметь любое количество размерностей, но в большинстве случаев дос- таточно одной или двух. Примером двумерного массива может служить шахматная доска. Одна размер- ность представляет собой восемь рядов по горизонтали, другая — восемь рядов по вертикали (рис. 15.3).
Час 15. Массивы 231 Рис. 15.3. Шахматная доска как двумерный массив Предположим, существует класс square (клетка). В этом случае объявление мас- сива Board (доска) выглядело бы следующим образом: SQUARE Во ard[8] [83; Те же данные можно представить в виде одномерного массива на 64 клетки: SQUARE Board[64]; Но это не будет соответствовать реальной шахматной доске. В начале игры король стоит на четвертой клетке в первом ряду, эта позиция соответствует первому элементу по первому индексу и четвертому — по второму. Board[0 3 [ 3 ] ; Инициализация многомерных массивов Многомерные массивы также можно инициализировать. Элементам массива при- сваивается список значений таким образом, что вслед за последним значением пер- вого индекса массива начинается первое значение второго индекса, и т.дЛ int theArray[5][3]; Следовательно, если объявлен такой массив, то первые три элемента входят в эле- мент theArray [ 0 ], следующие три — в элемент theArray [ 1 ], и т.д. Инициализируется этот массив следующим образом: int theArray[5][3] = { 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15 ); Чтобы было понятнее, значения при инициализации можно разделить фигурными скобками, например, так: int theArray[5][3] = { { 1, 2, 3), { 4, 5, 6), { 7, 8, 9), {10,11,12) , {13,14,15) } ; Компилятор проигнорирует внутренние фигурные скобки, но они сделают набор чисел нагляднее и понятнее. Вне зависимости от фигурных скобок, каждое значение следует отделять запятыми. Весь набор значений для инициализации расположен внутри внешних фигурных ско- бок и завершается точкой с запятой. 'Т.е. двумерный массив разворачивается построчно. — Прим. ред.
232 Часть IV. Дополнительные средства В листинге 15.3 создается двумерный массив. Первая размерность — это набор чи- сел от 0 до 4, а вторая состоит из удвоенного значения в первой. Листинг 15.3. Файл multidimarray. срр. Создание многомерного массива 0: // Листинг 15.3. Создание многомерного массива 1: #include <iostream> 2 : 3: int main() 4: { 5: int SomeArray[5][2] = { {0,0}, {1,2}, {2,4}, {3,6}, {4,8}}; 6: for (int i=0; i<5; i++) 7: for (int j=0; j<2; j++) 8: { 9: std::cout « ”SomeArray[" << i << “][" << j << ’]: 10: std::cout « SomeArray[i][j]<< std::endl; 11: } 12: return 0; 13: } Результат SomeArray[0][0]: 0 SomeArray[0][1]: 0 SomeArray[1][0]: 1 SomeArray[1][1]: 2 SomeArray[ 2 ] [ 0 ] : 2 SomeArray[2][1]: 4 SomeArray[3][0]: 3 SomeArray[3][1]: 6 SomeArray[4][0]: 4 SomeArray[4][1]: 8 Анализ В строке 5 объявлен двумерный массив SomeArray. Первый ряд элементов состоит из двух целых чисел, а второй — из пяти. В результате получится сетка размером 2x5 элементов (рис. 15.4). SomeArray[5][2] Рис. 15.4. Массив 2x5 В этом коде исходные значения заданы непосредственно, но их также можно было бы вычислить. Строки 6—7 содержат два цикла for. Внешний цикл for перебирает все элементы по первой размерности. Для каждого элемента этой размерности внутренний цикл for перебирает все элементы второй размерности. Этот процесс сопровождается выводом значений на экран. За элементом SomeArray[0] [0] следует элемент SomeArray[0] [1], и т.д. Приращение значения по первой размерности происходит толь- ко после полного перебора второй. Затем перебор второй размерности начинается снова.
Час 15. Массивы 233 Немного о памяти При объявлении массива компилятору точно указывают, сколько объектов планиру- ется в нем сохранить. Компилятор зарезервирует память для всех объектов массива, да- же если они не будут использованы. Если известно заранее, сколько элементов должен хранить массив, то никаких проблем не возникнет. Например, шахматная доска всегда имеет 64 клетки, а у кошки не может быть больше 10 котят. Но если количество эле- ментов массива неизвестно, придется применить иные средства организации данных. В этой книге рассматриваются массивы указателей, массивы, находящиеся в дина- мической памяти, и другие типы структур. На занятии 19, “Связанные списки”, рас- сматривается такая усовершенствованная структура данных, как связанный список. Бо- лее сложные структуры данных, которые способны решать проблемы хранения больших объемов данных, в этой книге не рассматриваются. О программировании никогда нельзя узнать все. Всегда остается достаточно тем для изучения и книг для чтения. Массивы указателей До сих пор обсуждались массивы, члены которых размещались в стеке. Но размер стека значительно меньше объема динамической памяти, поэтому объекты массива можно объявить в динамической памяти, а в самом массиве хранить лишь указатели на них. Это существенно уменьшает объем используемой стековой памяти Лис- тинг 15.4 является модификацией листинга 15.2, но здесь элементы массива располо- жены в динамической памяти. Поскольку появилась возможность занять больший объем памяти, размер массива увеличен с пяти до пятисот элементов, а имя массива Litter (потомство) изменено на Family (семья). Листинг 15.4. Файл arrayonheap. срр. Размещение массива в динамической памяти 0: // Листинг 15.4. Массив указателей на объекты 1: #include <iostream> 2: 3: class Cat 4: { 5: public: 6: Cat ( ) { itsAge = 1; itsWeight = 5} // Стандартный // конструктор 7: -Cat() {) // Деструктор 8: int GetAge!) const { return itsAge; } 9; int GetWeightf) const { return itsWeight; } 10: void SetAge(int age) { itsAge = age; } 11: 12: private: 13: int itsAge; 14: int itsWeight; 15: }; 16: 17 : int main ( ) 18: { 19: Cat * Family [ 500 ] ,- 20: int i; 21: Cat * pCat; 22: for (i=0; i<500; i++) 23: { 24: pCat = new Cat; 25: pCat->SetAge(2*i +1); 26: Family[i] = pCat; 27: }
234 Часть IV. Дополнительные средства 28 : 29: for (i = 0; i<500; i++) 30: std::cout « "Cat #" « i+1 « ": " 31: << Family[i]->GetAge() << std::endl; 32 : 33: for (i = 0; i<500; i++) 34: { 35: delete Family[i]; 36: Family[i] = NULL; 37: } 38 : 39: return 0; 40: } Результат Cat #1: 1 Cat #2: 3 Cat #3: 5 Cat #499: 997 Cat #500: 999 Анализ Класс Cat, объявленный в строках 3—15, идентичен классу Cat, объявленному в листинге 15.3. А массив Family, объявленный в строке 19, на сей раз будет содер- жать до пятисот элементов, являющихся указателями на объекты класса Cat. Цикл инициализации (строки 22—27) создает в динамической памяти 500 новых объектов класса Cat, и для каждого из них устанавливается возраст вдвое больше его номера, увеличенного на единицу. Следовательно, первый кот будет иметь возраст 1, второй — 3, третий — 5, и т.д. Затем указатели добавляются в массив. Поскольку массив был объявлен как содержащий указатели, в него добавляются указатели, а не значения, расположенные по их адресам. Второй цикл (строки 29—31) выводит каждое значение на экран. Для доступа к указателю применяется индекс массива Family [i], а полученный адрес использует- ся для доступа к методу GetAge () текущего объекта. В данном случае сам массив Family и все его указатели хранятся в стеке, но 500 объек- тов класса Cat созданы и размещены в динамической области памяти. Это позволит создать до пятисот объектов класса Cat, при этом останется еще достаточно много памяти, чтобы создать вдвое большее количество объектов. В этом примере количест- во создаваемых объектов определяли параметры цикла for, но его может задавать и пользователь или требования программы. Третий цикл удаляет объекты класса Cat из распределяемой памяти и присваивает всем элементам массива Family значение null. Объявление массивов в области динамической памяти В области динамической памяти можно разместить весь массив, а не только соз- данные объекты. Для этого при объявлении массива используется оператор new. Ре- зультатом окажется указатель на пространство в динамической памяти, где и будет со- держаться массив, например: Cat *Family = new Cat[500];
Час 15. Массивы 235 Здесь объявлено, что Family будет указателем на первый элемент массива из пя- тисот объектов класса Cat. Иными словами. Family указывает на элемент Family[0] (или содержит его адрес). Преимуществом такого способа использования массива является возможность осуществлять арифметические операции над указателями для доступа к каждому эле- менту массива Family. Например, можно написать следующее: Cat *Family = new Cat[500]; Cat *pCat = Family; pCat->SetAge(10); pCat++; pCat->SetAge(20) ; // pCat указывает на FamilytO] // присвоить Family[0] значение 10 // вперед к Family[1] // присвоить Family[l] значение 20 Здесь в динамической памяти объявлен массив из пятисот объектов класса Cat, а также создан указатель, содержащий адрес начала массива. С помощью этого указателя происхо- дит вызов метода SetAge () первого из объектов класса Cat, который и присваивает ему значение 10. Затем указатель увеличивается и указывает на следующий объект класса Cat, поэтому при вызове метода SetAge () значение 20 будет присвоено второму объекту. Указатель на массив и массив указателей Рассмотрим следующие три объявления: 1: Cat FamilyOne[500]; 2: Cat * FamilyTwo[500]; 3: Cat * FamilyThree = new Cat[500]; Здесь FamilyOne — это массив из пятисот объектов класса Cat, FamilyTwo — мас- сив из пятисот указателей на объекты класса Cat, a FamilyThree — указатель на мас- сив из пятисот объектов класса Cat. Различие между этими тремя строками весьма существенно и влияет на способ существования массивов. Но самое удивительное то, что указатель FamilyThree явля- ется вариантом FamilyOne, а от указателя FamilyTwo отличается принципиально. В этом вся суть проблемы взаимосвязи указателей и массивов. В третьем случае FamilyThree представляет собой указатель на массив. Т е. адрес, находящийся в указателе FamilyThree, является адресом первого элемента в этом массиве. Но это аналогично тому, что имеет место для FamilyOne! Имена массивов и указателей В языке C++ имя массива является постоянным указателем на первый элемент массива. Cat Family[50]; Следовательно, здесь Family представляет собой указатель на переменную {.Family[0], являющуюся первым элементом массива Family. Вполне допустимо использовать имена массивов как постоянные указатели, и на- оборот Следовательно, Family + 4 — вполне законный способ доступа к данным в элементе Family [ 4 ]. При инкременте и декременте (увеличении и уменьшении) указателя компилятор делает все вычисления сам. Адрес, полученный в результате вычисления выражения Family + 4, больше исходного не на четыре байта, а на четыре объекта. Если все объекты имеют размер четыре байта, то Family + 4 составит 16 байтов от начала массива. Таким образом, если каждый объект класса Cat будет содержать четыре пе- ременные-члена типа long размером четыре байта каждая и две — типа short разме- ром по два байта, то размер каждого объекта класса Cat составит 20 байтов, a Family + 4 будет на 80 байтов больше Family (адреса начала массива).
236 Часть IV. Дополнительные средства Листинг 15.5 иллюстрирует объявление и использование массива в динамической памяти. Листинг 15.5. Файл newarray. срр. Создание массива в динамической памяти 0: 1: 2 : 3 : 4 : 5: 6: // Листинг 15.5. Массив в динамической памяти #include <iostream> class Cat { public: Cat ( ) { itsAge = 1; itsWeight=5; } // Стандартный // конструктор 7 : 8 : 9: 10: 11: 12 : 13 : 14: 15: 16: 17 : 18 : 19: 20: 21: 22 : 23 : 24: 25: 26: 27 : 28: 29 : 30: 31: 32 : 33 : 34 : 35: 36: 37 : 38: 39: 40: 41: 42 : -Cat(); // Деструктор int GetAge() const { return itsAge; } int GetWeight!) const { return itsWeight; ) void SetAge(int age) { itsAge = age; ) private: int itsAge; int itsWeight; ) ; Cat :: -Cat() { // std::cout « "Destructor called!\n"; ) int main() { Cat * Family = new Cat[500]; int i ; Cat * pCat; for (i=0; i<500; i++) { pCat = new Cat; pCat->SetAge(2*i +1); Family[i] = *pCat; delete pCat; } for (i=0; i<500; i++) std::cout « "Cat #" « i+1 « ": " << Family[i].GetAge() << std::endl; delete [] Family; return 0; ) Результат Cat Cat Cat #1: 1 #2: 3 #3: 5 Cat Cat #499: 997 #500: 999
Час 15. Массивы 237 Анализ В строке 24 объявлен массив Family, содержащий 500 объектов класса cat. Весь массив создан в динамической памяти с помощью выражения new Cat [500]. JfeKfflL црочки Чисто технически Чисто технически код строки 24 объявляет неименованный массив в распределяемой памяти и возвращает указателю Family адрес его перво- го элемента. Собственно, этот указатель и называют массивом. Фактиче- ски, когда объявляется любой массив (даже в стеке), его имя в действи- тельности является не более чем указателем на первый элемент. Каждый добавленный в массив объект класса Cat также создается в распределяе- мой памяти (строка 29). Обратите, однако, внимание: теперь в массив добавляется не указатель, а сам объект. Данный массив является не массивом указателей на объекты класса Cat, а массивом объектов класса Cat. Не забывайте освободить память, выделенную для массива при его создании, как это сделано в строке 39 при помощи оператора delete. Удаление массивов из динамической памяти Family — это указатель на новый массив объектов класса Cat в динамической па- мяти. Когда в строке 3I из листинга 15.5 происходит обращение к указателю pCat, сам объект класса Cat остается в массиве. (Почему бы и нет? Ведь массив расположен динамической памяти.) Но указатель pCat используется снова при следующей итера- ции цикла. Что происходит с выделенной для объектов класса Cat памятью, когда массив удаляется? Не происходит ли утечка? Удаление массива Family автоматически возвращает всю выделенную для него память, если оператор delete использован с квадратными скобками []. Компилятор вычисляет размер всех элементов массива и освобождает занимаемую им область ди- намической памяти. Чтобы продемонстрировать это, модифицируем файл newarray.cpp (листинг 15.5). Сначала изменим размер массива с 500 до 10 элементов в строках 24, 27 и 35, рас- комментируем в строке 19 оператор cout. По достижении строки 39 массив будет удален, а для каждого объекта класса Cat вызван деструктор. При удалении элемента, созданного с помощью оператора new в динамической памяти, непременно следует освободить занимаемую им область памяти. Создав в ди- намической памяти массив с помощью оператора new <класо [размер], впоследст- вии необходимо применить оператор delete []<класс>, чтобы освободить эту об- ласть памяти. Квадратные скобки сообщают компилятору о том, что удаляется массив. Если забыть квадратные скобки, то освобожден будет лишь первый объект масси- ва. В этом легко убедиться, удалив скобки в строке 39 файла newarray.cpp (листинг 15.5). Если отредактировать строку 21 так, чтобы при каждом вызове дест- руктора отображалось сообщение, то можно увидеть, что был освобожден только один объект класса Cat. Поздравляем! Только что произошла утечка памяти.
238 Часть IV. Дополнительные средства Рекомендуется Не рекомендуется Помнить, что массив из п элементов пронумерован от 0 до п-1. Применять для доступа к элементам массива индексы, а для доступа к указателям массива — точечный оператор. Записывать и читать данные вне пределов массива. Путать массив указателей с указателем на массив. Массивы символов Строка (string) — это массив символов. До сих пор в этой книге единственными строками были безымянные строковые константы, применяемые в операторах cout. cout << "hello world.\n"; В языке C++ строка представляет собой массив элементов типа char, завершаю- щийся пустым значением (null). До этого момента в книге единственными строками в стиле С были безымянные строковые константы. Строку можно объявить и инициа- лизировать, как и любой другой массив, например: char Greeting!] = { 'Н', е’, 'I', '1', 'О', ' 'W, -о’,'г',, 'd', '\0' }; Последний символ \0 • является пустым (null). Именно он служит для функций языка C++ признаком конца строки. Хотя такой “посимвольный” подход и работо- способен, но труден для вывода и порождает слишком много ошибок. Язык C++ до- пускает использование более кратких форм; например, объявление предыдущей стро- ки может выглядеть так: char Greeting!] = "Hello World”; Обратите внимание на следующие две особенности такого синтаксиса: вместо отдельных символов в одинарных кавычках, разделенных запятыми и окруженных фигурными скобками, применяются лишь двойные кавычки; добавлять символ null в конец строки не нужно, компилятор сделает это сам. Строка “Hello World” занимает 12 байтов, Hello — 5 байтов, пробел — 1, World — 5 и символ null — еще один. Можно также создавать и неинициализированные символьные массивы. Однако при этом следует удостовериться, что в этот буфер будет записано данных не больше, чем он может вместить. Применение неинициализированного буфера демонстрирует листинг 15.6. Листинг 15.6. Файл arrayf illed.cpp. Заполнение массива 0 : // Листинг 15.6. Массив как символьный буфер 1: 2 : 3 : 4 : 5: 6: 7 : #include <iostream> int main() { char buffer[80]; std::cout << "Enter the string: Std::cin >> buffer; 8: 9 : 10 : } std::cout << "Here's the buffer: " <•< return 0; : buffer << std::endl;
Час 15. Массивы 239 Результат Enter the string: Hello world Here’s the buffer: Hello Анализ В строке 5 объявлен буфер размером 80 символов. Он достаточно велик, чтобы со- держать 79-символьную строку и завершающий символ null. В строке 6 пользователю предлагают ввести строку, которая будет (в строке 7) вве- дена в буфер. Концевой символ null добавляется оператором cin. Здесь возникают две проблемы (см. листинг 15.6). Во-первых, если пользователь вводит строку длиннее 79 символов, оператор cin осуществит запись за пределами буфера. Во-вторых, если пользователь введет пробел, оператор cin воспримет его как конец строки и остановит запись в буфер. Для разрешения этих проблем необходимо создать специальный метод cin.get О, которому передают три параметра: буфер для заполнения; максимальное количество символов; символ для завершения ввода. По умолчанию критерием завершения ввода является символ новой строки. При- менение этого метода демонстрирует листинг 15.7. Листинг 15.7. Файл arrayf illed2 . срр. Заполнение массива 0: // Листинг 15.7. Применение cin.gett) 1: #include <iostream> 2: 3: int main() 4: { 5: char buffer[80]; 6: std::cout « "Enter the string: 7: std::cin.get(buffer, 79); // больше 79 или новая строка 8: std::cout << "Here's the buffer: " << buffer << std::endl; 9: return 0; 10: } Результат Enter the string: Hello World Here's the buffer: Hello World Анализ В строке 7 расположен вызов метода cin.get О. Буфер, объявленный в строке 5, передается в качестве первого аргумента. Второй аргумент — максимальное количест- во вводимых символов, в данном случае — 79, чтобы учесть завершающий символ null. Третий параметр (символ завершения ввода) необязателен, поскольку по умол- чанию признаком завершения является новая строка. Объект cin и все его разновид- ности рассматриваются на занятии 21, “Препроцессор”. Функции strcpyO и strncpy () Язык C++ унаследовал от языка С библиотеку функций для строковых операций. Существует множество встроенных функций, две из них копируют одну строку в другую: strcpyО и strncpyO. Функция strcpyO копирует все содержимое строки в указан- ный буфер, а функция strncpyO копирует определенное количество символов из од- ной строки в другую. Применение функции strcpyO демонстрирует листинг 15.8.
240 Часть IV. Дополнительные средства I Листинг 15.8. Файл usingstrcpy. срр. Применение функции strcpy () 0: // Листинг 15.8. Применение функции strcpy!) 1: ttinclude <iostream> 2: #include <string.h> 3 : 4: int main() 5: { 6: char Stringl[] = "No man is an island"; 7: char String2[80]; 8 : 9: strcpy(String2,Stringl) ; 10: 11: std::cout « "Stringl: " << Stringl << std::endl; 12: std::cout << "String2: " << String2 << std::endl; 13: return 0; 14: } Результат Stringl: No man is an island String2: No man is an island Анализ Файл заголовка string.h подключен в строке 2. Этот файл содержит прототип функции strcpy (), получающей два символьных массива: результирующий и исход- ный. Если исходный массив окажется больше результирующего, функция strcpy!) осуществит запись за пределами результирующего буфера. Чтобы избежать этой ошибки, стандартная библиотека располагает функцией strncpy!). Данному варианту передают и максимальное количество копируемых символов. Функция strncpy () осуществляет копирование до первого символа null или максимального количества символов, определенного для результирующего буфе- ра. Применение функции strncpy!) демонстрирует листинг 15.9. Листинг 15.9. Файл usingstrncpy. срр. Применение функции strncpy () 0: // Листинг 15.9. Применение функции strncpy!) 1: #include <iostream> 2: #include <string.h> 3 : 4: int main() 5: { 6: const int MaxLength = 80; 7: char Stringl[] = "No man is an island"; 8: char String2[MaxLength+1]; 9: 10: strncpy(String2,Stringl,MaxLength); 11: String2[strlen(Stringl)] = '\0‘; // добавить null в конец 12: std::cout « "Stringl: " « Stringl << std::endl; 13: std::cout « "String2: " « String2 << std::endl; 14: return 0; 15: > J
Час 15. Массивы 241 Результат Stringl: No man is an island String?: No man is an island Анализ В строке 10 обращение к функции strcpyO было заменено на обращение к функ- ции strncpyO, которой передают третий параметр — максимальное количество сим- волов для копирования. Буфер string? объявлен как массив из MaxLength+1 симво- лов. Дополнительный элемент предназначен для символа null, который обе функции, strcpy () и strncpy (), добавляют в конец строки автоматически. Строковые классы Совместимые со стандартом ANSI/ISO компиляторы C++ обладают библиотекой, содержащей обширный набор разнообразных классов, предназначенных для манипу- лирования данными. В стандартную библиотеку входит и класс string. Язык C++ унаследовал завершающий строку символ null от языка С вместе с со- держащей функцию strcpyO библиотекой функций, но эти функции не интегрирова- ны в объектно-ориентированную среду. Стандартная библиотека содержит класс string, инкапсулирующий специальный набор данных и функций для управления ими. От- крыты лишь функции доступа, а сами данные класса string от клиента скрыты. Если используется компилятор, отличный от предоставляемого с этой книгой, он может и не обладать классом string. Так или иначе, но может возникнуть необходи- мость в разработке собственного класса string. Как минимум, такой класс string не должен быть подвержен основным ограни- чениям символьных массивов. Подобно всем остальным массивам, символьные мас- сивы статичны. Их размер задается при объявлении и, независимо от того, какое ко- личество элементов массива используется, размер занимаемого участка памяти остается неизменным, а запись за пределами массива чревата неприятностями. Грамотно разработанный класс string занимает столько памяти, сколько необхо- димо для хранения переданных ему данных. Если класс не сможет выделить достаточ- ного количества памяти, следует предусмотреть элегантный выход из этой ситуации. Вопросы и ответы Что случается, если присвоить значение элементу номер 25 в массиве из 24-х эле- ментов? Произойдет запись в область памяти, не относящуюся к массиву. Результат для программы может оказаться непредсказуем. Что находится в неинициализированном элементе массива? Любое значение, находившееся в этой области памяти. Результаты использова- ния неинициализированного элемента непредсказуемы. Можно ли объединять массивы? Да. Для объединения массивов можно использовать указатели. Объединять строки еще проще, для них можно использовать встроенные функции, напри- мер, strcat().
242 Часть IV. Дополнительные средства Коллоквиум Изучив возможности массивов, имеет смысл ответить на несколько вопросов и выполнить ряд упражнений, чтобы закрепить полученные знания. Контрольные вопросы 1. Каковы номера первого и последнего элементов массива? 2. Что случается при попытке сохранить данные в элементе массива, индекс кото- рого превосходит максимально допустимый? 3. В чем преимущество массивов? 4. Где можно узнать больше о классе string языка C++? Упражнения 1. Измените файл arrayonheap. срр (листинг 15.4) так, чтобы избежать примене- ния указателя pCat. 2. Измените исходный файл arrayonheap. срр (листинг 15.4) так, чтобы вместо литерала 500 использовать целочисленную константу maxsize. Уменьшите значение количества элементов и запустите программу на выполнение. Что случится, если удалить ключевое слово const? Массивы в языке C++ не явля- ются динамическими. 3. Что случится, если в упражнении 2 установить для константы maxsize значе- ние 200, но заменить не все литералы 500 константой maxsize? 4. Измените файл usingstrncpy.cpp (листинг 15.9) так, чтобы в строке 10 вместо MaxLength было значение 5. Откомпилируйте и запустите программу на выпол- нение. Сравните результат с первоначальным. Это наглядно демонстрирует всю мощь функции strncopy (), способной копировать лишь часть строки. Ответы на контрольные вопросы 1. Все массивы начинаются с нулевого элемента. Номер последнего элемента (последний допустимый индекс) — это размер массива минус 1. Последним элементом массива, объявленного как Аг [ 5 0 ], будет Аг [ 4 9 ]. 2. Точно сказать нельзя, но если повезет, то обращение произойдет к той области памяти, которую операционная система не согласится предоставить. В резуль- тате будет передано сообщение об ошибке. Если не повезет, то противоестест- венным способом будет изменено значение другой переменной, а, следователь- но, ошибка проявится в совершенно другом месте. Найти такую ошибку очень трудно. Однажды автор потратил весь день на поиск причины проблемы, кото- рой на самом деле не было. 3. Способность обращаться к группе взаимосвязанных элементов данных, исполь- зуя только одно имя переменной. 4. Воспользуйтесь справочной системой компилятора. Для компилятора Borland, например, достаточно выбрать в меню Help (Помощь) пункт Help Topics (Темы).
ЧАСТЬV Наследование и ПОЛИМОРФИЗМ В этой части... Час 16. Наследование Час 17. Полиморфизм и производные классы Час 18. Расширенное наследование Час 19. Связанные списки
ЧАС 16 Наследование На этом занятии вы узнаете: что такое наследование; как из одного класса получать другой; как получить доступ к базовым методам из производного класса; как переопределять базовые методы; что такое защищенный доступ и как его использовать. Что такое наследование? Фундаментальным аспектом человеческого интеллекта является способность изу- чать объекты и находить взаимоотношения между ними. Чтобы понять и объяснить способы взаимодействия объектов, создают иерархии, матрицы, сети и другие модели взаимосвязей. В языке C++ это реализуется в иерархиях наследования. Что такое собака? Что видишь, когда смотришь на домашнего питомца? Автор ви- дит четыре лапы и зубастую пасть. Биолог увидит систему взаимодействующих орга- нов, физик — упорядоченную систему атомов и совокупность различных видов энер- гии, а ученый, занимающийся систематикой млекопитающих, — типичного представителя вида canine domesticus (собака домашняя). А мать автора видит источник собачьей шерсти и слюней. Каждый смотрит на объект со своей точки зрения, но сегодня рассматривается предпоследнее утверждение, а именно: собака является представителем семейства волчьих, класса млекопитающих, и т.д. С точки зрения систематики любой объект живой природы рассматривается в плане принадлежности к иерархической системе: царству, типу, классу, отряду, семейству, роду и виду. В основе этой иерархии лежит принцип специализации и обобщения, устанавли- вающий взаимосвязь типа “принадлежит" или ‘'является”. Например, Homo sapiens принадлежит к приматам. Этот тип взаимосвязи виден повсюду: фургон является раз- новидностью автомобиля, который в свою очередь является разновидностью транс- портного средства. Фруктовое мороженое является разновидностью десерта, который в свою очередь является разновидностью пищи.
Час 16. Наследование 245 Таким образом, когда говорят, что нечто является разновидностью чего-то, то под- разумевают, что первое является специализацией второго. Т.е. автомобиль — специ- альный вид транспортного средства. Наследование и происхождение Говоря о собаке как о представителе класса млекопитающих, подразумевают, что она наследует все признаки, общие для класса млекопитающих. Поскольку собака — млеко- питающее, можно предположить, что это подвижный вид животных, дышащих возду- хом. Все млекопитающие по определению подвижны и дышат воздухом. Поскольку это собака, добавляется способность лаять, вилять хвостом и т.п. Собак можно условно разделить на охотничьих и терьеров, а терьеров — на йорк- ширских, эрделей, фоксов и т.п. Йоркширский терьер является разновидностью собаки, которая является разно- видностью млекопитающего, а, следовательно, и разновидностью животного. Эта ие- рархия представлена на рис. 16.1. Рис. 16.1. Иерархия млекопитающих В языке C++ подобная иерархия реализована в концепции классов, где один класс может происходить или наследовать от класса более высокого уровня. Происхождение (derivation) — это способ выражения связи “является” (is-a). Если новый класс Dog (собака) происходит от класса Mammal (млекопитающее), то нет необходимости указы- вать явно, что собака может бегать, поскольку она автоматически наследует это свой- ство от млекопитающих. Класс, который добавляет новые функциональные возможности к уже существую- щему, называют производным (derive) от базового (base), или исходного, класса. Если класс Dog происходит от класса Mammal, то класс Mammal будет базовым для класса Dog. Производные классы являются расширением своих базовых. Таким обра- зом, класс Dog расширяет возможности класса Mammal, добавляя к нему ряд новых методов или данных.
246 Часть V. Наследование и полиморфизм Как правило, базовый класс имеет более одного производного класса. Поскольку собаки, кошки и лошади являются разновидностью млекопитающих, их классы долж- ны были бы происходить от класса Mammal. В мире животных Чтобы легче понять концепции происхождения и наследования, в этой главе ос- новное внимание уделяется взаимосвязи многочисленных классов мира животных. Предположим, программисту поступил заказ на создание детской игры “Ферма”. Приступая к созданию классов обитающих на ферме животных (лошадей, коров, со- бак, кошек, овец и других), каждый класс потребуется снабдить такими методами, благодаря которым они смогут вести себя на экране так, как этого ожидает ребенок. Но на данном этапе каждый метод снабжен только функцией вывода на экран. Упрощение (stubbing out) функций — это обычная практика программирования, когда сначала создается лишь макет набора методов, а детальная их проработка откла- дывается на потом. Пожалуйста, не стесняйтесь дополнять весьма ограниченный код примеров этой главы, чтобы позволить животным действовать более реалистично. Синтаксис происхождения классов При объявлении класс можно указать как производный от другого. Для этого по- сле ключевого слова class указывают имя нового класса, а после двоеточия — имя базового с указанием его типа (открытый (public) или иной). class производныйКласс : типДоступа базовыйКласс Например, если необходимо создать новый класс Dog, который происходит от класса Mammal, применяется следующий код: class Dog : public Mammal Тип происхождения (типДоступа) обсуждается несколько позже в этой главе. Класс, от которого происходит новый класс, должен быть объявлен ранее, иначе ком- пилятор выдаст сообщение об ошибке. В листинге 16.1 представлено объявление класса Dog (собака), производного от Mammal (млекопитающее). Листинг 16.1. Файл simpleinherit. срр. Простое наследование 0: // Листинг 16.1. Простое наследование 1: #include <iostream> 2 : 3: enum BREED { YORKIE, CAIRN, DANDIE, SHETLAND, DOBERMAN, LAB }; 4 : 5: class Mammal 6: { 7: public: 8: // Конструкторы 9: Mamma1(); 10: -Mammal(); 11: 12: // Методы доступа 13: int GetAge() const; 14: void SetAge(int); 15: int GetWeight() const; 16: void Setweight () ,- 17 : 18: // Другие методы
Час 16. Наследование 247 19: void Speak(); 20: void Sleep(); 21: 22: 23: protected: 24: int itsAge; 25: int itsWeight; 26: }; 27: 28: class Dog : public Mammal 29: { 30: public: 31: // Конструкторы 32: Dog () ; 33: -Dog(); 34: 35: // Методы доступа 36: BREED GetBreed() const; 37: void SetBreed(BREED); 38: 39: // Другие методы 40: // WagTail(); 41: // BegForFood(); 42: 43: protected: 44: BREED itsBreed; 45: }; 46 : int main() 47: { 48: return 0; 49: ) Анализ Эта программа на экран ничего не выводит, поскольку представляет собой лишь набор объявлений классов без реализации. Тем не менее, здесь есть на что обратить внимание. Класс Mammal объявлен в строках 5—26. Обратите внимание, что в данном случае класс Mammal не происходит ни от какого другого класса, хотя в реальной жизни можно сказать, что класс млекопитающих происходит от класса животных. Но в С++ всегда отображается не весь окружающий мир, а лишь модель некоторой его части. Действительность слишком сложна и разнообразна, чтобы можно было отобразить ее в одной, даже очень большой программе. Неизвестно откуда берет свое начало иерархическая структура реального мира, но конкретная программа начинается с класса Mammal. В связи с этим некоторые пере- менные-члены, которые необходимы для работы базового класса, должны быть пред- ставлены в объявлении наивысшего базового. Например, все животные, независимо от вида и породы, имеют возраст и вес. Если бы класс Mammal происходил от класса Animal (животное), можно было бы ожидать, что он унаследует от него эти атрибуты. Атрибуты базового класса всегда становятся атрибутами производного. Чтобы облегчить работу с программой и ограничить ее сложность разумными рам- ками, в классе Mammal представлены только шесть методов: четыре метода доступа, а также функции Speak () (голос) и Sleep () (спать).
248 Часть V. Наследование и полиморфизм Класс Dog в строке 28 наследует класс Mammal. Каждый объект класса Dog будет иметь три переменные-члена: itsAge (его возраст), itsWeight (его вес) и itsBreed (его порода). Обратите внимание, что в объявлении класса Dog переменные itsAge и itsWeight не указаны. Объекты класса Dog унаследовали их из класса Mammal вместе с объявленными в нем методами, за исключением оператора копий, конст- руктора и деструктора. Закрытый или защищенный В строках 23 и 43 листинга 16.1 появляется новое ключевое слово protected (защищенный). До сих пор данные класса определялись с ключевым словом private (закрытый). Но члены класса, объявленные как private, недоступны для наследова- ния. Конечно, в этом листинге переменные-члены itsAge и itsWeight можно было объявить как public (открытый), но это нежелательно, поскольку прямой доступ к этим переменным получили бы все другие классы программы. Обозначение protected гласит: “Сделать эти члены класса видимыми и данному классу, и классам, производным от него”. В отличие от закрытых, защищенные пере- менные-члены и функции полностью видимы производным классам. Назначение защищенных членов состоит в следующем: сделать переменную-член доступной внутри этого класса и внутри всех его производных. Таково назначение всех защищенных членов — быть полностью доступными внутри самого класса и его потомков, но недоступными снаружи. Существуют три модификатора доступа: public (открытый), protected (защищенный) и private (закрытый). Если функция работает с объектом определен- ного класса, она может обращаться ко всем его открытым переменным-членам и функциям. Функции-члены в свою очередь способны обращаться ко всем закрытым переменным-членам, функциям их собственного класса и всем защищенным пере- менным-членам и функциям любого класса, от которого они происходят. Таким образом, функция Dog: :WagTail () (собака::виляет хвостом) может обра- щаться к закрытой переменной-члену itsBreed и защищенным переменным-членам, унаследованным от класса Mammal. Даже если бы класс Dog происходил не непосредственно от класса Mammal, а от какого-нибудь промежуточного класса (например, DomesticAnimals (домашние жи- вотные)), то все равно из класса Dog сохранился бы доступ к защищенным данным класса Mammal, правда, только в том случае, если бы класс Dog и все промежуточные классы использовали открытое наследование. Листинг 16.2 демонстрирует создание объекта класса Dog, а также пример обраще- ния к его данным и функциям. Листинг 16.2. Файл derivedobj ect. срр. Использование объекта производного класса 0: // Листинг 16.2. Использование объекта производного класса 1: #include <iostream> 2 : 3: enum BREED { YORKIE, CAIRN, DANDIE, SHETLAND, DOBERMAN, LAB } ; 4 : 5: class Mammal 6: { 7: public: 8: // Конструкторы 9: Mammal():itsAge (2), itsWeight(5) {} 10: -Mammal() {) 11:
Час 16. Наследование 249 12: // Методы доступа 13: int GetAge() const { return itsAge; } 14: void SetAge(int age) { itsAge = age; } 15: int GetWeight() const { return itsWeight; } 16: void SetWeight(int weight) { itsWeight = weight; ) 17: 18: // Другие методы 19: void Speak() const { std::cout « "Mammal sound!\n"; } 20: void Sleep() const { std::cout << “Shhh. I’m sleeping.\n“; } 21: 22: 23: protected: 24: int itsAge; 25: int itsWeight; 26: }; 27: 28: class Dog : public Mammal 29: { 30: public: 31: // Конструкторы 32: Dog():itsBreed(YORKIE) {} 33: -Dog() {) 34: 35: // Методы доступа 36: BREED GetBreed() const { return itsBreed; ) 37: void SetBreed(BREED breed) { itsBreed = breed; } 38: 39: // Другие методы 40: void WagTail() { std::cout << “Tail wagging...\n”; } 41: void BegForFood() { std::cout << "Begging for food...\n"; ) 42: 43: private: 44: BREED itsBreed; 45: }; 46: 47: int main() 48: { 49: Dog Fido; 50 : Fido.Speak() ; 51: Fido.WagTail(); 52: std::cout << "Fido is " << Fido.GetAge() << " years old\n"; 53: return 0; 54: ) Результат Mammal sound! Tail wagging... Fido is 2 years old Анализ В строках 5—26 объявлен класс Mammal (для краткости тела функций вставлены по месту их вызова). В строках 28—45 класс Dog объявлен как производный от класса
250 Часть V. Наследование и полиморфизм Mammal. Таким образом (и в соответствии с этим объявлением), все собаки будут иметь возраст, вес и породу. В строке 49 создается экземпляр класса Dog по имени Fido. Объект Fido наследу- ет все атрибуты класса Mammal, а также все атрибуты класса Dog. Таким образом, объ- ект Fido умеет не только вилять хвостом (wagTailO), но и подавать голос (Speak ()), И спать (Sleep ()). Конструкторы и деструкторы Объекты класса Dog одновременно являются и объектами класса Mammal. В этом и состоит сущность связи “является”. Когда создается объект Fido, сначала происхо- дит вызов конструктора, унаследованного от класса Mammal. Затем следует вызов кон- структора класса Dog, завершающий создание объекта Fido. Поскольку при создании объекта Fido ему не были переданы никакие параметры, то будет выбран стандарт- ный конструктор. Объект Fido не существует до тех пор, пока полностью не будет за- вершено его создание с использованием обоих конструкторов (классов Mammal и Dog). Таким образом, необходимо вызвать оба конструктора. При удалении объекта Fido из памяти компьютера сначала происходит вызов де- структора класса Dog, а затем — деструктора класса Mammal. Каждый деструктор уда- ляет из памяти ту часть объекта, которая была создана соответствующим конструкто- ром производного или базового класса. Не забывайте удалять объекты! Листинг 16.3 демонстрирует вызов конструкторов и деструкторов. Листинг 16.3. Файл conanddest. срр. Вызов конструкторов и деструкторов 0: // Листинг 16.3. Вызов конструкторов и деструкторов 1: #include <iostream> 2 : 3: enum BREED { YORKIE, CAIRN, DANDIE, SHETLAND, DOBERMAN, LAB }; 4 : 5: class Mammal 6: { 7: public: 8: // Конструкторы 9: Mammal(); 10: -Mammal(); 11: 12: // Методы доступа 13: int GetAge() const { return itsAge; } 14: void SetAge(int age) { itsAge = age; } 15: int GetWeight() const { return itsWeight; } 16: void Setweight(int weight) { itsWeight = weight; ) 17: 18: // Другие методы 19: void Speak() const { std::cout << "Mammal sound!\n"; ) 20: void Sleep!) const { std:-.cout « "Shhh. I’m sleeping.\n"; ) 21: 22: protected: 23: int itsAge; 24: int itsWeight; 25: }; 26: 27: class Dog : public Mammal 28: { 29: public:
Час 16. Наследование 251 30: // Конструкторы 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: Dog(); -Dog(); II Методы доступа BREED GetBreed() const { return itsBreed; } void SetBreed(BREED breed) { itsBreed = breed; } // Другие методы void WagTail() { std::cout << "Tail wagging...\n"; } void BegForFoodl) { std::cout « "Begging for food...\n"; } 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: private: BREED itsBreed; }; Mammal::Mammal(): itsAge(1), itsWeight(5) { std::cout « “Mammal constructor...\n"; } Mammal::-Mammal() { std::cout << "Mammal destructor...\n"; } Dog::Dog(): itsBreed(YORKIE) { std::cout « “Dog constructor...\n"; ) Dog::-Dog() { std::cout « “Dog destructor...\n“; } 69: 70: 71: 72: 73: 74: 75: 76: int main() { Dog Fido; // Создать объект класса Dog Fido.Speak(); Fido.WagTail(); std::cout « “Fido is " « Fido.GetAge() « " years old\n"; return 0; } Результат Mammal constructor... Dog constructor... Mammal sound! Tail wagging... Fido is 1 years old Dog destructor... Mammal destructor...
252 Часть V. Наследование и полиморфизм Анализ Листинг 16.3 похож на листинг 16.2, за исключением того, что конструкторы и де- структоры при вызове выводят на экран сообщение о себе. При создании объекта Fido сначала происходит вызов конструктора класса Mammal, а затем — класса Dog. После того как объект класса Dog создан полностью, происходит вызов его методов. Когда объект Fido выходит из области видимости, для его уничтожения вызываются деструкторы: сначала класса Dog, а затем класса Mammal. Передача аргументов в базовые конструкторы Не исключено, что придется перегрузить конструктор класса Mammal так, чтобы устанавливать определенный возраст, а конструктор класса Dog — так, чтобы задавать породу. Как получить возраст и вес, переданные в первом конструкторе класса Mammal? Что, если в конструкторе класса Dog необходимо инициализировать вес, а конструктор класса Mammal этого не делает? Инициализацию базового класса можно осуществить в процессе инициализации производного. Для этого необходимо указать имя базового класса и список ожидае- мых им параметров, как показано в листинге 16.4. Листинг 16.4. Файл overloadderive. срр. Перегрузка конструкторов в производных классах 0: 1: 2 : 3 : 4 : 5: 6: 7 : 8 : 9 : 10: 11: 12 : 13 : 14 : 15: 16: 17: 18: 19: 20: 21: 22 : 23: 24 : 25: 26: 27: 28: // Листинг 16.4. Перегрузка конструкторов в производных // классах #include <iostream> enum BREED { YORKIE, CAIRN, DANDIE, SHETLAND, DOBERMAN, LAB}; class Mammal { public: 11 Конструкторы Mammal(); Mammal(int age); -Mammal(); // Методы доступа int GetAge() const { return itsAge; } void SetAge(int age) { itsAge = age; } int GetWeight() const { return itsWeight; } void SetWeight(int weight) { itsWeight = weight; } // Другие методы void Speak() const { std::cout << "Mammal sound!\n”; } void Sleep() const { std::cout << "Shhh. I'm sleeping.\n"; } protected: int itsAge; int itsWeight; ) ; class Dog : public Mammal
Час 16. Наследование 253 29: { 30: public: 31: // Конструкторы 32: Dog(); 33: Dog(int age); 34: Dog(int age, int weight); 35: Dog(int age, BREED breed); 36: Dog(int age, int weight, BREED breed); 37: -Dog(); 38: 39: // Методы доступа 40: BREED GetBreed() const { return itsBreed; } 41: void SetBreed(BREED breed) { itsBreed = breed; } 42: 43: // Другие методы 44: void WagTailO { std::cout « "Tail wagging ...\n"; } 45: void BegForFood() { std::cout << "Begging for food...\n"; } 46: 47: private: 48: BREED itsBreed; 49: }; 50: 51: Mammal::Mammal(): 52 : itsAge(1) , 53: itsWeight(5) 54: { 55: std::cout << "Mammal constructor...\n"; 56: } 57: 58: Mammal::Mammal(int age): 59 : itsAge(age), 60: itsWeight(5) 61: { 62: std::cout << "Mammal(int) constructor...\n" ; 63: } 64: 65: Mammal::-Mammal() 66: { 67: std::cout << "Mammal destructor...\n"; 68: } 69: 70 : Dog: :Dog() : 71: Mammal(), 72: itsBreed(YORKIE) 73: { 74: std::cout << "Dog constructor...\n" ; 75: } 76: 77: Dog::Dog(int age): 78 : Mammal(age) , 79: itsBreed(YORKIE) 80: { 81: std::cout << "Dog(int) constructor ...\n“ ; 82: } 83: 84: Dog::Dog(int age, int weight):
254 Часть V. Наследование и полиморфизм 85: Mammal(age), 86: 87: 88: 89: 90: 91 : 92 : 93: 94: 95: 96: 97: 98: itsBreed(YORKIE) { itsWeight = weight; std::cout << "Dog(int, int) constructor...\n" ; ) Dog::Dog(int age, int weight, BREED breed): Mammal(age), itsBreed(breed) { itsWeight = weight; Std::cout << "Dog(int, int, BREED) constructor...\n" ; } 99 : 100 : 101 : 102 : 103 : 104 : 105: 106 : 107 : 108: 109: 110: 111: 112 : 113 : 114 : 115: 116: 117: 118: 119: 120: 121 : 122 : 123 : 124 : 125: 126 : Dog::Dog(int age, BREED breed): Mammal(age), itsBreed(breed) { std::cout << "Dog(int, BREED) constructor ...\n"; } Dog::-Dog() { std::cout << "Dog destructor...\n"; ) int main() { Dog Fido; Dog Rover(5); Dog Buster(6,8); Dog Yorkie (3,YORKIE); Dog Dobbie (4,20,DOBERMAN); Fido.Speak(); Rover.WagTail() ; std::cout << "Yorkie is " << Yorkie.GetAge() << “ years old\n"; std::cout << "Dobbie weighs " << Dobbie.GetWeight() << ” pounds\n"; return 0; ) Между______ прачки Нумерация строк результата Строки результата были пронумерованы специально, чтобы на них можно было ссылаться при анализе. Результат 1: Mammal constructor... 2: Dog constructor... 3: Mammal(int) constructor... 4: Dog(int) constructor... 5: Mammal(int) constructor... 6: Dog(int, int) constructor...
Час 16. Наследование 255 7: Mammal(int) constructor... 8: Dog(int, BREED) constructor... 9: Mammal(int) constructor... 10: Dog(int, int, BREED) constructor... 11: Mammal sound! 12: Tail wagging... 13: Yorkie is 3 years old 14: Dobbie weighs 20 pounds 15: Dog destructor. . . 16: Mammal destructor... 17: Dog destructor... 18: Mammal destructor... 19: Dog destructor... 20: Mammal destructor... 21: Dog destructor... 22: Mammal destructor... 23: Dog destructor... 24: Mammal destructor... Анализ В листинге 16.4 конструктор класса Mammal перегружен в строке 10 таким образом, чтобы ему можно было передать целочисленное значение возраста животного. Его реализация в строках 58—63 инициализирует переменную itsAge значением, пере- данным в конструктор в качестве параметра, а переменную itsWeight — значением 5. Класс Dog в строках 32—36 содержит объявления пяти перегруженных конструкто- ров. Первый конструктор — стандартный. Второму передают возраст, используя для этого тот же параметр, что и у конструктора класса Mammal. Третьему передают воз- раст и вес, четвертому — возраст и породу, а пятому — возраст, вес и породу. Обратите внимание, что в строке 71 стандартный конструктор класса Dog вызывает стандартный конструктор Mammal (). Хотя в этом нет необходимости, подобная запись лишний раз свидетельствует о намерении вызвать конструктор базового класса имен- но без параметров. Конструктор базового класса будет вызван в любом случае, но в данной строке это было сделано явно. Реализация конструктора Dog (), которому передают целое число, находится в стро- ках 77-82. При инициализации (строки 78 и 79) класс Dog инициализирует свой ба- зовый класс, передавая его конструктору в виде параметра значение возраста, а затем инициализирует породу. Другой конструктор класса Dog находится в строках 84—90. Этому конструктору пе- редают два параметра. Первое значение вновь инициализируется при обращении к соот- ветствующему конструктору базового класса, тогда как значение второго взято из пере- менной itsWeight базового класса. Обратите внимание, что присвоение значения переменной базового класса не может осуществляться на стадии инициализации конст- руктора производного класса. Таким образом, следующий код некорректен: Dog::Dog(int age, int weight): Mammal(age), itsBreed(YORKIE), itsWeight(weight) // Ошибка! { std::cout << "Dog(int, int) constructor...\n"; ) Следовательно, в базовом классе инициализировать значение нельзя. Точно так же недопустим следующий код:
256 Часть V. Наследование и полиморфизм Dog::Dog(int age, int weight): Mammal(age, weight), // Ошибка! itsBreed(YORKIE) { std::cout << "Dog(int, int) constructor...\n" ; ) Поскольку в классе Mammal нет конструктора, присваивающего значение этой пере- менной, присвоение значения должно выполняться в теле конструктора класса Dog. Dog::Dog(int age, int weight): Mammal(age), // Базовый конструктор itsBreed(YORKIE) // Инициализация { itsWeight = weight; // Присвоение Самостоятельно проанализируйте работу остальных конструкторов в программе, чтобы закрепить полученные знания. Обратите внимание на то, какие переменные можно инициализировать одновременно с инициализацией конструктора, а какие - в теле конструктора. Для удобства анализа работы программы строки результата пронумерованы. Пер- вые две строки соответствуют инициализации объекта Fido с помощью стандартных конструкторов. Строки 3—4 соответствуют созданию объекта Rover, а строки 5—6 — объекта Buster. Обратите внимание, что в последнем случае из конструктора класса Dog с двумя цело- численными параметрами вызывается конструктор класса Mammal, обладающий одним целочисленным параметром. Созданные объекты используются, а затем выходят из области видимости. Удале- ние каждого объекта сопровождается обращением сначала к деструктору класса Dog, а затем — к деструктору класса Mammal. Таким образом осуществляется перегрузка методов (функций) базового класса внутри производного. Переопределение функций Объект класса Dog имеет доступ ко всем функциям-членам класса Mammal, а также к любой функции-члену, объявление которой добавлено в класс Dog, например, к функции WagTaill (). Кроме того, любую функцию базового класса можно переоп- ределить. Переопределение функции заменяет ее реализацию в базовом классе реали- зацией в производном. Таким образом, после создания объекта производного класса будет использоваться новая функция. Когда в производном классе создается функция с тем же именем, типом возвра- щаемого значения и сигнатурой, что и у функции-члена базового класса, но с новой реализацией, такая функция считается переопределенной (overriding). При переопределении функции ее сигнатура должна согласоваться с сигнатурой, указанной в базовом классе. Сигнатура — это’ прототип функции без типа возвращае- мого значения, т.е. она включает имя функции, список параметров и ключевое слово const, если оно используется. Под сигнатурой (signature) понимают имя функции со списком параметров. В сиг- натуру не входит тип возвращаемого значения. В листинге 16.5 продемонстрировано переопределение в классе Dog функции Speak(), объявленной в классе Mammal. Для экономии места знакомые по предыду- щим листингам объявления методов доступа в этом примере пропущены.
Час 16. Наследование 257 Листинг 16.5. Файл baseoverride. срр. Переопределение метода базового класса в производном 0: // Листинг 16.5. Переопределение метода базового класса в // производном 1: #include <iostream> 2: 3: enum BREED { YORKIE, CAIRN, DANDIE, SHETLAND, DOBERMAN, LAB }; 4: 5: class Mammal 6: { 7: public: 8: // Конструкторы 9: Mammal() { std::cout << "Mammal constructor...\n"; } 10: -Mammal() { std::cout << "Mammal destructor...\n"; } 11: 12: // Другие методы 13: void Speak() const { std::cout << "Mammal sound!\n"; } 14: void Sleep() const { std::cout << “Shhh. I'm sleeping.\n"; } 15: 16: protected: 17: int itsAge; 18: int itsWeight; 19: }; 20: 21: class Dog : public Mammal 22: { 23: public: 24: // Конструкторы 25: Dog() { std::cout << "Dog constructor...\n"; } 26: -Dog() { std::cout << "Dog destructor...\n”; } 27: 28: // Другие методы 29: void WagTail() { std::cout << "Tail wagging...\n"; } 30: void BegForFood() { std::cout << "Begging for food...\n";} 31: void Speak() const { std::cout << "Woof!\n"; } 32: 33: private: 34: BREED itsBreed; 35: }; 36: 37: int main ( ) 38: { 39: Mammal bigAnimal; 40: Dog Fido; 41: bigAnimal.Speak(); 42: Fido . Speak () ; 43: return 0; 44: } Результат Mammal constructor... Mammal constructor. . . Dog constructor. . . Mammal sound!
258 Часть V. Наследование и полиморфизм Woof ! Dog destructor... Mammal destructor... Mammal destructor.. . Анализ В классе Dog метод Speak () переопределен (строка 31), что заставляет объекты класса Dog отображать на экране при его вызове слово Woof! (Гав!). В строке 39 соз- дается объект bigAnimal (большое животное) класса Mammal, и в результате вызова его конструктора на экран выводится первая строка. В строке 40 создается объект Fido класса Dog, что сопровождается последовательным вызовом сначала конструкто- ра класса Mammal, а затем конструктора класса Dog. В строке 41 объект обращается к методу Speak () класса Mammal, а в строке 42 нахо- дится вызов того же метода, но уже объекта класса Dog. На экран при этом выводятся разные сообщения, так как метод Speak () в классе Dog переопределен. И, наконец, оба объекта выходят из области видимости, после чего срабатывают их деструкторы. Перегрузка или переопределение? Процессы, которые они обозначают, несколько схожи. При перегрузке создаются несколько одноименных методов, различающихся сигнатурой, а при переопределении в производном классе создается метод с тем же именем, что и в базовом классе, при той же сигнатуре. Сокрытие метода базового класса В предыдущем листинге метод Speak () класса Dog скрывал одноименный метод базового класса. Казалось бы, это то, что нужно, однако результаты могут быть не- предвиденными. Если класс Mammal, например, имеет перегруженный метод Move О (двигаться), который в классе Dog будет переопределен, то метод класса Dog скроет все одноименные методы класса Mammal. Предположим, метод Move () перегружен в классе Mammal трижды: первому, стандартному, никаких параметров не передают, второму передают один целочис- ленный параметр, а третьему — два целых числа. Если класс Dog переопределяет лишь стандартный метод Move (), который не получает никаких параметров, то ос- тальные два метода в объектах класса Dog окажутся недоступными. Эту проблему иллюстрирует листинг 16.6. Листинг 16.6. Файл hidingbase. срр. Сокрытие методов 0: // Листинг 16.6. Сокрытие методов 1: 2: #include <iostream> 3: 4: class Mammal 5: { 6: public: 7: void Move() const { std::cout << "Mammal move one step\n"; } 8: void Movetint distance) const { std::cout << 9: "Mammal move " << distance << " steps.\n"; } 10: protected: 11: int itsAge; 12: int itsWeight;
Час 16. Наследование 259 13: }; 14: 15: class Dog : public Mammal 16: { 17: public: 18: void Move() const { std::cout << "Dog move 5 steps.\n"; } 19: }; // Здесь можно получить предупреждение о сокрытии функции! 20: 21: int main() 22: { 23: Mammal bigAnimal; 24: Dog Fido; 25: bigAnimal.Move(); 26 : bigAnimal.Move(2); 27: Fido.Move(); 28: // Fido.Move(10); 29: return 0; 30: } Результат Mammal move one step Mammal move 2 steps. Dog move 5 steps. Анализ Из этих классов удалены все дополнительные методы и данные. В строках 7—8 объяв- лены перегруженные методы MoveO класса Mammal. В строке 18 класс Dog переопре- деляет стандартный метод Move (). Вызовы обоих методов расположены в строках 25—27, а результат их выполнения отображен на экране. Строку 28 пришлось закомментировать, поскольку она приводила к ошибке при компиляции. Логично было бы предположить, что в классе Dog можно использовать метод Move (int), поскольку переопределен лишь метод Move (), но в действительно- сти, чтобы использовать метод Move(int), его также необходимо переопределить в классе Dog. В противном случае непереопределенный метод окажется скрыт. Это напоминает о том, что при объявлении какого-либо конструктора компилятор больше не будет предоставлять стандартный конструктор. Весьма распространенная ошибка: пытаясь переопределить метод базового класса, некоторые программисты забывают включить ключевое слово const; в результате ме- тод оказывается скрыт. Ключевое слово const является частью сигнатуры, его про- пуск приводит к созданию другой сигнатуры; таким образом, метод оказывается скрыт, а не переопределен. Некоторые компиляторы выдают предупреждения в строках 15—19. Компилятор Borland так не делает. Хотя скрывать методы базового класса от производных вполне допустимо, зачастую это приводит к ошибке, поэтому некоторые компиляторы, обна- ружив такое явление, выдают предупреждение. Вызов базового метода Переопределенный базовый метод остается доступным, если указать его полное имя. Полное имя члена класса состоит из имени класса, двойного двоеточия и имени метода. К методу Move () класса Mammal можно обратиться следующим образом: Mammal: :Move ()
260 Часть V. Наследование и полиморфизм Строку 28 листинга 16.6 вполне можно переписать так, чтобы она не вызывала ошибки компиляции: 28: Fido.Mammal::Move(10); Здесь метод Move () класса Mammal вызван явно. Эту идею иллюстрирует лис- тинг 16.7. Листинг 16.7. Файл callingbase. срр. Вызов базового метода из переопределенного 0: // Листинг 16.7. Вызов базового метода из переопределенного 1: #include <iostream> 2 : 3: class Mammal 4: { 5: public: 6: void Move() const { std::cout << "Mammal move one step\n"; } 7: void Move(int distance) const 8: { std::cout << "Mammal move " 9: << distance << " steps.\n"; } 10: protected: 11: int itsAge; 12: int itsWeight; 13: }; 14 : 15: class Dog : public Mammal 16: { 17: public: 18: void Move() const; 19: }; 20: 21: void Dog::Move() const 22 : { 23: std::cout << "In dog move...\n"; 24: Mammal::Move(3); 25: } 26: 27: int main() 28: { 29: Mammal bigAnimal; 30: Dog Fido; 31: bigAnimal.Move(2); 32: Fido.Mammal::Move(6); 33: return 0; 34: } Результат Mammal move 2 steps. Mammal move 6 steps. Анализ В строке 29 создается объект bigAnimal класса Mammal, а в строке 30 — объект Fido класса Dog. В строке 31 расположен вызов метода Move () базового класса Mammal. Программист хотел вызвать метод Move(int) объекта Dog, но столкнулся с про- блемой. Класс Dog переопределил метод Move (), но не перегрузил его версию для цело- численного параметра, поэтому в строке 32 было решено обратиться к методу Move(int) базового класса явным образом.
Час 16. Наследование 261 Рекомендуется Не рекомендуется Расширять функциональные возможности существующих классов за счет создания производных. Изменять поведение отдельных функций в производном классе, переопределяя методы базового. Скрывать функцию базового класса, изменив ее сигнатуру. Вопросы и ответы Наследуются ли данные и функции-члены базового класса последующими поколе- ниями производных классов? Скажем, если класс Dog произошел от класса Mammal, а класс Mammal — от класса Animal, то унаследует ли класс Dog данные и функции класса Animal? Да. Поскольку наследование продолжается бесконечно, все производные клас- сы унаследуют все функции и данные своих базовых классов. Можно ли в производном классе сделать функцию закрытой, если в базовом она объявлена открытой? Да, производный класс может переопределить метод и сделать его закрытым, каковым он и останется для всех поколений последующих классов. Коллоквиум Изучив возможности, предоставляемые наследованием, имеет смысл ответить на не- сколько вопросов и выполнить ряд упражнений, чтобы закрепить полученные знания. Контрольные вопросы 1. Для чего применяется упрощение? 2. Зачем нужны производные классы? 3. Коды листингов 16.1—16.5 содержат оператор enum. Для чего он предназначен? 4. Зачем скрывать в производном классе метод базового (см. листинг 16.6)? Упражнения 1. В коде листинга 16.6 (файл hidingbase.срр) раскомментируйте строку 28. Что случилось? Что необходимо сделать, чтобы заставить код работать? 2. Измените файл derivedobject .срр (листинг 16.2) так. чтобы для породы вме- сто перечисляемого типа данных использовать текстовую строку. 3. Измените файл baseoverride. срр (листинг 16.5) так, чтобы метод Sleep О существовал только в классе Mammal. Организуйте вызов этого метода для объек- тов bigAnimal И Fido.
262 Часть V. Наследование и полиморфизм Ответы на контрольные вопросы 1. Упрощение позволяет создавать необходимые методы (функции) без детальной реализации. Впоследствии их можно будет усовершенствовать. 2. Использовать производные классы проще, чем создавать их с нуля. В конце концов, если некий код уже реализован, то почему бы не использовать его? 3. Оператор enum позволяет использовать символьные названия (подобно YORKIE) вместо таких малопонятных значений констант, как 42. 4. Иногда поведение производного класса отличается от поведения базового, и некоторые его методы оказываются ненужны. Поскольку базовый класс до- пускает модификацию не всегда (если, например, нет его исходного кода), можно использовать этот механизм.
1 .«L ЧАС 17 Полиморфизм и производные классы На этом занятии вы узнаете: что такое виртуальный метод; как использовать виртуальные деструкторы и конструкторы копий; как виртуальные методы позволяют использовать базовые классы полиморфно; о риске и цене использования виртуальных методов. Реализация полиморфизма при помощи виртуальных методов На прошлом занятии неоднократно подчеркивалось, что объекты класса Dog одно- временно являются объектами класса Mammal. До сих пор под этим подразумевалось, что объекты класса Dog наследуют все атрибуты (данные) и возможности (методы) своего базового класса. Но в языке C++ взаимосвязь “является” открывает более ши- рокие возможности. Полиморфизм (polymorphism) позволяет обращаться с объектами производных клас- сов так, как будто они являются объектами базового. Предположим, например, что создается ряд классов (Dog, Cat, Horse и т.д.), производных от класса Mammal. По- скольку они происходят от класса Mammal, который обладает рядом методов, все они унаследуют их. Одним из таких методов является Speak (), ведь все млекопитающие способны издавать звуки. Речь каждого из производных классов (животных) следует специализировать, т.е. собака должна лаять, кот мяукать, и т.д. Поэтому метод Speak () каждого класса сле- дует переопределить. Кроме того, необходима коллекция Farm (ферма) объектов классов, производных от класса Mammal (млекопитающее), например, Dog (собака). Cat (кот), Horse (лошадь) и Cow (корова). При моделировании фермы каждое животное должно быть способно по- давать голос безо всякой заботы со стороны клиента о деталях реализации метода
264 Часть V. Наследование и полиморфизм Speak (). Для этого обращение к данным объектам осуществляется так, как будто все они являются объектами класса Mammal, т.е. вызов метода осуществляется так: Mammal. Speak (). В результате объект ведет себя полиморфно. Поли (гр. poly) означает много, морфе (гр. morph) — форма, т.е. речь идет о многообразии форм млекопитающих. Можно, например, объявить указатель на класс Mammal и присвоить ему адрес объекта класса Dog, который был создан в распределяемой памяти. Так как собака яв- ляется млекопитающим, следующий код вполне допустим. Mammal* pMammal = new Dog; Впоследствии этот указатель можно использовать для обращения к любому методу класса Mammal. Это именно то, что нужно: возможность вызова корректной версии ме- тода, переопределенного в производном классе (Dog). Виртуальные методы (virtual method) позволяют решить эту задачу. При полиморфном обращении с такими объекта- ми, можно вызвать метод, используя указатель класса Mammal и ничуть не заботиться ни о фактическом классе объекта (в данном случае — Dog), ни о реализации его метода. Реализацию полиморфизма за счет применения виртуальных функций демонстри- рует листинг 17.1. Листинг 17.1. Файл virtmethod. срр. Использование виртуальных методов 0: // Листинг 17.1. Использование виртуальных методов 1 : ♦include <iostream> 2 : 3: class Mammal 4 : { 5 : public: б: Mammal():itsAge(1) { std::cout << "Mammal constructor ...\n"; } 7 : -Mammal() { std::cout << "Mammal destructor...\n"; } 8 : void Move() const { std::cout << "Mammal move one step\n"; } 9: virtual void Speak() const { std::cout « "Mammal speak!\n"; 10: 11: protected: 12: int itsAge; 13: }; 14: 15: class Dog : public Mammal 16: { 17 : public: 18 : Dog() { std::cout << "Dog constructor. ..\n"; } 19: -Dog() { std::cout << "Dog destructor.. .\n"; } 20 : void WagTail() { std::cout << "Wagging Tail...\n"; ) 21: void Speak() const { std::cout << "Woof !\n"; } 22: void Move() const { std::cout << "Dog moves 5 steps ...\n“; } 23 : J; 24: 25: int main() 26: ( 27 : Mammal *pDog = new Dog; 28: pDog->Move(); 29: pDog->Speak(); 30: return 0; 31: }
Час 17. Полиморфизм и производные классы 265 Результат Mammal constructor. . . Dog Constructor... Mammal move one step Woof! Анализ В строке 9 объявлен виртуальный метод Speak () класса Mammal. Таким образом разработчик сообщает о том, что этот класс задуман как базовый для другого класса, а в производном классе данная функция, вероятно, будет переопределена. В строке 27 создан указатель pDog на класс Mammal, но присваивается ему адрес нового объекта класса Dog. Поскольку собака является млекопитающим, это вполне допустимо. Затем этот указатель используется для вызова функции Move (). Поскольку pDog известен компилятору как указатель на класс Mammal, результат получается та- ким же, как при обычном вызове метода Move () из объекта класса Mammal. Затем в строке 29 осуществляется вызов метода speak () через указатель pDog. По- скольку метод Speak () объявлен как виртуальный, выполняется тот его вариант, ко- торый переопределен в классе Dog. Это кажется волшебством. Хотя компилятору известно, что типом указателя pDog является класс Mammal, тем не менее, происходит вызов версии функции, объявлен- ной в классе, производном от него. Если создать массив указателей базового класса, каждый из которых будет указывать на объект своего производного класса, то, обра- щаясь попеременно к указателям этого массива, можно вызвать все их методы по очереди, причем для каждого объекта будет вызван нужный вариант. Эту идею иллю- стрирует листинг 17.2. Листинг 17.2. Файл multivirtual. срр. Поочередный вызов нескольких виртуальных функций 0: // Листинг 17.2. Поочередный вызов нескольких виртуальных // функций 1: #include <iostream> 2: 3: class Mammal 4: { 5: public: 6: Mammal():itsAge(1) {} 7: -Mammal() {} 8: virtual void Speak() const { std::cout « "Mammal speak!\n"; } 9: protected: 10: int itsAge; 11: } ; 12: 13: class Dog : public Mammal 14: { 15: public: 16: void Speak() const { std::cout << "Woof!\n"; } 17: ) ; 18: 19: 20: class Cat : public Mammal 21: { 22: public: 23: void Speak() const { std::cout << "Meow!\n“; }
266 Часть V. Наследование и полиморфизм 24: }; 25: 26 : 27: class Horse : public Mammal 28: { 29: public: 30: void Speak() const { std::cout << "Winnie!\n"; } 31: } ; 32: 33: class Pig : public Mammal 34: { 35: public: 36: void Speak() const { std::cout « ”0ink!\n"; } 37: }; 38: 39: int main!) 40: { 41: Mammal* theArray[5]; 42: Mammal* ptr; 43: int choice, i; 44: for ( i=0; i<5; i++) 45: { 46: std::cout << "(1)dog (2)cat (3)horse (4)pig: 47: std::cin >> choice; 48: switch (choice) 49: { 50: case 1: 51: ptr = new Dog; 52: break; 53: case 2: 54: ptr = new Cat; 55: break; 56: case 3: 57: ptr = new Horse; 58: break; 59: case 4: 60: ptr = new Pig; 61: break; 62: default: 63: ptr = new Mammal; 64: break; 65: } 66: theArray[i] = ptr; 67: } 68: for (i=0; i<5; i++) 69: theArray[i]->Speak(); 70: return 0; 71: } Результат (l)dog (2)cat (3)horse (4)pig: 1 (l)dog (2)cat (3)horse (4)pig: 2 (l)dog (2)cat (3)horse (4)pig: 3 (l)dog (2)cat (3)horse (4)pig: 4 (l)dog (2)cat (3)horse (4)pig: 5 Woof 1
Час 17. Полиморфизм и производные классы 267 Meow! Winnie! Oink! Mammal Speak! Анализ Эта несложная программа поддерживает лишь самые простые функциональные возможности каждого класса, предназначенные для иллюстрации виртуальных функ- ций в чистом виде. Объявлены четыре класса: Dog (собака), cat (кот), Horse (лошадь) и Pig (свинья). Все они происходят от класса Mammal (млекопитающее). В строке 8 объявлена виртуальная функция Speak () класса Mammal. В строках 16, 23, 30 и 36 все четыре производных класса переопределяют реализацию функ- ции Speak () (голос). Цикл в строках 44—65 запрашивает у пользователя тип объекта производного клас- са и добавляет в массив указатель класса Mammal на вновь созданный объект. Пап фак Позднее связывание Обратите внимание: на момент компиляции неизвестно, какие именно объекты будут созданы, а, следовательно, какие из методов Speak () использованы. Указатель ptr привязывается к своему объекту только в процессе выполнения. Это называется поздним связыванием (late binding), или связыванием в процессе выполнения (runtime binding), в отличие от статического связывания (static binding), или связывания во время компиляции (compile-time binding). Как работают виртуальные функции Когда создается объект производного класса (например. Dog), сначала происходит вызов конструктора базового класса, а затем — производного. На рис. 17.1 показано, как будет выглядеть объект класса Dog после создания. Отметим, что при размещении в памяти та часть объекта, которая относится к классу Mammal, неразрывна с частью относящейся к классу Dog. Часть Mammal Mammal Объект класса Dog Dog Рис. 17.1. Объект класса Dog непосредственно после создания Объект самостоятельно отслеживает созданные им виртуальные функции. Для этого большинство компиляторов создают таблицу виртуальных функций, называе- мую v-таблицей (v-table). Некоторые из них создаются для каждого типа, и каждый объект этого типа хранит указатель на такую таблицу. Этот указатель называют v-указателем (v-pointer), или vptr. Несмотря на различие в деталях реализации виртуальных функций для разных компиляторов, принцип их действия одинаков.
268 Часть V. Наследование и полиморфизм Итак, каждый объект обладает указателем vptr, который ссылается на таблицу виртуальных функций, содержащую указатели на все виртуальные функции. Указатель vptr объекта класса Dog инициализируется при создании части объекта, принадле- жащей базовому классу Mammal, как показано на рис. 17.2. Когда происходит вызов конструктора Dog О и к создаваемому объекту добавляет- ся часть Dog, указатель vptr корректируется так, чтобы он указывал на виртуальные функции (если они есть), переопределенные в классе Dog (рис. 17.3). Рис. 17.3. Таблица виртуальных функций . класса Dog Рис. 17.2. Таблица виртуальных функ- ций класса Mammal При использовании указателя на класс Mammal указатель vptr по-прежнему ссылает- ся на тот вариант виртуальной функции, который соответствует реальному типу объекта. Поэтому при обращении к методу Speak () в предыдущем примере выполнялась та функция, которая была определена в соответствующем производном классе. Попытка доступа к методам из базового класса Если объект Dog обладает методом WagTail () (махать хвостом), который отсутст- вует в классе Mammal, то получить доступ к этому методу, используя указатель на класс Mammal, невозможно (если только этот указатель не будет явно преобразован в указатель на класс Dog). Поскольку функция WagTail (} не является виртуальной и не принадлежит классу Mammal, доступ к ней можно получить только из объекта класса Dog или с помощью указателя этого класса. Несмотря на то что указатель класса Mammal можно преобразовывать в указатель класса Dog, обычно используют более надежные способы обращения к специализированным ме- тодам (таким, как WagTail О). В C++ не любят явных приведений, поскольку они соз- дают ошибки. Более подробно множественное наследование рассматривается на заняти- ях 18, “Расширенное наследование”, и 24, “Исключения, обработка ошибок и другое”. Отсечение Обратите внимание, что вся магия виртуальных функций проявляется только при обращении к ним с помощью указателей и ссылок. Если передать объект по значе- нию, виртуальную функцию вызвать не удастся. Это демонстрирует листинг 17.3. Листинг 17.3. Файл si icing, срр. Отсечение данных при передаче по значению 0: // Листинг 17.3. Отсечение данных при передаче по значению 1: #include <iostream> 2: 3: class Mammal 4: { 5: public:
Час 17. Полиморфизм и производные классы 269 6: Mammal() :itsAge(1) { } 7: 8: ~Mammal() {} virtual void Speak() const { std::cout << "Mammal speak!\n"; } 9: protected: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: int itsAge; ) ; class Dog : public Mammal { public: void Speak() const { std::cout << "Woof!\n"; } }; class Cat : public Mammal { public: void Speak() const { std::cout << "MeowIXn"; } }; void ValueFunction (Mammal); void PtrFunction (Mammal*); void RefFunction (Mammal&); int main() { Mammal* ptr=0; int choice; while (1) { bool fQuit = false; std::cout << "(1)dog (2)cat (O)Quit: "; std::cin >> choice; switch (choice) { case 0: fQuit = true; break; case 1: ptr = new Dog; break; case 2: ptr = new Cat; break; default: ptr = new Mammal; break; } if (fQuit) break; PtrFunction(ptr); RefFunction(*ptr); ValueFunction(*ptr); } return 0; } 62: void ValueFunction (Mammal Mammalvalue) // Эту функцию // вызывают последней
270 Часть V. Наследование и полиморфизм 63: { 64: Mammalvalue.Speak(); 65: } 66: 67: void PtrFunction (Mammal * pMammal) 68: { 69: pMammal->Speak(); 70: } 71: 72: void RefFunction (Mammal Sc rMammal) 73: { 7 4: rMammal.Speak() ; 75: } Результат (1)dog Woof Woof Mammal (1)dog Meow! Meow! Mammal (1)dog (2)cat Speak! (2)cat Speak! (2)cat (O)Quit: 1 (O)Quit: 2 (O)Quit: 0 Анализ В строках 3—23 объявлены упрощенные версии классов Mammal, Dog и Cat. Далее объявлены три функции: PtrFunction (), RefFunction() и ValueFunction (). Им пе- редают, соответственно, указатель класса Mammal, ссылку класса Mammal и объект класса Mammal. Все они выполняют одно и то же действие — вызывают метод Speak (). Пользователю предлагается выбрать объект класса Dog или Cat. На основании сделанного выбора в строках 38—52 создается указатель на правильный тип. Судя по первой строке результата, пользователь выбрал объект класса Dog. В стро- ке 44 объект класса Dog создается в динамической памяти. Затем он передается в три функции как указатель, ссылка и значение. Когда адрес объекта передавался в функцию по указателю или ссылке, функция-член Dog->Speak () выполнялась успешно. На экране компьютера дважды появилось сооб- щение, соответствующее объекту, выбранному пользователем. Но ссылка на указатель передается в виде значения. Функция ожидает объект Mammal, поэтому компилятор от- секает от объекта Dog ту часть, которая не относится к классу Mammal. В этом случае происходит вызов той версии метода Speak (), которая была объявлена для класса Mammal, что и отражено в третьей строке вывода после сделанного пользователем выбора. Тот же эксперимент был повторен для объекта класса cat с аналогичным результатом. Виртуальные деструкторы Вполне допустимо и общепринято передавать указатель на объект производного класса, когда ожидается указатель на объект базового. Что же получится, если указа- тель на объект производного класса удалить? Если деструктор объявлен как виртуальный, все пройдет отлично — будет вызван деструктор соответствующего производного класса, который затем автоматически вы- зовет деструктор базового класса, и указанный объект окажется удален целиком. От-
Час 17. Полиморфизм и производные классы 271 сюда следует правило: если в классе объявлены виртуальные функции, то деструктор должен быть виртуальным. Виртуальные конструкторы копий Конструкторы не могут быть виртуальными, а, следовательно, невозможны и вир- туальные конструкторы копий. Но иногда нужен способ передать указатель на объект базового класса или получить точную копию создаваемого объекта производного класса. Обычным решением этой проблемы является создание в базовом классе вир- туального метода clone О (клон). Метод Clone О создает новую копию (экземпляр) объекта текущего класса и возвращает ее как объект. Поскольку каждый производный класс переопределяет метод Clone (), при обра- щении к нему будет создана копия именно этого производного класса. В листин- ге 17.4 продемонстрировано, как это делается. Листинг 17.4. Файл virtualcopy. срр. Виртуальный конструктор копий 0: // Листинг 17.4. Виртуальный конструктор копий 1: #include <iostream> 2: 3: class Mammal 4: { 5: public: 6: Mammal():itsAge(1) { std::cout << "Mammal constructor...\n"; } 7: virtual -Mammal() { std::cout << "Mammal destructor ...\n"; } 8: Mammal (const Mammal & rhs); 9: virtual void Speak() const { std::cout « "Mammal speak!\n“; } 10: virtual Mammal* Clone() { return new Mammal(*this); } 11: int GetAge() const { return itsAge; } 12: 13: protected: 14: int itsAge; 15: }; 16: 17: Mammal::Mammal (const Mammal & rhs):itsAge(rhs.GetAge()) 18: { 19: std::cout « "Mammal Copy Constructor...\n“; 20: } 21: 22: class Dog : public Mammal 23: { 24: public: 25: Dog() { std::cout << "Dog constructor...\n"; } 26: virtual -Dog() { std::cout << "Dog destructor...\n"; } 27: Dog (const Dog & rhs); 28: void Speak() const { std::cout « "Woof!\n"; } 29: virtual Mammal* Cloned { return new Dog(*this); } 30: }; 31: 32: Dog::Dog(const Dog & rhs): 33: Mammal(rhs) 34: { 35: std::cout << "Dog copy constructor...\n"; 36: )
272 Часть V. Наследование и полиморфизм 37: 38: class Cat : public Mammal 39: { 40: public: 41: Cat() { std::cout << "Cat constructor...\n"; } 42: virtual -Cat() { std::cout << "Cat destructor...\n"; } 43: Cat (const Cat &); 44: void Speak() const { std::cout << "Meow!\n"; } 45: virtual Mammal* Clone() { return new Cat(*this); } 46: }; 47: 48: Cat::Cat(const Cat & rhs): 49: Mammal(rhs) 50: { 51: std::cout « "Cat copy constructor...\n"; 52: } 53: 54: enum ANIMALS { MAMMAL, DOG, CAT }; 55: const int NumAnima1Types = 3; 56: int main() 57: { 58: Mammal *theArray[NumAnima1Types]; 59: Mammal* ptr; 60: int choice,!; 61: for (i=0; i<NumAnimalTypes; i++) 62: { 63: std::cout << "(l)dog (2)cat (3)Mammal: "; 64: std::cin >> choice; 65: switch (choice) 66: { 67: case DOG: 68: ptr = new Dog; 69: break; 70: case Cat: 71: ptr = new Cat; 72: break; 73: default: 74: ptr = new Mammal; 75: break; 76: } 77: theArray[i] = ptr; 78: } 79: Mammal ‘OtherArray[NumAnimalTypes]; 80: for (i=0; i<NumAnimalTypes; i++) 81: { 82: theArray[i]->Speak(); 83: OtherArray[i] = theArray[i]->Clone(); 84: } 85: for (i=0; i<NumAnimalTypes; i++) 86: OtherArray[i]->Speak(); 87: return 0; 88: } Результат 1: (l)dog (2)cat (3)Mammal: 1 2: Mammal constructor...
Час 17. Полиморфизм и производные классы 273 3: Dog Constructor... 4: (l)dog (2) cat (3)Mammal: 2 5: Mammal constructor... 6: Cat constructor... 7: (l)dog (2)cat (3)Mammal: 3 8: Mammal constructor... 9: Woof! 10: Mammal copy constructor... 11: Dog copy constructor... 12: Meow! 13: Mammal copy constructor... 14: Cat copy constructor... 15: Mammal speak! 16: Mammal copy constructor... 17: Woof! 18: Meow! 19: Mammal speak! Анализ Листинг 17.4 очень похож на два предыдущих, но в последней программе в класс Mammal добавлен новый виртуальный метод — Clone (). Этот метод возвращает указа- тель на новый объект класса Mammal, вызывая конструктор копий и передавая ему как параметр себя (*this) в качестве постоянной (const) ссылки. Классы Dog и Cat переопределяют метод Clone (), а их объекты, инициализируя свои данные, передают себя для копирования в свои собственные конструкторы ко- пий. Поскольку метод Clone () является виртуальным, он создает виртуальный конст- руктор копий, как показано в строке 83. Подобно предыдущей программе, пользователю предлагается выбрать объект клас- са Dog, Cat или Mammal. Объект выбранного типа создается в строках 65—76. В строке 77 указатель на новый объект добавляется в массив данных. Затем начинается цикл, в котором для каждого объекта массива осуществляется вы- зов методов Speak () и Clone () (строки 82—83) В результате выполнения функции Cloned возвращается указатель на копию объекта, которая сохраняется во втором мас- сиве (строка 83). В строке 1 результата показан сделанный пользователем выбор: 1 — создание объ- екта класса Dog. В этом процессе принимают участие конструкторы базового (Mammal) и производного (Dog) классов. Та же операция повторяется для объектов класса Cat и Mammal в строках 4—8 результата. В строке 9 показан результат выполнения метода Speak () для объекта класса Dog. Поскольку функция speak () объявлена как виртуальная, при обращении к ней сра- батывает та ее версия, которая соответствует типу объекта. Затем следует обращение к функции Clone (), а поскольку она тоже объявлена виртуальной, происходит вызов метода cloned, принадлежащего классу Dog, что приводит к вызову конструктора Mammal () и конструктора копий класса Dog. То же повторяется для объекта класса Cat (строки 12—14) и объекта класса Mammal (строки 15—16). В результате перебирается весь новый массив объектов (строки 83—84), для каждого из его элементов происходит вызов своей версии функции Speak О. Их сообщения расположены в строках 17—19 результата. Различие между этим подходом и использованием конструктора копий заключает- ся в том, что программист вынужден явно обратиться к методу clone (). При копиро- вании объекта вызов конструктора копий происходит автоматически. Не забывайте, что метод Clone () всегда можно переопределять в производном классе, но этот под- ход уменьшит гибкость программы.
274 Часть V. Наследование и полиморфизм Цена виртуальных методов Поскольку объекты с виртуальными методами должны поддерживать v-таблицу, неизбежны определенные издержки. Для небольшого класса, который не будет ис- пользован в качестве базового для других, нет смысла создавать виртуальные методы. Объявляя метод виртуальным, расплатиться придется не только v-таблицей (каждый элемент которой занимает ресурсы оперативной памяти) В этом случае виртуальным придется сделать и деструктор, поскольку производные классы, вероятно, также будут иметь свои виртуальные методы. Внимательно рассмотрите все невиртуальные методы и убедитесь, что полностью понимаете, почему они не сделаны виртуальными. Рекомендуется Не рекомендуется Использовать виртуальные методы, если класс задуман как базовый. Использовать виртуальный деструктор, если в классе есть хотя бы один виртуальный метод. Отмечать конструктор как виртуальный. Вопросы и ответы Почему бы не сделать все функции класса виртуальными? При создании первой виртуальной функции создается v-таблица, которая тре- бует определенных затрат памяти. Добавление последующих виртуальных функций будет тривиальным. Многие программисты увлекаются созданием виртуальных функций и полагают, что если в программе уже есть одна вирту- альная функция, то и все другие должны быть виртуальными. В действительно- сти это не так. Сколько функций объявлять виртуальными, зависит от того, ка- кую конкретно задачу необходимо решить. Допустим, функция (someFunc ()) в базовом классе объявлена как виртуальная и перегружена для одного или двух целочисленных параметров, а производный класс переопределяет ту ее версию, которая получает один параметр. Что произойдет, ко- гда указатель на производный объект вызовет версию функции для двух параметров? Переопределение версии для одного параметра скроет все остальные варианты базового класса, а компилятор выдаст сообщение об ошибке, указывающее на то, что функции необходим лишь один параметр. Коллоквиум Изучив возможности полиморфизма и производных классов, имеет смысл отве- тить на несколько вопросов и выполнить ряд упражнений, чтобы закрепить полу- ченные знания. Контрольные вопросы 1. Можно ли изменить файл slicing.срр (листинг 17.3) так, чтобы было воз- можно вызвать метод speak () производного класса, переопределив функцию ValueFunction()? 2. Откуда программа узнает, которую из версий виртуальной функции следует ис- пользовать, когда при обращении к объекту указывается базовый класс? 3. Какой метод не может быть виртуальным? 4. Что такое полиморфизм?
Час 17. Полиморфизм и производные классы 275 Упражнения 1. Что случается, если в производном классе не переопределен виртуальный ме- тод базового? Измените файл virtmethod.cpp (листинг 17.1), закомменти- ровав строку 21 (метод Speak () класса Dog). Возможна ли ситуация, где это будет иметь смысл? 2. Что случится, если в строке 8 файла slicing.срр (листинг 17.3) удалить клю- чевое слово virtual (определение метода Speak () в базовом классе)? Почему переопределенная функция никогда не будет вызвана? (Подсказка: см. вопрос 1.) 3. Опробуйте все примеры кода самостоятельно. Чтобы лучше понять поведение кода, используйте значения, отличающиеся от приведенных в книге. Ответы на контрольные вопросы 1. Непосредственно — нет. Хотя версия этой функции для класса Dog может быть создана, обратиться к ней будет нельзя, поскольку основная переменная при- надлежит базовому классу (Mammal). Вызвана будет только та версия функции ValueFunction (), среди параметров которой есть класс Mammal. 2. Эту информацию автоматически отслеживает v-таблица. Связанные с виртуаль- ными функциями дополнительные затраты делают их применение более ресур- соемким по сравнению с обычными методами. 3. Конструктор (включая конструктор копий). 4. Полиморфизм заключается в обращении с объектами производных классов как с объектами базового. Поли (гр. poly) означает много, морфе (гр. morph) — форма.
ЧАС 18 Расширенное наследование На этом занятии вы узнаете: что такое “приведение вниз” и для чего оно применяется; что такое абстрактные типы данных; что такое чистая виртуальная функция. Проблемы одиночного наследования На предыдущем занятии рассматривалось создание виртуальных функций в базовых классах. Как уже было продемонстрировано, если базовый класс обладает методом SpeakO, который переопределен в производном классе, то применение указателя на объект базового класса позволит вызвать в объекте производного класса корректную версию этого метода. Данную идею иллюстрирует листинг 18.1. Листинг 18.1. Файл virtualmethods. срр. Виртуальные методы 0: 1: 2 : 3 : 4 : 5: 6: 7 : 8: 9 : 10 : 11 : 12: 13: 14 : 15: 16: 17 : 18: // Листинг 18.1. Виртуальные методы ♦include <iostream> class Mammal { public: Mammal():itsAge(1) { std::cout « "Mammal constructor ...\n“; } virtual -Mammal() { std::cout << "Mammal destructor...\n“; } virtual void Speak() const { std::cout << "Mammal speak!\n“; } protected: int itsAge; class Cat: public Mammal { public: Cat() { std::cout << "Cat constructor...\n"; } -Cat() { std::cout << "Cat destructor...\n“; } void Speak() const { std::cout << "Meow!\n"; }
Час 18. Расширенное наследование 277 19: }; 20: 21: int main() 22: { 23: Mammal *pCat = new Cat; 24: pCat->Speak(); 25: return 0; 26: } Результат Mammal constructor... Cat Constructor.. . Meow! Анализ В строке 8 метод Speak () объявлен как виртуальный. Он переопределен в строке 18 и вызван в строке 24. Обратите внимание: здесь указатель pCat снова объявлен как указатель на объект класса Mammal, но присвоен ему адрес объекта класса Cat. В этом и заключается сущность полиморфизма, рассматриваемого на заня- тии 17, “Полиморфизм и производные классы”. Но что же случается, если в класс Cat необходимо добавить метод, несвойствен- ный классу Mammal? Предположим, например, что необходимо добавить метод Purr() (мурлыканье). Коты мурлычут, а остальные млекопитающие — нет. Такой класс мож- но было бы объявить следующим образом: class Cat: public Mammal { public: Cat() { std::cout << "Cat constructor...\n"; } ~Cat() { std::cout « "Cat destructor...\n”; } void Speak() const { std::cout << "Meow\n"; } void Purr() const { std::cout << "rrrrrrrrrrrrrrrr\n"; } 1; Проблема здесь в следующем: если вызвать теперь метод Purr (), используя указа- тель на класс Mammal, компилятор сообщит об ошибке. Компилятор Borland, напри- мер, выдаст сообщение: "virtualmethods. срр" : Е2316 'Purr' is not a member of 'Mammal' in function main!) at line 50 Другой компилятор сообщит об ошибке иначе, например, так: error C2039:'Purr' :is not a member of 'Mammal' Когда компилятор попробует найти метод Purr() в виртуальной таблице класса Mammal, ему это не удастся. Хоть это и не очень хорошая идея, но метод можно пере- дать в базовый класс. Загромождение базового класса методами, специфическими для производных, не является практикой программирования; такие методы имеет смысл оставлять в производном классе. Фактически данная проблема является следствием и свидетельством плохо прора- ботанного проекта приложения. Уж если существует указатель на базовый класс, ко- торому присвоен адрес объекта производного класса, то это вовсе не означает, что и в данном случае объект следует использовать полиморфно, даже не попытавшись обратиться к методам, определенным непосредственно в производном классе. По правде говоря, проблема даже не в определенных методах, а в попытке доступа к ним при помощи указателя на базовый класс. В идеальном мире, имея этот указа- тель, обращаться к таким методам не’ придется.
278 Часть V. Наследование и полиморфизм Однако мир не идеален, и время от времени попадаешь в ситуацию, когда приходит- ся работать с коллекцией базовых объектов, например, зоопарком, в котором полно млекопитающих. Так или иначе, но следует понимать, что имеющийся объект класса Cat должен мурлыкать. В данном случае поможет только одно средство — обман (cheat). Вот как осуществить обман: указатель базового класса следует привести (cast) к ти- пу производного. Т.е. компилятору говорят: “Разработчику точно известно, что это действительно кот, поэтому иди и делай, что сказано”. Фраза, достойная головореза, напоминает вымогательство, поскольку по существу млекопитающее заставляют вести себя так, как ведет себя кот. Для этого применяется оператор dynamic_cast. Он гарантирует безопасность приведения. Кроме того, позволяет быстро найти те места в коде, где эта возможность используется, чтобы можно было их удалить, когда разработчик найдет более кор- ректный способ решения проблемы. Это работает так: если имеется указатель на базовый класс (например, Mammal), которому присвоен адрес объекта производного (например, Cat), то указатель класса Mammal можно использовать для полиморфного доступа к виртуальным функциям. Впоследствии, если понадобится обратиться к специфическому для класса Cat методу (например, Purr О), то при помощи оператора dynamic_cast необходимо привести используемый указатель к типу Cat. Во время выполнения программы указатель на базовый класс будет исследован, и если преобразование пройдет корректно, то новый указатель, уже класса Cat, прекрасно справится с задачей. Если преобразование прой- дет некорректно и допустимый указатель на объект класса Cat получен не будет, но- вый окажется нулевым. Эту концепцию иллюстрирует листинг 18.2. Листинг 18.2. Файл dynamiccast .срр. Динамическое приведение 0 : // Листинг 18.2. Динамическое приведение 1 : ♦include <iostream> 2 : 3 : using std::cout; // здесь используется std::cout 4: class Mammal 5: { б: public: 7 : Mammal():itsAge(1) { cout << “Mammal constructor...\n"; } 8 : virtual -Mammal() { cout << "Mammal destructor...\n"; } 9 : virtual void Speak() const { cout « "Mammal speak!\n“; } 10 : protected: 11 : int itsAge; 12: 13: } ; 14 : class Cat: public Mammal 15: { 16: public: 17: Cat() { cout << “Cat constructor...\n"; } 18: -Cat() { cout << "Cat destructor ...\n"; } 19: void Speak() const { cout << "Meow\n"; } 20: void Purr() const { cout << "rrrrrrrrrrr\n"; } 21: 22: }; 23: class Dog: public Mammal 24: { 25: public: 26: Dog() { cout << "Dog Constructor...\n"; } 27: -Dog() { cout << "Dog destructor ...\n"; } 28: void Speak() const { cout << "Woof!\n"; } 29: };
Час 18. Расширенное наследование 279 30: 31: 32: int main() 33: { 34: const int NumberMammaIs = 3; 35: Mammal* Zoo[NumberMammals]; 36: Mammal* pMammal; 37: int choice,!; 38: for (i=0; i<NumberMammals; i++) 39: { 40: cout << "(l)Dog (2)Cat: "; 41: std::cin >> choice; 42: if (choice == 1) 43: pMammal = new Dog; 44: else 45: pMammal = new Cat; 46: 47: Zoo[i] = pMammal; 48: } 49: 50: cout << "\n"; 51: 52: for (i=0; i<NumberMammals; i++) 53: { 54: Zoo[i]->Speak(); 55: 56: Cat *pRealCat = dynamic_cast<Cat *> (Zoo[i]); 57: 58: if (pRealCat) 59: pRealCat->Purr() ; 60: else 61: cout << “Uh oh, not a cat!\n"; 62: 63: delete Zoo[i]; 64: cout << "\n“ ; 65: } 66: 67: return 0; 68: } Результат (l)Dog (2)Cat: 1 Mammal constructor.. . Dog constructor... (l)Dog (2)Cat: 2 Mammal constructor... Cat constructor... (l)Dog (2)Cat: 1 Mammal constructor... Dog constructor... Woof! Uh oh, not a cat! Mammal destructor... Meow rrrrrrrrrrr
280 Часть V. Наследование и полиморфизм Mammal destructor... Woof ! Uh oh, not a cat! Mammal destructor... Анализ Код строк 38—48 предлагает пользователю выбрать класс (Cat или Dog), указатель на объект которого добавляется в массив указателей на класс Mammal. В строке 52 на- чинается цикл, перебирающий объекты массива, а в строке 54 происходит вызов вир- туального метода Speak () каждого из них. Эти методы срабатывают полиморфно: ко- ты мяукают, а собаки лают. В строке 59 кот должен мурлыкать, но для объектов класса Dog вызов метода Purr () следует предотвратить. Для того чтобы позволить обратиться к методу Purr () объекта класса Cat, в строке 56 используется оператор dynamic_cast. Вот где происходят реаль- ные действия! Если этот указатель не будет нулевым, он пройдет проверку в строке 58. Абстрактные типы данных Зачастую вся иерархия классов создается одновременно Например, можно создать класс shape (форма) и производные от него: Rectangle (прямоугольник) и Circle (круг). Из класса Rectangle можно создать класс Square (квадрат) как частный слу- чай класса Rectangle. В каждом из производных классов можно переопределить методы DrawO (нарисовать), GetAreaO (получить площадь) и т.д. Листинг 18.3 иллюстрирует реали- зацию скелета класса Shape и его производных классов Circle и Rectangle. Листинг 18.3. Файл shapeclass .срр. Семейство класса Shape 0: // Листинг 18.3. Семейство класса Shape 1: #include <iostream> 2: 3: class Shape 4: { 5: public: 6: Shape() {} 7: virtual -Shape() {} 8: virtual long GetArea() { return -1; } // ошибка 9: virtual long GetPeriml) { return -1; } 10: virtual void Draw() (} 11: ); 12: 13: class Circle : public Shape 14: { 15: public: 16: Circle(int radius):itsRadius(radius) {} 17: -Circled {} 18: long GetAreaO { return 3 * itsRadius * itsRadius; } 19: long GetPeriml) { return 9 * itsRadius; } 20: void Drawl); 21: private: 22: int itsRadius; 23: int itsCircumference; 24: }; 25:
Час 18. Расширенное наследование 281 26: void Circle::Draw() 27: { 28: std::cout << "Circle drawing routine here!\n"; 29: } 30: 31: 32: class Rectangle : public Shape 33: { 34: public: 35: Rectangle(int len, int width): 36: itsLength(len), itsWidth(width) {} 37: virtual -Rectangle() {} 38: virtual long GetArea() { return itsLength * itsWidth 39: virtual long GetPerim() { return 2*itsLength + 2*itsWidth; } 40: virtual int GetLength() { return itsLength; } 41: virtual int GetWidth() { return itsWidth; } 42: virtual void Draw(); 43: private: 44: int itsWidth; 45: int itsLength; 46: 47: } ; 48: void Rectangle::Draw() 49: { 50: for (int i=0; icitsLength; i++) 51: { 52: for (int j=0; jcitsWidth; j++) 53: 54: Std::cout << ”x " ; 55: std::cout << "\n"; 56: } 57: 58: } 59: class Square : public Rectangle 60: { 61: public: 62: Square(int len); 63: Squaretint len, int width); 64: -Square() {} 65: long GetPerimt) { return 4 * GetLength(); } 66: 67: } ; 68: Square::Square(int len): 69: Rectangle(len,len) 70: 71: {} 72: Square::Square(int len, int width): 73: Rectangle(len,width) 74: { 75: if (GetLength() != GetWidth()) 76: std::cout << "Error, not a square ... a Rectangle??\n" 77: 78: } 79: int main() 80: { 81: int choice; 82: bool fQuit = false;
282 Часть V. Наследование и полиморфизм 83 : Shape * sp.- 84 : 85 : while (1) 86 : { 87 : std::cout << ” (l)Circle (2)Rectangle (3)Square (0)Quit:“ 88: std::cin >> choice; 89 : 90: switch (choice) 91 : { 92 : case 1: 93 : sp = new Circle(5); 94 : break; 95: case 2 : 96 : sp = new Rectangle(4,6); 97 : break; 98: case 3: 99 : sp = new Square(5); 100: break; 101 : default: 102 : fQuit = true; 103 : break; 104 : } 105: if (fQuit) 106: break; 107 : 108: sp->Draw(); 109: Std::cout << "\n"; 110: } 111: return 0; 112: } Результат (1)Circle (2)Rectangle (3)Square (0)Quit: 2 X X X X X X X X X X X X X X X X X X X X (l)Circle X X X X (2)Rectangle (3)Square (0)Quit: 3 X X X X X X X X X X X X X X X X X X X X X X X X X (1)Circle (2)Rectangle (3)Square (O)Quit: О Анализ В строках 3—11 объявлен класс Shape (форма). Методы GetArea () и GetPerimO возвращают значение -1 как сообщение об ошибке, а метод Draw() не выполняет никаких действий. Давайте подумаем, можно ли в принципе нарисовать форму? Можно нарисовать окружность, прямоугольник или квадрат, но форма — это абст- ракция, изобразить которую невозможно. Класс circle объявлен производным от класса Shape, а все три его виртуальных метода перегружены. Обратите внимание: в данном случае нет необходимости исполь- зовать ключевое слово virtual, поскольку виртуальность функций наследуется. Но
Час 18. Расширенное наследование 283 указать его все-таки стоит, как показано в определении класса Rectangle (строки 38-42), поскольку это послужит своего рода напоминанием. Класс square происходит от класса Rectangle и наследует от него все методы, причем метод GetPerimO в новом классе перегружен. Тем не, менее существует одна проблема: клиент может попытаться создать экзем- пляр класса Shape, а этого нельзя допустить, ведь класс shape существует только для обеспечения интерфейса классов, производных от него, поэтому о таком типе данных го- ворят как об абстрактном, или ADT (Abstract Data Type — абстрактный тип данных). Абстрактный тип данных представляет собой скорее понятие (как форма), чем объект (как круг). Класс ADT в языке C++ всегда будет базовым по отношению к другим классам, а создание экземпляра абстрактного класса недопустимо. Чистые виртуальные функции Язык C++ поддерживает создание абстрактных типов данных с чистыми виртуаль- ными функциями. Чистыми виртуальными (pure virtual function) называют такие функции, которые инициализируются нулевым значением и подлежат обязательному переопределению в производном классе, например: virtual void Draw() = 0; Любой класс с одной или несколькими чистыми виртуальными функциями явля- ется абстрактным, а создание экземпляров объекта такого класса запрещено. Попытка сделать это приведет к ошибке во время компиляции. Наличие чистой виртуальной функции в классе свидетельствует о следующем: нельзя создать объект этого класса, его можно только наследовать; чистую виртуальную функцию необходимо переопределить. Любой класс, производный от абстрактного, наследует чистые виртуальные функ- ции в исходном виде и должен переопределить их все, если предполагается создание объекта этого класса. Таким образом, если класс Rectangle наследует класс shape, а класс Shape имеет три чистые виртуальные функции, то класс Rectangle должен переопределить все три, иначе он тоже останется абстрактным. В листинге 18.4 класс Shape изменен так, чтобы превратить его в абстрактный. Замените объявление класса Shape в листинге 18.3, строки 3—11, новым объявлением класса Shape (листинг 18.5) и запустите программу снова. Листинг 18.4. Файл adt. срр. Абстрактные классы 0: // Листинг 18.4. Абстрактные классы 1: class Shape 2: { 3: public: 4: Shaped {} 5: virtual -Shape() {} 6: virtual long GetArea() = 0; 7: virtual long GetPeriml)= 0; 8: virtual void Draw)) = 0; 9: private: 10: }; ЙйИ--------Т •сторожим.' Не компилируйте файл adt. срр! Не пытайтесь компилировать код листинга 18.4, он лишь демонстрирует необходимые изменения. Компилируется код листинга 18.5, который яв- ляется листингом 18.3 с внесенными изменениями листинга 18.4.
284 Часть V. Наследование и полиморфизм Листинг 18.5. Файл adtshapeclass. срр. Класс Shape с абстрактными типами данных 0: 1 : 2 : 3 : 4: 5: 6 : 7 : 8: 9 : 10: 11: 12: 13: 14: 15: 16: 17: 18: 19 : 20: 21 : 22 : 23 : 24 : 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: // Листинг 18.5. Семейство класса Shape #include <iostream> class Shape { public: Shape() {} virtual -Shape() {} virtual long GetArea() = 0; virtual long GetPeriml) = 0; virtual void Drawl) = 0; } ; class Circle : public Shape { public: Circle(int radius):itsRadius(radius) {} -Circle!) {) long GetArea!) { return 3 * itsRadius * itsRadius; } long GetPerim!) { return 9 * itsRadius; } void Draw!); private: int itsRadius; int itsCircumference; ) ; void Circle::Draw() { std::cout << "Circle drawing routine here!\n"; ) class Rectangle : public Shape { public: Rectangle(int len, int width): itsLength(len), itsWidth(width) {} virtual -Rectangle!) !) virtual long GetArea!) { return itsLength * itsWidth; } virtual long GetPerim!) { return 2*itsLength + 2*itsWidth; } 40: virtual int GetLength!) { return itsLength; } 41: 42 : 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53 : 54 : virtual int GetWidth!) { return itsWidth; ) virtual void Draw!); private: int itsWidth; int itsLength; }; void Rectangle::Draw() { for (int i=0; icitsLength; i++) { for (int j=0; j<itsWidth; j++) std::cout << “x "; 55: std::cout << "\n";
Час 18. Расширенное наследование 285 56: } 57 : 58: 59: 60: 61: 62: 63: 64: 65: 66: 67- 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82 : 83: 84: 85: 86: 87 : 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: 102: 103: 104: 105. 106: 107: 108: 109. 110: 111: 112: } class Square : public Rectangle { public: Squarefint len); Squarefint len, int width); -Square() {} long GetFerimO { return 4 * GetLength(); } ) ; Square::Square(int len): Rectangle(len,len) {) Square::Square(int len, int width): Rectangle(len,width) { if (GetLength() 1= GetWidth()) std::cout « "Error, not a square... a Rectangle??\n”; } int main() { int choice; bool fQuit = false; Shape * sp.- while (1) { std::cout << "(DCircle (2)Rectangle (3)Square (O)Quit:" std::cin » choice; switch (choice) { case 1: sp = new Circle(5); break; case 2: sp = new Rectangle(4,6); break; case 3: sp = new Square (5 ) ,- break; default: fQuit = true; break; ) if (fQuit) break; sp->Draw(); Std::COUt << "\n"; } return 0; )
286 Часть V. Наследование и полиморфизм Результат (l)Circle (2)Rectangle (3)Square (0)Quit:2 x x x x x x X X X X X X X X X X X X X X X X X X (l)Circle (2)Rectangle (3)Square (0)Quit:3 X X X X X X X X X X X X X X X X X X X X X X X X X (l)Circle (2)Rectangle (3)Square (0)Quit:0 Анализ Как можно заметить, работа программы никак не изменилась. Единственным от- личием является то, что теперь невозможно создать объект класса Shape. Абстрактные типы данных Объявить класс абстрактным можно, добавив одну или несколько чистых виртуаль- ных функций в его объявлении. Объявить функцию чистой виртуальной можно, доба- вив в ее объявление оператор = 0, например: class Shape { virtual void Drawl) = 0; // чистая виртуальная }; Реализация чистых виртуальных функций Как правило, чистые виртуальные функции в абстрактном классе никогда не реа- лизуют. Поскольку объекты этого типа никогда не создаются, то и необходимости в реализации его функций нет. Задача ADT состоит в определении и обеспечении ин- терфейса производных классов, а реализация функций для этого не нужна. Тем не менее, вполне возможно обеспечить реализацию и для чистой виртуальной функции. Впоследствии эти функции можно вызывать в объектах классов, производ- ных от ADT, что позволяет обеспечивать общие функциональные возможности для всех переопределенных функций. Листинг 18.6 аналогичен листингу 18.3, за исключе- нием класса shape, объявленного как ADT, и реализации чистой виртуальной функ- ции Drawl). Класс circle, как и положено, переопределяет метод Drawl), но сдела- но это так, что фактически происходит вызов одноименного метода базового класса с небольшим собственным дополнением. В данном случае дополнительные функциональные возможности заключаются в выводе на экран еще одной строки сообщения, но в принципе базовый класс мог бы поддерживать достаточно сложный централизованный механизм вывода рисунка на экран, которым могли бы воспользоваться все классы, производные от shape. Листинг 18.6. Файл implement. срр. Реализация чистых виртуальных функций 0: // Листинг 18.6. Реализация чистых виртуальных функций 1: #include <iostream> 2 : 3: class Shape 4: { 5: public:
Час 18. Расширенное наследование 287 6 : Shape() { } 7: virtual -Shape() {} 8: virtual long GetArea() = 0; 9: virtual long GetPerimf) = 0; 10: virtual void Drawl) = 0; 11: private: 12 : } ; 13: 14: void Shape::Draw() 15: { 16: std::cout << “Abstract drawing mechanism!\n"; 17: } 18: 19: class Circle : public Shape 20: { 21: public: 22: Circle(int radius):itsRadius(radius) {) 23: -Circle!) {} 24: long GetArea!) { return 3 * itsRadius * itsRadius; } 25: long GetPeriml) { return 9 * itsRadius; } 26 : void Draw(); 27: private: 28: int itsRadius; 29: int itsCircumference; 30: }; 31: 32: void Circle::Draw() 33: { 34: std::cout << "Circle drawing routine here!\n"; 35: Shape::Draw(); 36: ) 37 : 38: class Rectangle : public Shape 39: { 40: public: 41: Rectangle(int len, int width): 42: itsLength(len), itsWidth(width) {) 43: virtual -Rectangle!) {} 44: long GetArea!) { return itsLength * itsWidth; } 45: long GetPerim!) { return 2*itsLength + 2*itsWidth; } 46: virtual int GetLength!) { return itsLength; } 47: virtual int GetWidth!) { return itsWidth; ) 48: void Draw!); 49: private: 50: int itsWidth; 51: int itsLength; 52 : } ; 53: 54: void Rectangle::Draw() 55: { 56: for (int i=0; i<itsLength; i++) 57: { 58: for (int j=0; j<itsWidth; j++) 59: std::cout << “x "; 60: 61: std::cout << "\n"; 62: } 63: Shape::Draw();
288 Часть V. Наследование и полиморфизм 64 : } 65: 66: class Square : public Rectangle 67: { 68: public: 69: Square(int len); 70: Square(int len, int width); 71: -Square() {} 72: long GetPerim() { return 4 * GetLength(); } 73: }; 74 : 75: Square::Square(int len): 76: Rectangle(len,len) 77: {} 78: 79: Square::Square(int len, int width): 80: Rectangle(len,width) 81: { 82: if (GetLength() 1= GetWidth()) 83: std::cout << "Error, not a square... a Rectangle??\n“; 84: } 85: 86: int main() 87: { 88: int choice; 89: bool fQuit = false; 90: Shape * sp; 91: 92: while (1) 93: { 94: std::cout << "(l)Circle (2)Rectangle (3)Square (0)Quit:“; 95: std::cin >> choice; 96: 97: switch (choice) 98: { 99: case 1: 100: sp = new Circle (5); 101: break; 102: case 2: 103: sp = new Rectangle(4,6); 104: break; 105: case 3: 106: sp = new Square (5); 107: break; 108: default: 109: fQuit = true; 110: break; 111: } 112: if (fQuit) 113: break; 114: sp->Draw(); 115: std::cout << "\n"; 116: } 117: return 0; 118: }
Час 18. Расширенное наследование 289 Результат (l)Circle (2)Rectangle (3)Square (0)Quit:2 X X X X X X X X X X X X X X X X X X X X X X X X Abstract drawing mechanism! (l)Circle (2)Rectangle (3)Square (0)Quit:3 X X X X X X X X X X X X X X X X X X X X X X X X X Abstract drawing mechanism! (l)Circle (2)Rectangle (3)Square (0)Quit:0 Анализ В строках 3—12 объявлен абстрактный класс Shape с тремя чистыми виртуальными методами доступа. Впрочем, чтобы класс стал абстрактным, достаточно было бы объя- вить в нем чистым виртуальным хотя бы один из методов. Методы GetArea() и GetPerimO пока не реализованы, реализован лишь метод Draw(). Классы Circle и Rectangle переопределяют метод Draw(), но применяют при этом связь с одноименным базовым методом, пользуясь преимуществом совмест- ного использования функциональных возможностей базового класса. Сложная иерархия абстракций Иногда необходимо получить один абстрактный класс как производный от другого абстрактного, например, для того чтобы сделать в производном абстрактном классе часть методов обычными, а остальные оставить чистыми виртуальными. Так, например, в классе Animal можно объявить методы Eat О, Sleep О, Moved и Reproduce () (есть, спать, двигаться и размножаться) как чистые виртуальные функ- ции, а затем от класса Animal получить производные классы Mammal и Fish (рыба). Исходя из того, что все млекопитающие размножаются практически одинаково, в классе Mammal имеет смысл преобразовать метод Reproduce () в обычный, оставив при этом методы Eat (), Sleep () и Move () чистыми виртуальными функциями. От класса Mammal происходит класс Dog, в котором необходимо переопределить остальные чистые виртуальные функции, чтобы получить возможность создавать объ- екты класса Dog. Итак, с точки зрения создателя классов можно было бы сказать, что невозможно существование ни животных, ни млекопитающих как таковых (классы Animal и Mammal), но все млекопитающие наследуют возможность воспроизведения — метод Reproduce () — без необходимости переопределять его каждый раз. В листинге 18.7 приведен пример программы, где реализован этот подход. Листинг 18.7. Файл derivingadt. срр. Происхождение одного абстрактного класса от другого 0: // Листинг 18.7. 1: // Происхождение одного абстрактного класса от другого 2: #include <iostream> 3: 4: enum COLOR { Red, Green, Blue, Yellow, White, Black, Brown }; 5:
290 Часть V. Наследование и полиморфизм 6: class Animal // общий базовый для обоих классов 7 : 8: { public: 9: Animal(int); 10: virtual -Animal () { std::cout << "Animal destructor...\n”; } 11: virtual int GetAge() const { return itsAge; } 12 : virtual void SetAge(int age) { itsAge = age; } 13 : virtual void Sleep() const = 0; 14: virtual void Eat() const = 0; 15: virtual void Reproduced const = 0; 16: virtual void Move!) const = 0; 17 : 18: virtual void Speak() const = 0; private: 19: int itsAge; 2 0: } ; 21: 22: Animal::Animal(int age): ,23: itsAge(age) 24: { 25: std::cout << "Animal constructor...\n"; 26: } 27 : 28: class Mammal : public Animal 29: { 30: public: 31: Mammalfint age):Animal(age) 32: { std::cout << "Mammal constructor...\n} 33: virtual -Mammal() { std::cout << "Mammal destructor...\n";} 34: virtual void Reproduce() const 35: { std::cout << "Mammal reproduction depicted...\n" ; } 36: }; 37 : 38: class Fish : public Animal 39: { 40: public: 41: Fish(int age):Animal(age) 42: { std::cout << "Fish constructor...\n"; } 43: virtual ~Fish() 44: { std::cout << "Fish destructor...\n"; } 45: virtual void Sleep!) const 46: { std::cout << "fish snoring...\n"; } 47: virtual void Eat() const 48: { std::cout << "fish feeding...\n"; } 49: virtual void Reproduce!) const 50: { std::cout << "fish laying eggs...\n"; } 51: virtual void Move!) const 52: { std::cout << "fish swimming...\n“; } 53: virtual void Speak!) const {} 54: }; 55: 56: class Horse : public Mammal 57: { 58: public: 59: Horse(int age, COLOR color ): 60: Mammal(age), itsColor(color) 61: { std::cout << “Horse constructor...\n"; }
Час 18. Расширенное наследование 291 62: virtual -Horse() 63: { std::cout << "Horse destructor...\n"; } 64: virtual void Speak() const 65: { std::cout << "Whinny!... \n“; } 66: virtual COLOR GetltsColor() const 67: { return itsColor; } 68: virtual void Sleep() const 69: { std::cout « “Horse snoring...\n"; } 70: virtual void Eat () const 71: { std::cout « "Horse feeding...\n"; } 72: virtual void Move() const 73: { std::cout << "Horse running...\n";} 74: 75: protected: 76: COLOR itsColor; 77: }; 78: 79: class Dog : public Mammal 80: { 81: public: 82: Dog(int age, COLOR color ): 83: Mammal(age), itsColor(color) 84: { std::cout « “Dog constructor...\n"; ) 85: virtual -Dog() 86: { std::cout « "Dog destructor...\n“; } 87: virtual void Speak() const 88: { std::cout « "Whoof!... \n"; } 89: virtual void Sleep() const 90: { std::cout « "Dog snoring...\n"; } 91: virtual void Eat() const 92: { std: :cout « “Dog eating...\n"; } 93: virtual void Move() const 94: { std::cout « "Dog running...\n"; } 95: virtual void Reproduce() const 96: { std::cout « "Dogs reproducing...\n"; } 97: 98: protected: 99: COLOR itsColor; 100: } ; 101: 102: int main () 103: { 104: Animal *pAnimal=0; 105: int choice; 106: bool fQuit = false; 107: 108: while (1) 109: { 110: std::cout « “ (l)Dog (2)Horse (3)Fish (O)Quit: “ 111: std::cin >> choice; 112: 113: switch (choice) 114: { 115: case 1: 116: pAnimal = new Dog (5 , Brown) ; 117: break; 118: case 2: 119: pAnimal = new Horse (4 , Black) ;
292 Часть V. Наследование и полиморфизм 120: break; 121: case 3: 122 : pAnimal = new Fish (5) 123: break; 124 : default: 125: fQuit = true; 126: break; 127: } 128: if (fQuit) 129 : break; 130: 131: pAnimal->Speak(); 132 : pAnimal->Eat(); 133: pAnimal->Reproduce(); 134 : pAnimal->Move(); 135: pAnimal->Sleep(); 136: delete pAnimal; 137: std::cout << "\n"; 138: ) 139 : return 0; 140: ) Результат (l)Dog (2)Horse (3)Bird (O)Quit: 1 Animal constructor... Mammal constructor... Dog constructor... Whoo f!... Dog eating... Dog reproducing... Dog running... Dog snoring... Dog destructor... Mammal destructor... Animal destructor... (l)Dog (2)Horse (3)Bird (O)Quit: 0 Анализ В строках 6—20 объявлен абстрактный класс Animal. Единственный метод этого класса, не являющийся чистой виртуальной функцией, — это общий для объектов всех производных классов метод доступа к переменной itsAge. Остальные пять ме- тодов— Sleep О, Eat (), ReproduceO, Move () и Speak() — объявлены как чис- тые виртуальные функции. Класс Mammal как производный от класса Animal объявлен в строках 28—36. Он не содержит никаких данных, а функция Reproduce () переопределена так, чтобы задать общий способ размножения всех млекопитающих. Класс Fish происходит непосред- ственно от класса Animal, поэтому функция Reproduce () в нем переопределена ина- че, чем в классе Mammal (и это соответствует реальности) Все классы, производные от Mammal, более не нуждаются в переопределении функ- ции Reproduce (), но сделать это при необходимости вполне возможно (как, например, класс Dog в строках 95—96). Чтобы можно было создать объекты классов Fish, Horse и Dog, все унаследованные ими чистые виртуальные функции были переопределены. Для доступа ко всем объектам производных классов в теле программы использован указатель класса Animal. Виртуальные методы вызываются в соответствии с типом объ-
Час 18. Расширенное наследование 293 екта, на который в данный момент указывает этот указатель, что гарантирует вызов правильного метода. При попытке создать объекты абстрактных классов Animal или Mammal компиля- тор выдаст сообщение об ошибке. Какие классы являются абстрактными? В одной из программ класс Animal является абстрактным, а в другой — нет. Как определить, когда он должен быть абстрактным? Ответ не зависит от каких-либо четких критериев, а определяется лишь целесооб- разностью в каждом конкретном случае. Так, в программе, описывающей ферму или зоопарк, возможно, потребуется, чтобы животное было абстрактным типом, а соба- ка — тем классом, объекты которого придется создавать. С другой стороны, на анимационной псарне собака, возможно, понадобится в каче- стве абстрактного типа данных, а классами для экземпляров объектов собак будут овчарки, терьеры и т.д. Таким образом, уровень абстракции зависит от степени детализации. Рекомендуется Не рекомендуется Использовать абстрактные классы при создании общих функций для всех производных классов. Обязательно переопределить в производных классах все чистые виртуальные функции. Объявлять чистой виртуальной любую функцию, которую следует переопределить. Пытаться создать объект абстрактного класса. Вопросы и ответы Что означает перенос функций вверх? Речь идет о перемещении совместно используемых функций вверх, в общий ба- зовый класс. Если функцию совместно используют несколько классов, то ее желательно переместить в общий для них базовый класс. Во всех ли случаях перенос будет наилучшим решением? Да, если переносятся функции, используемые совместно. Нет, если переносит- ся весь интерфейс. Т.е. если не все производные классы будут использовать ме- тод, то его перемещение вверх в общий базовый класс будет ошибкой, по- скольку в этом случае придется в процессе выполнения программы распознавать тип объекта, чтобы принять решение о возможности или невоз- можности вызова той или иной функции. Что плохого в приведении типа объектов? Виртуальные функции отслеживает v-таблица, а об определении типа объекта во время выполнения должен позаботиться программист. Зачем беспокоиться о создании абстрактного типа данных? Почему бы не оставить его обычным и просто избегать создания объектов этого типа? Целью многих соглашений языка C++ является привлечение компилятора для поиска ошибок на раннем этапе, что позволяет избежать их в процессе выпол- нения. Объявление класса абстрактным, создание чистых виртуальных функций за- ставят компилятор пометить как недопустимые любые объекты абстрактного типа.
294 Часть V. Наследование и полиморфизм Коллоквиум Изучив явление полиморфизма подробнее, имеет смысл ответить на несколько во- просов и выполнить ряд упражнений, чтобы закрепить полученные знания. Контрольные вопросы 1. В чем разница между виртуальным методом и методом, переопределенным в базовом классе? 2. В чем разница между чистым виртуальным методом и обычным виртуальным методом? 3. В чем выражается “абстрактность” абстрактных классов? 4. Что не так с предоставляемыми базовым классом методами, которые примени- мы только в некоторых производных классах? Упражнения 1. Измените файл derivingadt. срр (листинг 18.7) так, чтобы создать экземпля- ры классов Animal и Mammal. Что сообщил компилятор и почему? 2. Приведите реальный пример приложения, где могли бы использоваться абст- рактные типы данных. Подсказка: банковские счета очень популярны, но раз- личаются видами вкладов. 3. Что случится, если в файле dynamiccast.срр (листинг 18.2) удалить проверку в операторе if (строка 58) (наряду со строками 60 и 61)? Какие объекты срабо- тают правильно, а какие нет? Ответы на контрольные вопросы 1. Виртуальный метод следует переопределить в производном классе. 2. Если класс обладает обычными виртуальными методами, то его объект может быть создан. Чистые виртуальные методы никакого исполняемого кода не содержат, поэтому попытка создать экземпляр такого класса приведет к ошибке во время компиляции. Чистый виртуальный метод следует переопределить. 3. Они на самом деле абстрактны, поскольку присутствующие в них чистые виртуаль- ные методы не позволят создать конкретный экземпляр такого класса. Для этого сначала необходимо получить класс, производный от абстрактного, реализовать со- ответствующие методы, а уж затем создавать экземпляр производного класса. 4. Честно говоря, это свидетельство недостаточной проработки проекта. В теории ме- тоды базового класса должны применяться в большинстве (если не во всех) произ- водных классах. Идея заключается в расширении возможностей методов базового класса в производных, а не полного игнорирования их частью производных классов.
ЧАС 19 Связанные списки На этом занятии вы узнаете: что такое связанный список; как создавать связанные списки; как инкапсулировать функциональные возможности при помощи наследования. Связанные списки и другие структуры Массивы напоминают лоток для яиц. Это прекрасный контейнер фиксированного размера. Если контейнер слишком велик, то избыток пространства будет растрачен впустую. Если контейнер мал, его не хватит для размещения содержимого. Одним решением этой проблемы являются связанные списки. Связанный список (linked list) — это структура данных, состоящая из небольших контейнеров, способных поддерживать связь с аналогичными контейнерами. Основная мысль состоит в том, чтобы создать класс, который содержит один объект пользовательского типа (например, Cat или Rectangle) и указатель на следующий контейнер. Таким обра- зом, созданный контейнер способен хранить один объект необходимого типа, а также поддерживать связь со следующим аналогичным контейнером, что позволяет постро- ить из них цепочку необходимой длины. Контейнеры называются блоками (node). Первый блок в списке называется головой (head), а последний — хвостом (tail) списка. Существуют три основных типа связанных списков. Они перечислены в порядке усложнения: односвязные; двухсвязные; деревья. В односвязном списке (singly linked list) каждый блок указывает только вперед, на следующий блок. Для поиска определенного блока приходится начинать с головы и дви- гаться от блока к блоку, как в детской игре про поиск сокровищ (“следующая записка находится под диваном”)- Двухсвязный список (doubly linked list) позволяет переме- щаться по цепочке вперед и назад. Дерево (tree) — это более сложная структура, со-
296 Часть V. Наследование и полиморфизм стоящая из блоков, способных указывать в двух, трех и более направлениях. На рис. 19.1 представлены все три основные структуры. Иногда создаются и более сложные, развитые структуры данных, почти все из ко- торых используют принцип взаимосвязи между узлами. Односвязный список Рис. 19.1. Связанные списки Применение связанных списков В этом разделе связанный список рассматривается более подробно как один из ком- понентов конкретной системы. Для большинства проектов создание сложной структуры не менее важно, чем применение наследования, полиморфизма и инкапсуляции. Делегирование ответственности Фундаментальный подход объектно-ориентированного программирования подра- зумевает, что каждый объект делает только одну вещь, но делает ее очень хорошо, а все остальное предоставляет другим. Автомобиль — это прекрасный пример такого
Час 19. Связанные списки 297 подхода в механике: двигатель создает вращающий момент, но его распределением между колесами не занимается. Эту задачу решает трансмиссия. Преобразованием вращения в движение автомобиля ни двигатель, ни трансмиссия не занимаются, эта задача делегирована колесам. В хорошо спроектированной машине есть множество мелких деталей, каждая из которых выполняет свою, простую и понятную задачу, но вместе получается большой и сложный автомобиль. В хорошо спроектированной программе каждый класс решает свою небольшую задачу, но вместе они образуют довольно сложное приложение. Компоненты связанных списков Связанный список состоит из блоков. Класс самого блока пока остается абстракт- ным; сейчас рассмотрим лишь функциональное назначение блоков в списке. Список имеет три разновидности компонентов: головной блок, возглавляющий список, хво- стовой блок (догадайтесь о его назначении!) и любое количество внутренних блоков. Фактически хранилищем данных в списке будут внутренние блоки. Обратите внимание, данные и список — это совершенно разные веши. Теоретиче- ски в списке можно хранить любой тип данных. Но список состоит не из них, а из блоков, которые могут быть связаны вместе и содержать любой тип данных. Основной программе ничего не известно о блоках, она работает со списком в це- лом. Список также не делает слишком много, он просто предоставляет блоки. Код листинга 19.1 подробнейшим образом демонстрирует работу со связанным списком. Листинг 19.1. Файл linkedlist .срр. Связанный список 0: 1: 2: 3: 4: 5: 6: 7 : 8; 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: /I *********************************************** // Листинг 19.1. // // ЦЕЛЬ: Демонстрация связанного списка // // // II II II Демонстрация объектно-ориентированного подхода применения // связанных списков. Список лишь предоставляет блоки. // Блоки имеют абстрактный тип данных. Используются три типа // блоков: головные, хвостовые и внутренние. // Данные содержат только внутренние блоки. // // Класс Data создан специально для примера, его объекты // будут содержаться в связанном списке. // #include <iostream> enum { klsSmaller, klsLarger, klsSame }; // Класс Data для размещения в связанном списке. // Любой класс в связанном списке должен обладать двумя II методами: Show() (отображает значение) и // Compare() (возвращает относительную позицию) class Data { public:
298 Часть V. Наследование и полиморфизм 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42 : 43 : 44: 45: 46: 47 : 48: 49: 50: 51 : 52 : 53 : 54: 55 : 56: 57 : 58: 59 : 60: 61: 62: 63 : 64: 65: 66: 67: 68: 69: 70: 71: 72 : 73 : 74 : 75: 76: 77 : 78 : 79 : 80: 81: 82: 83: 84: 85: 86: 87: 88: Data(int val):myValue(val) {} -Data() {} int Compare(const Data &) ; void Show() { std::cout << myValue << std::endl; } private: int myValue; } ; // Метод Compare() используется для определения // относительного положения объекта в списке, int Data::Compare(const Data & theOtherData) { if (myValue < theOtherData.myValue) return klsSmaller; if (myValue > theOtherData.myValue) return klsLarger; else return klsSame; } // далее объявления class Node; class HeadNode; class TaiiNode; class InternalNode; // ADT - представление объекта блока в списке. Производный // класс должен переопределить методы Insert() и Show() class Node { public: Node() {} virtual -Node() {} virtual Node * Insert(Data * theData) = 0; virtual void Show() = 0; private: }; // Этот блок будет содержать сам объект // В данном случае объект имеет тип Data // Более общий случай будет продемонстрирован // при рассмотрении шаблонов class InternalNode: public Node { public: InternalNode(Data * theData, Node * next); virtual -InternalNode!) { delete myNext; delete myData; } virtual Node * Insert(Data * theData); virtual void Show() { myData->Show(); myNext->Show(); } // Делегирование! private: Data * myData; // собственно данные Node * myNext; // указатель на следующий блок списка }; // Конструктор лишь инициализирует значения InternalNode::InternalNode(Data * theData, Node * next): myData(theData).myNext(next)
Час 19. Связанные списки 299 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: { } // Суть списка. // При поступлении в список нового объекта // он передается в блок, который // и будет добавлен в список. Node * InternalNode::Insert(Data * theData) { // Новичок больше текущего или меньше? int result = myData->Compare(*theData); 102: 103: switch(result) 104: { 105: // По соглашению, если он такой же, то идет первым 106: case klsSame: // придется пропустить 107: case klsLarger: // новые данные впереди себя 108: { 109: InternalNode * dataNode = 110: new InternalNode(theData, this); 111: return dataNode; 112: } 113: 114: // Он больше текущего, поэтому послать 115: // к следующему блоку, и пусть ОН его обработает. 116: case klsSmaller: 117: myNext = myNext->Insert(theData); 118: return this; 119: } 120: return this; // успокоить компилятор 121: } 122: 123: 124: // Хвостовой блок списка - не более чем сторож 125: class TaiiNode : public Node 126: { 127: public: 128: TaiiNode () {} 129: virtual -TaiiNode() {} 130: virtual Node * Insert(Data * theData); 131: virtual void Show() {} 132: private: 133 : } ; 134: 135: // Если данные дошли сюда, их следует вставить впереди, 146: // ведь здесь хвост, и НИЧЕГО не может быть вставлено после 137: Node * TaiiNode::Insert(Data * theData) 138: { 139: InternalNode * dataNode = new InternalNode(theData, this); 140: return dataNode; 141: } 142: 143: // Головной блок не содержит никаких данных, 144: // он только указывает на начало списка. 145: class HeadNode : public Node 146: { 147: public:
300 Часть V. Наследование и полиморфизм 148 : HeadNode(); 149 : 150: 151. 152 : '153: 154 : 155: 156: 157: 158: 159 : 160: 161: 162 : 163: 164: 165: 166: 167: 168: 169 : 170: 171 : 172: 173 : 174 : 175 : 176: 177 : 178: 179: 180 : 181: 182 : 183 : 184 : 195: 196: 187 : 188: 189 : 190: 191 : 192 : 193: 194: 195: 196: 197: 198: 199 : 200: 201: 202: 203 : 204 : 205 : 206: virtual -HeadNode() { delete myNext; } virtual Node * Insert(Data * theData); virtual void Show() { myNext->Show(); } private: Node * myNext; }; // Хвост списка создается точно так же, // как и голова. HeadNode::HeadNode() { myNext = new TaiiNode; } II Ничто не может находиться перед головным блоком, // поэтому данные передаются следующему блоку. Node * HeadNode::Insert(Data * theData) { myNext = myNext->Insert(theData); return this; } // Только раздать данные, но самостоятельно не делать ничего, class LinkedList { public: LinkedList(); -LinkedList() { delete myHead; } void Insert(Data * theData); void ShowAll() { myHead->Show(); } private: HeadNode * myHead; }; // При рождении создается блок заголовка //Он создает хвост списка, // так что пустой список указывает на голову, которая // указывает на хвост, и между ними ничего нет LinkedList::LinkedList() { myHead = new HeadNode; } // Передать, передать, передать void LinkedList::Insert(Data * pData) { myHead->Insert (pData) ,- } // Основная программа int main() { Data * pData; int val; LinkedList 11; // Просит пользователя ввести значения // и помещает их в список
Час 19. Связанные списки 301 207 : for (;;) 208: { 209: std::cout << "What value? (0 to stop): ”; 210: std::cin >> val; 211: if (!val) 212: break; 213: pData = new Data(val); 214: 11.Insert(pData); 215: } 216: 217: // Теперь пройтись по списку и показать данные. 218: 11.ShowAll(); 219: return 0; //11 выходит из области видимости и удаляется! 220 : } Результат What value? (0 to stop) What value? (0 to stop) What value? (0 to stop) What value? (0 to stop) What value? (0 to stop) What value? (0 to stop) What value? (0 to stop) 2 3 5 8 9 10 5 8 3 9 2 10 О Анализ В первую очередь следует обратить внимание на перечисление, которое содержит три постоянных значения: klsSmaller, klsLarger и kisSame. Каждый сохраняемый в спи- ске объект должен иметь метод Compare (), возвращающий значение именно этого типа. Исключительно для иллюстрации работы со списком в строках 27—36 был создан класс Data, метод compare () которого реализован в строках 40—48. Объект класса Data содержит значение, которое можно сравнить с аналогичным значением другого объекта класса Data. Он также содержит метод Show О, отображающий значение объ- екта на экране. Простейший способ понять работу связанного списка — это использовать его на практике. В строке 199 начинается основная программа, в строке 201 объявлен указа- тель на объект класса Data, а в строке 203 — локальный связанный список 11 (LinkedList — связанный список). При создании связанного списка в строке 187 происходит вызов его конструктора. Единственной задачей конструктора является создание объекта HeadNode (головного блока) и присвоение его адреса указателю на связанный список в строке 180. Создание объекта HeadNode приводит к вызову одноименного конструктора, как показано в строке 158, что в свою очередь создает объект Tai INode (хвостовой блок) и присваивает его адрес указателю myNext (мой следующий) головного блока. Созда- ние объекта Tai INode приводит к вызову одноименного конструктора, как показано в строке 128; он является стандартным и ничего не делает.
302 Часть V. Наследование и полиморфизм Вот так легко и просто создается связанный список. Сначала в стеке создается сам список, а затем создаются головной и хвостовой блоки списка, и, наконец, между ни- ми устанавливается связь (рис. 19.2). Связанный список Рис. 19.2. Связанный список сразу после создания В строке 207 начинается бесконечный цикл. Пользователю предлагается ввести значение, которое будет добавлено в связанный список. Он может ввести очень много значений, пока не будет введено число 0, которое и завершает цикл. Введенное зна- чение проверяется в строке 211. Если введенное значение отличается от о, то в строке 213 создается новый объект класса Data, а в строке 214 он вводится в связанный список. Предположим, пользова- тель ввел число 15, тогда в строке 193 будет вызван метод insert (). Связанный список немедленно передает ответственность за вставку объекта своему головному блоку, который в строке 165 вызывает метод insert!), а тот в свою оче- редь передает ответственность блоку, на который указывает его указатель myNext. В данном (первом) случае он указывает на хвостовой блок списка (помните, при соз- дании головного блока был создан и связан с ним хвостовой блок). Следовательно, будет вызван именно его метод insert () (строка 137). Метод Tai INode:: insert () “знает”, что переданный ему объект следует вставить непосредственно перед собой, т.е. новый объект будет расположен в списке непосред- ственно перед хвостовым блоком. Следовательно, он создаст новый объект класса InternalNode (внутренний блок), передаст ему данные и указатель на себя. Это при- ведет к вызову конструктора объекта InternalNode, представленного в строке 87. Конструктор InternalNode () просто инициализирует свой указатель класса Data адресом переданного ему объекта Data, а свой указатель myNext — адресом того бло- ка, который передал ему этот объект. В данном случае это будет хвостовой блок спи- ска (помните, именно хвостовой блок списка передал его в указателе this). Теперь, когда внутренний блок InternalNode создан и адрес его присвоен указа- телю dataNode (в строке 139), сам адрес возвращается методом Tai INode:: Insert!). Вернемся к методу HeadNode: : Insert (), где адрес InternalNode присваивается указателю myNext объекта HeadNode (в строке 167). В заключение адрес HeadNode возвращается связанному списку в строке 195, дальнейшая передача прекращается, поскольку связанный список уже “знает” адрес головного блока. Зачем же возвращать адрес, если он не используется? Метод Insert () объявлен в базовом классе Node, и возвращение его значения необходимо в других производ- ных классах. Поэтому, если не принять значение, возвращаемое методом HeadNode:: insert (), во время компиляции произойдет ошибка; чтобы избежать этого, необходимо позволить связанному списку принять значение, возвращаемое объектом HeadNode, и просто не использовать его. Так что же произошло? Данные были вставлены в список. Список передал их го- ловному блоку. Головной блок вслепую передал их по адресу в своем указателе, а в первом случае он указывал на хвост списка. Хвост списка немедленно создал но- вый внутренний блок, инициализировав его так, чтобы он указывал на хвост списка. Затем хвостовой блок возвратил адрес нового блока голове списка, которая перена-
Час 19. Связанные списки 303 значила указатель myNext так, чтобы он указывал на новый блок. Все! Конец! Данные находятся в списке в нужном месте, как показано на рис. 19.3. Связанный список Рис. 19.3. Связанный список после добавления первого блока После вставки первого блока управление программой возвращается строке 209. Пользователя снова запрашивают о новом значении. Предположим, введено значение 3. Это приведет к созданию и включению в список нового объекта класса Data (строки 213 и 214). И вновь в строке 195 список передает данные в свой объект HeadNode. Метод HeadNode:: insert () в свою очередь передает новое значение по адресу в указателе myNext, который указывает теперь на блок, содержащий объект Data, значением кото- рого является 15. Это приводит к вызову метода InternalNode:: Insert () в строке 96. В строке 100 объект InternalNode использует свой указатель myData для обращения к методу Compare () своего объекта класса Data (значением которого является 15). Это позволит сравнить его с вновь переданным объектом класса Data (значением которого является 3) и приводит к вызову метода compare (), представленному в строке 40. Сравниваются два значения и, поскольку myValue равно 15, a theOtherData.my- Value — 3, возвращается значение klsLarger (если больше). После этого процесс выполнения программы перейдет к строке 107. Для нового объекта Data создается блок InternalNode. Новый блок указывает на текущий блок InternalNode, а новый адрес InternalNode, возвращенный методом InternalNode: : Insert (), передается блоку HeadNode. Таким образом, новый блок, значение объекта которого меньше значения объекта текущего блока, вставляется в список, который теперь выглядит, как показано на рис. 19.4. Связанный список Рис. 19.4. Связанный список после добавления второго блока
304 Часть V. Наследование и полиморфизм В третий раз пользователь добавляет значение 8. Оно больше 3, но меньше 15, по- этому должно быть вставлено между двумя существующими блоками. Процесс проте- кает аналогично предыдущему, за исключением того, что при сравнении с объектом, содержащим значение 3, вместо значения kisLarger будет возвращено значение kisSmaller (означающее, что объект, содержащий значение 3, меньше нового объек- та, содержащего значение 8). Это приведет к вызову метода InternalNode: : Insert () в строке 116. Вместо создания и вставки нового блока InternalNode лишь передаст новые данные в ме- тод insert () объекта, адрес которого содержит указатель myNext. В данном случае будет вызван метод insertNodeO того блока InternalNode, значение объекта Data которого равно 15. И вновь выполняется сравнение, в результате которого создается новый блок InternalNode. Указатель нового блока InternalNode содержит адрес того блока InternalNode, значением объекта Data которого является 15 и адрес которого бу- дет передан обратно тому блоку InternalNode, значением объекта Data которого является 3 (строка 118). Главным результатом всех этих действий будет следующим: новый блок вставлен в список в соответствующем месте. При желании код программы можно запустить в отладчике и с его помощью пона- блюдать, как все методы вызывают друг друга, а указатели меняются значениями. Так что же из этого следует? Как говорила Дороти, героиня книги The Wizard of Oz (“Мудрецы из страны Оз”): “Когда я думаю о том, что дорого моему сердцу, то не имею в виду тот хлам, который свален на моем заднем дворе.” Хотя это и правда, и нет места милее дома, но правда также и то, что процедурное программирование “упокоилось на заднем дворе”. В процедурном программировании основным методом было бы исследование данных и вызов соответствующих функций. Объектно-ориентированный подход подразумевает, что каждый объект имеет чет- кие и узкоспециализированные обязанности. Связанный список осуществляет под- держку головного блока. Головной блок передает новые данные по адресу, содержа- щемуся в его указателе, не придавая значения тому, на что именно он указывает. Хвостовой блок создает и добавляет новый внутренний блок всякий раз, когда по- лучает данные. Он “знает” только одно: если пришли данные, их следует вставить в список перед собой. Внутренние блоки немного сложнее; они требуют, чтобы их объект сравнил себя с новым объектом. В зависимости от результата новый объект будет либо добавлен перед существующим блоком, либо передан следующему. Обратите внимание, что внутренние блоки в сравнении не участвуют, оно выполняется самим объектом. Все, что должен уметь внутренний блок, это потребовать у объектов сравнения и ожидать один из трех возможных результатов. Получив положительный ответ, он вставляет блок в список перед собой, в противном же случае посылает его по адресу. Итак, кто же несет ответственность? В хорошо разработанной объектно- ориентированной программе никто'. Каждый объект хорошо делает свое маленькое дело, а в результате получается хорошая программа. Преимуществом связанного списка является возможность хранения данных лю- бого типа подобно классу Data. В данном случае использовалось целое число, но это мог быть любой встроенный тип данных или даже объекты любых классов, включая другие связанные списки.
Час 19. Связанные списки 305 Однажды автор работал над проектом, в котором необходимо было хранить текст, состоящий из абзацев расположенных внутри глав. Данные были сохранены в базе так, чтобы можно было получить доступ к отдельным главам. Полученный в результате документ состоял из связанного списка глав, включающего заголовок главы, ее номер, некоторую дополнительную информацию о ней и связанный список абзацев. Связан- ный список абзацев содержал информацию о каждом абзаце и его текст. Применение динамической памяти позволяет связанным спискам занимать мини- мум памяти при небольшом объеме данных и наращивать ее, если данных становится больше. Важнее всего то, что связанные списки занимают достаточно памяти, чтобы содержать хранимые в них данные. Массивы, напротив, занимают в памяти заранее заданный размер, что одновременно приводит и к расточительству, когда он больше занимаемых данных, и к ограничению, когда он меньше. Почему бы не использовать связанные списки всегда? Возникает вполне резонный вопрос: если связанные списки столь хороши, то почему бы не использовать их всегда? За все хорошее приходится платить. Доступ к элементам связанных списков осуществляется последовательно, в результате поиск необходимого элемента приходится начинать сначала, а это относительно долго. Представьте, если для того, чтобы найти нужный номер телефона, пришлось бы про- листать весь телефонный справочник! Вопросы и ответы Зачем создавать связанный список, если можно использовать массив? Массив имеет фиксированный размер, в то время как связанный список спосо- бен динамически изменять свой размер в процессе выполнения программы. Зачем отделять объект данных от узла? Добившись правильной работы узлов, их можно впоследствии многократно ис- пользовать в коде для любого количества любых объектов, которые нужно хра- нить в списке. Если в список необходимо добавить иные объекты, то следует ли создать новый тип списка и новый тип узла? Пока да. Решение этого вопроса рассматривается на занятии 23, “Шаблоны”. Коллоквиум Изучив возможности таких сложных структур данных, как связанные списки, име- ет смысл ответить на несколько вопросов и выполнить ряд упражнений, чтобы закре- пить полученные знания. Контрольные вопросы 1. Какие три типа списков существуют? 2. Для чего предназначен головной указатель? 3. Для чего предназначен следующий указатель? 4. Для чего необходим двухсвязный список?
306 Часть V. Наследование и полиморфизм Упражнения 1. Запустите на выполнение в режиме отладки программу файла linkedlist.cpp (листинг 19.1) и отследите вставку нескольких узлов. Теперь можно увидеть во- очию, как методы вызывают друг друга и как корректируются указатели. 2. Измените файл linkedlist.cpp (листинг 19.1) так, чтобы класс Data пред- ставлял собой тип double, а не int. Что необходимо для этого сделать? 3. Измените файл linkedlist.cpp (листинг 19.1) так, чтобы класс Data состоял из нескольких встроенных типов данных, а не из одного (int или double). Что еще необходимо изменить? Подсказка: требуется какой-то критерий, позво- ляющий выяснить, большее это значение или меньшее. Ответы на контрольные вопросы 1. Односвязный список, двухсвязный список и дерево. 2. Как и следует из его названия, он содержит адрес головного блока или начала связанного списка. Чтобы обратиться к списку, важно знать, где именно он на- чинается. 3. Как и следует из его названия, он содержит адрес следующего элемента связан- ного списка. Его можно рассматривать как голову остальной части списка. Это и есть связь в связанном списке! 4. Двухсвязный список имеет и голову, и хвост, следующие и предыдущие указа- тели, поэтому поиск можно начинать с обеих концов и продолжать его как вперед, так и назад.
ЧАСТЬ VI Специальные темы В этой части... Час 20. Специальные классы, функции и указатели Час 21. Препроцессор Час 22. Объектно-ориентированный анализ и проектирование Час 23. Шаблоны Час 24. Исключения, обработка ошибок и другое
ЧАС 20 Специальные классы, функции и указатели На этом занятии вы узнаете: что такое статические переменные-члены и статические функции-члены; как использовать статические переменные-члены и статические функции-члены; что такое дружественные функции и дружественные классы; как использовать дружественные функции для решения специфических проблем; как использовать указатели на функции-члены. Статические данные-члены До сих пор считалось, что данные в каждом объекте являются безраздельной соб- ственностью именно этого объекта и не используются другими объектами класса. На- пример, если существуют пять объектов класса cat, то каждый из них имеет собст- венный возраст, вес и другие данные. При этом возраст одного кота никак не влияет на возраст другого. Иногда необходима информация, совместно используемая всеми объектами класса. Например, может возникнуть необходимость узнать, сколько объектов определенного класса было создано на данный момент и сколько из них существует до сих пор. В отличие от обычных переменных, статические переменные-члены (static member data) доступны всем экземплярам класса. Они представляют собой компромисс между глобальными данными, доступными всем элементам программы, и данными-членами, доступными только объектам конкретного класса. Статический элемент можно рассматривать как принадлежащий всему классу, а не отдельному объекту. Обычные данные-члены и в самом деле принадлежат индивиду- альному объекту, но статический член создается в одном экземпляре для всех объек- тов класса. В листинге 20.1 объявлен объект класса Cat, который содержит статиче- скую переменную-член HowManyCats (сколько котов). Эта переменная содержит количество объектов класса Cat, созданных во время выполнения программы. Для этого она увеличивается на единицу при каждом вызове конструктора класса cat, а при вызове деструктора уменьшается.
Час 20. Специальные классы, функции и указатели 309 Листинг 20.1. Файл staticmember. срр. Статические данные-члены 0: // Листинг 20.1. Статические данные-члены 1: #include <iostream> 2: 3: class Cat 4: { 5: public: 6: Cat(int age = 1):itsAge(age) { HowManyCats++; } 7: virtual -Cat () { HowManyCats--; } 8: virtual int GetAge() { return itsAge; } 9: virtual void SetAge(int age) { itsAge = age; } 10: static int HowManyCats; 11: 12: private: 13: int itsAge; 14: 15: }; 16: 17: int Cat::HowManyCats = 0; 18: 19: int main!) 20: ( 21: const int MaxCats = 5; 22: Cat ‘CatHouse[MaxCats]; 23: int i ; 24: for (i=0; i<MaxCats; i++) 25: CatHouse[i] = new Cat(i); 26: 27: for (i = 0; KMaxCats; i + +) 28: { 29: std::cout « "There are " ; 30: std::cout << Cat::HowManyCats; 31: std:: cout << " cats left.'Xn"; 32: std::cout << "Deleting the one which is “; 33: std::cout « CatHouse[i]->GetAge(); 34: std::cout << ” years old\n"; 35: delete CatHouse[i]; 36: CatHouse[i] = 0; 37: } 38: return 0; 39: } Результат There аге 5 cats left! Deleting the one which is 0 years old There are 4 cats left! Deleting the one which is 1 years old There are 3 cats left! Deleting the one which is 2 years old There are 2 cats left! Deleting the one which is 3 years old There are 1 cats left! Deleting the one which is 4 years old
310 Часть VI. Специальные темы Анализ Упрощенный класс Cat объявлен в строках 3—15. В строке 10 объявлена статиче- ская переменная-член HowManyCats типа int. Само объявление переменной HowManyCats не создает переменной типа int, и память под нее не выделяет. В отличие от нестатических переменных-членов, об- ласть памяти для переменной HowManyCats не будет выделена и при создании объек- та класса Cat, поскольку она не является переменной-членом объекта. Поэтому в строке 17 переменная HowManyCats определяется и инициализируется явно. Довольно широко распространена такая ошибка: программисты забывают явно оп- ределить статические переменные-члены классов. Не допускайте таких промахов! Ко- нечно, если это все же случится, компоновщик заметит ошибку и выдаст сообщение примерно такого содержания: “Error: Unresolved external * 1 Cat::HowManyCats' referenced from C:\PROJECTS\STATICMEMBER.OBJ" или что-нибудь вроде следующего (при использовании другого компоновщика): undefined—reference toCat::HowManyCats Для переменной itsAge это не нужно, поскольку она не является статической и бу- дет определена при каждом создании объекта класса Cat, что и происходит в строке 25. В строке 6 конструктор класса Cat увеличивает значение статической переменной на единицу. Деструктор в строке 7 уменьшает это значение на единицу. Таким обра- зом, в любой момент времени переменная HowManyCats содержит текущее количест- во объектов класса cat. Основная программа находится в строках 19—39- Вначале создаются и помещаются в массив пять экземпляров объекта класса cat. При этом вызов конструктора класса Cat происходит пять раз, т.е. переменная HowManyCats пять раз увеличивается на единицу, начиная со значения 0. Затем в цикле программа вызывает каждый из пяти элементов массива и выводит на экран значение HowManyCats перед удалением текущего объекта. Вначале объектов бы- ло 5 (что и отображено на экране), затем на каждом цикле количество котов уменьшается. Обратите внимание: переменная HowManyCats является открытой (public), и обра- щаться к ней можно непосредственно из функции main(). В принципе нет необходи- мости выставлять эту переменную-член подобным образом. Такие переменные принято делать закрытыми наряду с остальными переменными-членами, а для доступа к ним создавать открытые методы, которыми можно воспользоваться, обратившись к экземп- ляру объекта класса cat. Но, с другой стороны, чтобы избежать всех этих сложностей с функциями доступа и обращаться к данным напрямую, можно воспользоваться одним из двух вариантов: оставить их открытыми либо создать статическую функцию-член. Статические функции-члены Статические функции-члены (static member function) подобны статическим пере- менным-членам: они не принадлежат одному объекту, а находятся в области видимо- сти всего класса. Таким образом, чтобы их вызвать, вовсе не обязательно создавать объект содержащего их класса (это показано в листинге 20.2). Листинг 20.2. Файл staticfunction, срр. Статические функции-члены 0: // Листинг 20.2. Статические функции-члены 1: ^include <iostream> 2 : 3: class Cat
Час 20. Специальные классы, функции и указатели 311 4: { 5: 6: 7: 8: 9: 10: И: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: public: Cat(int age = 1):itsAge(age) { HowManyCats++; } virtual -Cat() { HowManyCats--; } virtual int GetAge() { return itsAge; } virtual void SetAge(int age) { itsAge = age; } static int GetHowMany() { return HowManyCats; } private: int itsAge; static int HowManyCats; ) ; int Cat::HowManyCats = 0; void TelepathicFunction(); int main() { const int MaxCats = 5; Cat ‘CatHouse[MaxCats]; int i ; for (i=0; i<MaxCats; i++) { CatHouse[i] = new Cat(i); TelepathicFunction(); } for (i=0; i<MaxCats; i++) { delete CatHouse[i]; TelepathicFunction(); } return 0; ) void TelepathicFunction() { std::cout << "There are " << Cat::GetHowMany() << " cats alive!\n"; } Результат There are 1 cats alive There are 2 cats alive There are 3 cats alive There are 4 cats alive There are 5 cats alive There are 4 cats alive There are 3 cats alive There are 2 cats alive There are 1 cats alive There are 0 cats alive Анализ В строке 13 объявлена закрытая статическая переменная-член HowManyCats класса Cat. Открытый метод доступа к ней, GetHowMany (), объявлен как статиче- ский в строке 10.
312 Часть VI. Специальные темы Так как метод GetHowMany () объявлен открытым, доступ к нему может получить лю- бая функция, а при объявлении ее статической отпадает необходимость в существовании объекта класса cat. Именно поэтому функция TelepathicFunctionO в строке 41 может получить доступ к методу GetHowMany () без помощи объекта класса Cat. Безусловно, к методу GetHowMany() можно обратиться и из функции main(), ведь это такая же функция, как и остальные. ЛЙЖДГ______ цючп Указатель this Статические функции-члены не имеют указателя this. Следовательно, они не могут быть объявлены как const. А в связи с тем, что к данным- членам в функциях-членах обращаются при помощи указателя this, статические функции-члены не могут обращаться к переменным- членам, не являющимся статическими! Объединение классов Как уже было продемонстрировано на предыдущих примерах, нередки ситуации, когда объекты одного класса входят в состав других, т.е. являются вложенными. Про- граммисты на C++ говорят, что внешний класс содержит внутренний. Таким обра- зом, класс Employee (служащие) мог бы содержать объекты класса string (имя слу- жащего) и объекты типа int (зарплата служащего и т.д.). Упрощенный вспомогательный класс string представлен в листинге 20.3. Листинг 20.3. Файл string .hpp. Класс String 0: // Листинг 20.3. Класс String 1: ♦include <iostream> 2 : ♦include <string.h> 3: class String 4 : { 5: public: 6: // Конструкторы 7 : String(); 8 : String(const char ‘const); 9: String(const String S); 10: -String(); 11: 12 : // Перегруженные операторы 13: char & operator[](int offset); 14: char operator[](int offset) const; 15: String operator*(const Strings); 16: void operator+=(const Strings); 17: String S operator= (const String S); 18 : 19 : // Общие методы доступа 20: int GetLenf) const { return itsLen; } 21: const char * GetString() const { return itsString 22: // static int Constructorcount; 23: 24: private: 25: String(int); // Закрытый конструктор 26: char * itsString; 27: int itsLen; 28: }; 29:
Час 20. Специальные классы, функции и указатели 313 30: // стандартный конструктор создает строку нулевой длины 31: String::String() 32: { 33: itsString = new char[l]; 34: itsString[0] = '\0'; 35: itsLen=0; 36: // std::cout << "XtDefault string constructor^" ; 37: // ConstructorCount++; 38: } 39: 40: // Закрытый (вспомогательный) конструктор, 41: // используемый только методами класса для создания 42: // строк необходимой длины, заполненных символом null. 43: String::String(int len) 44: ( 45: itsString = new char[len+l]; 46: int i; 47: for (i = 0; i<=len; i++) 48: itsString[i] = '\0 ' ; 49: itsLen=len; 50: // std::cout << "XtString(int) constructor\n"; 51: // ConstructorCount++; 52: } 53: 54: String::String(const char * const cString) 55: { 56: itsLen = strlen(cString); 57: itsString = new char[itsLen+1]; 58: int i; 59: for (i=0; icitsLen; i++) 60: itsStringfi] = cString[i]; 61: :itsString [itsLen ]='\0'; 62: // std::cout « "XtString(char*) constructor\n"; 63: // ConstructorCount++; 64: } 65: 66: String::String (const String & rhs) 67: { 68: itsLen=rhs.GetLen(); 69: itsString = new char[itsLen+1]; 70: int i; 71: for ( i=0; icitsLen; i++) 72: itsString[i] = rhs[i]; 73: itsString[itsLen] = '\0 ' ; 74: // std::cout << "\tString(String&) constructorXn"; 75: // ConstructorCount++; 76: } 77: 78: String::-String () 79: { 80: delete [] itsString; 81: itsLen = 0; 82: // std::cout << "XtString destructorXn"; 83: ) 84: 85: // Оператор присвоения, освобождает существующую память, 86: // а затем копирует строку и ее размер 87: String& String::operator=(const String & rhs)
314 Часть VI. Специальные темы 88 : { 89 : if (this == &rhs) 90: return *this; 91: delete [] itsString; 92: itsLen=rhs.GetLen(); 93: itsString = new char[itsLen+1]; 94: int i ; 95 : for (i=0; i<itsLen; i++) 96: itsString[i] = rhs[i]; 97: itsString[itsLen] = '\0'; 98 : return *this; 99: // std::cout << "\tString operator=\n"; 100: 101: } 102 : // Непостоянный оператор индексирования, возвращает 103 : // ссылку на символ, так что ее можно 104 : // изменить! 105 : char & String::operator[](int offset) 106: { 107: if (offset > itsLen) 108: return itsString[itsLen-1]; 109 : else 110: return itsString[offset]; 111: 112 : } 113: // Постоянный оператор индексирования для использования 114 : // с постоянными объектами (см. конструктор копий) 115 : char String::operator[](int offset) const 116: { 117: if (offset > itsLen) 118 : return itsString[itsLen-1]; 119: else 120: return itsString[offset]; 121: 122 : ) 123 : // Создает новую строку, добавляя текущую 124 : // строку к rhs 125 : String String::operator+(const Strings rhs) 126: { 127 : int totalLen = itsLen + rhs.GetLen(); 128 : int i,j; 129: String temp(totalLen); 130: for (i=0; i<itsLen; i++) 131: temp[i] = itsString [i]; 132 : for (j=0; j<rhs.GetLen(); j++, i + + ) 133: 134: temp[i] = rhs[j]; 135: return temp; 136: 137: } 138: // Изменяет текущую строку, ничего не возвращая 139: void String::operator+=(const Strings rhs) 140: { 141: int rhsLen = rhs.GetLen(); 142 : int totalLen = itsLen + rhsLen; 143 : int i,j; 144 : String temp(totalLen); 145: for (i=0; i<itsLen; i++)
Час 20. Специальные классы, функции и указатели 315 146: temp[i] = itsString[i]; 147: for (j=0; j<rhs.GetLen() ; j++, i++) 148: temp[i] = rhs[i-itsLen] ; 149: temp [totalLen ]='\0 150: *this = temp; 151: } 152: 153: // int String::Constructorcount = 0; Результат Отсутствует. Анализ В строке 22 объявлена статическая переменная-член Constructorcount (счетчик конструктора), а в строке 153 она инициализирована. Эта переменная увеличивается при каждом вызове конструктора. В листинге 20.4 описан класс Employee (служащий), который содержит три стро- ковых объекта. Обратите внимание, что некоторые операторы листинга 20.3 заком- ментированы. Они будут использоваться в упражнениях. Italy Многократное использование Код листинга 20.3 расположен в файле string, hpp. Впоследствии, ко- гда понадобится класс String, можно будет подключить его, использо- вав директиву #include "String.hpp”. Листинг 20.4. Файл employeemain. срр. Класс Employee и основная программа 0: #include “string.hpp" 1: // Библиотека iostream уже подключена 2: using std::cout; // здесь используется std::cout 3: 4: 5: class Employee 6: { 7: public: 8: Employee!); 9: Employee(char *, char *, char *, long); 10: -Employee () ; 11: Employee(const Employees); 12: Employee & operator (const Employee &) ; 13: 14: const String & GetFirstName() const { return itsFirstName; } 15: const String & GetLastName() const { return itsLastName; } 16: const String & GetAddress() const { return itsAddress; } 17: long GetSalary() const { return itsSalary; } 18: 19: void SetFirstName(const String & fName) 20: { itsFirstName = fName; } 21: void SetLastName(const String & IName) 22: { itsLastName = IName; } 23: void SetAddress(const String & address) 24: { itsAddress = address; } 25: void SetSalary(long salary) { itsSalary = salary; }
316 Часть VI. Специальные темы 26: private: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40 : 41: 42 : 43: 44: 45: 46: 47: 48: 49: 50 : 51: 52: 53: 54: 55: 56: 57 : 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71 : 72: 73 : 74 : 75: 76: 77 : String itsFirstName; String itsLastName; String itsAddress; long itsSalary; }; Employee::Employee(): itsFirstName(""), itsLastName itsAddress(""), itsSalary(0) {} Employee::Employee(char * firstName, char * lastName, char * address, long salary): itsFirstName(firstName), itsLastName(lastName), itsAddress(address), itsSalary(salary) {} Employee::Employee(const Employee & rhs): itsFirstName(rhs.GetFirstName()), itsLastName(rhs.GetLastName()), itsAddress(rhs.GetAddress()), itsSalary(rhs.GetSalary()) {} Employee::-Employee() {} Employee & Employee::operator= (const Employee & rhs) { if (this == &rhs) return *this; itsFirstName = rhs.GetFirstName(); itsLastName = rhs.GetLastName(); itsAddress = rhs.GetAddress(); itsSalary = rhs.GetSalary(); return *this; } int main() { Employee Edie("Jane”,"Doe","1461 Shore Parkway", 20000); Edie.SetSalary(50000); String LastName("Levine"); Edie.SetLastName(LastName); Edie.SetFirstName("Edythe"); 78: 79: 80: 81: 82: 83: cout << "Name: ”; cout << Edie.GetFirstName().GetString(); cout << “ " << Edie.GetLastName().GetString(); cout << "AnAddress: cout << Edie.GetAddress().GetString(); cout « "AnSalary:
Час 20. Специальные классы, функции и указатели 317 84: cout << Edie.GetSalary(); 85: return 0; 86: } Результат Name; Edythe Levine Address: 1461 Shore Parkway Salary: 50000 Анализ Листинг 20.4 представляет класс Employee, содержащий три строковых объекта: ItsFirstName (его фамилия), itsLastName (его имя) и itsAddress (его адрес). В строке 72 объект Employee создается и инициализируется четырьмя значениями. В строке 73 расположен вызов метода доступа SetSalary() (установить оклад) класса Employee, которому передают значение 50000. В реальной программе это значение определялось бы динамически в процессе выполнения программы либо устанавлива- лось как постоянное. В строке 74 создается и инициализируется с помощью строковой константы С++ объект LastName класса String (или просто строка LastName). Чуть ниже, в строке 75, эта строка используется как аргумент метода SetLastName () объекта Edie класса Employee (т.е. служащему присвоено имя). В строке 76 расположен вызов функции SetFirstName() объекта Edie класса Employee, которому в качестве аргумента передана другая строковая константа. Но если внимательнее присмотреться к классу Employee, окажется, что он не имеет ме- тода SetFirstName (), получающего символьную строку в качестве аргумента; функ- ции SetFirstName () необходима постоянная ссылка на строку. Тем не менее, ошибка не произойдет, а компилятор самостоятельно решит эту проблему, поскольку в строке 8 листинга 20.3 ему было указано, как это сделать. Доступ к членам вложенного класса В объектах класса Employee отсутствуют специальные методы доступа к его собст- венным переменным-членам класса String. Поэтому, если объект Edie класса Employee попытается обратиться к переменной-члену itsLen своей собственной пе- ременной-члена itsFirstName, произойдет ошибка времени компиляции. Но это, кстати, не является недостатком. Класс string обладает определенным интерфейсом, функции доступа которого обеспечивают все необходимое его клиентам, и класс Employee вовсе не должен заботиться о подробностях реализации того, как хранят свою информацию его переменные-члены. Контроль доступа к вложенным членам Обратите внимание: класс string содержит перегруженный оператор operator+, а в классе Employee он блокирован, поскольку все строковые методы доступа этого класса (типа GetFirstName ()) объявлены как возвращающие постоянные ссылки. Поскольку оператор operator+ не является (и не может являться) постоянной (const) функцией (ведь она изменяет обратившийся к ней объект), попытка написать следующее выражение приведет к ошибке во время компиляции: String buffer = Edie.GetFirstName() + Edie.GetLastName(); Функция GetFirstName () возвращает постоянный объект класса String, а вы- звать оператор operator+ для постоянного объекта невозможно.
318 Часть VI. Специальные темы Для устранения этого противоречия перегрузим метод GetFirstName () как непо- стоянный: const String & GetFirstName() const { return itsFirstName; } String & GetFirstName() { return itsFirstName; } Обратите внимание: возвращаемое значение больше не является постоянным (const), и сама функция-член также более не является постоянной. Для изменения возвращаемого значения недостаточно перегрузить только имя функции, необходимо изменить ее состояние. Цена объединения Важно отметить, что за каждый объект класса String при каждом создании или копировании объекта класса Employee придется “расплачиваться”. Копирование при передаче по значению или передача по ссылке При передаче объекта класса Employee по значению все содержащиеся в нем строки также копируются, следовательно, происходит вызов их конструкторов. Это обходится недешево как с точки зрения потребления памяти, так и потраченного времени. При передаче объекта класса Employee по ссылке (с помощью указателей или ссылок) используется исходный объект и ничего не копируется. Вот почему про- граммисты C++ не обязаны прикладывать максимум усилий при передаче большого и сложного объекта, ведь достаточно передать лишь несколько байтов адреса. Дружественные классы Иногда приходится создавать классы совместно, как набор или комплект. Такие взаимосвязанные классы могут нуждаться в доступе к закрытым членам друг друга, а открытой сделать эту информацию нельзя. Таким образом, если необходимо предоставить данные или закрытые методы ка- кому-либо иному классу, достаточно объявить его дружественным (friend). Это позво- лит расширить интерфейс класса, дополнив его возможностями дружественного класса. Очень важно заметить, что дружественные отношения нельзя передать другим классам. Иными словами, если вы — мой друг, а Джо — ваш друг, то это вовсе не означает, что Джо — мой друг. Дружба не наследуется. Если вы — мой друг и я хочу поделиться с вами своими секретами, то это вовсе не означает, что я хочу поделиться этими сек- ретами с вашими детьми. И, наконец, дружественные отношения не взаимны. Объявление первого класса другом второго не делает второй класс другом первого. Вы можете сообщать мне ваши секреты, но не надейтесь, что я сообщу вам свои. Объявление дружественных классов следует применять осторожно. Если оба класса взаимозависимы и один из них часто нуждается в доступе к данным другого, то сущест- вует достаточно оснований, чтобы объявить класс дружественным. Но зачастую проще организовать взаимодействие между классами с помощью открытых методов доступа. JfeKV______ й/ючш Инкапсуляция От начинающих программистов на C++ очень часто можно услышать жалобы на то, что объявление дружественных классов “подрывает” ин- капсуляцию, столь важную для объектно-ориентированного програм- мирования. Это заблуждение, поскольку объявление класса дружест- венным просто расширяет интерфейс другого класса и подрывает инкапсуляцию не больше, чем открытое наследование.
Час 20. Специальные классы, функции и указатели 319 Дружественные функции Иногда необходимо предоставить права доступа не всему классу, а только одной или нескольким функциям-членам. Для этого достаточно объявить функцию-член другого класса дружественной, не объявляя таковым весь класс. Дружественной мож- но объявить любую функцию, вне зависимости от того, является ли она функцией- членом другого класса. Указатели на функции Подобно имени массива, являющегося постоянным указателем на первый элемент массива, имя функции является постоянным указателем на саму функцию. Можно объявить переменную (указатель), содержащую адрес функции, и вызывать ее по это- му указателю. Это может оказаться весьма удобным, поскольку позволит обращаться к любым функциям программы по собственному выбору. Единственной сложностью в применении указателей на функции является необхо- димость распознать тип объекта, на который указывает указатель. Указатель на тип int указывает на целочисленную переменную, а указатель на функцию должен ука- зывать на тип значения, возвращаемого функцией и соответствующего сигнатуре. long (* funcPtr) (int); Здесь funcPtr объявлен как указатель (обратите внимание на * перед именем), который указывает на функцию, получающую целочисленный параметр и возвра- щающую значение типа long. Круглые скобки вокруг * funcPtr обязательны, по- скольку скобки вокруг int имеют больший приоритет по сравнению с оператором косвенного обращения (*). Без первых круглых скобок это было бы объявлением функции funcPtr, получающей целочисленный параметр и возвращающей указатель на значение типа long (Не забудьте, что все пробелы в языке C++ игнорируются.) Рассмотрим два объявления: long * Function (int); long (* funcPtr) (int); Здесь Function () является функцией, получающей параметр типа int и возвра- щающей указатель на переменную типа long, a funcPtr является указателем на функцию, получающую параметр типа int и возвращающую значение типа long. Объявление указателя на функцию всегда содержит тип возвращаемого значения и типы параметров (если они есть), заключенные в круглые скобки. Объявление и применение указателей на функции иллюстрирует листинг 20.5. Листинг 20.5. Файл ptrtof unction. срр. Указатели на функции 0: // Листинг 20.5. Применение указателей на функции 1: #include <iostream> 2: 3: void Square (int&,int&); 4: void Cube (int&, int&); 5: void Swap (int&, int &); 6: void GetVals (int&, int&); 7: void PrintVals (int, int); 8: 9: int main() 10: { 11: void (* pFunc) (int &, int &); 12: bool fQuit = false; 13: 14: int valOne = l, valTwo=2; 15: int choice;
320 Часть VI. Специальные темы 16: while (fQuit = = false) 17: { 18: std::cout << ”(0)Quit (1)Change Values " 19: << : "(2)Square (3)Cube (4)Swap: "; 20: std::cin >> choice; 21: switch (choice) 22: { 23: case 1: 24: pFunc = GetVais; 25: break; 26: case 2: 27: pFunc = Square; 28 : break; 29: case 3 : 30: pFunc = Cube ; 31: break; 32: case 4: 33: pFunc = Swap ; 34: break; 35: default : 36: fQuit = true; 37: break; 38: } 39: 40 : if (fQuit) 41: break; 42: 43: PrintVals(valOne, valTwo); 44: pFunc(valOne, valTwo); 45: PrintVals(valOne, valTwo); 46: } 47 : return 0; 48: ) 49: 50: void PrintVals(i nt x, int y) 51: { 52: Std::cout « " x: " « x << " у: " << у << std::endl; 53 : ) 54 : 55: void Square (int & rX, int & rY) 56: ( 57 : rX * = rX; 58 : rY *= rY; 59: } 60: 61: void Cube (int & rX, int & rY) 62: { 63: int tmp; 64: 65: tmp = rX ; 66: rX *= rX; 67: rX = rX * tmp ; 68: 69: tmp = rY ; 70: rY *= rY; 71: rY = rY * tmp; 72: } 73 : 74 : void Swap(int & rX, int & rY)
Час 20. Специальные классы, функции и указатели 321 75: { 76: int temp; 77: temp = rX; 78: rX = rY; 79: rY = temp; 80: } 81: 82: void GetVals (int & rValOne, int & rValTwo) 83: { 84: std::cout << "New value for ValOne: "; 85: std::cin >> rValOne; 86: std::cout << "New value for ValTwo: "; 87: std::cin » rValTwo; 88: } Результат (O)Quit (1) Change Values (2)Square (3)Cube (4)Swap: 1 X: 1 y:2 New value for ValOne: 2 New value for ValTwo: 3 x: 2 y:3 (O)Quit (1) Change Values (2)Square (3)Cube (4)Swap: 3 x: 2 y:3 x: 8 y: 27 (O)Quit (1) Change Values (2)Square (3)Cube (4)Swap: 2 X: 8 y: 27 x:64 y:729 (O)Quit (1) Change Values (2)Square (3)Cube (4)Swap: 4 x:64 y:729 x:729 y:64 (O)Quit (1) Change Values (2)Square (3)Cube (4)Swap: 0 Анализ В строках 3—6 объявлены четыре функции с одинаковым типом возвращаемого значения и сигнатурой. Все функции возвращают тип void и получают по две ссылки на тип int. В строке 11 переменная pFunc объявлена как указатель на функцию, получающую две ссылки на тип int и возвращающую void. Указатель pFunc может содержать адрес любой из вышеперечисленных функций. Пользователю предлагается вызвать одну из них. Адрес выбранной функции присваивается указателю pFunc. В строках 43—45 на экран выводятся текущие значения двух целочисленных переменных, затем происходит вызов выбран- ной пользователем функции, а результат вычисления снова выводится на экран. Упрощенный вызов Обращаться к функции по адресу в указателе на функцию вовсе не обязательно, хотя вполне возможно. Таким образом, если pFunc является указателем на функцию, получающую параметр типа int и возвращающую значение типа long, то, присвоив ему соответствующую функцию, можно вызывать ее следующим образом: pFunc (X) ; ИЛИ (‘pFunc) (х) ; Оба выражения идентичны, но первая форма записи короче второй.
322 Часть VI. Специальные темы Массивы указателей на функции Точно так же, как объявляется массив указателей на тип int, можно объявить массив указателей на функции с одинаковой сигнатурой и возвращающие значения одного типа. Эту концепцию демонстрирует листинг 20.6. Листинг 20.6. Файл ptrarrayfunction.срр. Применение массива указателей на функции 0: // Листинг 20.6. Массив указателей на функции 1: #include <iostream> 2 : 3: void Square (int&,int&); 4: void Cube (int&, int&); 5: void Swap (int&, int &); 6: void GetVals (int&, int&); 7: void PrintVals (int, int); 8 : 9: int main() 10: { 11: int valOne = 1, valTwo = 2; 12: int choice,!; 13: const int MaxArray = 5; 14: void (‘pFuncArray[MaxArray])(int&, int&); 15: 16: for (i=0; i<MaxArray; i++) 17: { 18: std::cout « "(l)Change Values " 19: « "(2)Square (3)Cube (4)Swap: "; 20: std::cin >> choice; 21: switch (choice) 22: { 23: case 1: 24: pFuncArray[i] = GetVals; 25: break; 26: case 2: 27: pFuncArray[i] = Square; 28: break; 29: case 3: 30: pFuncArray[i] = Cube; 31: break; 32: case 4: 33: pFuncArray[i] = Swap; 34: break; 35: default: 36: pFuncArray[i] = 0; 37: } 38: } 39: 40: for (i=0; i<MaxArray; i++) 41: { 42: pFuncArray[i](valOne,valTwo); 43: PrintVals(valOne,valTwo); 44: } 45: return 0; 46: } 47 : 48: void PrintVals(int x, int y)
Час 20. Специальные классы, функции и указатели 323 49: { 50: std::cout « "х: " « х << " у: " << у « std::endl; 51: } 52: 53: void Square (int & rX, int & rY) 54: { 55: rX *= rX; 56: rY *= rY; 57: } 58: 59: void Cube (int & rX, int & rY) 60: { 61: int tmp; 62: 63: tmp = rX; 64: rX *= rX; 65: rX = rX * tmp; 66: 67: tmp = rY ; 68: rY *= rY; 69: rY = rY * tmp; 70: } 71: 72: void Swap(int & rX, int & rY) 73: { 74: int temp; 75: temp = rX; 76: rX = rY; 77: rY = temp; 78: } 79: 80: void GetVals (int & rValOne, int & rValTwo) 81: { 82: std::cout << "New value for ValOne: “; 83: std::cin >> rValOne; 84: std::cout « "New value for ValTwo: "; 85: std::cin >> rValTwo; 86: } Ш----- «стомам Будьте внимательным с этим примером Как уже говорилось, для примеров этой книги не применялись меро- приятия по защите от ошибок. Если пользователь выберет в этом примере значение, отличное от 1, 2, 3 или 4, произойдет отказ. Кон- кретное сообщение будет зависеть от операционной системы, ком- пилятора и отладчика, но его причиной будет попытка вызвать неоп- ределенную функцию (см. строку 36). Эту проблему можно предотвратить, но код программы получится длиннее и менее наглядным. Результат (1) Change (1) Change (1) Change (1) Change Values Values Values Values (2)Square (2)Square (2)Square (2)Square (3)Cube (4)Swap: 1 (3)Cube (4)Swap: 2 (3)Cube (4)Swap: 3 (3)Cube (4)Swap: 4
324 Часть VI. Специальные темы (1)Change Values (2)Square (3)Cube (4)Swap: 2 New Value for ValOne: 2 New Value for ValTwo: 3 x: 2 y: 3 x: 4 y: 9 x: 64 y: 729 x: 729 y: 64 x: 531441 y:4096 Анализ В строках 16—38 пользователю предлагается выбрать функции для вызова. Адрес каждой выбранной функции передается в массив и становится его элементом. Код строк 40—44 по очереди вызывает все функции (код строки 42 выполняет функцию, ад- рес которой хранится в массиве). Результат выводится на экран после каждого вызова. Передача указателей на функции другим функциям Указатели на функции (а также массивы указателей на функции) могут быть пере- даны другим функциям, которые в ходе выполнения своих задач могут, в свою оче- редь, обращаться к этим функциям, используя переданный указатель. Листинг 20.6 можно усовершенствовать, передав указатель на выбранную функцию другой функции (кроме main ()), которая выведет исходные значения на экран, вызо- вет функцию и вновь выведет на экран измененные значения. Именно такой подход применен в листинге 20.7. Листинг 20.7. Файл passingptrfunction. срр. Передача указателя на функцию как аргумента другой функции 0: II Листинг 20.7. Передача указателя на функцию 1: #include <iostream> 2: using namespace std; II здесь используются объекты std:: 3 : 4: void Square (int&,int&); 5: void Cube (int&, int&); 6: void Swap (int&, int &); 7: void GetVals (int&, int&); 8: void PrintVals (void (*)(int&, int&),int&, int&); 9 : 10: int main)) 11: { 12: int valOne=l, valTwo=2; 13: int choice; 14: bool fQuit = false; 15: 16: void (*pFunc)(int&, int&); 17 : 18: while (fQuit == false) 19: { 20: cout « "(0)Quit (1)Change Values " 21: << "(2)Square (3)Cube (4)Swap: "; 22: cin >> choice; 23: switch (choice) 24: { 25: case 1: 26: pFunc = GetVals;
Час 20. Специальные классы, функции и указатели 325 27: break,- 28: case 2: 29: pFunc = Square; 30: break; 31: case 3: 32 : pFunc = Cube; 33: break; 34: case 4: 35: pFunc = Swap; 36: break; 37: default: 38: fQuit = true; 39: break; 40: } 41: if (fQuit == true) 42: break; 43: PrintVals ( pFunc, valOne, valTwo); 44: } 45: 46: return 0; 47: } 48: 49: void PrintVals! void (‘pFunc) (int&, int&),int& x, int& y) 50: { 51: cout << "x: " << x « " у: " « у << endl; 52: pFunc (x, у) ; 53: cout << "x: " « x « " у: " « у « endl; 54: } 55: 56: void Square (int & rX, int & rY) 57: { 58: rX *= rX; 59: rY *= rY; 60: } 61: 62: void Cube (int & rX, int & rY) 63: { 64: int tmp; 65: 66: tmp = rX; 67: rX *= rX; 68: rX = rX * tmp; 69: 70: tmp = rY; 71: rY *= rY; 72: rY = rY * tmp; 73: } 74: 75: void Swap (int & rX, int & rY) 76: { 77: int temp; 78: temp = rX; 79: rX = rY; 80: rY = temp; 81: } 82: 83: void GetVals (int & rValOne, int & rValTwo) 84: {
326 Часть VI. Специальные темы 85: cout << "New value for ValOne: 86: cin >> rValOne; 87: cout « "New value for ValTwo: 88: cin >> rValTwo; 89: } Результат (O)Quit (1)Change Values (2(Square (3)Cube (4)Swap: 1 х: 1 у:2 New value for ValOne: 2 New value for ValTwo: 3 x: 2 y:3 (O)Quit (1)Change Values (2(Square (3)Cube (4)Swap: 3 x: 2 y:3 X: 8 y: 27 (O)Quit (1)Change Values (2(Square (3)Cube (4)Swap: 2 X: 8 y: 27 X:64 y:729 (O)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 4 x:64 y:729 X:729 y:64 (O)Quit (1)Change Values (2(Square (3)Cube (4)Swap: 0 Анализ В строке 16 объявлен указатель на функцию pFunc, получающую две ссылки на тип int и возвращающую тип void. В строке 8 объявлена получающая три параметра функция PrintVals. Первым параметром является указатель на функцию, возвра- щающую тип void и получающую две ссылки на тип int, а второй и третий парамет- ры функции PrintVals представляют собой ссылки на значения типа int. После того как пользователь выберет нужную функцию, в строке 43 происходит вызов функции PrintVals при помощи указателя на функцию pFunc. Спросите у знакомого программиста на C++, что означает следующее выражение: void PrintVals(void (*)(int&, int&), int&, int&); Этот вид объявления используется крайне редко и вынуждает программистов, встретивших нечто подобное в тексте, обращаться к учебнику. Но временами подоб- ный подход позволяет значительно усовершенствовать код программы, как в данном случае. На самом деле это объявление функции, которая ничего не возвращает и по- лучает в качестве параметров указатель на функцию (в свою очередь получающую две целочисленные ссылки) и еще две целочисленные ссылки. Но существует более про- стой способ записи — с применением ключевого слова typedef. Использование ключевого слова typedef с указателями на функции Конструкция void (*) (int&, int&) достаточно громоздка. Для ее упрощения с помощью ключевого слова typedef можно объявить новый тип указателей на функции (назовем его VPF), возвращающие тип void и получающие две ссылки на значения типа int. Листинг 20.8 представляет собой улучшенную версию листин- га 20.7 с использованием этого подхода.
Час 20. Специальные классы, функции и указатели 327 Листинг 20.8. Файл usingtypedef. срр. Применение ключевого слова typedef упрощает работу с указателями на функции 0: II Листинг 20.8. Применение ключевого слова typedef 1: #include <iostream> 2: using namespace std; // здесь используются объекты std:: 3: 4: void Square (int&, int&); 5: void Cube (int&, int&); 6: void Swap (int&, int &); 7: void GetVals (int&, int&); 8: typedef void (*VPF) (int&, int&); 9: void PrintVals(VPF, int&, int&); 10: 11: int main() 12: { 13: int valOne = 1, valTwo = 2; 14: int choice; 15: bool fQuit = false; 16: 17: VPF pFunc; 18: 19: while (fQuit == false) 20: { 21: cout << "(O)Quit (l)Change Values" 22: « "(2)Square (3)Cube (4)Swap: ”; 23: cin >> choice; 24: switch (choice) 25: { 26: case 1: 27: pFunc = GetVals; 28: break; 29: case 2: 30: pFunc = Square; 31: break; 32: case 3: 33: pFunc = Cube; 34: break; 35: case 4: 36: pFunc = Swap; 37: break; 38: default: 39: fQuit = true; 40: break; 41: ) 42: if (fQuit == true) 43: break; 44: PrintVals ( pFunc, valOne, valTwo); 45: } 46: return 0; 47: } 48: 49: void PrintVals( VPF pFunc,int& x, int& y) 50: { 51: cout << "x: " « x << " у: " << у « endl; 52: pFunc(x,y); 53: cout « "x: " << x << " у: " << у << endl; 54: }
328 Часть VI. Специальные темы 55: 56: void Square (int & rX, int & rY) 57: { 58: rX *= rX; 59: rY * = rY; 60: } 61 : 62: void Cube (int & rX, int & rY) 63: { 64: int tmp; 65: 6 6: tmp = rX; 67: rX *= rX; 68: rX = rX * tmp; 69: 7 0: tmp = rY; 71: rY *= rY; 72: rY = rY * tmp; 73: } 74: 75: void Swap(int & rX, int & rY) 76: { 77: int temp; 78: temp = rX; 79: rX = rY; 80: rY = temp; 81: } 82 : 83: void GetVals (int & rValOne, int & rValTwo) 84: { 85: cout « "New value for ValOne: ”; 86: cin >> rValOne; 87: cout << "New value for ValTwo: ”; 88: cin >> rValTwo; 89: } Результат (O)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 1 x: 1 у:2 New value for ValOne: 2 New value for ValTwo: 3 x: 2 у:3 (O)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 3 x: 2 y:3 x: 8 y: 27 (O)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 2 x: 8 y: 27 x:64 y:729 (O)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 4 x:64 y:729 x:729 y:64 (O)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 0
Час 20. Специальные классы, функции и указатели 329 Анализ В строке 8 с помощью ключевого слова typedef объявлен новый тип (vpf) указа- теля на функции, возвращающие тип void и получающие две ссылки на тип int. В строке 9 объявлена функция PrintVals (), которая получает три параметра: vpf и две ссылки на тип int. В строке 17 объявлен указатель pFunc на тип vpf. Определение типа vpf упрощает использование указателя pFunc и функции PrintVals(). Не забывайте, что ключевое typedef создает синоним, поэтому един- ственным различием между листингами 20.7 и 20.8 является “удобочитаемость”! Указатели на функции-члены До сих пор все указатели на функции создавались для применения вне класса. Но можно создать указатели и на функции, являющиеся членами классов. Для создания такого указателя используется тот же синтаксис, что и для обычного указателя на функцию, но содержащий имя класса и оператор области видимости (::). Таким образом, если pFunc указывает на функцию-член класса Shape, которая получает два целочисленных параметра и возвращает тип void объявить его можно следующим образом: void (Shape::*pFunc) (int, int); Указатели на функции-члены используются точно так же, как и указатели на обыч- ные функции, за исключением того, что для вызова им требуется объект соответствую- щего класса. Листинг 20.9 иллюстрирует применение указателей на функции-члены. будьте осторожны! Некоторые компиляторы не обеспечивают использование имен функций как синонимов для их адресов. Если для приведенного ниже кода исполь- зуемый компилятор выдает предупреждение или сообщение об ошибке, попробуйте заменить строки 71 -76 следующими: 71: case 1: 72: pFunc = & Mammal::Speak; 73: break; 74: default: 75: pFunc = & Mammal::Move; 76: break; А затем внесите то же изменение во все последующие листинги. (Не забудьте символ амперсанда (&) для каждой строки.) Для компилятора Borland этого делать не нужно. Листинг 20.9. Файл ptrtomember. срр. Указатели на функции-члены 0: II Листинг 20.9. Указатели на функции-члены 1: #include <iostream> 2: 3: enum BOOL { FALSE, TRUE }; 4: 5: class Mammal 6: { 7: public: 8: Mammal():itsAge(1) {} 9: virtual -Mammal() {} 10: virtual void Speak() const = 0;
330 Часть VI. Специальные темы 11: virtual void Move () const = 0; 12: protected: 13: int itsAge; 14 : } ; 15: 16: class Dog : public Mammal 17: { 18: public: 19: void Speak() const { std::cout « "Woof!\n"; } 20: void Move() const { std::cout << "Walking to heel...\n"; } 21: }; 22 : 23: class Cat : public Mammal 24: { 25: public: 26: void Speak() const { std: : cout << "Meow!\n"; ) 27 : void Move() const { std: : cout << "slinking...\n"; } 28: }; 29: 30: class Horse : public Mammal 31: { 32: public: 33 : void Speak() const { std: : cout << "Winnie!\n"; } 34: void Move() const { std: : cout << "Galloping...\n"; } 35: }; 36: 37: int main() 38: { 39: void (Mammal::‘pFunc)() const =0; 40: Mammal* ptr =0; 41: int Animal; 42: int Method; 43: bool fQuit = false; 44: 45: while (fQuit == false) 46: { 47: std::cout << "(0)Quit (1)dog (2)cat (3)horse: "; 48: std::cin >> Animal; 49: switch (Animal) 50: { 51: case 1: 52: ptr = new Dog; 53: break; 54: case 2: 55: Ptr = new Cat; 56: break; 57: case 3: 58: ptr = new Horse; 59: break; 60: default: 61: fQuit = true; 62: break; 63: } 64: if (fQuit) 65: break; 66: 67: std::cout « "(1)Speak (2)Move: "; 68: std::cin » Method; 69: switch (Method)
Час 20. Специальные классы, функции и указатели 331 70: { 71: case 1: 72: pFunc = Mammal::Speak 73: break; 74: default: 75: pFunc = Mammal::Move; 76: break; 77: } 78: 79: (ptr->*pFunc)(); 80: delete ptr; 81: } 82: return 0; 83: } Результат (O)Quit (l)Dog (2) Cat (1)Speak (2)Move: 1 Woof! (3)Horse: 1 (O)Quit (l)Dog (2)Cat (1)Speak (2) Move: 1 (3)Horse: 2 Meow! (O)Quit (l)Dog (2)Cat (1)Speak (2)Move: 2 Galloping (3)Horse: 3 (O)Quit (l)Dog (2)Cat (3)Horse: 0 Анализ В строках 5—14 класс Mammal объявлен как абстрактный с двумя виртуальными ме- тодами Speak () и Move (). Классы Dog, Cat и Horse объявлены как производные от класса Mammal. В каждом из них функции Speak() и Move О переопределены соот- ветствующим образом. В функции main () пользователю предлагается выбрать животное и создать новый экземпляр объекта класса, производного от Animal, соответствующий выбранному животному. В строках 49—63 его адрес присваивается указателю ptr. Затем пользователя запрашивают о методе, который необходимо вызвать. Адрес выбранного метода присваивается указателю pFunc. В строке 79 происходит вызов выбранного метода для созданного объекта. Для доступа к объекту используется ука- затель ptr, а для доступа к функции — указатель pFunc. И, наконец, в строке 80 оператор delete использует указатель ptr для освобожде- ния выделенной для объекта области в динамической памяти. Обратите внимание, что для указателя pFunc нет необходимости применять оператор delete, поскольку это указатель на код, а не на объект в динамической памяти. Попытка сделать это приве- дет к ошибке во время компиляции. Массивы указателей на функции-члены Подобно указателям на обычные функции, указатели на функции-члены могут быть сохранены в массиве. Массив инициализируется адресами различных функций- членов, вызываемых по индексу в массиве. Этот подход демонстрирует листинг 20.10.
332 Часть VI. Специальные темы Листинг 20.10. Файл arrayptr function, срр. Массив указателей на функции-члены 0: // Листинг 20.10. Массив указателей на функции-члены 1: #include <iostream> 2 : 3: class Dog 4: { 5: public: 6: void Speak() const { std::cout << "WoofIXn"; } 7: void Moved const { std::cout « "Walking to heel...\n"; } 8: void Eat() const { std::cout << "Gobbling food...\n”; } 9: void Growl() const { std::cout << "GrrrrrXn"; } 10: void Whimper() const {std::cout << "Whining noises ...\n";} 11: void RollOverO const { std::cout << "Rolling over...\n";J 12: void PlayDead() const 13: { std::cout << "Is this the end of Little Caesar?\n“; } 14: }; 15: 16: typedef void (Dog::* PDF) () const; 17 : int rtiaind 18: { 19: const int MaxFuncs = 7; 20: PDF DogFunctions[MaxFuncs] = 21: { Dog::Speak, 22: Dog::Move, 23: Dog::Eat, 24 : Dog::Growl, 25: Dog::Whimper, 26: Dog: .-Rollover, 27 : Dog::PlayDead 28: }; 29: 30: Dog* pDog =0; 31: int Method; 32 : bool fQuit = false; 33 : 34: while (!fQuit) 35: { 36: std::cout << "(0)Quit (1)Speak (2)Move 37 : std::cout << " (5)Whimper (6)Roll Over 38: std::cin >> Method; 39 : if (Method == 0) 40: { 41: fQuit = true; 42 : break; 43 : ) 44 : else 45: { 46: pDog = new Dog,- 47 : (pDog->*DogFunctions[Method-1])(); 48 : delete pDog; 49 : } 50: ) 51: return 0; 52: } (3)Eat (4)Growl" ; (7)Play Dead: ";
Час 20. Специальные классы, функции и указатели 333 Результат (O)Quit (l)Speak (2)Move (3)Eat (4)Growl Woof! (O)Quit (l)Speak (2)Move (3)Eat (4)Growl Grrrrrr (O)Quit (l)Speak (2)Move (3)Eat (4)Growl Is this the end of Little Caesar? (O)Quit (l)Speak (2)Move (3)Eat (4)Growl (5)Whimper (6)Roll Over (7)Play Dead: 1 (5)Whimper (6)Roll Over (7)Play Dead: 4 (5(Whimper (6(Roll Over (7)Play Dead: 7 (5)Whimper (6)Roll Over (7)Play Dead: 0 Анализ В строках 3—14 создается класс Dog, содержащий семь функций-членов с одинако- выми сигнатурами и типами возвращаемого значения. В строке 16 с помощью ключе- вого слова typedef объявлен постоянный указатель pdf на не получающие и не воз- вращающие никаких значений функции-члены класса Dog. В строках 20—28 массив DogFunctions, предназначенный для хранения семи ука- зателей на функции-члены, объявлен и инициализирован адресами этих функций. В строках 36—37 пользователю предлагается выбрать метод. Выбор любого элемента, кроме Quit, приводит к созданию объекта класса Dog, после чего из массива вызывается соответствующий метод (строка 46). Есть еще одна строка кода, которая позволит посадить в лужу программистов на языке C++ в любой компании; спросите их, что она делает? (pDog->*DogFunctions[Method-1]) О; Здесь происходит обращение к методу объекта при помощи хранимого в массиве указателя, по смешению Method-1. Это еще одна редкая, но крайне необходимая возможность, позволяющая создать таблицу функций-членов, которая поможет упростить программу. Вопросы и ответы Зачем использовать статические данные, если есть глобальные? Область видимости статических данных ограничивается классом. Обращаться к статической переменной-члену следует из объектов класса либо из внешнего кода программы, явно указав имя класса (если статическая переменная-член объявлена как public), либо с помощью открытой статической функции-члена этого класса. Статические переменные-члены относятся к типу данных того класса, которому они принадлежат. Зачем использовать статические функции-члены, если можно воспользоваться гло- бальными? Статические функции-члены принадлежат классу и могут быть вызваны только при помощи объектов класса или при явном указании имени класса, например, ClassName::FunctionName(). Почему бы не сделать все используемые классы дружественными друг другу? Объявление одного класса дружественным другому предоставит ему детали реа- лизации и снизит инкапсуляцию. В идеале желательно скрыть как можно больше деталей реализации каждого класса от других.
334 Часть VI. Специальные темы Коллоквиум Изучив возможности специальных классов, функций и указателей, имеет смысл ответить на несколько вопросов и выполнить ряд упражнений, чтобы закрепить полу- ченные знания. Контрольные вопросы 1. Для чего предназначены статические данные-члены класса? 2. Для чего предназначены статические методы класса? 3. В чем преимущество применения подключаемого файла, такого, как string.hpp (листинг 20.3)? Почему бы не внедрить его содержимое в код (листинг 20.4)? 4. В чем преимущество применения дружественных классов и методов? Упражнения 1. Раскомментируйте операторы cout в файле string.hpp (листинг 20.3) (строки 38, 52, 64, 76, 84 и 101) и повторно запустите на выполнение код файла employeemain.cpp (листинг 20.4). Это наглядно продемонстрирует, как часто происходит вызов этих методов. 2. Как можно изменить файл ptrarrayfunction.cpp (листинг 20.6) так, чтобы предотвратить ввод пользователем неподходящих значений? (Подсказка: какое значение сохраняется в элементе массива для недопустимого пункта меню? Просмотрите также строку 42.) 3. Сравните возможности класса string, определенного в файле string.hpp (листинг 20.3), и одноименного класса стандартной библиотеки, поставляе- мой вместе с компилятором. У компилятора Borland эту информацию можно получить, выбрав в меню Help (Помощь) пункт Help Topics (Темы) и проведя поиск класса string. Ответы на контрольные вопросы 1. Для всех объектов одного класса создается только один экземпляр таких дан- ных. Это позволяет сделать их общедоступными для всех объектов данного класса (так, например, можно создать счетчик количества объектов). 2. Подобно статическим данным-членам, для всех объектов одного класса создает- ся только одна версия таких функций. Как правило, они применяются для дос- тупа к статическим данным-членам. 3. Подключаемый файл, такой, как string.hpp, позволяет совместно использо- вать тот же код в нескольких модулях или даже поделиться им с коллегами. Это хорошая практика программирования. 4. Дружественные отношения позволяют классам использовать данные друг друга.
ЧАС 21 Препроцессор На этом занятии вы узнаете: что такое условная компиляция и как с ней работать; как создавать макрокоманды препроцессора; как использовать препроцессор для поиска ошибок. Препроцессор и компилятор При каждом запуске компилятора сначала запускается препроцессор, который ищет инструкции препроцессора, начинающиеся символом фунта (#). Задачей этих ин- струкций является изменение текста исходного кода. В результате получается новый, временный файл исходного кода, который пользователю обычно не виден. Однако при желании с помощью соответствующей инструкции можно указать компилятору на необходимость сохранить такой файл для последующего анализа. Сам компилятор читает не настоящий файл исходного кода, а результат работы препроцессора, и именно его компилирует в исполняемый файл. Этот подход уже применялся для директивы #include, которая заставляет препроцессор найти и включить указанный файл в текст исходного кода. Результат записывается в проме- жуточный файл, расположенный там же, где и исходный. Таким образом, к тому вре- мени, когда компилятор примется за файл исходного кода, все подключаемые файлы окажутся добавленными в его текст, как будто их ввели с клавиатуры. Просмотр промежуточного файла Почти каждый компилятор обладает параметром, который можно установить в ин- тегрированной среде разработки или в командной строке так, чтобы компилятор не удалял промежуточный файл. Более подробная информация о том, как правильно ус- тановить параметры компилятора, приведена в документации, прилагаемой к компи- лятору. Установив необходимый параметр, можно исследовать этот файл. Использование директивы #define Директива #define позволяет задать строку подстановки. # define BIG 512
336 Часть VI. Специальные темы В этой строке препроцессору предписано заменять слово big строкой 512 в любом месте программы. Имеется в виду не строковая переменная C++, замене подлежат символы в самом тексте исходного кода. Лексема, или термин (token) — это строка символов (слово), которую можно применять вместо любого другого набора символов или константы. #define BIG 512 int myArray[BIG]; Таким образом, приведенная выше запись из файла исходного кода после обработ- ки препроцессором в промежуточном файле будет выглядеть следующим образом: int myArray[512]; Обратите внимание: директива #define исчезла. Директивы препроцессора из промежуточного файла удаляются, поскольку они больше не нужны. Использование директивы #def ine для констант Одним из вариантов использования директивы #define является создание кон- стант. Но этим не стоит злоупотреблять, поскольку директива #define просто заме- няет строку, не контролируя соответствие типов. Как пояснялось на занятии, посвя- щенном константам, гораздо безопаснее вместо директивы ttdefine использовать ключевое слово const. Использование директив #def ine и ttifdef для проверки Другим вариантом использования директивы #define является объявление о су- ществовании строки символов (лексемы), например; #define BIG Впоследствии можно проверить, был ли определен элемент по имени BIG, и при- нять соответствующие меры. Для этого существуют специальные директивы препро- цессора, позволяющие проверить, была ли соответствующая лексема определена: ttifdef (if defined — если определена), #ifndef (if not defined — если не определена). Обе директивы перед завершением текущего блока (до закрывающей фигурной скоб- ки) следует закрывать директивой ttendif (end if — конец if). Директива #ifdef возвращает значение true, если искомая лексема была опреде- лена, поэтому можно написать следующее: #ifdef DEBUG cout << "Debug defined"; #endif Читая директиву #ifdef, препроцессор проверяет созданную им таблицу, чтобы узнать, была ли уже в программе определена лексема debug. Если да, то директива #ifdef возвращает значение true, и все, что находится до следующей директивы #else или #endif, записывается в промежуточный файл для компиляции. Если эта директива возвращает значение false, то ни одна строка кода, находящаяся между директивами #ifdef debug и #endif, не будет записана в промежуточный файл, т.е. будет получен такой вариант промежуточного файла, как будто этих строк никогда и не было в исходном коде. Директива ttifndef является логической противоположностью директивы #ifdef. Директива #ifndef возвращает значение true в том случае, если до этого места в программе искомая лексема определена не была.
Час 21. Препроцессор 337 Директива препроцессора #else Директива #else может находиться между директивами #ifdef (или #ifndef) и за- вершающей директивой #endif. Их использование продемонстрировано в листинге 21.1. Листинг 21.1. Файл usingdefine.срр. Использование директивы #define 0: // Листинг 21.1. Использование директивы #define 1: #define DemoVersion 2: #define DOS_VERSION 5 3: ttinclude <iostream> 4: int main() 5: { 6: std::cout << "Checking on the definitions of DemoVersion," 7: << "DOS_VERSION and WINDOWS_VERSION...\n”; 8: 9: #ifdef DemoVersion 10: std::cout « "DemoVersion defined.\n"; 11: #else // DemoVersion 12: std::cout << "DemoVersion not defined.\n"; 13: #endif // DemoVersion 14: 15: #ifndef DOS_VERSION 16: std::cout << "DOS_VERSION not defined!\n"; 17: Helse // DOS_VERSION 18: std::cout « "DOS_VERSION defined as: " 19: « DOS_VERSION « std::endl; 20: #endif // DOS_VERSION 21: 22: #ifdef WINDOWS_VERSION 23: std::cout << "WINDOWS_VERSION defined!\n”; 24: Helse // WINDOWS_VERSION 25: std: .-cout « "WINDOWS_VERSION was not defined.\n"; 26: #endif // WINDOWS_VERSION 27: 28: std::cout << "Done.Xn"; 29: return 0; 30: } Результат Checking on the definitions of DemoVersion, DOS_VERSION and WINDOWS_VERSION... DemoVersion defined DOS_VERSION defined as: 5 WINDOWS—VERSION was not defined. Done. Анализ В строках 1—2 определены две лексемы — DemoVersion и dos_version, причем лексеме dos_version назначен литерал 5. В строке 9 проверяется наличие лексемы DemoVersion, а поскольку она определена (хотя и без значения), результат оказывает- ся истинным, и строка 10 выводит соответствующее сообщение. В строке 15 с помощью директивы #ifndef проверяется отсутствие лексемы DOS—VERSION. Но поскольку она определена, возвращается значение false, и выполнение
338 Часть VI. Специальные темы программы продолжается со строки 18. Именно здесь слово dos_VERSIon заменяется символом 5, т е. компилятор воспринимает эту строку кода в следующем виде: cout « "DOS—VERSION defined as: " << 5 « endl; Обратите внимание, первое слово dos_version в сообщении не замещается стро- кой 5, поскольку является частью текстовой строки, заключенной в кавычки. Но лек- сема dos_version между операторами вывода замещается; таким образом, что ком- пилятор видит вместо нее символ 5, как будто он и был там изначально. Наконец, в строке 22 программа проверяет наличие лексемы windows_version. По- скольку эта лексема в программе не определена, возвращается значение false, и в строке 23 выводится соответствующее сообщение. Подключение файлов и предупреждение ошибок подключения Обычно проекты состоят из множества разнообразных файлов. Традиционно ката- логи организуют так, чтобы каждый класс имел собственный файл заголовка (.hpp), содержащий объявления класса, и собственный файл реализации (.срр), содержащий исходный код реализации методов класса. Функция main () тоже будет располагаться в своем собственном файле . срр, а все файлы . срр будут скомпилированы в файлы . obj, которые будут затем скомпонова- ны в единый исполняемый файл программы. Поскольку программы используют методы многих классов, к каждому из файлов придется подключить множество файлов заголовков. Зачастую файлы заголовка долж- ны включать в себя друг друга. Например, файл заголовка для объявления производ- ного класса должен подключать файл заголовка своего базового класса. Представьте, что класс Animal объявлен в файле Animal.hpp. Класс Dog (производный от класса Animal) должен подключить файл Animal.hpp в свой файл объявлений Dog.hpp, в противном случае класс Dog не будет производным от класса Animal. По той же причине файл заголовка класса cat подключает файл Animal .hpp. При создании метода, использующего оба класса (Cat и Dog), можно столкнуться с опасностью двойного подключения файла Animal .hpp. Это приведет к ошибке во время компиляции, поскольку компилятор не позволит дважды объявить класс Animal, даже несмотря на идентичность объявлений. Пробле- му можно решить с помощью директив препроцессора. Код файла заголовка Animal необходимо подключить следующим образом: #ifndef ANIMAL_HPP #define ANIMAL_HPP ... // сам файл находится здесь #endif Это означает, что если слово animal_hpp уже существует, то весь код определения, вплоть до директивы #endif, необходимо пропустить. А все содержимое файла как раз и располагается между директивами #define и #endif. Когда программа подключает этот файл в первый раз, препроцессор читает первую строку. Результат проверки, конечно же, оказывается истиной, так как до сих пор лексема animal_hpp еще не была определена. Следующая директива препроцессора #define определяет эту лексему, после чего подключается код файла. При попытке подключить файл Animal. hpp во второй раз препроцессор читает первую строку, которая возвращает значение False, поскольку лексема animal_hpp уже была определена. Поэтому управление программой переходит к следующей ди- рективе — #else (в данном случае отсутствует) или директиве #endif (находящейся
Час 21. Препроцессор 339 в конце файла). Следовательно, на этот раз все содержимое файла будет пропущено, и дважды класс объявлен не будет. Реальное имя лексемы (в данном случае — animal_hpp) абсолютно не важно, хотя традиционно используют имя файла, написанное прописными буквами, где точка (.), отделяющая имя от расширения, заменена символом подчеркивания (_). Но это не правило, а соглашение, которое следует рассматривать лишь как рекомендацию. ш? Защита! Не стоит пренебрегать этим средством защиты, оно способно значитель- но сэкономить время отладки. Определение в командной строке Почти все компиляторы языка C++ позволят передавать значения в командной строке или из интегрированной среды разработки (или и то, и другое). Следовательно, строки 1 и 2 листинга 21.1 можно отбросить и определить слова DemoVersion и dos_version из командной строки. Специальный отладочный код размещают, как правило, внутри директив #ifdef debug и #endif. При компиляции окончательной версии программы это позволяет легко удалить весь отладочный код, достаточно лишь убрать слово debug. Директива #undef Если определенное в коде имя необходимо исключить, можно воспользоваться ди- рективой #undef. Она работает прямо противоположно директиве #define. Условная компиляция Объединив директиву #define, определения командной строки и директивы #ifdef, #else и #ifndef, можно написать программу так, чтобы в зависимости от переданных слов компилировался разный код. Это позволяет создать единый исход- ный код, компилируемый на различных платформах, например, DOS и Windows. Другим общепринятым применением этой методики является условная компиля- ция на основании определения в коде слова debug (отладка). Как будет продемонст- рировано вскоре, это очень облегчает отладку. Макрофункции Директиву #define можно использовать также для создания макросов. Макрофунк- ция (macro function) — это создаваемое с помощью директивы #define выражение, ко- торому передают аргументы подобно обычной функции. Препроцессор заменяет строку подстановки любым заданным параметром. Например, макрофункцию twice можно определить следующим образом: #define TWICE(х) ( (х) * 2 ) А затем в программе использовать ее так: TWICE(4 ) Вся строка TWICE(4) будет удалена, а вместо нее будет стоять значение 8! Когда препроцессор считывает параметр 4, он выполняет следующую подстановку: ((4) * 2}, это выражение вычисляется сразу, а полученный результат — число 8 — заменяет ис- ходную строку.
340 Часть VI. Специальные темы Макрофункция способна иметь несколько параметров, причем каждый может ис- пользоваться многократно. Вот как можно определить два обычных макроса мах и min: ttdefine MAX(x,y) ( (х) > (у) ? (х) : (у) ) #define MIN(x,y) ( (х) < (у) ? (х) : (у) ) Обратите внимание, что в определении макрофункции открывающая круглая скобка списка параметров должна следовать за именем макроса непосредственно, без всяких про- белов. В отличие от компилятора, препроцессор никогда не прощает лишних пробелов. Если сначала написать объявление: ttdefine МАХ (х,у) ( (х) > (у) ? (х) : (у) ) а затем использовать макрофункцию мах следующим образом: int х = 5, у = 7, z; z = MAX(х,у); то промежуточный код будет таким: int х = 5, у = 7, z; z = (х,у) ( (х) > (у) ? (х) : (у) ) (х,у) В этом случае произошла простая замена текста, а не вызов макроса, т.е. лексема мах была заменена выражением (х,у) ( (х) > (у) ? (х) : (у) ), за которым сохранилась строка (х,у). Но после удаления пробела между словом мах и списком параметров (х,у) промежуточный код выглядит уже по-другому. int х = 5, У = 7, z; z =7 ; Зачем столько круглых скобок? Множество круглых скобок в макросах может показаться довольно странным. На самом деле препроцессор не требует, чтобы вокруг параметров в строке подстановки стояли круглые скобки, но они помогают избежать нежелательных побочных эффек- тов при передаче макросу сложных значений. ttdefine МАХ(х,у) х > у ? х : у Если определить макрос мах таким образом, а затем передать ему значения 5 и 7 в качестве параметров, он будет работать нормально. Но если передать более сложные выражения, то можно получить неожиданные результаты, как показано в листинге 21.2. Листинг 21.2. Файл usingparen. срр. Круглые скобки в макрокомандах 0: 1: 2 : 3: 4: 5: 6 : 7 : 8 : 9 : 10: 11: 12 : 13 : 14 : // Листинг. 21.2 Круглые скобки в макрокомандах ttinclude <iostream> #define CUBE(а) ( (a) * (a) * (a) ) ttdefine THREE(a) a * a * a int main() { 1ong x = 5; long у = CUBE(x); long z = THREE(x); std::cout << "y: 11 << у << std::endl; std::cout << "z: " << z << std::endl; 15: 16 : 1ong a = 5, b = 7; у = CUBE(a+b);
Час 21. Препроцессор 341 17: z = THREE(а+Ь); 18: 19: std::cout << "у: ” << у << std::endl; 20: std::cout << "z: " << z << std::endl; 21: return 0; 22: } Результат у: 125 z: 125 у: 1728 z: 82 Анализ В строке 3 определен макрос cube (куб) с аргументом а, заключенным в круглые скобки. В строке 4 определен макрос three (дерево), параметр которого используется без круглых скобок. При первом использовании этих макросов в качестве параметра передается значе- ние 5, и оба макроса прекрасно справляются со своей работой. Макрос cube (5) пре- образуется в выражение ( (5) * (5) * (5) ), которое при вычислении дает резуль- тат 125, а макрос three(5) преобразуется в выражение 5*5*5, которое также возвращает значение 125. При повторном обращении в строках 15—17 в качестве параметра передается выраже- ние 5+7. В этом случае строка cube (5+7) преобразуется в следующее выражение: ( (5+7) * (5+7) * (5+7) ) которое, в свою очередь, преобразуется в: ( (12) * (12) * (12) ) что составляет 1728. Но макрос three (5+7) преобразуется в выражение иного вида: 5 + 7*5 + 7*5 + 7 А поскольку умножение имеет более высокий приоритет чем сложение, то преды- дущее выражение эквивалентно следующему: 5 + (7 * 5) + (7 * 5) + 7 которое, в свою очередь, преобразуется в: 5 + (35) + (35) + 7 что составляет 82. Как можно заметить, без круглых скобок происходит ошибка! Функции, шаблоны или макросы? Макросы языка C++ страдают четырьмя недостатками. Во-первых, при увеличе- нии они становятся чрезвычайно запутанными, поскольку весь макрос должен быть определен в одной строке. Безусловно, ее можно продлить с помощью символа об- ратной косой черты (\), но большие макросы сложны для понимания и с ними трудно работать. Во-вторых, при использовании макросы подставляются в код, при этом его размер увеличивается. Это означает, что если макрос используется 12 раз, то столько же раз в программу будет вставлено соответствующее выражение (вместо одного раза, как при обращении к обычной функции). Но, с другой стороны, встраиваемые выражения работают быстрее, чем вызовы функций, поскольку позволяют избежать самой проце- дуры обращения к функции.
342 Часть VI. Специальные темы Факт встраивания ведет к третьей проблеме, которая заключается в том, что мак- рос отсутствует в промежуточном коде, используемом компилятором; следовательно, они недоступны большинству отладчиков, что делает их отладку крайне сложной. И, наконец, последняя проблема, возможно, наиболее серьезная, заключается в том, что макросы не осуществляют контроль над типами данных. Несмотря на то, что возможность передачи макросу любого аргумента достаточно удобна, она полно- стью подрывает принцип строгого контроля типов данных языка C++, а посему пре- дана анафеме большинством программистов на языке C++. Но все же следует отме- тить, что существует корректный способ решения этой проблемы с помощью шаблонов, описанных на занятии 23, “Шаблоны”. Операции со строками Препроцессор обладает двумя специальными операторами манипулирования стро- ками в макрокомандах. Оператор взятия в кавычки (#) берет в кавычки любую сле- дующую за ним строку. Оператор конкатенации (##) объединяет две строки в одну. Оператор взятия в кавычки Оператор взятия в кавычки (stringizing) превращает в строку и берет в кавычки лю- бые следующие за ним символы, вплоть до очередного символа пробела. Следователь- но, если написать: ttdefine WRITESTRING(х) cout << #х а затем осуществить вызов WRITESTRING(This is a string); препроцессор превратит его в следующую строку кода: cout << "This is a string"; Обратите внимание, что теперь строка “This is a string” заключена в кавычки, как и требуется для объекта cout. Конкатенация Оператор конкатенации (concatenation) позволяет связывать несколько строк в од- ну. На самом деле новая строка представляет собой лексему, которую можно исполь- зовать как имя класса, имя переменной, индекс массива или в любом другом месте, где может понадобиться набор символов. Предположим, существуют пять функций: fOnePrint (), fTwoPrint (), fThreePrint (), fFourPrint () и fFivePrint (). Можно сделать следующее объявление: #define fPRINT(х) f ## х ## Print а затем использовать макрос fPRlNTO с аргументом Two, чтобы создать строку “fTwoPrint”, и с аргументом Three, чтобы создать строку “fThreePrint”. В конце занятия 19, “Связанные списки”, был разработан класс PartsList. Этот список мог обрабатывать лишь объекты типа List. Предположим, он зарекомендовал себя настолько хорошо, что теперь необходимо создать списки животных, автомоби- лей, компьютеров и т.д. Одним из решений этой задачи могло бы стать создание списков AnimalList, CarList, ComputerList и т.п. с помощью переноса и вставки необходимого кода в нужное место. Но это достаточно быстро превратится в настоящий кошмар, по- скольку любое из изменений, сделанных для одного из списков, потребует внесения изменений во все остальные.
Час 21. Препроцессор 343 Альтернативным решением является использование макрокоманд и оператора конкатенации. Например, можно сделать такое объявление: #define Listof(Type) class Type##List \ ( \ public: \ Type##List(){} \ private: \ int itsLength; \ }; Пример немного грубоват, но он позволяет включить в одно определение все не- обходимые методы и данные. Для создания списка животных (AnimalList) достаточ- но написать: Listof (Animal) В результате приведенная выше запись превратится в объявление класса AnimalList. В процессе применения этого подхода возможно возникновение ряда проблем, подробных рассмотренным на занятии 23, “Шаблоны”. Встроенные макросы Большинство компиляторов содержит ряд полезных встроенных макрокоманд, вклю- чая date__,___time , __line и____file__(дата, время, строка и файл). Каждое из этих имен окружено двумя знаками подчеркивания, что уменьшает вероятность совпадения с именами, используемыми в программе (во избежание конфликта имен). Встретив один из этих макросов, препроцессор делает соответствующую подста- новку. Вместо лексемы date__ устанавливается текущая дата. Вместо лексемы __time_— текущее время. Лексемы __line__ и __file__ заменяются номером строки исходного кода и именем файла соответственно. Следует отметить, что эти за- мены выполняются еще до компиляции. Учтите, что при выполнении программы вместо макроса date________________________________будет стоять не текущая дата, а дата компиляции програм- мы. Встроенные макрокоманды очень полезны при отладке. Макрос assert () Большинство компиляторов содержат макрокоманду assert(). Макрос assert() возвращает значение True, если его параметр возвращает значение True, и выполняет определенные действия, если он возвращает значение False. В этом случае одни ком- пиляторы прерывают выполнение программы, а другие передают исключение (более подробно это описывается на занятии 24, “Исключения, обработка ошибок и другое”). Одной из мощнейших особенностей макроса assert () является то, что препро- цессор вообще не замещает его никаким кодом, если лексема debug не определена. Это очень удобно, поскольку позволяет включить в рабочую версию программы отла- дочный код, который не нужен (и даже вреден) в окончательной версии. Чтобы не зависеть от конкретной версии компилятора, т.е. от его реакции на мак- рос assertO, можно написать собственный вариант этого макроса. В листинге 21.3 содержится простой макрос assert (), а также пример его применения. Листинг 21.3. Файл simpleassert. срр. Простой макрос assert () 0: // Листинг 21.3. Макрос assert 1: #define DEBUG 2: #include <iostream> 3:
344 Часть VI. Специальные темы 4 : #ifndef DEBUG 5: fldefine ASSERT(x) 6: #else 7 : #define ASSERT(x) \ 8: if (1 (x)) \ 9 : { \ 10: std::cout << "ERROR!! Assert " << #x << “ failedXn"; \ 11: std::cout << " on line “ << LINE << "\n”; \ 12: std:-.cout << " in file " << FILE << "\n"; \ 13 : } 14 : #endif 15: 16 : int main() 17 : { 18: int х = 5; 19: std::cout << "First assert: \n" ; 20: ASSERT(x==5); 21: std::cout << "XnSecond assert: \n"; 22: ASSERT(x != 5); 23: std::cout << "\nDone.\n"; 24: return 0; 25: } «сторож»? Различия в формате файлов Не исключена ситуация, что используемый компилятор не соберет строки 7-13 листинга 21.3 в единую строку. В этом случае для правильной ком- пиляции необходимо удалить разделители между строками и собрать их в одну. Причиной проблемы является различие между обозначающими конец строки символами в разных операционных системах, средах раз- работки и компиляторах. Они могут по разному интерпретировать символ новой строки. ЛИВД ярочп Макрокоманды состоят из одной строки Технически макрокоманды могут занимать только одну строку. Макрокоманда assert О, определенная в строках 7-13, выглядит как состоящая из нескольких строк, но в действительности это одна строка. Символ наклонной черты влево (\) в конце каждой строки отменяет символ новой строки, имитируя управляющий символ. Это именно тот случай, когда пробелы и отступы существенны. Результат First assert: Second assert: ERROR!! Assert x!=5 failed on line 24 in file E:\adisk\cppin24 new\Hour21\simpleassert.cpp Анализ В строке 1 определено слово debug. Иногда это делается из командной строки (или в интегрированной среде разработки) во время компиляции, что позволяет включить или выключить участки отладочного кода. В строках 7— 13 определен макрос
Час 21. Препроцессор 345 assert!). Как правило, это делается в файле заголовка Assert.hpp, который будет включен во все файлы реализации. В строке 4 проверяется наличие слова debug. Если его нет, макрос assert () не будет создавать никакого кода, в противном случае выполняются строки 7—13. Макрос assert () представляет собой один большой оператор, разделенный на семь строк исходного кода. Он предназначен для обработки препроцессором. Прове- ряемое значение передается ему в виде параметра в строке 8; если оно возвращает значение False, то операторы в строках Ю—12 выводят сообщение об ошибке, в про- тивном случае никакие действия не выполняются. Отладка с помощью макроса assert () Создавая программу, разработчик абсолютно уверен, что данной функции, напри- мер, передано необходимое значение, что указатель допустим и т.д. Это вполне обычное заблуждение — уверенность в том, что все сделано правильно и будет работать в любых условиях. Но грубая реальность такова, что, казалось бы, вполне допустимый указатель приводит к сбою программы. Макрос assert () поможет найти ошибки этого типа, но только при условии регулярного применения в программном коде. Каждый раз, при- сваивая или передавая указатель в виде параметра или возвращая его в виде значения из функции, удостоверьтесь с помощью макроса assert () в его допустимости. Не бойтесь использовать макрос assert () слишком часто, он все равно будет удален из окончательной версии кода, когда будет удалено определение слова debug. Он также обеспечит хорошее внутреннее документирование кода, выделив все важные участки. Побочные эффекты Нередко ошибка проявляется только после удаления экземпляров макроса assert (). Почти всегда это происходит из-за того, что программа попадает в зависимость от по- бочных эффектов, вызванных выполнением макроса assert (), или другими частями кода, используемыми только для отладки, например: ASSERT (х = 5) Имелась в виду проверка значения х == 5, а получилась крайне коварная ошибка. Предположим, как раз перед выполнением макроса assert () была вызвана функция, которая присвоила переменной х значение 0. С помощью макроса assert () предполага- лось проверить равенство переменной х значению 5. На самом же деле переменной х бы- ло присвоено значение 5. Тем не менее, проверка возвращает значение True, поскольку выражение х = 5 не только присваивает переменной х значение 5, но и возвращает зна- чение 5, а так как 5 не равно нулю, то это значение расценивается как истинное. При передаче в макрос assert!) переменной х, содержащей значение 5 (только что присвоенное!), программа работает прекрасно. Все кажется готовым и отлажен- ным. Блоки отладки отключаются, компилируется окончательная версия и передается заказчику. Теперь макрос assert () исчезает, и переменной х больше не присваива- ется значение 5. Поэтому, как только переменной х будет присвоено значение, от- личное от 5, программа зависнет без всяких объяснений. Рассерженный заказчик возвращает программу, программист восстанавливает бло- ки отладки. Вот это да! Ошибка исчезла! Такие вещи довольно забавно наблюдать со стороны, но не переживать самому, поэтому остерегайтесь побочных эффектов при использовании средств отладки. И если ошибка появляется только при отключении блоков отладки, то внимательно просмотрите именно отладочный код на предмет проявления возможных побочных эффектов.
346 Часть VI. Специальные темы Jfeav______ прачки Побочные эффекты макрокоманд Как можно заметить в строках 16-17 листинга 21.2, макрокоманду можно вызвать для достаточно сложного выражения, а не для одиноч- ной переменной или значения (подобно строкам 9-10). Но если слож- ное выражение изменяет переменную, то могут возникнуть непред- виденные последствия, например, при вызове макроса cube для оператора а++. у = CUBE (а++); Если ожидалось увеличение переменной а на единицу после выполне- ния макрокоманды, то на самом деле произойдет следующее: ( (а++) * (а++) * (а++) ) Так что инкремент произойдет два раза, а не один, как ожидалось! Инварианты класса Для многих классов существует ряд условий, которые всегда должны выполняться при завершении работы с функцией-членом класса. Эти обязательные условия выпол- нения класса называются инвариантами класса (invariants). Например, обязательными мо- гут быть следующие условия: объект класса Circle никогда не должен иметь нулевой ра- диус или объект класса Animal всегда должен иметь возраст больше нуля и меньше 100. Весьма полезным может оказаться объявление метода Invariants (), который возвращает значение True только в том случае, если каждое из этих условий является истинным. Затем макрос Assert (Invariants ()) можно вставить в начале и в конце каждого метода класса. Обратите внимание: метод Invariants () не возвращает зна- чение True до вызова конструктора и после выполнения деструктора. Использование метода Invariants () для обычного класса показано в листинге 21.4. Листинг 21.4. Файл usinginvariants. срр. Использование метода Invariants () 0 : // Листинг 21.4. Метод Invariants!) 1: 2 : 3 : 4 : 5: 6: 7 : 8 : 9 : 10: 11: 12: #define DEBUG #define SHOW_INVARIANTS #include <iostream> #include <string.h> #ifndef DEBUG #define ASSERT(x) #else #define ASSERT(x) \ if (! (x)) \ { \ std::cout << "ERROR!! Assert " << #x < < " fai led\n"; 13 : std::cout << " on line " << LINE < < "\n"; \ 14 : std::cout << " in file ” << FILE < < ” \ n" ; \ 15: 16: 17: 18 : 19 : 20: 21: 22: 23 : 24: } #endif class String { public: // Конструкторы String(); String(const char ‘const); String(const String &);
Час 21. Препроцессор 347 25: -String(); 26: 27: 28: 29: 30: 31: 32 : 33 : 34: 35: 36: 37: 38: 39: 40: 41: 42 : 43: 44: 45: 46: 47: 48: 49: 50: 51: 52 : 53 : 54: 55: 56: 57: 58: 59: 60: 61: 62 : 63 : 64: 65: 66: 67: 68: 69: 70: 71 : 72 : 73 : 74: 75: 76: 77: 78: 79: 80: 81: char & operator[](int offset); char operator[](int offset) const; String & operator (const String &) ; int GetLen() const { return itsLen; } const char * GetString() const { return itsString; } bool Invariants() const; private: String (int); // Закрытый конструктор char * itsString; unsigned short itsLen; } ; // стандартный конструктор создает строку нулевой длины String::String() { itsString = new char[l]; itsStringfO] = 1 \0‘; itsLen=0; ASSERT(Invariants()); ) // Закрытый (вспомогательный) конструктор, // используемый только методами класса для создания // строк необходимой длины, заполненных символом null. String::String(int len) { itsString = new char[len+l]; for (int i=0; i<=len; i++) itsString[i] = '\0‘; itsLen=len; ASSERT(Invariants()); ) // Преобразует символьный массив в строку String::String(const char * const cString) { itsLen = strlen (cString) ,- itsString = new char[itsLen+1]; for (int i=0; icitsLen; i++) itsString[i] = cStringfi]; ASSERT(Invariants()); } // Конструктор копий String::String (const String & rhs) { itsLen=rhs . GetLen () ,- itsString = new char[itsLen+1]; for (int i=0; icitsLen; i++) itsString[i] = rhs[i]; itsString[itsLen] = 1 \0‘; ASSERT(Invariants());
348 Часть VI. Специальные темы 82: } 83 : 84 : // Деструктор, освобождает выделенную память 85: String::-String () 86: { 87: ASSERT(Invariants()); 88: delete [] itsString; 89 : itsLen = 0; 90: } 91: 92: // Оператор присвоения, освобождает существующую память 93 : // а затем копирует строку и ее размер 94 : String& String::operator=(const String & rhs) 95: { 96: ASSERT(Invariants()); 97 : if (this == &rhs) 98: return *this; 99 : delete [] itsString; 100: itsLen=rhs.GetLen(); 101: itsString = new char[itsLen+1]; 102 : for (int i=0; i<itsLen; i++) 103 : itsString[i] = rhs[i); 104 : itsString[itsLen] = 1 \0'; 105: ASSERT(Invariants()); 106 : return ‘this; 107 : ) 108: 109 : // Непостоянный оператор индексирования, возвращает 110: // ссылку на символ, так что ее можно 111: // изменить! 112 : char & String::operator[](int offset) 113 : ( 114 : ASSERT(Invariants( ) ) ; 115: if (offset > itsLen) 116: return itsString[itsLen-1]; 117: else 118: return itsString[offset]; 119: ASSERT(Invariants()); 120: ) 121: 122: // Постоянный оператор индексирования для использования 123 : // с постоянными объектами (см. конструктор копий) 124 : char String::operator[](int offset) const 125: ( 126 : ASSERT(Invariants()); 127: if (offset > itsLen) 128: return itsString[itsLen-1]; 129 : else 130 : return itsString [of f set] ,- 131: ASSERT(Invariants()); 132 : } 133: 134: // Удостовериться, что строка имеет некоторую длину 13 5 : // или указатель не является нулевым. 136: bool String::Invariants() const 137: { 138: #ifdef SHOW_INVARIANTS
Час 21. Препроцессор 349 139: std::cout << " String OK " ; 140: 141 : #endif return ( (itsLen && itsString) || ('itsLen && 'itsString) ); 142: } 143 : 144: 145: 146: 147 : 148: 149: 150: 151: 152: 153 : 154: 155: 156: 157: 158: 159: 160: 161: 162: 163: 164 : 165: 166: 167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177 : 178: 179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189: class Animal { public: Animal():itsAge(1),itsName(“John Q. Animal") { ASSERT(Invariants()); } Animal(int, const String&); ~Animal() {} int GetAge() { ASSERT(Invariants()); return itsAge;} void SetAge(int Age) { ASSERT(Invariants()); itsAge = Age; ASSERT(Invariants()); } String& GetName() {ASSERT(Invariants()); return itsName;} void SetName(const String& name) { ASSERT(Invariants()); itsName = name; ASSERT(Invariants()); } bool Invariants(); private: int itsAge; String itsName; } ; Animal::Animal(int age, const String& name): itsAge(age), itsName(name) { ASSERT(Invariants()); } bool Animal::Invariants() { #ifdef SHOW_INVARIANTS Std::COUt << " Animal OK "; #endif return (itsAge > 0 && itsName.GetLen()) ; } int main() { Animal sparky(5,"Sparky"); std::cout << "\n" << sparky.GetName().GetString() << " is "; 190: std::cout << sparky.GetAge() << " years old."; 191: 192: sparky.SetAge(8); std::cout << "\n" << sparky.GetName().GetString() << " is ";
350 Часть VI. Специальные темы 193: std::cout << sparky.GetAge() << " years old."; 194: return 0; 195: } Результат String ОК String OK String OK String OK String OK String OK String ОК String OK Animal OK String OK Animal OK Sparky is Animal OK 5 years old. , Animal OK Animal OK Animal OK Sparky is Animal OK 8 years old. String OK Иалг_____ речи Компилятор может выдать предупреждение При компиляции этого кода можно получить предупреждения, подоб- ные приведенным ниже. Они свидетельствуют о том, что некоторые из макросов assert () никогда не будут выполнены! Хотя это может показаться странным, исходя из того, что они находятся в начале и конце каждой функции. ''usinginvariants .срр": W8066 Unreachable code in function String::operator [](int) at line 119 "usinginvariants.cpp": W8070 Function should return a value in function String::oper-ator [](int) at line 120 "usinginvariants.cpp": W8066 Unreachable code in function String::operator [](int) const at line 131 "usinginvariants.cpp": W8070 Function should return a value in function String::oper-ator [](int) const at line 132 Анализ В строках 6—16 определен макрос assert(). Если слово debug определено и мак- рос assert(), обработав параметр, возвращает значение False, то на экран выводит- ся сообщение об ошибке. В строке 33 объявлена функция-член Invariants () класса string, а ее реализа- ция занимает строки 136—142. Конструктор объявлен в строках 22—24, а в строке 47, после создания объекта, вызывается функция-член Invariants!), подтверждающая, что объект создан удачно. Этот алгоритм применен и для других конструкторов, а для деструктора вызов функции-члена invariants!) осуществляется только перед удалением объекта. Ос- тальные методы класса вызывают функцию Invariants () перед выполнением лю- бого действия, а затем еще раз перед выходом из функции. Здесь подтверждается и проверяется фундаментальный принцип языка C++: все функции-члены, за исклю- чением конструкторов и деструкторов, должны работать с корректными объектами и оставить их в корректном состоянии. В строке 165 класс Animal объявляет собственный метод invariants!), реализо- ванный в строках 178—184. Обратите внимание на строки 148, 151, 154, 156, 161 и 163: встраиваемые функции также могут вызывать метод Invariants (). Вывод промежуточных значений Кроме подтверждения корректности операций, с помощью макроса assert!) можно вывести на экран текущие значения указателей, переменных и строк. Это очень удобно, поскольку появляется возможность сравнить ожидаемые значения с ре- альными, возникающими в процессе выполнения программы, особенно в циклах. Листинг 21.5 демонстрирует этот подход.
Час 21. Препроцессор 351 Листинг 21.5. Файл printingvalues. срр. Вывод значений в режиме отладки 0: // Листинг 21.5. Вывод значений в режиме отладки 1: #include <iostream> 2: #define DEBUG 3: 4: #ifndef DEBUG 5: ttdefine PRINT(x) 6: Helse 7: tfdefine PRINT(x) \ 8: std::cout << #x « ” :\t" << x << std::endl; 9: #endif 10: 11: int main() 12: { 13: int x = 5; 14: long у = 738981; 15: PRINT(x); 16: for (int i=0; i < x; i++) 17: { 18: PRINT(i); 19: } 20: 21: PRINT (y); 22: PRINT("Hi."); 23: int *px = &x; 24: PRINT(px); 25: PRINT (*px); 26: return 0; 27: } Результат х: i: i: i: i: i: У: "Hi. " : px: *px: 5 0 1 2 3 4 73898 Hi. 1245064 5 Анализ Макрос print (x) (строки 4—9) выводит на экран текущее значение переданного параметра. Обратите внимание: объекту cout сначала передается параметр, взятый в кавычки (т.е. при передаче параметра х объект cout получит "х"). Затем объект cout получает заключенную в кавычки строку ": \ t “, которая обеспе- чивает вывод двоеточия и символа табуляции. Затем объект cout получает значение па- раметра (х), а объект endl выполняет переход на новую строку и очищает буфер. Результат выполнения строки 24 может выглядеть иначе, если используется ком- пилятор, отличный от компилятора Borland. Это может быть нечто вроде 0x2100 или что-то другое, в зависимости от применяемого компилятора.
352 Часть VI. Специальные темы Уровни отладки В больших и сложных проектах, возможно, понадобится более тонкое управление, чем просто включение и выключение режима DEBUG. Для этого необходимо опреде- лить уровни отладки и применять их для переключения между наборами используе- мых для отладки макрокоманд. Чтобы определить уровень отладки, укажите за оператором #define debug его номер. Несмотря на то что количество уровней неограничено, обычно применяют всего четыре: high, medium, low и none (высокий, средний, низкий и “отсутствует”). Листинг 21.6 демонстрирует их применение для классов string и Animal из листин- га 21.4. Для экономии места определения других методов класса, кроме метода invariants (), здесь не приведены, поскольку они совпадают с кодом листинга 21.4. Листинг 21.6. Файл debugging levels. срр. Уровни отладки 0: // Листинг 21.6. Уровни отладки 1: 2 : 3 : 4: 5: 6: 7 : 8: #include <iostream> #include <string.h> enum LEVEL { NONE, LOW, #define DEBUGLEVEL HIGH #if DEBUGLEVEL < LOW // MEDIUM, HIGH }; должен быть LOW, MEDIUM или HIGH 9: 10: 11: 12 : 13 : 14: #define ASSERT(x) #else #define ASSERT(x) \ if (! (x)) \ { \ std::cout << "ERROR!! Assert " << #x << “ failed\n"; \ 15: std::cout << ” on line " << LINE << "\n"; \ 16: Std::COUt << " in file " « FILE << "\n"; \ 17: } 18: #endif 19: 20: #if DEBUGLEVEL < MEDIUM 21: #define EVAL(x) 22: #else 23: #define EVAL(x) \ 24: std::cout << #x << ":\t” << x << std::endl; 25: #endif 26: 27: #if DEBUGLEVEL < HIGH 28: #define PRINT(x) 29: #else 30: #define PRINT(x) \ 31: std::cout << x << std::endl; 32 : #endif 33 : 34: class String 35: { 36: public: 37: // Конструкторы 38: String(); 39: String(const char ‘const); 40: String(const String &); 41: -String(); 42 :
Час 21. Препроцессор 353 43: char & operator[](int offset); 44: char operator[](int offset) const; 45: 46: String & operator= (const String &); 47: int GetLen() const { return itsLen; } 48: const char * GetString() const 49: { return itsString; } 50: bool Invariants() const; 51: 52: private: 53: String (int); // Закрытый конструктор 54: char * itsString; 55: unsigned short itsLen; 56: }; 57 : 58: bool String::Invariants() const 59: { 60: PRINT(“(String Invariants Checked)"); 61: return ( (bool) (itsLen && itsString) || 62: (’itsLen SS ’itsString) ); 63: } 64: 65: class Animal 66: { 67: public: 68: Animal():itsAge(1),itsName("John Q. Animal") 69: ( ASSERT(Invariants()); } 70: 71: Animal(int, const Strings); 72: -Animal() {} 73: 74: int GetAge() 75: { 76: ASSERT(Invariants()); 77: return itsAge; 78: } 79: 80: void SetAge(int Age) 81: { 82: ASSERT(Invariants()); 83: itsAge = Age; 84: ASSERT(Invariants()); 85: } 86: Strings GetName() 87 : { 88: ASSERT(Invariants()) ; 89: return itsName; 90: } 91: 92: void SetName(const Strings name) 93: { 94: ASSERT(Invariants()); 95: it sName = name; 96: ASSERT(Invariants()); 97: } 98: 99: bool Invariants(); 100: private:
354 Часть VI. Специальные темы 101: int itsAge; 102 : String itsName; 103 : 104 : }; 105 : // стандартный конструктор создает строку нулевой длины 106 : String::String() 107 : { 108: itsString = new char[l]; 109 : itsString[0] = '\0'; 110: itsLen=0; 111: ASSERT(Invariants()); 112 : 113 : } 114 : // Закрытый (вспомогательный) конструктор. 115: // используемый только методами класса для создания 116: // строк необходимой длины, заполненных символом null. 117 : String::String(int len) 118 : ( 119 : itsString = new char[len+l]; 12 0: for (int i = 0; i«=len; i + + ) 121: itsString(i) = '\0'; 122 : itsLen=len; 123 : ASSERT(Invariants ( ) ) ; 124 : 125: } 126 : // Преобразует символьный массив в строку 127 : String::String(const char * const cString) 128 : { 129 : itsLen = strlen(cString); 13 0: itsString = new char[itsLen+1]; 131 : for (int i=0; icitsLen; i++) 132 : 133 : rtsString[i] = cStringtr]; 134: ASSERT(Invariants()); 135 : 136: } 137 : II Конструктор копий 138: String::Strxng (const String & rhs) 139: { 140: itsLen = rhs.GetLen(); 141 : itsString = new char[itsLen+1]; 142 : for (int i=0; icitsLen; i++) 143 : itsString [i] = rhs[ij; 144 : itsString[itsLen] = '\0‘; 145: ASSERT(Invariants ()); 146 : 147: } 148 : // Деструктор освобождает выделенную память 149 : String::-String () 150: { 151 : ASSERT(Invariants ()) ; 152 : delete [] itsString; 153 : itsLen = 0; 154: 155: ) 156: // Оператор присвоения, освобождает существующую память 157 : II а затем копирует строку и ее размер 158 : Strings String::operator=(const String & rhs)
Час 21. Препроцессор 355 159: { 160: 161: 162 : 163 : 164 : 165: 166 : 167: 168 : 169: 170: 171: 172 : 173: 174: 175: 176 : 177: 178 : 179: 180: 181: 182 : 183 : 184: 185: 186: 187: 188: 189: 190: 191: 192 : 193 : 194: 195: 196: 197 : 198 : 199 : 200: 201: 202: 203 : 204 : 205: 206: 207 : 208 : 209 : ASSERT(Invariants()); if (this == &rhs) return *this; delete [] itsString; itsLen=rhs.GetLen(); itsString = new char[itsLen+1]; for (int i=0; i<itsLen; i++) itsString[i] = rhs[i]; itsString[itsLen] = '\0'; ASSERT(Invariants()); return *this; } // Непостоянный оператор индексирования, возвращает // ссылку на символ, так что ее можно // изменить! char & String::operator[](int offset) { ASSERT(Invariants()); if (offset > itsLen) return itsString[itsLen-1]; else return itsString[offset]; ASSERT(Invariants()); ) // Постоянный оператор индексирования для использования // с постоянными объектами (см. конструктор копий) char String::operator[](int offset) const { ASSERT(Invariants()); if (offset > itsLen) return itsString[itsLen-l]; else return itsString[offset]; ASSERT(Invariants()); } Animal::Animal(int age, const String& name): itsAge(age), itsName(name) { ASSERT(Invariants()); } bool Animal::Invariants() { PRINT("(Animal Invariants Checked)”); return (itsAge > 0 && itsName.GetLen()); } 210: 211 : 212 : 213 : 214: 215 : 216 : int main() { const int AGE = 5; EVAL(AGE); Animal sparky(AGE,"Sparky”); std::cout << ”\n" « sparky.GetName().GetString();
356 Часть VI. Специальные темы 217 : 218: 219 : 220 : 221 : 222 : 223 : 224 : std::cout << " is std::cout << sparky.GetAge () << " years old.”; sparky.SetAge(8); std::cout << ”\n” << sparky.GetName() .GetString() ; std::cout << ” is std::cout << sparky.GetAge() << " years old.”; return 0; Результат AGE: 5 (String Invariants Checked) (String Invariants Checked) (String Invariants Checked) (String Invariants Checked) (String Invariants Checked) (String Invariants Checked) (String Invariants Checked) (String Invariants Checked) (String Invariants Checked) (String Invariants Checked) Sparky is (Animal Invariants Checked) 5 Years old. (Animal Invariants Checked) (Animal Invariants Checked) (Animal Invariants Checked) Sparky is (Animal Invariants Checked) 8 years old. (String Invariants Checked) (String Invariants Checked) // повторный запуск c DEBUG = MEDIUM AGE: 5 Sparky is 5 years old. Sparky is 8 years old. №ЖЦ____ RpO4M Компилятор может выдать предупреждение При компиляции этого кода может быть получено такое же предупреж- дение, как и при компиляции кода листинга 21.4. Причина та же. Анализ В строках 8—18 макрос assert О определяется так, чтобы вообще не создавать ни- какого кода, если уровень отладки debuglevel меньше, чем low (т.е. debuglevel ра- вен none). Когда отладка разрешена, макрос assert () приступает к работе. В строках 20—25 макрос eval отключается, если уровень отладки debuglevel меньше, чем medium; иными словами, если уровень отладки debuglevel установлен равным зна- чению none или low, макрос eval не работает. Наконец, в строках 27—32 макрофункция print объявляется “бездействующей”, если уровень отладки debuglevel меньше, чем high. Макрофункция print исполь- зуется только в том случае, когда уровень отладки debuglevel установлен равным значению high, т.е. этот макрос можно удалить, установив уровень отладки
Час 21. Препроцессор 357 debuglevel равным значению medium, но поддерживать при этом использование макросов EVAL и assert (). Макрос print используется в методах invariants О для вывода сообщения. Макрос eval используется в строке 214 для отображения текущего значения целочис- ленной константы AGE. Рекомендуется Не рекомендуется Использовать ПРОПИСНЫЕ буквы для имен макросов. Это общепринятое соглашение, поэтому его несоблюдение может ввести в заблуждение других программистов. Заключать все аргументы макросов в круглые скобки. Изменять и присваивать значения переменных в отладочном коде, поскольку это чревато появлением побочных эффектов. Вопросы и ответы Если язык C++ обладает средствами, превосходящими препроцессор, то почему он все еще используется? Во-первых, язык C++ совместим с языком С, поэтому все существенные компо- ненты языка С должны поддерживаться в языке C++. Во-вторых, некоторые воз- можности препроцессора все еще используются в языке C++, например, защита от повторного включения файла. Зачем использовать макрофункции, если можно воспользоваться обычными? Макрофункции встраиваются компилятором прямо в код программы, что ус- коряет их выполнение. Кроме того, с помощью макросов можно динамически менять типы в объявлениях, хотя предпочтительнее использовать для этого шаблоны. Имеется ли альтернатива использованию препроцессора для вывода промежуточ- ных значений в процессе отладки? Наилучшей альтернативой является использование встроенного оператора отлад- чика watch, информация о котором содержится в документации, прилагаемой к каждому компилятору или отладчику. Коллоквиум Изучив возможности препроцессора, имеет смысл ответить на несколько вопросов и выполнить ряд упражнений, чтобы закрепить полученные знания. Контрольные вопросы 1. Как можно избавиться от макрокоманд? 2. Когда происходит развертывание макрокоманд и установка их значений? 3. Как параметры макрокоманд преобразуются в строку? 4. Какой оператор используется в макрокомандах для конкатенации?
358 Часть VI. Специальные темы Упражнения 1. Измените файл employeemain.cpp (листинг 20.4) так, чтобы дважды подклю- чить файл string.hpp (просто продублируйте первую строку). Что произошло при компиляции? 2. Реализуйте в файле string.hpp (листинг 20.3) защиту от повторного подклю- чения и перекомпилируйте файл employeemain.cpp, измененный в упражне- нии 1. Что произошло при компиляции теперь? 3. Выясните способ задания значения переменной макрокоманды из командной строки используемого компилятора. Напишите код, способный отобразить пе- реданное макрокоманде значение. Ответы на контрольные вопросы 1. При помоши директивы #undef можно отменить определение макрокоманд, сделанных в другом коде или командной строке. 2. Развертывание (expansion) макрокоманд (модификация исходного кода) происходит во время компиляции. Именно в это время будет изменен код и заданы значения переменных. Как только компиляция завершается, значения в макрокомандах из- менить будет нельзя, поскольку они окажутся вставлены непосредственно в код. 3. При помоши оператора взятия в кавычки #. В результате указанный аргумент окажется помещен в кавычки, а, следовательно, будет рассматриваться как строковая константа ("х"), а не содержимое переменной х. 4. Оператор конкатенации ##.
ЧАС 22 Объектно-ориентированный анализ и проектирование На этом занятии вы узнаете: как анализировать проблемы и искать решения на основе объектно-ориентированного подхода; как на основе объектно-ориентированного подхода находить простые и надежные решения; о процессе анализа и реализации его результата в проекте C++; как обеспечить возможность расширяемости и многократного использования кода. Цикл разработки Теме цикла разработки посвящено множество книг и справочников. Некоторые предпочитают каскадный (waterfall) метод, который подразумевает, что сначала во всех деталях выясняют требования к готовому продукту, далее архитекторы определяют по- строение программы, используемые ею классы и т.д., а затем программисты реализуют дизайн и архитектуру. Т.е. программисту передается готовый проект и архитектура про- граммы, а он должен лишь реализовать необходимые функциональные возможности. Хотя каскадный метод вполне работоспособен, при разработке достаточно слож- ных программ его недостаточно. Во время работы программист неизбежно опирается на прежний опыт и постоянно обдумывает, что уже было написано ранее и что пред- стоит сделать далее. Хорошие программы на C++ способны самостоятельно проду- мать весь проект прежде, чем приступить к созданию его кода. Никакой гарантии того, что проект останется после этого неизменным, нет. Прежде чем приступать к программированию, следует уяснить объем проекта. Для сложного проекта, реализация которого потребует работы множества программистов в течение нескольких месяцев, необходимо более полное и четкое формулирование ар- хитектуры, чем для простой утилиты, написанной одним программистом за один день. На этом занятии речь идет о проектировании больших и сложных программ, кото- рые впоследствии будут дополняться и модифицироваться в течение многих лет. Не- мало программистов предпочитает работать на грани технологических возможностей и стараются писать программы на пределе сложности используемых инструментальных
360 Часть VI. Специальные темы средств и собственных знаний. Со временем разработчики на языке C++ находят множество способов, позволяющих расширять и усложнять программы, созданные одним или группой программистов. Моделирование системы сигнализации Моделирование (simulation) — это создание компьютерной модели реальной систе- мы. Для создания модели существует множество причин, но хороший проект должен начинаться с осознания вопросов, на которые должна ответить модель. Для начала рассмотрим следующую проблему: необходимо смоделировать систему сигнализации для дома. Дом имеет центральный зал с колоннами, четыре спальни, полуподвал и подземный гараж. Внизу есть несколько окон: три на кухне, четыре в столовой, одно в ванной, по два в гостиной и прихожей, а также два маленьких окна рядом с входной дверью. Че- тыре спальни находятся наверху; каждая имеет по два окна, за исключением главной, в которой четыре окна. Наверху также есть две ванные комнаты, каждая с одним ок- ном. И, наконец, в полуподвале и гараже находятся четыре полуокна. Обычный вход в дом — через переднюю дверь. Кроме того, на кухне есть сдви- гающаяся стеклянная дверь, а в гараже — две двери для автомобилей и одна — для входа в полуподвал. На заднем дворе также есть дверь в полуподвал. Все окна и двери находятся под сигнализацией, на каждом телефоне и рядом с кроватями в главной спальне оборудованы “кнопки тревоги”. Полуподвал тоже на- ходится под сигнализацией, однако она настроена так, чтобы мелкие животные и птицы не могли стать причиной ее срабатывания. При срабатывании расположенный в полуподвале центр системы сигнализации подает щебечущий звуковой сигнал, предупреждая о тревоге. Если тревога не будет отключена изнутри дома за определенный период времени, то будет вызвана полиция. Если нажата кнопка тревоги, полиция будет вызвана немедленно. Система сигнализации связана также с детекторами огня, дыма и средствами раз- брызгивания воды. Она обладает собственными средствами обеспечения бесперебой- ной работы, включая блок резервного питания и огнестойкий корпус. Концептуализация На этапе концептуализации (conceptualization) следует попытаться понять, что именно клиент ожидает от программы и для чего она вообще нужна? На какие вопро- сы должна отвечать эта модель? Например, используя моделирование, можно было бы получить ответ на такой вопрос: “Как долго датчик может оставаться неисправным, прежде чем это будет замечено?” или “Существует ли способ отключить сигнализа- цию окна без сообщения полиции?” Этап концептуализации — это время, когда следует обдумать, что является для программы внутренним, а что — внешним. Следует ли моделировать действия поли- ции? Относится ли к системе сигнализации средство подачи звукового сигнала? Анализ и требования За концептуализацией следует этап анализа. При анализе необходимо помочь кли- енту понять, чего именно он требует от программы, как она должна себя вести и ка- кие виды взаимодействий отражать. Как правило, эти требования фиксируют в наборе документов, которые могут со- держать прецеденты. Прецедент (use case) — это подробное описание способов при- менения программы. Сюда относится описание взаимодействий и схемы применения, что помогает программисту лучше понять задачи проекта.
Час 22. Объектно-ориентированный анализ и проектирование 361 ЙЖИИ____ ВЫ? Унифицированный язык моделирования (Unified Modeling Language — UML) Унифицированный язык моделирования — это способ представления и анализа требований. Представление информации в формате UML имеет два преимущества: во-первых, графическое представление на- глядно, а во-вторых, оно является стандартным, что делает его понят- ным другим разработчикам. Хоть описание UML и выходит за рамки данной книги (этой теме посвящено много хороших изданий), следует заметить, что он обеспечивает способы представления прецедентов. Это особенно удобно при разработке объектно-ориентированных про- грамм, поскольку позволяет непосредственно представить абстракт- ные и обычные классы. Очень хорошая книга по этой теме — Освой самостоятельно UML за 24 часа, 3-е издание, издательство "Вильямс", 2005 г., автор Джозеф Шмуллер (Joseph Schmuller) (ISBN: 5-8459-0855-8). Низкоуровневое и высокоуровневое проектирование После выяснения, полного осознания и фиксации в соответствующей документа- ции требований к программному продукту наступает время переходить к высокоуров- невому проектированию. На этом этапе проектирования программист, ничуть не заботясь о проблемах платформы, операционной системы или языка программирова- ния, выясняет, как именно будет работать система: каковы ее главные компоненты и как они будут взаимодействовать друг с другом. Как вариант, можно отложить проблемы пользовательского интерфейса и сосредо- точиться только на компонентах пространства проблем Пространство проблем (problem space) — это набор задач, которые должна решать разрабатываемая программа. Пространство решений (solution space) — это набор воз- можных решений поставленных задач. На этапе высокоуровневого проектирования нужно обдумать обязанности объек- тов: что они делают и какую информацию содержат. Необходимо также обдумать спо- собы взаимодействия объектов между собой. В рассматриваемом примере присутствуют датчики разных типов, центральная система сигнализации, кнопки, провода и телефоны. Но здравый смысл убеждает, что в состав модели следует также включить помещения (возможно, и их полы) и, воз- можно, группы людей, например, жильцов и полицию. Датчики могут быть разделены на детекторы движения, разрывные ленты, звуко- вые детекторы, детекторы дыма и т.д. Все это датчики разных типов, но пока их мож- но рассматривать как датчик вообще. Таким образом, понятие датчик (sensor) полно- стью соответствует абстрактному типу данных (Abstract Data Type — ADT). Как абстрактный тип данных, класс Sensor (датчик) должен предоставлять пол- ный интерфейс для датчиков всех типов, а каждый производный класс — обеспечи- вать его реализацию. Таким образом, клиенты смогут использовать разные датчики, ничуть не заботясь об их типе, и каждый из них будет решать именно свою задачу в соответствии с реальным типом Чтобы создать хороший класс ADT, необходимо иметь полное представление о том, что делают датчики (или как они работают). Например, являются датчики пас- сивными или активными устройствами? Ожидают ли они, что некий элемент нагреет- ся или будет порвана проводящая ток лента, или расплавится контакт, или они иссле- дуют окружающую их среду? Возможно, некоторые из датчиков имеют только два
362 Часть VI. Специальные темы состояния (сработал или нет), а другие, аналоговые, имеют большое количество со- стояний (например, текущая температура). Интерфейс абстрактного класса должен быть достаточно полным, чтобы соответствовать всем ожидаемым возможностям многочисленных производных классов. Другие объекты Далее следует выявить другие классы, которые будут необходимы для решения постав- ленных задач. Например, если предполагается сохранять записи в журнале, то, вероятно, понадобится таймер. Должна ли система записи в журнал опрашивать каждый датчик или каждый датчик должен периодически осуществлять запись в собственный файл отчета? Пользователь должен быть способным включать, отключать и программировать систему сигнализации, поэтому понадобится некоторая разновидность пульта управ- ления или терминала. Для этого в модели можно предусмотреть отдельный объект. Какие классы нужны? По мере решения этих проблем начинается разработка необходимых классов. На- пример, уже известно, что класс Heatsensor (датчик температуры) будет происходить от класса Sensor. Если этот датчик должен периодически осуществлять запись в от- чет, то он мог бы также (при помощи множественного наследования) происходить от класса Timer (таймер) либо мог бы содержать таймер как переменную-член. Класс Heatsensor, вероятно, будет обладать такими функциями-членами, как CurrentTemp () (текущая температура) и SetTempLimit() (установить предел температу- ры), а также, наверняка, унаследует от базового класса Sensor функцию soundAlarm() (звуковой сигнал). Одной их проблем объектно-ориентированного проектирования является инкапсуля- ция. Проектируемая система может обладать параметром махТетр (максимальная тем- пература). Система сигнализации опрашивает датчик температуры, выясняет текущую температуру, сравнивает ее с максимально допустимой и подает звуковой сигнал, если она слишком велика. Можно сказать, что это нарушает принцип инкапсуляции. Воз- можно, было бы лучше, чтобы система сигнализации не знала и не заботилась о деталях анализа температуры, а все эти задачи взял на себя класс Heatsensor. Знается» вы? Дополнительные ресурсы По этой теме есть очень хорошая книга: The Object-Oriented Thought Process, Second Edition, автор Мэтт Вейсфилд (Matt Weisfeld), издатель- ство Sams (ISBN: 0-672-32611-6). С аргументом, приведенным выше, можно согласиться, а можно не согласиться, но именно таким проблемам уделяют внимание на этапе анализа проблемы. Продол- жая анализ, можно задаться вопросом, должны ли заниматься регистрацией только объекты датчика и класса Log (журнал), а объект класса Alarm (Тревога) — нет? При хорошей инкапсуляции каждый класс имеет полный и достаточный набор средств, необходимых для выполнения его обязанностей, и никакие другие классы его не дублируют. Если класс Sensor несет ответственность за контроль текущей темпе- ратуры, то никакой другой класс этой ответственности нести не должен. С другой стороны, дополнительные классы могли бы помочь и предоставить необ- ходимые функциональные возможности. Например, ответственность за регистрацию текущей температуры мог бы нести и класс Sensor, но эту задачу вполне можно де- легировать классу Log, объект которого фактически осуществляет запись данных.
Час 22. Объектно-ориентированный анализ и проектирование 363 Жесткое разделение обязанностей между классами упростит саму программу, а также ее дальнейшее расширение и поддержку. Когда придет время изменить систе- му сигнализации, дополнив ее новыми модулями, интерфейс журнала и датчиков мо- жет остаться неизменен. Т.е. изменения в системе сигнализации не должны влиять на классы датчиков, и наоборот. Должен ли класс HeatSensor иметь функцию ReportAlarm() (сообщение о трево- ге)? Возможностью сообщать о тревоге должны обладать все датчики. Поэтому метод ReportAlarmf) имеет смысл сделать виртуальным методом класса Sensor, а сам класс Sensor сделать абстрактным. Возможно, обобщенный метод ReportAlarm() класса Sensor придется переопределить в классе HeatSensor, чтобы реализовать де- тали, характерные только для данного типа датчиков. Как передается сигнал тревоги? Когда наступят условия срабатывания, датчики должны передать информацию объекту, который позвонит в полицию и сделает запись в журнале. Возможно, имеет смысл создать класс Condition (условие), конструктору которого будут передавать несколько параметров. В зависимости от сложности, параметры также могут быть объектами или переменными таких простых типов, как целые числа. Возможно, объекты класса Condition будут переданы центральному объекту систе- мы сигнализации, а, возможно, объекты класса Condition будут входить в состав объектов класса Alarm, которые сами способны предпринимать действия в случае тревоги. Возмож- но, никакого центрального объекта и не будет, может быть, датчики сами смогут создавать объекты класса Condition. В этом случае некоторые объекты класса Condition могли бы самостоятельно регистрировать информацию, а другие — вызывать полицию. Хорошо разработанная управляемая событиями система не должна иметь цен- трального координатора. Все датчики должны быть независимыми объектами, полу- чающими и посылающими сообщения друг другу, самостоятельно устанавливающими параметры и контролирующими дом. При обнаружении нарушения создается объект класса Alarm, который делает запись в журнале (или посылает сообщение объекту класса Log) и предпринимает соответствующее действие. Цикл событий Чтобы смоделировать такую управляемую событиями систему, разрабатываемая программа должна обладать циклом событий. Как правило, цикл событий (event loop) представляет собой бесконечный цикл, например, while (1), который получает сооб- щения от операционной системы (щелчки мыши, нажатия клавиш и т.д.) и обрабаты- вает их один за другим, пока не возникнет условие выхода из цикла. Пример простого никла событий приведен в листинге 22.1. Листинг 22.1. Файл simpleevent. срр. Простой цикл событий 0: // Листинг 22.1. Простой цикл событий 1: #include <iostream> 2: 3: class Condition 4: { 5: public: 6: Condition() {} 7: virtual -Condition() {} 8: virtual void Log() = 0; 9: }; 10:
364 Часть VI. Специальные темы 11: class Normal : public Condition 12: { 13: public: 14: Normal() { Log(); } 15: virtual 'Normal() {} 16: virtual void Log() 17: ( std::cout << "Logging normal conditions...\n”; } 18: }; 19 : 20: class Error : public Condition 21: { 22: public: 23: Error() { Log!); } 24: virtual -Error() {} 25: virtual void Log() { std::cout << "Logging error!\n”; } 26: }; 27 : 28: class Alarm : public Condition 29: { 30: public: 31: Alarm (); 32: virtual -Alarm!) {} 33: virtual void Warn!) { std::cout << "Warning!\n"; } 34: virtual void Log!) ! std::cout << "General Alarm log\n"; } 35: virtual void Call!) = 0; 36: }; 37 : 38: Alarm::Alarm() 39: { 4 0: Log() ; 41: Warn(); 42: } 43 : 44: class FireAlarm : public Alarm 45: { 46: public: 47: FireAlarm!) { Log!); }; 48: virtual -FireAlarm!) {} 49: virtual void Call!) { std::cout« "Calling Fire Dept.!\n"; } 50: virtual void Log!) { std::cout << "Logging fire call.\n"; } 51: }; 52 : 53: int main() 54: { 55: int input; 56: int okay = 1; 57: Condition * pCondition; 58: while (okay) 59: { 60: std::cout << "(0)Quit (l)Normal (2)Fire: "; 61: std::cin » input; 62: okay = input; 63: switch (input) 64 : { 65: case 0: 66: break; 67 : case 1:
Час 22. Объектно-ориентированный анализ и проектирование 365 68: pCondition = new Normal; 69: delete pCondition; 70: break; 71: case 2: 72: pCondition = new FireAlarm; 73: delete pCondition; 74: break; 75: default: 76: pCondition = new Error; 77: delete pCondition; 78: okay = 0; 79: break; 80: } 81: } 82: return 0; 83: } Результат (O)Quit (l)Normal (2)Fire: 1 Logging normal conditions... (O)Quit (l)Normal (2)Fire: 2 General Alarm log Warning! Logging fire call. (O)Quit (1)Normal (2)Fire: 0 Анализ Расположенный в строках 58—81 простой цикл позволяет пользователю выбрать сигнал, имитирующий сообщение от датчика тревоги или пожарного датчика. В ответ на это сообщение создается объект класса Condition, конструктор которого вызывает различные функции-члены. Вызов виртуальных функций-членов из конструктора может привести к непредска- зуемым результатам, если нарушить порядок создания объектов. Например, когда объект класса FireAlarm создается в строке 72, порядок создания объектов будет та- ким: Condition, Alarm, FireAlarm. Конструктор класса Alarm вызывает метод Log(), но это метод класса Alarm, а не FireAlarm, несмотря на то что метод Log О объявлен виртуальным. Дело в том, что во время работы конструктора Alarm О ника- кого объекта класса FireAlarm еще не существует. Позже, когда объект класса FireAlarm уже будет создан, его конструктор вызовет метод Log () снова, и на сей раз это будет метод FireAlarm:: Log (). Проект системы PostMaster Объектно-ориентированный анализ решает и другие проблемы. Предположим, компания Acme Software, Inc. решила начать новый проект и наняла группу програм- мистов на C++ для его реализации. Босс компании хочет, чтобы группа разработала утилиту PostMaster, способную читать сообщения от нескольких разных провайдеров электронной почты. Потенциальный клиент — это деловой человек, использующий несколько систем электронной почты, таких, например, как CompuServe, America Online, Internet Mail, Lotus Notes и т.д. Клиент должен быть способен настроить утилиту PostMaster так, чтобы она могла подключиться к каждому из провайдеров электронной почты, получить сообщения и предоставить их клиенту единообразно организованным способом, а также позво- лить послать ответ, переслать сообщение и т.д.
366 Часть VI. Специальные темы Впоследствии предполагается выпуск утилиты PostMaster версии Professional. В нее будет добавлен вспомогательный административный режим, который позволит поль- зователю назначить другое лицо, способное читать некоторую или всю почту клиента, обрабатывать рутинную корреспонденцию и т.д. Возможно, также потребуется ком- понент “искусственного интеллекта”, способный сортировать почту по приоритетам на основании ключевых слов в тексте темы и содержимого. Рассматривается возможность и других дополнений, например, способность работать не только с электронной почтой, но и группами новостей Internet, телеконференциями, списками рассылки. Вполне очевидно, что руководство компании Acme Software, Inc. возлагает большие надежды на программу PostMaster и горит нетерпением извлечь из ее продажи существенную коммерческую прибыль. Поэтому бюджет проекта практиче- ски неограничен (в разумных пределах), но сроки разработки крайне сжаты. Дважды отмерь, один раз отрежь На первом же совещании, пока еще не весь персонал нанят и оргтехника не рас- ставлена по местам, ставится вопрос о полной спецификации на программный про- дукт. Исследовав рынок, маркетинговый отдел выбирает платформу (Unix, Macintosh, Windows), на которой будет работать система. В ходе длительных и тяжелых совещаний у шефа компании становится ясно, что ни- какой спецификации на программный продукт нет, только общие пожелания. Поэтому принимается решение отделить пользовательскую часть (т.е. пользовательский интер- фейс или UI) приложения от его внутренней структуры (системы связи и базы данных). Чтобы ускорить процесс, принимается решение разработать ее сначала для платформы Windows, а уже затем модифицировать продукт под платформы Unix и, возможно, Мас. Это простое решение имеет для проекта серьезные последствия. Сразу становится очевидно, что понадобится одна или несколько библиотек классов для управления памятью, различных пользовательских интерфейсов, а также, возможно, для компо- нентов базы данных и связи. Шеф компании абсолютно уверен, что жизнь или смерть проекта — в руках одного человека, который ясно понимает поставленные задачи, поэтому он требует, чтобы исходный анализ архитектуры и проектирование были завершены еще до того, как он наймет программистов для его реализации. Так что проблему придется анализировать. Разделяй и властвуй Очень быстро становится ясно, что в действительности придется решать не одну про- блему, а несколько. Поэтому общую задачу следует разделить на несколько более простых. Связь. Возможность программного обеспечения связаться с провайдером элек- тронной почты через модем или установить соединение по сети. База данных. Возможность сохранять данные на диске и обращаться к ним Редактирование. Поддержка современных редакторов для создания и манипу- лирования сообщениями. Проблемы платформы. Проблемы UI, обусловленные различиями между плат- формами. Расширяемость. Планирование будущего расширения приложения. Организация и планирование. Организация работы разработчиков и обеспече- ние совместимости создаваемого ими кода. Каждая группа должна составить и оформить расписание и план работы. Руководство и отдел маркетинга долж- ны точно знать, когда будет готов программный продукт.
Час 22. Объектно-ориентированный анализ и проектирование 367 Возможно, имеет смысл нанять менеджера, чтобы он взял на себя вопросы органи- зации и планирования. Затем следует нанять старших разработчиков, чтобы они по- могли провести анализ и проектирование, а затем приняли участие в реализации про- екта. Эти старшие разработчики возглавят группы. Связь. Группа несет ответственность и за модемную связь, и за связь по сети. Им придется иметь дело с пакетами, потоками и битами, а не с сообщениями электронной почты как таковыми. Формат сообщения. Группа несет ответственность за преобразование сообще- ний от каждого провайдера электронной почты в формат, стандартный для сис- темы PostMaster, и обратно. Эта же группа решит задачу записи сообщений на диск и их чтения при необходимости. Редактор сообщений. Эта группа несет ответственность за все разновидности U1 для каждой платформы. Задачей группы будет также обеспечение интерфей- са между внутренней структурой приложения и его пользовательским интер- фейсом. Впоследствии это позволит распространить продукт на другие плат- формы без дублирования кода. Формат сообщений В первую очередь следует сосредоточиться на формате сообщений, оставив про- блемы связи и пользовательского интерфейса на потом. Они будут исследованы уже после того, как появится более полное понимание того, с чем придется иметь дело. Пока не до конца ясно, какую именно информацию следует предоставлять пользова- телю, не стоит и беспокоиться об этом. Исследования различных форматов электронной почты показывают, что они име- ют много общего, но между ними есть и различия. Каждое сообщение электронной почты имеет отправителя, получателя и дату создания. Почти все сообщения имеют заголовок, тему и тело, которое может состоять из простого или форматированного текста, графики, а, возможно, даже звука или других дополнений. Большинство про- вайдеров электронной почты позволяют также передавать почтовые приложения, что- бы пользователи могли пересылать друг другу прикрепленные к письму файлы. Как уже говорилось, ранее было принято решение преобразовывать каждое почтовое сообщение из его исходного формата в формат программы PostMaster. Это позволит не только хранить сообщения в унифицированном формате, но и существенно упростит остальные манипуляции с ними. Принято также решение отделить информацию заго- ловка (отправитель, получатель, дата, тема и т.д.) от тела сообщения. Зачастую пользова- тель просматривает только заголовки, не читая сразу содержимое всех сообщений. Воз- можно, пользователь захочет загрузить с хоста провайдера только заголовки сообщений, а их текст не получать вообще, однако первая версия программы PostMaster будет полу- чать сообщения полностью, хотя отображать сможет и только заголовки. Предварительное проектирование классов Анализ сообщений наводит на мысль о необходимости разработать класс Message (сообщение). Предвидя модернизацию программы для работы с сообщениями, не от- носящимися к электронной почте, класс EmailMessage (сообщение электронной почты) имеет смысл получить как производный от абстрактного класса Message. Как производные от класса EmailMessage можно получить также такие классы, как PostMasterMessage, InterchangeMessage, CISMessage, ProdigyMessage и т.д. Объект сообщения — это естественный выбор для программы обработки почтовых сообщений, но корректная унификация объектов в сложной системе представляет со- бой одну из сложнейших проблем объектно-ориентированного программирования.
368 Часть VI. Специальные темы В некоторых случаях, как с сообщениями, первичные классы, казалось бы, продикто- ваны задачами проекта. Однако чаще всего для выявления необходимых объектов приходится приложить намного больше усилий. Но не впадайте в отчаяние. Большинство проектов сначала не совершенны. Имеет смысл обсудить проблемы вслух. Создайте список из всех существительных и глаго- лов, услышанных в ходе обсуждения проекта. Существительные — это хорошие кан- дидаты на объекты, а глаголы могли бы стать их методами (или самостоятельными специализированными объектами). Это не догма, а лишь один из опробованных на практике подходов начального этапа проектирования. Простая часть позади. Теперь возникает вопрос: “Должен ли класс заголовка сообще- ния быть отделен от класса тела?”. Если да, то необходимо создать параллельные иерархии классов: NewsGroupBody и NewsGroupHeader, а также EmailBody и EmailHeader. Параллельные иерархии — это зачастую свидетельство плохого проекта. Такая ошибка, когда набор подчиненных объектов в одной иерархии соответствует набору основных объектов в другой, является довольно распространенной при объектно- ориентированном проектировании. Дополнительные затраты на хранение и синхро- низацию этих иерархий между собой очень скоро становятся чрезмерными — это классический кошмар для последующей поддержки. Безусловно, это тоже не догма. Иногда такие параллельные иерархии являются наиболее эффективным способом решения некоторых проблем. Тем не менее, заме- тив смешение проекта в этом направлении, имеет смысл заново продумать проблему. Возможно существует более изящное решение. Полученные от провайдера электронные сообщения не обязательно будут разделе- ны на заголовок и тело, многие будут представлять собой один большой поток дан- ных, который создаваемая программа должна будет обработать. Возможно, иерархия классов должна отражать именно эту концепцию? Дальнейшая проработка задач приводит к необходимости составить перечень свойств сообщений, что позволит выявить их возможности, а также расположить хра- нимые ими данные на надлежащем уровне абстракции. Список свойств объектов — это наилучший способ определения необходимых им переменных-членов, а также вы- явления других объектов, которые могут понадобиться. Почтовые сообщения должны будут сохраняться в порядке, указанном пользовате- лем, например, по номерам телефонов и т.п. Хранение, безусловно, должно быть ор- ганизовано на самом верхнем уровне иерархии. Но обязательно ли почтовые сообще- ния должны совместно использовать базовый класс с предпочтениями пользователя? Корневые и некорневые иерархии Существуют два общепринятых подхода создания иерархий наследования: можно все, или почти все, классы сделать производными от одного общего корневого базо- вого класса либо создать несколько иерархий наследования. Преимуществом общего корневого класса является возможность избежать множественного наследования, а недостатком — вероятность переноса реализации в базовый класс. Корневая иерархия удобна в случае, когда у всех классов набора есть общий предок. Классы некорневых иерархий общего базового класса не имеют Поскольку известно, что разрабатываемый программный продукт предназначен для нескольких платформ, множественное наследование здесь не подходит, так как его поддерживают компиляторы не всех платформ. Таким образом, использование корневой иерархии и единого наследования является правильным решением. Но можно выявить и те области, где множественное наследование могло бы быть исполь- зовано в будущем. Впоследствии иерархию можно будет разделить и применить мно- жественное наследование, если это не повредит проекту.
Час 22. Объектно-ориентированный анализ и проектирование 369 Для имен всех собственных внутренних классов применим префикс в виде симво- ла р, чтобы можно было сразу выяснить, какие классы были разработаны самостоя- тельно, а какие принадлежат внешним библиотекам. Корневым будет класс pObject. Каждый создаваемый класс будет происходить именно от него. Сам класс pObject останется довольно простым, он будет содержать только те данные, которые будут присутствовать абсолютно в каждом разрабатываемом классе. Базовый класс корневой иерархии обладает, как правило, общепринятым именем (например pObject) и ограниченными возможностями. Задачей корневого класса явля- ется обеспечение возможности создания коллекций объектов любых производных от него классов так, как будто они являются экземплярами класса pObject. Недостаток корневой иерархии в том, что необходимо переносить интерфейс в корневой класс. Следующими вероятными кандидатами в верхнюю часть иерархии являются классы pstored и pwired. Объекты класса pStored несут ответственность за сохранение дан- ных на диске, а объекты класса pwired — за их передачу через модем или по сети. По- скольку почти все объекты придется сохранять на диске, имеет смысл разместить эти функциональные возможности довольно высоко в иерархии. Хотя все передаваемые через модем объекты подлежат сохранению, не все хранимые объекты следует передавать по се- ти. Таким образом, класс pwired имеет смысл сделать производным от класса pstored. Каждый производный класс наследует все данные и функциональные возможности (методы) своего базового класса. Кроме того, он будет иметь и собственные дополни- тельные возможности. Таким образом, в класс pwired следует добавить набор мето- дов, обеспечивающих передачу данных через модем. Не исключено, что передаваемые объекты подлежат сохранению; не исключено, что все хранимые объекты можно передавать, однако не исключено и то, что ни одно из этих предположений не верно. Если сохранению подлежат только некоторые из передаваемых через модем объектов и только некоторые из хранимых объектов следу- ет передавать, то придется использовать множественное наследование или как то об- ходить проблему. Потенциально в такой ситуации можно было бы, например, сделать класс pwired производным от класса pstored, а затем переделать методы сохранения так, чтобы они не делали ничего или возвращали сообщение об ошибке для тех объ- ектов, которые передаются через модем, но не подлежат сохранению. Таким образом, стало известно, что не все хранимые объекты подлежат передаче (например, объект предпочтений пользователя передаче не подлежит), но все объек- ты, передаваемые через модем, следует сохранять. На настоящий момент иерархия на- Рис. 22.1. Предварительная схема иерархии наследования
370 Часть VI. Специальные темы Купить или разработать? Этот вопрос неизбежно возникает на этапе проектирования программы. Какие из функций имеет смысл реализовать самостоятельно, а какие можно купить. Для решения некоторых или даже всех проблем связи можно воспользоваться уже го- товыми библиотеками. Существует множество коммерческих компаний (и неком- мерческих источников), поставляющих подобные библиотеки. Зачастую библиотеку дешевле купить, чем тратить время и силы на изобретение очередного колеса. Можно даже рассмотреть вопрос о покупке таких библиотек, которые не предназначены для использования именно в языке C++. Если они спо- собны предоставить необходимые функциональные возможности, то встроить их в проект будет проще, чем разработать с нуля. Это могут быть также специальные инструментальные средства, облегчающие решение поставленной задачи. Проектирование интерфейсов На данном этапе проектирования очень важно избегать попыток реализации кода. Всю энергию следует сосредоточить на проектировании четкого и понятного интер- фейса взаимодействия классов, а затем определить данные и методы, необходимые для каждого класса. Прежде чем приступать к разработке остальных производных классов, имеет смысл полностью завершить список возможностей базовых классов. Поэтому на данном эта- пе основное внимание следует уделить классам pobject, pStored и pwired. Корневой класс, pObject, будет иметь только те данные и методы, которые явля- ются общими для всех объектов системы. Каждый объект, вероятно, должен иметь индивидуальный идентификационный номер. Таким образом, для класса pobject можно создать член рю (идентификатор), но сначала следует спросить себя, для каж- дого ли объекта, который даже не подлежит сохранению или передаче по сети, потре- буется такой номер. Вопрос спорный, ведь даже если объект не подлежит сохране- нию, он остается частью иерархии. Если же таких объектов нет, имеет смысл рассмотреть вопрос о слиянии классов pobject и pStored в один. В конце концов, если все объекты следует сохранять, то чем различаются эти классы? Обдумав ситуацию можно прийти к выводу, что некото- рые классы, например объектов адресов, имеет смысл наследовать непосредственно от класса pobject, ведь их никогда не придется сохранять самостоятельно, поскольку они войдут в состав других объектов. Теперь стало ясно, что наличие отдельного класса pobject имеет смысл. Нетрудно догадаться, что такая программа будет иметь книгу адресов, представляющую собой коллекцию объектов класса pAddress. Поскольку объекты класса pAddress никогда не будут сохраняться самостоятельно, индивидуальные идентификационные номера для каждого из них будут весьма полезны. Выяснив, что классу pobject необходима как минимум переменная-член рю, можно, наконец, предварительно определить его состав, который будет выглядеть следующим образом: class pOjbect ( publiс: pObject(); -pObject() ; pID GetID() const; void SetID(); private: pID itsID; }
Час 22. Объектно-ориентированный анализ и проектирование 371 В этом объявлении класса есть на что обратить внимание. Во-первых, оно свидетель- ствует о том, что класс не происходит ни от одного другого класса, а, следовательно, яв- ляется корневым. Во-вторых, здесь нет реализации его методов, даже таких, как метод GetlD (), который, вероятно, будет иметь встраиваемую реализацию. В-третьих, уже оп- ределены постоянные методы — это часть интерфейса, а не реализация. И, наконец, здесь присутствует новый тип данных — plD. Для него, вероятнее всего, подойдет тип unsigned long, поскольку он вполне достаточен для номеров объектов. Кроме того, если для переменной plD тип unsigned long окажется чересчур ве- лик или мал, его можно будет централизовано изменить именно здесь, причем эта модификация повлияет абсолютно на все производные классы и в них не придется искать места применения переменной plD, чтобы изменить способ ее применения. Теперь при помощи ключевого слова typedef объявим переменную plD как имеющую тип ULONG, который в свою очередь объявлен как тип unsigned long. Возникает вполне резонный вопрос: где именно расположить эти объявления? При разработке большого проекта неизбежно совместное использование файлов. Демонстрируемый здесь стандартный подход подразумевает, что каждый класс объяв- лен в собственном файле заголовка, а реализация его методов расположена в соответ- ствующем файле с расширением .срр. Таким образом, будет создан файл object.hpp и соответствующий ему файл object.срр. В проект войдут и другие файлы, напри- мер, msg.hpp и msg.срр, содержащие объявление класса pMessage и реализацию его методов соответственно. Создание прототипа Маловероятно, что проект такой сложности, как PostMaster, сразу получится полно- стью законченным и работоспособным. Было бы просто наивным полагать, что столь масштабные проблемы будут безошибочно решены, а проектирование всех классов и их интерфейсов завершится еще до того, как разработчики приступят к созданию кода. Для опробования создаваемого проекта на прототипе есть множество причин. Прото- тип позволит быстро проверить на практике основные идеи проекта. Существуют не- сколько разновидностей прототипов, однако каждый из них применяется д ля своих целей. Прототип пользовательского интерфейса проекта позволяет наглядно проверить внешний вид программного продукта и его удобство для потенциальных пользователей. Прототип функциональных возможностей не имеет законченного пользователь- ского интерфейса, но позволяет опробовать основные функции программы, такие, например, как передача сообщений или вложенных файлов. И, наконец, прототип архитектуры поможет оценить на упрощенной версии про- граммы степень улучшений при внесении изменений в проект еще до того, как он бу- дет реализован в деталях. Задачи прототипа должны быть предельно просты и понятны: исследование поль- зовательского интерфейса, эксперименты с функциональными возможностями, созда- ние упрощенной модели финальной версии. Хороший прототип архитектуры может и не обладать совершенным пользовательским интерфейсом, и наоборот. Кроме того, при создании прототипа будут отработаны фрагменты кода, которые впоследствии могут стать основой или элементами финального кода. Правило 80/80 На этом этапе проектирования срабатывает эмпирическое правило, по которому 80% сотрудников способны выполнить 80% работ, поэтому основное внимание следу- ет уделить оставшимся 20%. Вариации, конечно, возможны, но, как правило, проект осуществляется по принципу 80/80.
372 Часть VI. Специальные темы Следовательно, имеет смысл начинать с разработки основных классов, оставив второстепенные на потом. Когда основные классы будут определены почти полностью и потребуют лишь небольших финальных усовершенствований, можно будет выбрать один из главных классов и сосредоточиться на нем, оставив проектирование и реали- зацию похожих классов на потом. Лежад______ прим Еще одно правило Существует и другое правило, 80/20, которое гласит, что первые 20% кода программы потребуют 80% рабочего времени, а остальные 80% кода займут еще 80% времени! Это вполне справедливо, что 20% работы займут 80% времени, но справедливо и то, что 80% прибыли компании принесут 20% клиентов Это правило имеет многочисленные следствия. Разработка класса PostMasterMessage Помня о сказанном выше, сосредоточимся на классе PostMasterMessage. Этот класс будет под особым контролем ведущего разработчика. Безусловно, в интерфейсе класса PostMasterMessage следует учесть его воз- можность работы с сообщениями различных типов. Поскольку предстоит взаимо- действовать с разными провайдерами и получать от них сообщения в разном фор- мате, понадобится специальный механизм, распознающий формат поступающих на компьютер сообщений. Тем не менее, уже известно, что каждый объект класса PostMasterMessage будет содержать информацию об отправителе, получателе, дате и теме сообщения, а также тело сообщения и, возможно, присоединенные файлы. Следовательно, понадобятся методы доступа к каждому из этих атрибутов, а также методы, позволяющие выяснить размер присоединенных файлов, сообщений и т.д. Некоторые службы, с которыми придется взаимодействовать, используют форма- тированный текст (т.е. текст, способный содержать команды для установки шрифта, размера символов и таких атрибутов, как полужирный шрифт и курсив). Другие служ- бы подобные атрибуты не поддерживают, но могут существовать и такие, которые ис- пользуют собственную систему форматирования текста. Таким образом, данный класс нуждается в методах преобразования форматированного текста в обычный текст ASCII, а, возможно, и в другой формат, принятый для программы PostMaster. Интерфейс прикладных программ Интерфейс прикладных программ (Application Programming Interface — API) — это набор документации и функций для использования служб. Большинство провайдеров без проблем предоставляют свои API, поэтому программа PostMaster сможет восполь- зоваться большинством дополнительных возможностей их сообщений (такими, на- пример, как отформатированный текст и вложенные файлы). API программы PostMaster также имеет смысл опубликовать, чтобы в будущем другие провайдеры могли ориентироваться на работу с ними. Класс PostMasterMessage должен иметь хорошо проработанный открытый ин- терфейс, и функции преобразования будут основным компонентом его API. Интер- фейс класса PostMasterMessage выглядит на настоящий момент так, как показано в листинге 22.2. Ще------ осторожны: Даже не пытайтесь! Этот код не является определением базового класса (MaiiMessage) и не подлежит компиляции.
Час 22. Объектно-ориентированный анализ и проектирование 373 Листинг 22.2. Файл postmasterinterface. срр. Интерфейс класса PostMasterMessage 0: // Листинг 22.2. Класс PostMasterMessage 1: 2: cl ass PostMasterMessage : public MailMessage 3 : { 4 : public: 5: PostMasterMessage(); 6: PostMasterMessage( 7 : pAddress Sender, 8: pAddress Recipient, 9: pString Subject, 10: pDate creationDate); 11 : 12: // Здесь располагаются другие конструкторы. 13 : II Не забудьте включать конструктор копий. 14 : // конструктор для сохранения и конструктор 15: // транспортного формата. 16: // Включите также конструкторы для других форматов. 17: -PostMasterMessage(); 18 : pAddressb GetSender() const; 19: void SetSender (pAddressb) ,- 20: // Другие методы доступа. 21: // Здесь будут расположены операторы, включая оператор 22: // равенства и функции преобразования. Они должны 23: // преобразовывать сообщения PostMaster в форматы 24: // других сообщений. 25: 26: private: 27: pAddress itsSender; 28: pAddress itsRecipient; 29: pString itsSubject; 30: pDate itsCreationDate; 31 : pDate itsLastModDate; 32: pDate itsReceiptDate; 33 : pDate itsFirstReadDate; 34 : pDate itsLastReadDate; 35: }; Класс PostMasterMessage объявлен как производный от класса MailMessage. Наличие нескольких конструкторов облегчит создание экземпляров класса PostMasterMessages для других типов почтовых сообщений. Понадобится набор методов доступа для чтения и установки значений данных- членов класса, а также операторы для преобразования всех или некоторых сообщений в другие форматы. Поскольку сообщения предстоит сохранять на диске и получать их по линии связи, понадобятся методы и для этих целей. Программирование в больших группах Даже этой предварительной архитектуры вполне достаточно, чтобы поставить зада- чи группам разработчиков. Над внутренней структурой той части приложения, кото- рая относится к связи, может начинать работу группа связи тесно взаимодействуя с группой формата сообщений. Группа формата сообщения, вероятно, займется общим интерфейсом класса Message, который был начат ранее и отложен в связи с вопросами чтения и записи данных на
374 Часть VI. Специальные темы диск. После того как взаимодействие с диском будет проработано достаточно хорошо, группа может переходить к завершению интерфейса на уровне передачи данных. Весьма соблазнительно применить готовые редакторы сообщений, способные ра- ботать с сообщениями разнообразных форматов и внутренними классами сообщений, но это не самая лучшая идея. Группа должна разработать интерфейс для класса сооб- щения, ведь объекты редактора сообщений не должны знать много о внутренней структуре сообщений. Продолжение проекта По мере продолжения проекта будет неоднократно возникать вопрос: в какой класс необходимо поместить данный набор функциональных возможностей (или информа- цию)? Такую функцию должен содержать класс сообщения или класс адреса? Должен ли сохранять эту информацию редактор сообщений или само хранилище сообщений? Объекты классов должны действовать в зависимости от обстоятельств, как секретные агенты. Они не должны совместно использовать больше информации, чем необходимо. Решения в процессе проектирования При разработке программы придется решать сотни проблем. Их следует располо- жить в порядке от более общих (в чем задача проекта?) к более определенным (как именно работает данная функция?). Хотя детали реализации не будут завершены до тех пор, пока не окажутся вопло- щены в коде, а некоторые из интерфейсов продолжат перемешаться и изменяться по мере работы, необходимо удостовериться в полном понимании общей структуры и за- дач проекта прежде, чем приступать к созданию и компиляции кода. Одной из наибо- лее распространенных причин неудачи при разработке программного обеспечения яв- ляется недостаточное понимание его задач. Решения, решения и снова решения Чтобы получить представление о задачах проекта, имеет смысл ответить на вопрос: какие пункты будет содержать меню приложения? Для программы PostMaster, вероят- но, первым будет пункт New Mail Message (Новое почтовое сообщение), а это немед- ленно порождает следующую проблему: что случится, когда пользователь выбирает его? Должен ли сообщение создать редактор или созданный объект нового сообщения должен запустить редактор сообщений? Задача пункта меню New Mail Message (Новое почтовое сообщение) вполне проста и очевидна — создание нового почтового сообщения. Но что случится, если пользова- тель щелкнет по кнопке Cancel (Отмена) уже после начала создания сообщения? Воз- можно, для простоты имело бы смысл сначала создать редактор и сделать так, чтобы он самостоятельно создавал новое сообщение. Проблема такого подхода заключается в том, что редактор должен будет по- разному действовать в случаях, когда сообщение создается и когда редактируется уже существующее. Если сообщение сначала создается, а затем передается редактору, то должен существовать только один набор кода, поскольку в обоих случаях редактирует- ся уже существующее сообщение. Если сообщение сначала создается, то кто это делает? Выполняет ли это код пунк- та меню? Если да, то должен ли он указывать объекту сообщения на необходимость его редактирования или это должно быть задачей конструктора объекта сообщения? На первый взгляд это имеет смысл сделать в конструкторе, но, в конце концов, ведь не каждое созданное сообщение обязательно нужно редактировать. Так что это не самая лучшая идея. Во-первых, может понадобиться создавать “фиксированные” со- общения (например, сообщения об ошибках, отправляемые оператору системы по поч- те). Такие сообщения не нужно помещать в редактор. Во-вторых, и это важнее всего,
Час 22. Объектно-ориентированный анализ и проектирование 375 задача конструктора заключается в создании объекта: не больше, но и не меньше. После того как объект почтового сообщения будет создан, задача конструктора считается вы- полненной. Добавление обращения к методу редактирования только запутает конструк- тор и сделает объект почтового сообщения уязвимым для отказов редактора. Однако хуже всего то, что метод передачи на редактирование обратится к другому классу — классу редактора, что приведет к вызову его конструктора. Класс редактора не является базовым для класса сообщения и не содержится внутри него. Было бы крайне неудачно, если бы создание объекта класса сообщения зависело от успеха ра- боты конструктора класса редактора. И, наконец, если сообщение не может быть создано успешно, то вызывать редак- тор не нужно вообще. Однако успех создания сообщения в этом случае зависел бы от успеха вызова редактора! Следовательно, перед вызовом метода Message::Edit О не- обходимо полностью выйти из конструктора класса сообщения. Работа с управляющей программой Один из подходов выявления проблем проекта подразумевает создание управляю- щей программы на раннем этапе процесса проектирования. Управляющая программа (driver program) — это пробная программа, которая предназначена только для провер- ки и демонстрации возможностей других функций. Например, управляющая про- грамма для системы PostMaster могла бы предоставлять пользователю очень простое меню, позволяющее создавать объекты класса PostMasterMessage и управлять ими. Это позволит на практике опробовать основные элементы проекта. Листинг 22.3 содержит несколько усовершенствованное определение класса PostMasterMessage и простую управляющую программу. Листинг 22.3. Файл driverprogram. срр. Управляющая программа для класса PostMasterMessage 0: II Листинг 22.3. Управляющая программа 1: ((include <iostream> 2: ((include <string.h> 3 : 4: typedef unsigned long pDate; 5: enum SERVICE { PostMaster, Interchange, 6: CompuServe, Prodigy, AOL, Internet }; 7 : 8: class String 9: { 10: public: 11: II Конструкторы 12: String(); 13: String(const char ‘const); 14: String (const String s); 15: -String(); 16: 17: II Перегруженные операторы 18: char & operator[](int offset); 19: char operator[](int offset) const; 20: String operator*(const Strings); 21: void operator+=(const Strings); 22: String S operator= (const String S); 23: friend std::ostream S operator« 24: (std::ostreamS theStream, Strings theString); 25: II Общие методы доступа 26: int GetLen() const { return itsLen; }
376 Часть VI. Специальные темы 27 : const char * GetString() const { return itsString; } 28 : 2 9: 30 : 31: 32: 33 : 34: 35 : 36: 37 : 38 : 39: 40 : 41: 42 : 43 : 44: 45 : 46 : 47 : 48 : 49 : 50 : 51 : 52: 53: 54 : 55: 56 : 57 : 58 : 59: 60 : 61: 62 : 63 : 64: 65 : 66: 67 : 68 : 69 : 70 : 71 : 72 : 73 : 74: 75 : 76 : 77 : 78 : 79 : 80: 81: 82 : 83 : 84 : II static int Constructorcount; private: String (int); II Закрытый конструктор char * itsString; int itsLen; } ; // Стандартный конструктор создает строку нулевой длины String::String() { itsString = new char[l]; itsString[0] = '\0'; itsLen=0; // std::cout << "XtDefault string constructor^" ; // ConstructorCount++; } // Закрытый (вспомогательный) конструктор, // используемый только методами класса для создания // строк необходимой длины, заполненных символом null. String::String(int len) ( itsString = new char[len+l]; int i; for (i=0; i<=len; i++) itsString[l] = '\0'; itsLen=len; // std::cout << "XtString(int) constructorXn"; // ConstructorCount++; ) II Преобразует символьный массив в строку String::String(const char * const cString) { itsLen = strlen(cString); itsString = new char[itsLen+1]; int 1 ; for (i=0; icitsLen; i++) itsString[i] = cString[i]; itsString[itsLen]='\0'; II std::cout << ”XtString(char*) constructorXn"; // ConstructorCount++; ) II Конструктор копий String::String (const String & rhs) { itsLen=rhs.GetLen(); itsString = new char[itsLen+1]; int i; for (i=0; icitsLen; i++) itsString[i] = rhs[i]; itsString[itsLen] = 1\0'; // std::cout << "XtString(Strings) constructorXn"; II ConstructorCount++; )
Час 22. Объектно-ориентированный анализ и проектирование 377 85: 86: // Деструктор, освобождает выделенную память 87: String::-String () 88: { 89: delete [] itsString; 90: itsLen = 0; 91: // std::cout << "\tString destructor\n"; 92: } 93 : 94: Strings String::operator=(const String & rhs) 95: { 96: if (this == &rhs) 97: return *this; 98: delete [] itsString; 99: itsLen=rhs.GetLen(); 100: itsString = new char[itsLen+1]; 101: int i; 102: for (i=0; i<itsLen; i++) 103: itsString[i] = rhs[i]; 104: itsString[itsLen] = '\0'; 105: return *this; 106: // std::cout << "\tString operator=\n"; 107: } 108: 109: // Непостоянный оператор индексирования, возвращает 110: // ссылку на символ так, что ее можно изменить! Ill: char & String::operator[](int offset) 112: { 113: if (offset > itsLen) 114: return itsString[itsLen-1]; 115: else 116: return itsString[offset]; 117: } 118: 119: // Постоянный оператор индексирования для использования 120: // с постоянными объектами (см. конструктор копий) 121: char String::operator[](int offset) const 122: { 123: if (offset>itsLen) 124: return itsString[itsLen-1]; 125: else 126: return itsString[offset]; 127: } 128: 129: // Создает новую строку, добавляя текущую 130: II строку к rhs 131: String String::operator+(const Strings rhs) 132: { 133: int totalLen = itsLen + rhs.GetLen(); 134: String temp(totalLen); 135 : int i , j ; 136: for (i=0; i<itsLen; i++) 137: temp[i] = itsString[i]; 138: for (j=0; j<rhs.GetLen(); j++, i++) 139: temp[i] = rhs[j]; 140: temp[totalLen] = '\0 ' ; 141: return temp; 142: }
378 Часть VI. Специальные темы 143 : 144 : 145 : 146 : 147 : 148 : 149 : 150: 151 : 152 : 153 : 154 : 155 : 156 : 157 : 158 : 159 : 160 : 161 : 162 : 163 : 164 : 165: 166 : 167 : 168 : 169 : 170 : 171 : 172 : 173 : 174 : 175 : 176 : 177 : 178 : 179 : 180 : 181 : 182 : 183 : 184 : 185 : 186 : 187 : 188 : 189 : 190 : 191 : 192 : 193 : 194: 195: 196 : 197 : 198: 199 : II Изменяет текущую строку, ничего не возвращая void String::operator+=(const Strings rhs) { int rhsLen = rhs.GetLen(); int totalLen = itsLen + rhsLen; String temp(totalLen); int i, j ; for (i=0; i<itsLen; i++) temp[i] = itsString[i]; for (j=0; j<rhs.GetLen(); j++, i++) temp[i] = rhs[i-itsLen]; temp[totalLen] = '\0'; *this = temp; } // int String::Constructorcount = 0; std::ostreams operator« ( std::ostreamb theStream, Strings theString) { theStream << theString.GetString(); return theStream; } class pAddress { public: pAddress(SERVICE theService, const Strings theAddress, const Strings theDisplay): itsService(theService), itsAddressString(theAddress), itsDisplayString(theDisplay) {) // pAddress(String, String); // pAddress(); // pAddress (const pAddressS); -pAddress() {} friend std: : ostreams operator« ( std::©streams theStream, pAddressS theAddress); Strings GetDisplayString() { return itsDisplayString; } private: SERVICE itsService; String itsAddressString; String itsDisplayString; }; s td: : os treams operator« ( std::ostreams theStream, pAddressS theAddress) ( theStream « theAddress.GetDisplayString(); return theStream; ) 200: class PostMasterMessage
Час 22. Объектно-ориентированный анализ и проектирование 379 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 { public: II PostMasterMessage(); PostMasterMessage(const pAddressb Sender, const pAddressb Recipient, const Strings Subject, const pDates creationDate) ,- -PostMasterMessage() {} void Edit(); // invokes editor on this message pAddress& GetSender() { return itsSender; } pAddressb GetRecipient() { return itsRecipient; } Strings GetSubject() { return itsSubject; } // void SetSender(pAddressS ); II Другие методы доступа II Здесь будут расположены операторы, включая оператор // равенства и функции преобразования. Они должны // преобразовывать сообщения PostMaster в форматы // других сообщений. private: pAddress itsSender; pAddress itsRecipient; String pDate pDate pDate pDate pDate itsSubj ect ; itsCreationDate; itsLastModDate; itsReceiptDate; itsFirstReadDate; itsLastReadDate; } ; PostMasterMessage::PostMasterMessage( const pAddressS Sender, const pAddressS Recipient, const Strings Subject, const pDateS creationDate): itsSender(Sender), itsRecipient(Recipient), itsSubject(Subject) , itsCreationDate(creationDate), itsLastModDate(creationDate) , itsFirstReadDate(0), itsLastReadDate(0) { std::cout << "Post Master Message created. \n"; void PostMasterMessage::Edit() { std::cout << “PostMasterMessage edit function called\n"; int main() {
380 Часть VI. Специальные темы 259: pAddress Sender 260: (PostMaster, "jliberty@PostMaster", "Jesse Liberty"); 261: pAddress Recipient 262: (PostMaster, “sliberty@PostMaster”,"Stacey Liberty"); 263: PostMasterMessage PostMasterMessage 264: (Sender, Recipient, "Saying Hello", 0); 265: std: : cout << "Message review... \n"; 266 : std: : cout << "From:\t\t" 267 : << PostMasterMessage.GetSender() << std::endl; 268 : std: : COUt << "To:\t\t" 269 : << PostMasterMessage.GetRecipient() << std: :endl; 270: std: : cout << "Subject:\t" 271 : << PostMasterMessage.GetSubject() « std::endl 272 : return 0; 273: } Результат Post Master Message created. Message review... From: Jesse Liberty To: Stacey Liberty Subject: Saying Hello Анализ В строке 4 тип pDate определен как соответствующий типу unsigned long. Сохра- нение даты в переменной типа unsigned long является вполне обычным явлением. Как правило, оно содержит количество секунд, прошедших с такой произвольно назна- ченной даты, как 1 января 1900 года. В данной программе это лишь место для реального значения, которое будет определено впоследствии при завершении класса pDate. В строке 5 определена перечисляемая константа service, позволяющая объектам адресов отслеживать типы адресов, включая PostMaster, CompuServe и т.д. Строки 8—167 содержат интерфейс и реализацию класса string. Они аналогичны приведенным в предыдущих главах. Класс string применяется для нескольких пере- менных-членов всех классов сообщений, некоторых других классов, используемых классом сообщений, а также основной программой. Для завершения классов сообще- ний понадобится надежный полнофункциональный класс string. В строках 169—191 объявлен класс pAddress. Здесь реализованы лишь основные функциональные возможности этого класса, которые будут дополнены впоследствии, по мере усовершенствования программы. Эти объекты представляют неотъемлемые компоненты каждого сообщения — адрес отправителя и адрес получателя. Объект полнофункционального класса pAddress будет способен переадресовать сообшение, ответить на сообщение и т.д. В задачи объекта класса pAddress входит отслеживание отображаемой строки ад- реса и внутренней строки маршрутизации для ее службы. В проекте остается один от- крытый вопрос: должен ли существовать только один класс pAddress или для каждой службы следует создать собственный, производный от него? В настоящий момент служба отслеживает значение перечисляемой константы, хранимое в переменной- члене каждого объекта класса pAddress. Строки 200—233 содержат интерфейс класса PostMasterMessage. В данном конкрет- ном листинге этот класс является самостоятельным, но очень скоро его придется сде- лать частью иерархии наследования. При переделке, когда он окажется унаследован от
Час 22. Объектно-ориентированный анализ и проектирование 381 класса Message, некоторые из переменных-членов могут переместиться в базовые клас- сы, а некоторые из функций-членов будут переопределять методы базового класса. Чтобы сделать этот класс полнофункциональным, понадобится набор разных кон- структоров, функций доступа и других функций-членов. Обратите внимание, этот листинг доказывает, что на момент создания простой управляющей программы, по- зволяющей проверить некоторые из сделанных предположений, рассматриваемый класс не обязан быть законченным на все 100%. В строках 251—254 содержится функция Edit(). Она существенно упрощена и лишь отображает на экране сообщение. Ее функциональные возможности, обеспе- чивающие редактирование сообщения, будут реализованы позже, уже после того, как рассматриваемый класс станет полнофункциональным. Строки 257—273 содержат управляющую программу. В настоящее время она прове- ряет лишь несколько функций доступа и перегруженный оператор operator«. Тем не менее, это наглядно демонстрирует способы экспериментирования с классом PostMasterMessage и средой разработки, позволяющей изменять данные классы и исследовать влияние сделанных изменений Вопросы и ответы Чем объектно-ориентированный анализ и проектирование отличаются от других подходов? До появления объектно-ориентированных технологий аналитики и программисты рассматривали программы как группы функций для обработки данных. Объект- но-ориентированное программирование объединило данные и функции в само- стоятельные блоки, способные хранить и обрабатывать данные Процедурное программирование в основном сосредоточено на функциях и их работе с данны- ми. Как уже говорилось, программы на языках Pascal и С являются наборами процедур, а программы на языке C++ — набором классов. Является ли объектно-ориентированное программирование той палочкой-выручал- очкой, которая решит все проблемы программирования? Нет, это и не предполагалось. Но для больших и сложных проектов объектно- ориентированный анализ, проектирование и программирование могут стать един- ственно возможными инструментальными средствами, способными справиться со сложной проблемой такими способами, которые ранее были недоступны. Коллоквиум Изучив возможности объектно-ориентированного анализа и проектирования, име- ет смысл ответить на несколько вопросов и выполнить ряд упражнений, чтобы закре- пить полученные знания. Контрольные вопросы 1. Что необходимо сделать прежде, чем приступать к созданию кода? 2. Что такое интерфейс прикладных программ (API)? 3. Что делает управляющая программа? 4. В чем преимущество разделения больших приложений на меньшие фрагменты?
382 Часть VI. Специальные темы Упражнения Не забывайте: решения находятся на прилагаемом CD. 1. Выберите небольшую проблему из повседневной или деловой жизни и выпол- ните описанные на этом занятии действия для ее решения. 2. Измените файл simpleevent.срр (листинг 22.1) и добавьте в каждый деструк- тор оператор std: :cout, выводящий на экран сообщение. Запустите програм- му на выполнение и проследите, когда происходит вызов деструкторов. Как уже было продемонстрировано, вызов конструкторов произойдет при передаче под- лежащих регистрации сообщений. 3. Исследуйте доступные по Internet классы и библиотеки C++. Рассмотрите их и решите, следует ли при случае их купить или пытаться реализовать само- стоятельно. Ответы на контрольные вопросы 1. Перед началом создания кода необходимо решить множество задач: выявить требования к программному продукту (что именно хочет пользователь?), про- вести анализ (что необходимо для выполнения этих требований?) и разработать проект (как выполнить требования проекта?). К сожалению, очень много ком- паний исповедуют совершенно неправильную философию: “Пусть все эти программисты садятся и сразу принимаются писать код! Не моя забота, что список требований не завершен!” 2. API — это набор документации и методов, необходимых для использования службы. Он не содержит информацию о работе службы, только о том, как ее использовать. Подробности реализации от клиента скрыты. 3. Управляющая программа позволяет проверять работу классов и функций по мере их разработки. Она может быть довольно простой, но вполне позволяю- щей выяснить поведение кода и взаимодействие компонентов. 4. Одним из самых больших преимуществ является возможность выполнения работ несколькими разработчиками или даже группами. В то время как одна группа ра- ботает над первым классом, никто не мешает второй группе работать над вторым.
ЧАС 23 Шаблоны На этом занятии вы узнаете: что такое шаблоны и как их использовать; чем шаблоны превосходят макрокоманды; как создать шаблон класса. Что такое шаблоны? На занятии 19, “Связанные списки”, рассматривалось создание связанных спи- сков. Список получился хорошо инкапсулированным, он хранил лишь указатель на головной блок, который хранил указатель на внутренний блок, и т.д. Вполне очевидная проблема связанного списка заключалась в том, что он был спо- собен работать только с вполне определенными объектами данных, для которых был предназначен. В такой связанный список нельзя было поместить объект другого клас- са. Невозможно создать, например, связанный список из объектов класса Саг или Cat, или любого другого класса, тип которого не был задан в исходном списке. Старый способ решения этой проблемы подразумевал создание новых версий для объектов каждого нового типа данных. Более объектно-ориентированный способ под- разумевает разработку базового класса List и последующее создание производных от него классов CatsList и CarList. При этом в класс LinkedList просто добавлялось объявление нового класса CatsList. Однако на следующей неделе, когда понадобится создать список объектов класса Саг, придется снова удалить объявление старого клас- са и вставить объявление нового. Само собой разумеется, ни одно из этих решений нельзя признать удовлетвори- тельным. Какое-то время спустя класс List и классы и производные от него придется модернизировать. Поверьте, последующее внесение изменений во все классы, связан- ные с модернизированным, является настоящим кошмаром. Шаблоны, являющиеся относительно новым средством языка C++, помогают ре- шить эту проблему. Кроме того, в отличие от старомодных макрокоманд, шаблоны являются интегрированным компонентом языка, а, следовательно, они безопаснее и более гибкие. Шаблон (template) позволяет определить общий класс, а для создания экземпляров конкретных классов передавать необходимый тип как параметр.
384 Часть VI. Специальные темы Экземпляры шаблона Шаблон позволяет создать список из объектов любого типа, избежав необходимости в наборе списков для каждого типа. Предположим, PartsList — это список деталей, a CatList — список котов Единственное различие между ними заключается в хранимом типе данных. В шаблонах тип определяемых классов передается в списке параметров. Под созданием экземпляра (instantiation) понимают создание определенного типа из шаблона. Полученные в результате классы называют экземплярами шаблона. Определение шаблона Ниже приведено объявление параметрического класса List (шаблона для списков), template <class Т> // объявление шаблона и параметра class List // параметрический класс { public: List(); // здесь должно быть полное объявление класса } ; Ключевое слово template используется в начале каждого объявления и определе- ния класса шаблона. Параметры шаблона располагаются за ключевым словом template. Параметры — это элементы, которые изменяются в зависимости от необ- ходимого экземпляра. Например, в приведенном выше шаблоне будет изменяться тип сохраняемых в списке объектов. Один экземпляр шаблона может хранить список це- лых чисел, а другой — объектов класса Animal. В этом примере используется ключевое слово class, за которым следует иденти- фикатор т. Ключевое слово class означает, что параметром является тип Идентифи- катор т используется и в остальной части определения шаблона, указывая тем самым на параметрический тип. В одном экземпляре этого класса вместо идентификатора т будет находиться тип int, а в другом — тип Cat. Чтобы объявлять экземпляры параметрических классов списков для типов int и cat, применяется следующий код. List<int> anlntList; List<Cat> aCatList; Таким образом, объект anintArray представляет собой список целых чисел, а объект aCatList — список объектов класса Cat. Теперь тип List<int> можно применять в любом месте, где обычно использовался тип int — для возвращаемого функцией значения, для параметра функции и т.д. Код параметрического класса List приведен в листинге 23 1. Это прекрасный способ создания шаблонов: сначала получить одиночный работоспособный объект, как на заня- тии 19, “Связанные списки”, а затем, обобщив его, получить шаблон для любых типов. Листинг 23.1. Файл parmlist. срр. Пример параметрического списка 0: 1 : 2 : 3 : 4 : 5 : 6 : 7 : // // // // // // // // Листинг 23.1. Пример параметрического списка
Час 23. Шаблоны 385 8: // 9: // Демонстрация объектно-ориентированного подхода применения 10: // связанных списков. Список лишь предоставляет блоки. 11: // Блоки имеют абстрактный тип данных. Используются три типа 12: // блоков: головные, хвостовые и внутренние. 13: // Данные содержат только внутренние блоки. 14: // 15: // Класс Object создан специально для примера, его объекты 16: // будут содержаться в связанном списке. 17: // 18: // *********************************************** 19: #include <iostream> 20: 21: enum { klsSmaller, klsLarger, klsSame }; 22 : 23: // Класс Object создан для размещения в связанном списке. 24: // Любой класс в связанном списке должен обладать двумя 25: // методами: Show() (отображает значение) и 26: // Compare() (возвращает относительную позицию) 27: class Data 28: { 29: public: 30: Data(int val):myValue(val) {} 31: ~Data() 32: { 33: std::cout << "Deleting Data object with value: ”; 34: std::cout << myValue << "\n"; 35: } 36: int Compare (const Data &) ,- 37: void Show() { std::cout << myValue << std::endl; } 38: private: 39: int myValue; 40: }; 41: 42: // Метод Compare() используется для определения 43: // относительного положения объекта в списке. 44: int Data::Compare(const Data & theOtherObject) 45: { 46: if (myValue < theOtherObject.myValue) 47: return klsSmaller; 48: if (myValue > theOtherObject.myValue) 49: return klsLarger; 50: else 51: return klsSame; 52: } 53 : 54: // Еще один класс для размещения в связанном списке. 55: // Напомним, что любой класс в связанном списке должен 56: // обладать двумя методами: 57: // Show() (отображает значение) и 58: // Compare() (возвращает относительную позицию) 59: class Cat 60: { 61: public: 62: Cat(int age): myAge(age) {} 63: ~Cat() 64: { 65: std::cout << "Deleting ";
386 Часть VI. Специальные темы 66: std::cout « myAge << " years old Cat.\n”; 67 : 68 : 69 : 70: 71 : 72 : 73 : 74: 75: 76 : 77: 78: 79 : 80: 81: 82 : 83 : 84: 85: 86: 87 : 88: 89 : 90: 91: 92 : 93 : 94: 95: 96: 97 : 98: 99 : 100: 101: 102 : 103 : 104: 105: 106 : 107 : 108: 109: 110: 111: 112 : 113 : 114: 115: 116 : 117 : 118: 119 : 120: 121: 122 : 123 : } int Compare(const Cat &) ; void Show() { std::cout << "This cat is std::cout << myAge « " years old\n"; ) private: int myAge; } ; // Метод Compare() используется для определения // относительного положения объекта в списке. int Cat::Compare(const Cat & theOtherCat) { if (myAge < theOtherCat.myAge) return klsSmaller; if (myAge > theOtherCat.myAge) return klsLarger; else return klsSame; ) // ADT - представление блока в списке. Каждый производный // класс должен переопределить методы Insert() и Show() template <class Т> class Node { public: Node() {} virtual -Node() {} virtual Node * Insert (T * theObject) = 0; virtual void Show() = 0; private: } ; template cclass T> class InternalNode: public Node<T> { public: InternalNode(T * theObject, Node<T> * next); -InternalNode() { delete myNext; delete myObject; } virtual Node<T> * Insert(T * theObject); virtual void Show() { myObject->Show(); myNext->Show(); ) // передача! private: T * myObject; // сам объект Node<T> * myNext; // указатель на следующий блок списка }; // Конструктор лишь инициализирует значения template <class Т>
Час 23. Шаблоны 387 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 InternalNode<T>::InternalNode(Т * theObject, Node<T> * next): myObject(theObject), myNext(next) { } // Суть списка // При поступлении в список нового объекта // он передается в блок, который // и будет добавлен в список template <class Т> Node<T> * InternalNode<T>::Insert(Т * theObject) { // новичок больше текущего или меньше? int result = myObject->Compare(*theObject); switch(result) { // по соглашению, если он такой же, то идет первым case klsSame: // придется пропустить case klsLarger: // новый объект впереди себя { InternalNode<T> * ObjectNode = new InternalNode<T>(theObject, this); return ObjectNode; } // Он больше текущего, поэтому пошел // к следующему блоку, и пусть ОН его обработает. case klsSmaller: myNext = myNext->Insert(theObject); return this; } return this; // Успокоить MSC } // Хвостовой блок списка - не более чем сторож template <class Т> class TaiiNode : public Node<T> { public: TailNode() {} virtual ~TailNode() {} virtual Node<T> * Insert(T * theObject); virtual void Show() {} private: }; // Если данные дошли сюда, их следует вставить впереди, // ведь здесь хвост, и НИЧЕГО не может быть вставлено после template cclass Т> Node<T> * TailNode<T>::Insert(Т * theObject) { InternalNode<T> * ObjectNode = new InternalNode<T>(theObject, this); return ObjectNode; } // Головной блок не содержит никакого объекта, // он только указывает на начало списка
388 Часть VI. Специальные темы 182: template cclass Т> 183: class HeadNode : public Node<T> 184: { 185: public: 186: HeadNode(); 187: virtual -HeadNode() { delete myNext; } 188: virtual Node<T> * Insert(T * theObject); 189: virtual void Show() { myNext->Show(); ) 190: private: 191: Node<T> * myNext; 192: }; 193: 194: // Хвост списка создается точно так же, 195: // как и голова 196: template <class Т> 197: HeadNode<T>::HeadNode() 198: { 199: myNext = new TailNodecT>; 200: } 201: 202: // Ничто не может находиться перед головным блоком, 203: // поэтому объект передается следующему блоку 204: template <class Т> 205: Node<T> * HeadNode<T>::Insert(T * theObject) 206: ( 207: myNext = myNext->Insert(theObject); 208: return this; 209 : } 210 : 211: // Только раздать данные, но самостоятельно не делать ничего 212: template cclass Т> 213: class LinkedList 214: { 215: public: 216: LinkedList(); 217: -LinkedList() { delete myHead; } 218: void Insert(T * theObject); 219: void ShowAll() { myHead->Show(); } 220: private: 221: HeadNode<T> * myHead; 222: }; 223 : 224: // При рождении создается блок заголовка 225: // Он создает хвост списка, 226: // так что пустой список указывает на голову, которая 227: // указывает на хвост, и между ними ничего нет 228: template cclass Т> 229: LinkedList<T>::LinkedList() 230: { 231: myHead = new HeadNode<T>; 232: } 233 : 234: // Передать, передать, передать 235: template cclass T> 236: void LinkedListcT>::Insert(T * pObject) 237: { 238: myHead->Insert(pObject); 239: )
Час 23. Шаблоны 389 240: 241: // Основная программа 242: int main() 243 : { 244: Cat * pCat; 245: Data * pData; 246: int val; 247: LinkedList<Cat> ListOfCats; 248: LinkedList<Data> ListOfData; 249 : 250: // Просит пользователя ввести значения 251: // и помещает их в список 252: for (;;) 253: ( 254: std::cout << "What value? (0 to stop): ”; 255: std::cin >> val; 256: if (!val) 257: break; 258: pCat = new Cat(val); 259: pData= new Data(val); 260: ListOfCats.Insert(pCat); 261: ListOfData.Insert(pData); 262: } 263 : 264: // Теперь пройтись по списку и показать объекты 265: std::cout << "\п"; 266: ListOfCats.ShowAll(); 267: std::cout « "\n"; 268: ListOfData.ShowAll(); 269: std::cout << "\n ************ \n\n”; 270: return 0; 271: } // Список выхолит из области видимости и удаляется! Результат What value? What value? What value? What value? What value? What value? (0 (0 (0 (0 (0 (0 to stop): 5 to stop): 13 to stop): 2 to stop): 9 to stop): 7 to stop): 0 This cat is This cat is This cat is This cat is This cat is 2 years old 5 years old 7 years old 9 years old 13 years old 2 5 7 9 13 Deleting Data object with value: 13
390 Часть VI. Специальные темы Deleting Data object with value: 9 Deleting Data object with value: 7 Deleting Data object with value: 5 Deleting Data object with value: 2 Deleting 13 years old Cat. Deleting 9 years old Cat. Deleting 7 years old Cat. Deleting 5 years old Cat. Deleting 2 years old Cat. Анализ Обратите внимание на сходство этого листинга с листингом занятия 19, “Связанные списки”. Однако теперь объявлениям классов и методов предшествуют следующая строка: template class <Т> Она уведомляет компилятор о том, что настоящий тип будет задан (передан в виде параметра) позже, при создании экземпляра списка. Например, объявление класса Node выглядит теперь так. template <class Т> class Node Это означает, что Node не будет существовать как класс сам по себе, вместо него будут созданы экземпляры классов Cat или Data. Фактический тип будет передан в виде параметра т. Таким образом, класс блока InternalNode теперь задан как lnternalNode<T> и указывает не на объект класса Data и следующий блок, а на объект класса, задан- ного параметром т (т.е. объект любого типа), и блок Node<T>. Это можно увидеть в строках 118—119. Внимательно рассмотрите метод insert!), определенный в строках 133—156. Ло- гика осталась та же, которая использовалась при определении типа, т.е. параметр (т) вместо класса (Data). Таким образом, в строке 134 объявлен указатель на параметри- ческий класс т. Впоследствии, когда будут созданы экземпляры реальных списков, компилятор заменит параметр т соответствующим типом (Data или Cat). Важнее всего то, что теперь блок InternalNode получился работоспособным неза- висимо от фактического типа хранимых в нем данных. Это значит, что объекты долж- ны быть способны самостоятельно сравнивать себя друг с другом. Т.е. список не дол- жен заботиться о том, сравниваются ли объекты класса cat или Data; механизм сравнения должен быть одинаков. Фактически класс Cat можно переделать, чтобы он хранил не возраст котов, но дату рождения, а их относительный возраст вычислял по запросу, т.е. блок InternalNode не должен заниматься такими вопросами. Использование экземпляров шаблона С экземплярами шаблона можно обращаться так же, как с любыми другими типами данных. Их можно передавать в функции по ссылке или значению и возвращать как ре- зультат выполнения функции (по ссылке или значению). Способы передачи экземпля- ров шаблона продемонстрированы в листинге 23.2. Сравните его с кодом листинга 23.1. Листинг 23.2. Файл passparmlist. срр. Пример параметрического списка (передача по ссылке) 0: // 1: // Листинг 23.2.
Час 23. Шаблоны 391 2 : // 3 : // Пример параметрического списка 4: // 5: // 6: // 7 : // 8: // 9: // Демонстрация объектно-ориентированного подхода применения 10: // связанных списков. Список лишь предоставляет блоки. 11: // Блоки имеют абстрактный тип данных. Используются три типа 12 : // блоков: головные, хвостовые и внутренние. 13: // Данные содержат только внутренние блоки. 14: // 15: // Класс Object создан специально для примера, его объекты 16: // будут содержаться в связанном списке. 17: // 18: 19: 11 *********************************************** 20: 21 : #include <iostream> 22 : 23 : enum { klsSmaller, klsLarger, klsSame }; 23 : // Класс Object создан для размещения в связанном списке. 24: // Любой класс в связанном списке должен обладать двумя 25 : // методами: Show() (отображает значение) и 26: // Compare() (возвращает относительную позицию) 28: class Data 29: { 30: publi с: 31: Data(int val):myValue(val) {} 32 : -Data() 33 : { 34: std::cout << "Deleting Data object with value: 35: std::cout << myValue << "\n"; 36: } 37 : int Compare (const Data &),- 38: void Show() { std::cout « myValue << std::endl; } 39 : private: 40: int myValue; 41 : 42 : J ; 43 : // Метод Compare() используется для определения 44: // относительного положения объекта в списке. 45: int Data::Compare(const Data & theOtherObject) 46: { 47 : if (myValue < theOtherObject.myValue) 48: return klsSmaller; 49: if (myValue > theOtherObject.myValue) 50: return klsLarger; 51: else 52 : return klsSame; 53 : 54: ) 55 : // Еще один класс для размещения в связанном списке. 56: // Напомним, что любой класс в связанном списке должен 57: // обладать двумя методами: 58: // Show() (отображает значение) и 59: // Compare() (возвращает относительную позицию)
392 Часть VI. Специальные темы 60: class Cat 61: { 62 : public: 63 : Cat(int age): myAge(age) {} 64 : -Cat() 65 : { 66 : std::cout << "Deleting " << myAge 67 : << " years old Cat.Xn"; 68: ) 69: int Compare (const Cat St.- 70: void Show() 71 : { 72 : std::cout << "This cat is “ << myAge 73 : << " years oldXn"; 74 : } 75: private: 76: int myAge; 77 : }; 78: 79 : 80: // Метод Compare() используется для определения 81 : // относительного положения объекта в списке. 82 : int Cat::Compare(const Cat & theOtherCat) 83 : { 84 : if (myAge < theOtherCat.myAge) 85: return klsSmaller; 86: if (myAge > theOtherCat.myAge) 87: return klsLarger; 88: else 89: return klsSame; 90: 1 91: 92 : 93 : // ADT - представление блока в списке. Каждый производный 94: // класс должен переопределить методы Insert() и Show() 95: template cclass Т> 96: class Node 97 : ( 98: public: 99 : Node() {} 100: virtual -Node() {} 101: virtual Node * Insert(T * theObject) = 0; 102: virtual void Show() = 0; 103: private: 104 : }; 105 : 106: template cclass T> 107 : class InternalNode: public Node<T> 108: { 109: public: 110: InternalNode(T * theObject, Node«T> * next); 111: virtual -InternalNode() {delete myNext; delete myObject 112: virtual NodecT> * Insert(T * theObject); 113 : virtual void Show() // передача! 114: { 115 : myObject->Show(); myNext->Show(); 116: } 117 : private:
Час 23. Шаблоны 393 118 : Т * myObject; // сам объект 119 : 120: 121 : 122 : 123 : 124: 125 : 126: 127 : 128: 129 : 130: 131 : 132 : 133 : 134: 135 : 136: 137 : 138: 139: 140: 141 : 142: 143 : 144: 145 : 146: 147 : 148 : 149: 150: 151: 152 : 153 : 154 : 155: 156: 157: 158: Node<T> * myNext; // указатель на следующий блок списка }; // Конструктор лишь инициализирует значения template <class Т> InternalNode<T>::InternalNode(Т * theObject, Node<T> * next): myObject(theObject).myNext(next) { } // Суть списка // При поступлении в список нового объекта // он передается в блок, который // и будет добавлен в список template <class Т> Node<T> * InternalNode<T>::Insert(Т * theObject) { // новичок больше текущего или меньше? int result = myObject->Compare(*theObject); switch(result) { // по соглашению, если он такой же, то идет первым case klsSame: // придется пропустить case klsLarger: // новый объект впереди себя { InternalNode<T> * ObjectNode = new InternalNode<T>(theObject, this); return ObjectNode; } // Он больше текущего, поэтому пошел // к следующему блоку, и пусть ОН его обработает, case klsSmaller: myNext = myNext->Insert(theObject); return this; } return this; // Успокоить MSC } 159: 160: 161 : 162 : 163 : 164: 165 : 166: 167 : 168: 169 : // Хвостовой блок списка - не более чем сторож template <class Т> class TaiiNode : public Node<T> { public: TaiiNode() {} virtual -TaiiNode() {} virtual Node<T> * Insert(T * theObject); virtual void Show() {} private: }; 170: 171 : 172 : 173 : 174 : 175: // Если данные дошли сюда, их следует вставить впереди, // ведь здесь хвост, и НИЧЕГО не может быть вставлено после template <class Т> Node<T> * TailNode<T>::Insert(Т * theObject) {
394 Часть VI. Специальные темы 176: lnternalNode«T> * ObjectNode = 177: new InternalNode«T>(theObject, this); 178: return ObjectNode; 179 : } 180: 180: // Головной блок не содержит никакого объекта, 181: // он только указывает на начало списка 183: template «class Т> 184: class HeadNode : public Node<T> 185: { 186: public: 187: HeadNode(); 188: virtual -HeadNode() { delete myNext; } 189: virtual Node«T> * Insert(T * theObject); 190: virtual void Show() { myNext->Show(); } 191: private: 192: Node<T> * myNext; 193: }; 194 : 195: // Хвост списка создается точно так же, 196: // как и голова 197: template «class Т> 198: HeadNode«T>::HeadNode() 199: { 200: myNext = new TailNode«T>; 201: } 202 : 203: // Ничто не может находиться перед головным блоком, 204: // поэтому объект передается следующему блоку 205: template «class Т> 206: Node«T> * HeadNode«T>::Insert(Т * theObject) 207 : { 208: myNext = myNext->Insert(theObject); 209: return this; 210: } 211: 212: // Только раздать данные но самостоятельно не делать ничего 213: template «class Т> 214: class LinkedList 215: { 216: public: 217: LinkedList(); 218: -LinkedList() { delete myHead; } 219: void Insert(T * theObject); 220: void ShowAll() { myHead->Show(); } 221: private: 222: HeadNode«T> * myHead; 223: }; 224: 225: // При рождении создается блок заголовка 226: // Он создает хвост списка, 227: // так что пустой список указывает на голову, которая 228: // указывает на хвост, и между ними ничего нет 229: template «class Т> 230: LinkedList«T>::LinkedList() 231: { 232: myHead = new HeadNode«T>; 233 : }
Час 23. Шаблоны 395 234 : 235 : // Передать, передать, передать 236: template cclass Т> 237 : void LinkedList<T>::Insert(Т * pObject) 238 : ( 239 : myHead->Insert(pObject); 240 : 241 : } 242 : void myFunction(LinkedList<Cat>& ListOfCats); 243 : 244: void myOtherFunction(LinkedList<Data>& ListOfData); 245 : // Основная программа 246: int main() 247 : { 248: LinkedList<Cat> ListOfCats; 249: 250: LinkedList<Data> ListOfData; 251 : myFunction(ListOfCats); 252 : 253 : myOtherFunction(ListOfData); 254: // Теперь пройтись по списку и показать объекты 255: Std::COUt << "\П"; 256: ListOfCats.ShowAll(); 257 : std::cout << "\n"; 258: ListOfData.ShowAl1() ; 259: std::cout << "\n ************ \n\n“; 260: return 0; 261: 262 : } // Список выходит из области видимости и удаляется 263 : void myFunction(LinkedList<Cat>& ListOfCats) 264: { 265: Cat * pCat; 266: 267 : int val; 268: // Просит пользователя ввести значения 269 : // и помещает их в список 270: for (;;) 271: ( 272: std:: cout << '\nHow old is your cat? (0 to stop) 273 : std::cin >> val; 274 : if (!val) 275: break; 276: pCat = new Cat(val); 277 : ListOfCats.Insert(pCat); 278: 279: } 280: 281 : } 282 : void myOtherFunction(LinkedList<Data>& ListOfData) 283 : { 284: Data * pData; 285 : 286: int val; 287 : // Просит пользователя ввести значения 288: // и помешает их в список 289 : for (;;) 290: { 291 : std::cout << "\nWhat value? (0 to stop): ";
396 Часть VI. Специальные темы 292: std::cin >> val; 293: if (!val) 294 : break; 295: pData = new Data(val); 296: ListOfData.Insert(pData) ; 297: } 298: 299: } Результат How old is your cat? (0 to stop): 12 How old is your cat? (0 to stop): 2 How old is your cat? (0 to stop): 14 How old is your cat? (0 to stop): 6 How old is your cat? (0 to stop): 0 value? (0 value? (0 value? (0 value? (0 value? (0 to to to to to stop): 3 stop): 9 stop): 1 stop): 5 stop): 0 What What What What What This cat This cat This cat This cat is is is is 2 years old 6 years old 12 years old 14 years old 1 3 5 9 Deleting Data object with value: 9 Deleting Data object with value: 5 Deleting Data object with value: 3 Deleting Data object with value: 1 Deleting 14 years old Cat. Deleting 12 years old Cat. Deleting 6 years old Cat. Deleting 2 years old Cat. Анализ Этот код очень похож на предыдущий, но на сей раз объекты LinkedList переда- ются для обработки соответствующим функциям по ссылке. Это очень удобная воз- можность. После того как экземпляры списка будут созданы, с ними можно обра- щаться как с полностью определенными типами, т.е. их можно передавать в функции и получать как возвращаемые значения. Стандартная библиотека шаблонов Отличительной чертой языка C++ является наличие стандартной библиотеки шабло- нов (Standard Template Library — STL). Все основные разработчики компиляторов пред- лагают библиотеку STL как составную часть своих программных продуктов. STL — это
Час 23. Шаблоны 397 библиотека классов-контейнеров, базирующихся на шаблонах. Она содержит векторы, списки, очереди и стеки, а также ряд таких общих алгоритмов, как сортировка и поиск. Задача STL заключается в том, чтобы избавить программистов от очередного изо- бретения колеса и выполнить при разработке все рутинные общепринятые процессы. STL проверена и отлажена, она обладает высокой эффективностью и не требует до- полнительных затрат. Важнее всего то, что библиотеку STL можно использовать для разработки собственных приложений многократно. Необходимо только один раз разо- браться в принципах использования библиотеки STL и классов-контейнеров. Не забывайте, что залогом высокой эффективности разработки и безотказности работы приложений является многократное использование кода! Вопросы и ответы Зачем использовать шаблоны, если можно использовать макросы? Шаблоны безопаснее и встроены в язык. В чем разница между параметрическим типом функции шаблона и параметром для нормальной функции? Обычной функции (не шаблонной) передают параметры, с которыми она может выполнять определенные действия. Функция шаблона позволяет с помощью па- раметра шаблона устанавливать тип параметра, передаваемого функции. Когда следует использовать шаблоны, а когда — наследование? Используйте шаблоны, когда поведение класса неизменно, за исключением ти- па элемента, с которым работает класс. Если проблему можно решить, скопи- ровав класс и заменив только тип одного или нескольких его членов, можно применять шаблон. Коллоквиум Изучив возможности шаблонов, имеет смысл ответить на несколько вопросов и выполнить ряд упражнений, чтобы закрепить полученные знания. Контрольные вопросы 1. Как уведомить компилятор о том, что определяется шаблон, а не обычный класс? 2. Как шаблон связанного списка в листинге 22.1 определяет порядок расположе- ния объектов внутри созданного экземпляра связанного списка? 3. Как, работая с классом шаблона, объявить, объект какого именно класса он бу- дет содержать? 4. Как компилятор узнает, когда удалять объекты, содержащиеся внутри связан- ного списка? Упражнения 1. Сравните файлы parmlist.cpp (листинг 22.1) и linkedlist.cpp (листинг 19.1). Обратите внимание, насколько эти два файла похожи. 2. Выясните в документации, прилагаемой к используемому компилятору, какие из классов стандартной библиотеки шаблонов доступны? У компилятора Borland для этого можно воспользоваться пунктом Help Topics (Темы) меню Help (Помощь).
398 Часть VI. Специальные темы 3. Сравните шаблон связанного списка листинга 22.1 и класс списка, описанный в документации стандартной библиотеки шаблонов используемого компилято- ра. В чем они похожи и чем различаются? Ответы на контрольные вопросы 1. Для этого применяется префикс template <class т>. 2. Код строки 137 вызывает функцию Compare () объекта используемого типа. О реализации соответствующего поведения этой функции должен позаботиться разработчик класса (соответствующие функции начинаются в строках 44 и 81). Если бы в связанном списке необходимо было хранить объекты класса Dog, то его также следовало бы снабдить функцией Compare (). 3. Пример находится в строках 247—248 листинга 22.1. Формат очень прост: имя_шаблона <имя_содержащееся_класса> имя_объекта_шаблона. 4. При завершении функции main () связанный список выходит из области видимо- сти, а, следовательно, происходит вызов деструктора для его класса. Поскольку содержащиеся в нем объекты созданы в динамической памяти, деструктор свя- занного списка удалит и их. Это происходит в строке 110 листинга 23.1.
ЧАС 24 Исключения, обработка ошибок и другое На этом занятии вы узнаете: что такое исключение; как использовать исключения и какие проблемы они решают; как сделать код устойчивым к ошибкам; что дальше. Ошибки, недоработки и просчеты Весь код, представленный в этой книге, был разработан исключительно для де- монстрации приемов программирования. Возможные ошибки не рассматривались специально, чтобы не отвлекать внимание от изучаемых проблем. Но в реальных программах обязательно должны быть учтены условия, при которых возможно воз- никновение ошибок. Все программы содержат ошибки. Чем больше программа, тем больше в ней оши- бок, и как не грустно, но факт: большинство коммерческих программ, даже от самых крупных производителей, содержат ошибки, причем весьма серьезные. Это жестокая реальность и понять ее — значит, сделать первый шаг на пути создания надежных, безошибочных программ. Одной из наиболее острых проблем в индустрии программного обеспечения является нестабильный код, содержащий редко возникающие ошибки. В любом серьезном про- екте дороже всего обходится его проверка и исправление ошибок. Тот, кто решит про- блему создания добротных, надежных и безотказных программ за короткий срок и при низких затратах, произведет революцию во всей индустрии программных продуктов. Все ошибки в программах можно разделить на несколько групп. Один тип ошибок вызван недостаточно проработанной логикой алгоритма выполнения программы. Другой тип — синтаксические ошибки, т.е. использование неправильной идиомы, функции или структуры. Эти два типа ошибок наиболее распространены, поэтому именно на них сосредоточено внимание программистов. Теория и практика неопровержимо доказали, что чем позже в процессе разра- ботки обнаруживается проблема, тем дороже обходится ее устранение. Оказывается,
400 Часть VI. Специальные темы проблемы и ошибки в программах дешевле всего обойдутся компании в том случае, если своевременно принять меры по предупреждению их появления. Не слишком до- рого обойдутся и те ошибки, которые распознаются компилятором. Стандарт языка C++ рекомендует разработчикам привлечь компилятор для поиска как можно боль- шего количества ошибок на этапе компиляции программы. Ошибки, которые прошли этап компиляции, но были выявлены при первом же тести- ровании или проявляются регулярно, также легко устранимы, чего не скажешь о “минах замедленного действия”, проявляющих себя внезапно в самый неподходящий момент. Еще большей проблемой, чем логические и синтаксические ошибки, является не- устойчивость программ. Т.е программа сносно работает, если пользователь вводит данные, которые предусматривались, но дает сбой, если по ошибке, например, вместо числа введена буква. Другие программы внезапно зависают из-за переполнения памя- ти, при извлечении из дисковода гибкого диска или при потере связи с модемом. Чтобы повысить устойчивость программ, программисты стремятся предупредить непредвиденные ситуации и сделать программу “пуленепробиваемой”. Пуленепроби- ваемой (bulletproof) считают программу, которая способна справиться со всеми не- штатными ситуациями, возникающими во время работы: от ввода пользователем не- верных данных до исчерпания памяти компьютера. Необходимо различать ошибки, возникшие в результате неверного синтаксиса программного кода, логические ошибки, возникающие из-за неверного понимания или решения проблемы, и исключения, которые возникают из-за необычных, но предсказуемых проблем, связанных, например, с конфликтами ресурсов (нехватка па- мяти или дискового пространства). Реакция на непредвиденное Программисты, используя мощные компиляторы и средства отладки кода, безус- ловно, пытаются избежать ошибок Они применяют различные способы проверки проектов и поиска вероятных логических ошибок, однако гарантировать отсутствие проблем не удается. Избежать непредвиденных обстоятельств нельзя, но можно быть готовым к ним. Не исключена ситуация, когда пользователь исчерпает память компьютера, и единст- венным вопросом станет то, что разработанная программа предпримет в ответ. Воз- можны варианты: программа зависнет; выдаст сообщение и элегантно завершит работу; выдаст сообщение и позволит попытаться восстановить работоспособность; исправит ситуацию и продолжит работу, не беспокоя пользователя. Хотя тихо и автоматически выходить из непредвиденной ситуации нужно не все- гда, поверьте, это лучше, чем зависание. Обработка исключений в языке C++ включает проверку типа, а также интегриро- ванные методы для разрешения предсказуемых, но необычных условий, которые воз- никают при выполнении программы. Исключения В C++ исключение (exception) — это объект, передаваемый из той области кода, где возникла проблема, в ту область, которая пытается с ней справиться. Процесс возник- новения объекта исключения называют передачей (raise, или thrown). Обработку (handle) исключения называют также перехватом (caught).
Час 24. Исключения, обработка ошибок и другое 401 Тип исключения определяет, какая область кода будет обрабатывать ошибку, а со- держимое переданного объекта, если оно есть, может использоваться для обратной связи с пользователем. Основная идея использования исключений довольно проста: компьютер пытается выполнить фрагмент кода. Этот код может, например, по- пытаться распределить память, блокировать файл или любую другую операцию; логика действий, предпринимаемых в случае сбоя (например, память не может быть распределена или файл не может быть найден), обычно требует взаимо- действия с пользователем (т.е. спрашивает его, что делать); исключения обеспечивают срочный переход от участка кода, где произошла ошибка, к участку, способному справиться с ней. Если для такой ситуации преду- смотрены специальные функции, то они должны не только сообщить о возникшей проблеме, но и по возможности принять адекватные меры по ее устранению, на- пример, очистить выделенную память при неудачной попытке ее распределения. Применение исключений Блок try (попытка) предназначен для размещения участка кода, в котором потен- циально возможно возникновение проблем. Потенциально опасный код располагается в фигурных скобках блока try, например: » try { SomeDangerousFunction!) ) Блок catch (обработка), располагающийся непосредственно после блока try, со- держит код, собственно осуществляющий обработку исключения; например: try { SomeDangerousFunction(); ) catch(OutOfMemory) { // отреагировать на нехватку памяти ) catch(FileNotFound) { // отреагировать на невозможность найти файл ) Основные этапы применения исключений таковы: 1. выявить те области кода программы, где осуществляются операции, способные привести к передаче исключений, и поместить их в блоки try; 2. создать блоки catch, способные обработать переданные исключения. Пример применения блоков try и catch приведен в листинге 24.1. прочим Передача и обработка При передаче исключения управление переходит к соответствующе- му блоку catch, расположенному непосредственно после текущего блока try.
402 Часть VI. Специальные темы Листинг 24.1. Файл exceptions. срр. Передача исключения 0: 1: 2: 3 : 4 : 5 : 6: 7 : 8 : 9: 10: 11: 12: 13 : 14: 15 : 16: 17 : 18: 19: 20: 21: 22: 23: 24: 25: 26: 27 : 28: 29: // Листинг 24.1. Передача и обработка исключения #include <iostream> const int Defaultsize = 10; // Определение класса исключения class xBoundary { public: xBoundary() {} -xBoundary() {} private: }; class Array { public: // Конструкторы Arrayfint itsSize = Defaultsize); Array(const Array &rhs); -Array() { delete [] pType;) // Операторы Arrays operator=(const Arrays); int& operator[](int offset); const int& operator[](int offset) const; // Методы доступа int GetitsSize() const { return itsSize; } 30: 31: // Дружественная функция friend std::©streams operator« (std::ostreamS, const Arrays); 32: 33 : 34 : 35: 36: 37 : 38: 39: 40: 41: 42 : 43 : 44: 45: 46: 47 : 48: 49: 50: 51 : 52 : 53 : 54 : 55: 56 : private: int *pType; int itsSize; }; Array::Array(int size): itsSize(size) { pType = new int[size]; for (int i=0; i<size; i++) pType[i] = 0 ; } Arrays Array::operator=(const Array Srhs) { if (this == Srhs) return *this; delete [] pType; itsSize = rhs.GetitsSize (); pType = new int[itsSize]; for (int i=0; i<itsSize; i++) pType[i] = rhs[i];
Час 24. Исключения, обработка ошибок и другое 403 57: return *this; 58: } 59 : 60: Array::Array(const Array &rhs) 61: { 62: itsSize = rhs.GetitsSize(); 63: pType = new int[itsSize]; 64: for (int i=0; i<itsSize; i++) 65: pType[i] = rhs[i]; 66 : } 67 : 68: 69: int& Array::operator[](int offset) 70: { 71: int size = GetitsSize(); 72: if (offset >= 0 && offset < size) 73: return pType[offSet]; 7 4: throw xBoundary() ; 75: return pType[offSet]; // Успокоить MSC! Компилятор Borland // выдаст предупреждение! 76: } 77: 78: 79: const int& Array::operator[](int offset) const 80: { 81: int mysize = GetitsSize(); 82: if (offset >= 0 && offset < mysize) 83: return pType[offSet]; 84: throw xBoundaryl); 85: return pType[offSet]; // Успокоить MSC! Компилятор Borland // выдаст предупреждение! 86: } 87 : 88: std::ostreamb operator« (std::ostreami output, 89: const Arrays theArray) 90: { 91: for (int i=0; i<theArray.GetitsSize(); i++) 92: output << "[“ << i << ”] " « theArray[i] << std::endl; 93 : return output; 94 : } 95: 96: int main!) 97 : { 98 : Array intArray(20); 99: try 100: { 101: for (int j=0; j< 100; j++) 102 : ( 103: intArray[j] = j; 104 : std::cout « "intArrayP « j 105: << "] okay..." << std::endl; 106: } 107: } 108: catch (xBoundary) 109 : { 110: std::cout << "Unable to process your input!\n"; 111: ) 112 : std::cout << "Done.Xn"; 113 : return 0; 114 : }
404 Часть VI. Специальные темы Результат intArray[0] intArray[1] intArray[2] intArray[3] intArray[4] intArray[5] intArray[6] intArray[7] intArray[8] intArray[9] intArray[10] intArray[11] intArray[12] intArray[13] intArray[14] intArray[15] intArray[16] intArray[17] intArray[18] intArray[19] Unable to process your input! Done. okay.. okay.. okay.. okay.. okay.. okay.. okay.. okay.. okay.. okay.. okay. okay, okay, okay. okay, okay, okay. okay. okay, okay. Анализ Листинг 24.1 содержит упрощенный класс Array, который предназначен лишь для демонстрации применения исключений. В строках 6—12 объявлен очень простой класс исключения xBoundary (граница). В первую очередь обратите внимание на то, что он абсолютно ничего не делает, т.е. не содержит никаких данных и методов, что, тем не менее, не мешает ему быть классом исключения. Фактически любой класс с любым именем, любым количеством методов и переменных прекрасно подойдет для исключения. Исключение может быть только передано, как показано в строке 74, и перехвачено (строка 108)! Когда клиент класса делает попытку получить доступ к данным за пределами мас- сива (строки 74 и 84), оператор индекса ([ ]) передает исключение xBoundary. Эта реакция на попытку чтения данных за пределами массива значительно лучше стан- дартной, при которой возвращены будут случайные данные, расположенные в памяти по указанному адресу. Такой подход позволяет избежать серьезной ошибки, которая может привести к непредвиденным результатам. В строке 99 ключевое слово try открывает одноименный блок, который заверша- ется в строке 107. Внутри этого блока в массив, который был объявлен в строке 98, добавляются сто целочисленных значений. В строке 108 блок catch перехватывает и обрабатывает исключение xBoundary. Блок try Блок try— это набор операторов, начинающийся ключевым словом try, за кото- рым в фигурных скобках следует участок кода, например: try { Function(); )
Час 24. Исключения, обработка ошибок и другое 405 Блок catch Блок catch — это набор операторов, каждый из которых начинается ключевым сло- вом catch, сопровождаемым типом исключения в круглых скобках, например: try { Function(); } catch (OutOfMemory) { // принять меры ) Применение блоков try и catch Выяснить, где именно нужно размещать блоки try, не так-то просто, ведь не все- гда очевидно, какие именно действия способны привести к ошибке. Вопрос заключа- ется в том, чтобы выяснить, где именно обработать эту ошибку. Возможно, понадо- бится отслеживать исключения, контролирующие память, ее распределение, а, возможно, ранее в программе придется передавать исключения, относящиеся к поль- зовательскому интерфейсу. Определяя расположение блоков try, обратите внимание на те участки программы, где происходит выделение памяти или использование ресурсов. Другие элементы можно проверять на ошибки переполнения, выхода за пределы, некорректного ввода и т.д. Обработка исключений Вот как все это происходит. Переданное исключение попадает в стек вызовов. Стек вызовов (call stack) — это список вызванных функций, где регистрируются обра- щения участков программы к каким-либо функциям. Стек вызовов содержит всю “историю” выполнения программы. Если функция main() вызывает функцию Animal: :GetFavoriteFood(), а функция GetFavorite Food () Animal:: Lookuppreferences () в свою очередь вызывает оператор fstream: :operator» (), то именно в этом порядке они будут находиться в стеке вызовов. Рекурсивная функция может повторяться в стеке вызовов несколько раз. Переданное в стек исключение проходит все его блоки, пытаясь найти свой обра- ботчик Это называется прокруткой стека (unwinding the stack). По мере прокрутки стека необходимость в отвергнутых локальных объектах отпадает, к ним применяются соответствующие деструкторы, и объекты удаляются. За каждым блоком try следует один или несколько блоков catch. Если передан- ное исключение соответствует одному из блоков catch, то оно обрабатывается опера- торами этого блока. Если же соответствующий обработчик (блок catch) найден не будет, прокрутка стека продолжится. Если исключение, пройдя весь путь вплоть до начала программы (функции main О), не окажется обработано (перехвачено), то будет вызван встроенный обработчик (функция terminate ()), который и завершит программу, вызвав функцию abort (). Следует заметить, что прокрутка стека исключением — процесс необратимый. По мере прокрутки стека объекты разрушаются. Вернуться невозможно; даже если ис- ключение обработано, программа возобновит работу лишь после того оператора catch, который обработал исключение, переданное блоком try. Таким образом, в листинге 24.1 выполнение программы продолжится со строки 111 — первой строки после оператора catch, обработавшего исключение xBoundary,
406 Часть VI. Специальные темы переданное блоком try. Помните, что при передаче исключения выполнение про- граммы продолжится после того блока catch, который исключение обработал, а не там, где оно возникло. Применение нескольких обработчиков catch Вполне возможна ситуация, когда причин возникновения исключения будет не- сколько. В этом случае операторы catch можно расположить один за другим, как в операторе switch. Эквивалентом оператора default будет оператор catch (...), означающий “обработать все”. вше-------. осторожны! Будьте внимательны Используя два блока catch, где первый предназначен для исключения базового класса (например, MyException), а второй — для исключения более специфического класса, производного от него (например, MySpecif icException), следует быть внимательным, поскольку факти- чески сработает код обоих блоков catch. Иногда такое поведение необ- ходимо, иногда — нет. Учитывайте при работе это обстоятельство. Обработка по ссылке и полиморфизм Тем фактом, что исключения являются не более чем объектами класса, вполне можно воспользоваться для их полиморфного применения. Передавая исключение по ссылке, можно использовать иерархию наследования и предпринимать соответствую- щие действия на основании типа переданного исключения. Пример полиморфного применения исключений демонстрирует листинг 24.2. Результат демонстрирует три попытки запуска программы на выполнение, причем в первом случае размер массива был задан равным 5, во втором — 50 000, а в третьем — 12. Листинг 24.2. Файл polyexceptions. срр. Полиморфное применений исключений 0: II Листинг 24.2. Полиморфное применений исключений 1: #include <iostream> 2 : 3: const int Defaultsize = 10; 4 : 5: // Определение класса исключения 6: class xBoundary {}; 7 : 8: class xSize 9: { 10: public: 11: xSize(int size):itsSize(size) {} 12: -xSize() {) 13: virtual int GetSizeO { return itsSize; } 14: virtual void PrintError() 15: { std::cout << "Size error. Received: " 16: << itsSize << std::endl; } 17: protected: 18: int itsSize; 19: }; 20: 21: class xTooBig : public xSize 22: {
Час 24. Исключения, обработка ошибок и другое 407 23: public: 24: xTooBig(int size):xSize(size) {} 25: virtual void PrintErrorO 26: { 27: std::cout << "Too big! Received: 28: std::cout << xSize::itsSize << std::endl; 29 : } 30: }; 31: 32: class xTooSmall : public xSize 33: { 34: public: 35: xTooSmall(int size):xSize(size) {) 36: virtual void PrintErrorO 37: { 38: std::cout << "Too small! Received: 39: std::cout « xSize::itsSize << std::endl; 40: } 41: }; 42 : 43: class xZero : public xTooSmall 44: { 45: public: 46: xZerofint size):xTooSmall(size) {} 47: virtual void PrintErrorO 48: { 49: std::cout << "Zero!!. Received: 50: std::cout << xSize:: itsSize << std::endl; 51: } 52: }; 53 : 54: class xNegative : public xSize 55: { 56: public: 57: xNegative(int size):xSize(size) {} 58: virtual void PrintError() 59: { 60: std::cout « "Negative! Received: 61: std:-.cout « xSize:: itsSize << std::endl; 62: } 63 : } ; 64 : 65: class Array 66: { 67: public: 68: // Конструкторы 69: Arraylint itsSize = Defaultsize); 70: Arraylconst Array irhs); 71: -Array() { delete [] pType;} 72 : 73: // Операторы 74: Arrays operator=(const Arrays); 75: ints operator[](int offset); 76: const ints operator[](int offset) const; 77 : 78: // Методы доступа 79: int GetitsSize() const { return itsSize; } 80:
408 Часть VI. Специальные темы 81: // Дружественная функция 82: friend std::ostreami operator<< (std::ostreamk, const Arrayb); 83 : 84 : 85: private: 86: int *pType; 87: int itsSize; 88: }; 89 : 90: Array::Array(int size): 91: itsSize(size) 92: { 93: xf (size == 0) 94: throw xZero(size); 95 : 96: if (size < 0) 97: throw xNegative(size); 98: 99: if (size < 10) 100: throw xTooSmall(size); 101: 102: if (size > 30000) 103: throw xTooBig(size) ; 104 : 105: pType = new int[size); 106: for (int i=0; i<size; i++) 107: pType[i] = 0; 108: } 109 : 110: intb Array::operator[] (int offset) 111: { 112: int size = GetitsSize(); 113: if (offset >= 0 && offset < size) 114: return pType[offset]; 115: throw xBoundary)); 116: return pType[offset]; // Успокоить MSC! Компилятор Borland // выдаст предупреждение I 117: } 118: 119: const int& Array::operator[] (int offset) const 120: { 121: int size = GetitsSize(); 122: if (offset >= 0 && offset < size) 123: return pType[offset]; 124: throw xBoundaryO; 125: return pType[offset]; // Успокоить MSC! Компилятор Borland // выдаст предупреждение! 126: ) 127 : 128: int main() 129: { 130: try 131: { 132: int choice; 133: std::cout << "Enter the array size: 134: std::cin >> choice; 135: Array intArray(choice);
Час 24. Исключения, обработка ошибок и другое 409 136 : for (int j=0; j< 100; j++) 137 : { 138: intArray[j] = j; 139 : std::cout << "intArray[" << j << "] okay..." 140: << Std::endl; 141 : } 142 : } 143 : catch (xBoundary) 144 : { 145 : std::cout << "Unable to process your input!\n"; 146 : } 147 : catch (xSize& theException) 148: { 149 : theException.PrintError(); 150: } 151: catch (...) 152 : { 153 : std::cout << “Something went wrong," 154: « "but I've no idea what!" « std::endl; 155 : } 156 : std::cout << "Done.\n"; 157 : return 0; 158 : } Результат Enter the array size: 5 Too small! Received: 5 Done. Enter the array size: 50000 Too big! Received: 50000 Done. Enter the array size: 12 intArray[0] intArray[1] intArray[2] intArray[3] intArray[4] intArray[5] intArray[6] intArray[7] intArray[8] intArray[9] intArray [10] okay... intArray[11] okay... Unable to process your input! Done. okay.. okay.. okay. . okay.. okay.. okay. . okay.. okay. . okay. . okay.. Анализ Листинг 24.2 демонстрирует объявление в классе xSize виртуального метода PrintError (), который выводит на экран сообщение об ошибке и истинный размер класса. Этот метод переопределен во всех производных классах исключений. В строке 147 объект исключения объявлен как ссылка. Благодаря полиморфизму при обращении к функции PrintError () со ссылкой на объект в качестве параметра
410 Часть VI. Специальные темы происходит вызов нужной версии функции PrintErrorO. В первый раз размер мас- сива был задан равным 5. В результате было передано исключение xTooSmall, пере- хваченное обработчиком базового класса xSize в строке 147. Во второй раз размер массива был задан равным 50 000, что привело к передаче исключения xTooBig. Оно также было перехвачено обработчиком базового класса в строке 147, но благодаря по- лиморфизму на экране отобразилось правильное сообщение об ошибке. Когда, нако- нец, задается допустимый размер (12), массив заполняется, пока не передается и не обрабатывается в строке 143 исключение xBoundary. Стиль программирования Овладение шаблонами и исключениями обеспечивает хороший уровень подготовки для перехода к более сложным аспектам программирования на языке C++. Однако прежде чем отложить эту книгу, обсудим несколько вопросов создания кода на профес- сиональном уровне. Профессионал, в отличие от любителя, работает, как правило, в со- ставе группы разработчиков, а, следовательно, должен создавать код, который будет не только работать, но и окажется понятен другим разработчикам. Грамотно написанный код должен обеспечивать возможность последующей модификации не только автором, но и другими программистами, ведь требования клиентов со временем изменятся. Фактически не имеет значения, какой именно стиль программирования принят в коллективе. Главное, чтобы он был и неукоснительно соблюдался. Единообразный и однозначный стиль именования существенно упрощает понимание отдельных фрагментов кода и позволяет избежать необходимости поиска имен переменных и функций в предыдущем коде, чтобы написать их правильно. Приведенные ниже рекомендации никого ни к чему не обязывают. Они основаны на принципах, которых автор придерживается при работе над проектами. Читатель может выработать собственные правила, но приведенные ниже помогут выявить ос- новные моменты, на которые следует обратить внимание. Как сказал Эмерсон (Emerson), “идиотская пунктуальность гоблинов — это свиде- тельство ограниченности их ума”, но в программном коде жесткий порядок — вещь полезная. Безусловно, общий стиль программирования упростит жизнь всем сотруд- никам группы, но это вовсе не означает, что он должен оставаться неизменным все- гда, ведь новые идеи о его улучшении появляются регулярно. Напомним, что приведенные ниже рекомендации не являются догмой и выпол- няться могут не всегда. Фигурные скобки Способ выравнивания фигурных скобок вызывает, возможно, самые бурные споры между программистами на языках C++ и С. Автор лично придерживается следующих правил: соответствующие пары фигурных скобок должны быть выровнены по вертикали; фигурные скобки первого уровня в определении или объявлении должны быть выровнены по левому полю. Все строки блока объявления или определения запи- сываются с отступом. Все вложенные пары фигурных скобок должны быть выров- нены по одной линии со строкой программы, за которой начинается этот блок; в больших блоках кода имеет смысл располагать комментарий после закры- вающей фигурной скобки, чтобы напомнить задачу блока. Если, видя закры- вающую фигурную скобку, не знаешь, где именно располагается открывающая, это означает, что блок действительно является “большим”, например: if (condition==true) {
Час 24. Исключения, обработка ошибок и другое 411 // много строк кода, включая другие блоки // много строк кода, включая другие блоки // много строк кода, включая другие блоки } // if (condition== true) в строке, содержащей фигурную скобку, не должно быть другого кода, например: if (condition==true) { j = k; SomeFunction() ; } m++ ; Длинные строки Удерживайте ширину строк в таких пределах, чтобы они помещались на экране. Код, который “убегает” вправо, можно легко пропустить, а горизонтальная прокрутка всегда раздражает. Старайтесь разбивать строку, следуя логике и здравому смыслу. Ос- тавляйте оператор в конце предыдущей строки (а не в начале следующей), чтобы было понятно, что данная строка является продолжением предыдущей. Функции в языке C++ лаконичнее, чем в С, но по-прежнему остается в силе ста- рый добрый совет: старайтесь писать короткие функции. Функция должна быть видна на экране целиком. Размер табуляции должен быть равен трем или четырем пробелам. Удостоверьтесь, что применяемый редактор поступает именно так. Операторы switch Выравнивайте оператор switch так, чтобы выдержать строки по горизонтали, switch(variable) { case ValueOne: ActionOne(); break; case ValueTwo: ActionTwo(); break; default: assert("bad Action"); break; ) Текст программы Для создания читабельного кода можно воспользоваться следующими советами (это упростит последующую поддержку): правильный отступ повышает читабельность; по сути имена объектов и массивов являются ссылками. Не используйте в них пробелы и такие символы, как ., -> и [ ]; унарные операторы логически связаны со своими операндами, поэтому не ставьте между ними пробелов. К унарным операторам относятся следующие: !, ++, —, * (для указателей), & (преобразования типа) и sizeof;
412 Часть VI. Специальные темы бинарные операторы, такие как +, =, *, /, %, », «, <, >, ==, !=, &, |, &&, | |, ?:, -=, += и т.д., должны иметь пробелы с обеих сторон; не используйте отсутствие пробелов для обозначения приоритета (4 + 3*2); используйте пробелы после запятых и точек с запятой, но не перед ними; круглые скобки не должны отделяться пробелами от заключенных в них па- раметров: ключевые слова, такие, как if, следует отделять пробелами: if (а == Ь); текст комментария следует отделять пробелом от символов //; размещайте символы указателей и ссылок рядом с названием типа, а не имени переменной. char* foo; int& thelnt; но не: char *foo; int &theInt; не объявляйте больше одной переменной в одной строке, если они не взаимо- связаны. Имена идентификаторов Ниже приведены рекомендации для работы с идентификаторами. Имена идентификаторов должны быть разумной длины. Избегайте непонятных аббревиатур. Не жалейте времени и энергии для подбора подходящих имен. Короткие имена (i, р, х и т.д.) должны использоваться только там, где их крат- кость способствует читабельности кода, а назначение достаточно очевидно. Длина имени переменной должна быть пропорциональна области ее действия. Во избежание путаницы и конфликтов имен убедитесь в том, что все иденти- фикаторы пишутся и читаются по-разному. Имена функций (или методов) обычно представляют собой глаголы или отглаголь- ные существительные: SearchO, Reset(), FindParagraph(), ShowCursor() (искать, сбросить, найти абзац, показать курсор). В качестве имен переменных обычно используются абстрактные существительные, иногда с дополнительным существительным: count, state, windspeed, windowHeight (количество, со- стояние, скорость вращения, высота окна). Логические переменные должны на- зываться в соответствии с их назначением: windowlconi zed, filelsOpen (свернутое окно, если файл открыт). Правописание и регистр символов имен Правописание и регистр символов также не стоит упускать из виду при создании собственного стиля. Вот некоторые советы по этому поводу: идентификаторы должны быть однозначны, поэтому при необходимости можно использовать символы в разном регистре. Имена функций, методов, классов, типов и структур должны начинаться с прописных букв (например, MyFunction). Переменные-члены и локальные переменные обычно начинают- ся со строчных букв (например, myVariable);
Час 24. Исключения, обработка ошибок и другое 413 перечисляемые константы должны начинаться с нескольких строчных букв, как, например, сокращение епшп: enum Textstyle ( tsPlain, tsBold, tsltalic, tsUnderscore, } ; Комментарии Комментарии могул сделать код программы более понятным. Иногда приходится работать над программой в течение нескольких дней или даже месяцев. За это время вполне можно забыть, что именно делает участок кода и зачем он это делает. Пробле- мы могут возникнуть и при чтении кода, написанного другим программистом. Ком- ментарии, используемые в соответствии с согласованным и хорошо продуманным стилем, оправдывают затраченные на них усилия. Вот несколько советов, которые стоит учесть при использовании комментариев: по возможности используйте комментарии в стиле C++ //, а не в стиле С /* */. Оставьте стиль С (/* */) для временного комментирования блоков кода, способных содержать комментарии в стиле C++; хороший комментарий — не просто подробное описание процессов в програм- ме. Выражение “увеличить значение на единицу” — это не более чем повторе- ние того, что и так очевидно из написанного в коде. n++; II увеличить значение п на’единицу Этот комментарий не стоит того, чтобы тратить время на его ввод. Уделите внима- ние семантике функций и блоков кода. Опишите, что делает функция. Укажите по- бочные эффекты, типы параметров и возвращаемые значения. Опишите все допу- щения, которые были сделаны (или не сделаны), например “предположим, что п неотрицателен” или “функция возвращает -1, если х имеет недопустимое значе- ние”. В случае ветвления программы указывайте, при каких условиях будет выпол- няться каждая часть кода; используйте законченные предложения с соответствующей пунктуацией и пропис- ными буквами в начале предложений. Избегайте условных обозначений и сокраще- ний, понятных только автору. То, что сейчас кажется очевидным, через несколько месяцев может стать абсолютно непонятным; располагайте комментарии в начале программы, функции и заголовке модуля исходного кода, чтобы указать задачу этого модуля, вводимых и выводимых данных, а также параметров; используйте пустые строки для отделения логических блоков программы. Объе- диняйте строки программы в логические группы. Доступ Организация доступа к данным и методам также должна подчиняться определен- ным правилам. Ниже приведен ряд советов, относящихся к наглядности описания в программе различных степеней доступа. Всегда используйте явное указание public:, private: и protected:. Не сле- дует полагаться на установки, сделанные по умолчанию.
414 Часть VI. Специальные темы Сначала объявите открытые (public) члены, затем защищенные (protected), а за ними закрытые (private). Объявляйте переменные-члены после методов. Сначала объявите конструктор (конструкторы), а затем — деструктор. Распола- гайте одноименные перегруженные методы рядом. Методы доступа также ста- райтесь собрать в одну группу. Методы и переменные-члены внутри каждой из групп желательно располагать в алфавитном порядке. Желательно в том же порядке подключать файлы с по- мощью директивы #include. Несмотря на то что при переопределении методов использование ключевого сло- ва virtual необязательно, старайтесь использовать его; это поможет не забыть, что метод является виртуальным, и сделает объявление более корректным. Определения классов Старайтесь сохранять порядок определения методов таким, как в объявлении. Это позволит скорее найти нужный метод. При определении функции размещайте тип возвращаемого значения и все другие спецификаторы на предыдущей строке, чтобы имена класса и функции располагались в начале строки. Это значительно облегчит поиск функций. Подключение файлов Старайтесь избегать подключения файлов в файлы заголовков, за исключением файла заголовка базового класса, от которого происходит данный класс. Использова- ние директив #include необходимо также и в тех случаях, когда в объявляемом клас- се используются объекты другого класса. Для классов, на которые просто делаются ссылки, достаточно передать ссылку или указатель. Не загромождайте файл заголовка излишними файлами только потому, что предпочи- таете подключать все файлы . срр проекта независимо от того, нужны они там или нет. Wt------ осторожны! Защищайте заголовки Во всех файлах заголовков следует использовать систему защиты от по- вторного подключения. Макрос assert () Используйте макрос assert () без всяких ограничений. Он не только помогает на- ходить ошибки, но и облегчает чтение программы. Он также заставит обратить вни- мание на то, что является допустимым, а что — нет. Ключевое слово const Используйте ключевое слово const везде, где считаете нужным: для параметров, переменных и методов. Зачастую необходимо существование как константных, так и неконстантных версий некоторых методов. Будьте очень осторожны при явном при- ведении константного типа к неконстантному и наоборот (хотя иногда такой подход оказывается единственным способом решения проблемы). Убедитесь в целесообраз- ности этих действий и добавьте подробный комментарий.
Час 24. Исключения, обработка ошибок и другое 415 Следующие шаги Долгие часы интенсивного изучения языка C++ позади. Теперь читатель может считать себя компетентным программистом, но на этом его образование нельзя счи- тать законченным. Осталось еще достаточно тем для изучения и мест, где можно най- ти ценнейшую информацию на многотрудном пути от начинающего программиста до признанного эксперта на C++. Куда обратиться за помощью и консультацией Первое, что следует сделать, — это отыскать в Internet одну из конференций по C++. Эти группы обеспечивают прямую связь с сотнями и даже тысячами програм- мистов, которые смогут ответить на любые вопросы, предложить советы и подсказать решения любых проблем. Существуют также тематические группы новостей, телекон- ференции и т.д. (например, comp.std.C++ и др.). Рекомендованная литература Языку C++ и программированию вообще посвящено очень много прекрасных книг. Авторы хотели бы порекомендовать некоторые из них. Effective C++ автор Скотт Мейерс (Scott Meyers) (издательство Addison-Wesley, ISBN: 0201924889, 1997). Имеется в виду второе издание. C++ Unleashed авторы Микки Уильямс (Mickey Williams) и другие (издательство Sams, ISBN: 0672312417, 1999). Передовые технологии C++. Clouds То Code автор Джесс Либерти (Jesse Liberty) (издательство Wrox Press, ISBN: 1861000952, 1997). Создание реальных приложений на языке C++ с ис- пользованием объектно-ориентированного подхода. Sams Teach Yourself C++ for LINUX in 21 Days авторы Джесс Либерти (Jesse Liberty) и другие (ISBN: 0672318954, 2000). Подробнее о языке C++ и програм- мировании для Linux. Можно также порекомендовать следующие ресурсы: The C++ Programming Language автор Бьярн Страуструп (Bjarne Stroustrup) (издательство Addison-Wesley, ISBN: 0201700735, 2000). The Elements of Programming Style авторы Брайан У Керниган (Brian W. Kemighan) и П.Ж. Плаугер (P.J. Plauger) (издательство McGraw-Hill, ISBN: 0070342075, 1988). C Elements of Style: The Programmer’s Style Manual for Elegant C and C++ Programs автор Стив Оуаллин (Steve Oualline) (издательство Hungry Minds, ISBN: 1558512918, 1992). Искусство программирования (3 тома): Основные алгоритмы, Получисленные методы и Сортировка и поиск автор Дональд Э. Кнут (Donald Е. Knuth) (ИД Вильямс, 2000). Практика программирования авторы Брайан У. Керниган (Brian W. Kemighan) и Роб Пайк (Rob Pike) (ИД Вильямс, ISBN: 5-8459-0679-2, 2004). Software Tools авторы Брайан У Керниган (Brian W. Kemighan) и П.Ж. Плаугер (P.J. Plauger) (издательство Addison-Wesley, ISBN: 020103669Х, 1976). C/C++ Users Journal (ежемесячный журнал) http: //www.cuj . com. The Complete Idiot’s Guide to a Career In Computer Programming by Джесс Либерти (Jesse Liberty) (издательство Que, ISBN: 0789719959, 1999). Более подробная информация о том, как применить полученную квалификацию для решения серьезных задач.
416 Часть VI. Специальные темы Между ярочки Оставайтесь на связи Если у читателя есть комментарии, идеи или предложения по этой или другим книгам авторов, посетите их личные Web-сайты: http: / / www. LibertyAssociates. com (Джесс Либерти) и http: / /www. cobs. com (Дэвид Б. Хорват, ССР). Вопросы и ответы Зачем использовать отдельные блоки try, когда обработку исключений можно осуществлять централизованно? Блок try позволяет обработать определенное исключение внутри текущего раздела кода. Обработку любых исключений на самом деле имеет смысл осуществлять цен- трализованно, но для специфического исключения выгоднее применять блок try. Что делать по окончанию изучения книги? Продолжать обучение! Опробуйте примеры, пишите код самостоятельно и изу- чайте другие ресурсы, доступные в сети и печати. Коллоквиум Завершив изучение языка C++ за 24 часа, имеет смысл ответить на несколько во- просов и выполнить ряд упражнений, чтобы закрепить полученные знания. Контрольные вопросы 1. Для чего предназначен блок try? 2. Как определить и передать собственное исключение? 3. В чем разница между ключевыми словами throw и try? 4. Как объявить блок catch по умолчанию? Упражнения В этой главе есть только одно упражнение: идите и программируйте! Но сначала удостоверьтесь, что опробовали примеры всех программ этой книги. Ответы на контрольные вопросы 1. Блок try содержит набор операторов, в котором вероятна передача исключе- ния. Операторы обработки исключения располагают в блоке catch. 2. Чтобы определить собственное исключение, следует создать его класс, а для пе- редачи используется оператор throw, в котором указано имя класса исключе- ния. Для обработки исключения необходим соответствующий блок catch. 3. Ключевое слово throw обеспечивает немедленную передачу исключения, т.е. про- грамма говорит “Хьюстон, у нас проблема”. Ключевое слово try уведомляет ком- пилятор о том, что все переданные в данном блоке кода исключения (включая переданные из функций библиотек, операционной системы и даже собственных операторов throw) необходимо обработать в следующем за ним блоке catch. 4. Чтобы определить блок catch, перехватывающий все исключения, не обработанные в других блоках catch (подобно метке default в операторе switch), в списке ар- гументов вместо имени исключения следует указать три точки: catch (...).
ЧАСТЬ VII Приложения В этой части... Приложение А. Двоичные и шестнадцатеричные числа Приложение Б. Глоссарий
ПРИЛОЖЕНИЕ A Двоичные и шестнадцатеричные числа С основами арифметики знакомишься так рано, что даже трудно вообразить свою жизнь без этих знаний. Взглянув на число 145, любой без малейших колебаний ска- жет, что это сто сорок пять. Понимание двоичных и шестнадцатеричных чисел потребует по-новому взглянуть на число 145 и увидеть в нем не число, а некоторый код для его выражения. Начнем с малого. Рассмотрим взаимосвязь между числом три и символом “3”. Символ числа (цифра) 3 — это некая “закорючка” на листе бумаги, а само число три — это уже абстрактная концепция. Для представления определенного числа ис- пользуется определенная цифра. Отличие между концепцией и символом становится яснее, если осознать, что для представления одного и того же понятия могут использоваться совершенно разные символы: три, 3, |||, in или ***. В десятичной системе счисления для представления чисел используются цифры О, 1, 2, 3, 4, 5, 6, 7, 8 и 9. А как же представить число десять? Предположим, необходимо разработать систему счисления. Для представления числа десять можно было бы использовать символ А (символьная система, как у древ- них славян) или “сороконожку” шиши (которую рисуют на стене в местах не столь отдаленных). Римляне использовали пятеричный символ X. В арабской системе счисления (которой придерживается большинство людей) основную роль для пред- ставления значения имеет позиция цифры в числе. Первый (младший) разряд исполь- зуется для единиц, следующий (слева) — для десятков и т.д. Таким образом, число пятнадцать имеет вид 15 (один, пять), т.е. 1 десяток и 5 единиц. Итак, вырисовываются некоторые правила, позволяющие сделать ряд обобщений. 1. Счисление на основании 10 использует цифры от 0 до 9. 2. Разряд представляет собой степень числа 10: единицы (1), десятки (10), сотни (100) и т.д. 3. Поскольку третья позиция в числе представляет сотни, самым большим двух- разрядным числом может быть 99. В общем случае, используя п разрядов, мож- но представить числа от 0 до 10"'. Следовательно, с помощью трех разрядов можно представить числа от 0 до 103', или 0—999.
Приложение А. Двоичные и шестнадцатеричные числа 419 Другие системы счисления Отнюдь не случайно люди используют основание 10, ведь пальцев на руках имен- но 10'. Но можно представить систему счисления и с другим основанием. Применяя правила, сформулированные для основания 10, можно описать представление чисел в системе счисления с основанием 8. 1. Счисление на основании 8 использует цифры от 0 до 7. 2. Разряд представляет собой степень числа 8: 1, 8, 64 и т.д. 3. Общий случай представления для п разрядов — от 0 до 8" '. Чтобы различать числа, написанные с использованием разных оснований, это осно- вание записывают радом с числом как нижний индекс. Значит, число пятнадцать с ос- нованием 10 можно записать как 1510 и читать как “один, пять с основанием десять”. Таким образом, для представления числа 1510 с основанием 8 следует записать 178. Это читается как “один, семь с основанием восемь”. Обратите внимание, что это чис- ло также можно прочитать как “пятнадцать”, поскольку именно оно и имеется в виду, просто использовано другое обозначение. Откуда взялось число 17? Цифра 1 означает одну восьмерку, а цифра 7 означает 7 единиц. Одна восьмерка плюс семь единиц равно пятнадцати. Рассмотрим пятнадцать звездочек: Возникает вполне естественное желание создать две группы: одна содержит десять звездочек, а другая — пять. В десятичной системе эта “композиция” представляется чис- лом 15 (1 десяток и 5 единиц). Но те же звездочки можно сгруппировать и по-другому: ******** ******* Т.е. снова получаются две группы: с восемью и семью звездочками. Такое распре- деление звездочек может служить иллюстрацией представления числа 178 с основани- ем восемь (одна восьмерка и семь единиц). Еще об основаниях Число пятнадцать с основанием десять представляется как 15, с основанием де- вять — как 169, с основанием восемь — как 178, а с основанием семь — как 217. В сис- теме счисления с основанием 7 нет цифры 8, поэтому для представления числа пят- надцать нужно использовать две семерки и одну единицу. Как же прийти к какому-нибудь общему принципу? Перед тем как преобразовать де- сятичное число в число с основанием 7, вспомните о значении разрядов. В семеричной системе счисления переход к следующему разряду будет происходить на значениях, со- ответствующих десятичным числам: единица, семь, сорок девять, триста сорок три и т.д. Откуда взялись эти числа? Так ведь это же степени числа семь: 7°, 71, 72, 73 и т.д. Построим следующую таблицу: Разряд 4 3 2 1 Степень 73 72 7* 7° Значение 343 49 7 1 1 Как ни странно, у римлян по той же причине основание было 5. Либо на другой руке начи- нался следующий разряд, либо вторую руку использовали для подсчетов на первой. — Прим. ред.
420 Часть VII. Приложения Лад___ цюш Любое число в степень ноль Напомним одно из математических правил: любое число в нулевой степени равняется единице. 7°= 1, 10°= 1,217 549 343°= 1. В первой строке представлен разряд числа. Во второй — степень числа семь, а в третьей — десятичное представление соответствующего разряда числа семь. Чтобы представить десятичное число в системе счисления с основанием 7, необхо- дима следующая процедура. Проанализируйте число и определите его старший разряд. Возьмем, например, число 200. Как известно, значение четвертого разряда равно 343, поэтому о нем можно не волноваться, этот разряд будет пуст. Теперь выясним, сколько значений следующего разряда (49) содержит число. Разделим 200 на 49, в результате получится 4, его и поместим в столбец третьего разряда, а остаток, 4, исследуем дальше. Поскольку значение следующего разряда (7) в числе 4 не помещает- ся, то его значением также будет нуль. Нетрудно догадаться, что в остатке 4 содержатся 4 единицы, поэтому в первый разряд ставим цифру 4. В итоге получаем число 4047. Представим число 968|0 в системе счисления с основанием 6: Разряд 5 4 3 2 1 Степень 64 63 62 6' 6° Значение 1296 216 36 6 1 Число 1296 на помещается в числе 968 ни разу, поэтому в столбец (разряд) 5 за- пишем 0. Разделим 968 на 216, получится 4 с остатком 104. В столбец 4 запишем 4. Т.е. столбец 4 представит число 4*216 (864). Разделим 104 на 36, получится 2 с остат- ком 32. В столбец 3 запишем 2. Разделим 32 на 6, получится 5 с остатком 2. В резуль- тате получим ответ 42526. Разряд 5 4 3 2 1 Степень 64 63 62 61 6° Значение 1296 216 36 6 1 968 с основанием 6 0 4 2 5 2 Десятичное значение 0 4*216=864 2*36=72 5*6=30 2*1=2 Таким образом, для преобразования из одной системы счисления в другую (из 6 в 10) достаточно умножения и сложения. 4 * 216 = 864 2 * 36 = 72 5*6 =30 2*1 = 2 968 Двоичная система счисления Минимальным допустимым основанием является 2. Здесь существуют только две цифры: 0 и 1. Вот как выглядят разряды двоичного числа: Разряд 8 7 6 5 4 3 2 1 Степень 27 26 2s 24 23 22 2' 2° Значение 128 64 32 16 8 4 2 1
Приложение А. Двоичные и шестнадцатеричные числа 421 Для преобразования числа 8810 в двоичное (с основанием 2) выполним описанную выше процедуру. В числе 88 число 128 не укладывается ни разу, поэтому в восьмой разряд пишем 0. В числе 88 число 64 укладывается только один раз, поэтому в седьмой разряд пи- шем 1, а остаток равен 24. В числе 24 число 32 не укладывается ни разу, поэтому шес- той разряд тоже будет 0. В числе 24 число 16 укладывается один раз, поэтому пятой цифрой двоичного чис- ла будет 1. Остаток при этом равен 8. В остатке 8 число 8 (значение четвертого разря- да) укладывается один раз, следовательно, в четвертой позиции ставим 1. Новый оста- ток равен нулю, поэтому в оставшихся разрядах будут стоять нули. В результате получится 01011000. Проверим ответ, выполнив обратное преобразование: 1 х 64 = 64 0 х 32 = 0 1 х 16 = 16 1x8= 8 0x4=0 0x2= 0 0x1=0 88 Почему именно основание 2? Система счисления с основанием 2 более всего соответствует способу представления информации в компьютере. На самом деле компьютеры “понятия не имеют” ни о каких буквах, цифрах, командах или программах, поскольку представляют собой сложные электрические схемы, которые способны распознавать лишь напряжение и силу тока. Для упрощения логики микросхем инженеры отказались от измерения силы тока (слабый, средний, большой и очень большой) и различают лишь два состояния (есть ток или его нет)2. Состояния “есть ток” и “нет тока” можно выразить и иначе, на- пример: да и нет, ИСТИНА и ЛОЖЬ, true и false, 1 и 0. В соответствии с общеприня- тым соглашением 1 равна true, или ИСТИНЕ, или да, но это лишь соглашение. С та- ким же успехом она могла означать false, или нет. ‘ Теперь, если напрячь интуицию, несложно догадаться, в чем заключается неоспо- римое преимущество двоичной системы счисления: с помощью единиц и нулей мож- но описать состояние отдельного элемента электрической схемы (есть ток или его нет). Ведь все элементы компьютера оперируют лишь двумя понятиями: есть или нет. Если есть — значит, I, если нет — значит, 0. Биты, байты и полубайты Решив представлять данные последовательностями единиц и нулей, минимальную единицу информации, содержащую один двоичный разряд, назвали битом (bit от binary digif — двоичная цифра). В связи с тем, что первые компьютеры были способны обрабатывать лишь по 8 битов одновременно, считалось вполне естественным писать код, используя восьми разрядные двоичные числа, называемые байтами (byte). Между арочка Маленькие биты Половина байта (4 бита) называется полубайтом (nybble)! 2 Собственно, в этом и состоит разница между аналоговой техникой и цифровой. — Прим. ред.
422 Часть VII. Приложения С помощью восьми двоичных разрядов можно представить до 256-ти различных значений. Почему? Рассмотрим разряды: если все 8 битов установлены (1), значение составит 255 (128+64+32+16+8+4+2+1), если не установлен ни один (все биты равны нулю), значение составит 0. А в диапазоне от 0 до 255 как раз и содержатся 256 воз- можных вариантов. Что такое килобайт? Оказалось, что 210 (1 024) приблизительно равно Ю3 (1 000). Это совпадение было слишком заманчивым, чтобы его упустить. Поэтому в компьютерных кругах 210 байтов стали называть килобайтом (1 Кбайт), используя метрический префикс “кило”, озна- чающий одну тысячу. Аналогично число 1024*1024 (1 048 576) достаточно близко к одному миллиону, чтобы назвать его мегабайтом (1 Мбайт), а 1 024 мегабайтов называются гигабайтом (1 Гбайт) (“гига” означает тысячу миллионов, или миллиард/. Двоичные числа Компьютер использует наборы нулей и единиц для представления всего, с чем он работает. Машинные коды представляют собой пакеты нулей и единиц, понятных центральному процессору и другим микросхемам. Отдельные наборы нулей и единиц можно перевести обратно в числа, понятные людям, но было бы ошибкой полагать, что это числа и что они на самом деле имеют именно эти значения. Например, процессор Intel 8086 интерпретировал набор битов 1001 0101 как ко- манду. Конечно, его можно представить в десятичном виде (149), но для людей это число не имеет никакого смысла. Иногда числа представляют собой команды, иногда — значения, иногда — про- граммный код. Одним из стандартизованных кодовых наборов является ASCII. В нем каждая буква или знак препинания имеет семиразрядное двоичное представление. Например, строчная буква “а” представлена двоичным числом 0110 0001. Хотя это значение можно преобразовать в десятичное число 97 (64 + 32 + 1), но понимать его следует не как число, а как букву. Поэтому когда говорят, что буква “а” в ASCII пред- ставлена числом 97, на самом деле имеют в виду десятичное представление 97 двоич- ного числа 0110 0001, являющегося кодом буквы “а”. Шестнадцатеричная система счисления Поскольку двоичные числа трудно читать, был найден более простой способ пред- ставления тех же значений. Перевод двоичного числа на десятичное основание доста- точно труден, а на шестнадцатеричное — нет. Почему? Давайте рассмотрим сначала, что представляют собой шестнадцатеричные числа. Для представления шестнадцатеричных чисел используются 16 символов; 0, 1,2, 3, 4, 5, 6, 7, 8, 9, А, В, С, D, Е и F. Как видите, последние шесть символов — не цифры, а буквы. Буквы A—F были выбраны произвольно, просто как первые буквы латин- ского алфавита. Разряды в шестнадцатеричном представлении имеют следующий вид: Разряд 4 3 2 1 Степень 163 162 16' 16° Значение 4096 256 16 1 3 Жизнь не стоит на месте. Теперь счет идет на терабайты (1 Тбайт). — Прим. ред.
Приложение А. Двоичные и шестнадцатеричные числа 423 При переводе шестнадцатеричного числа в десятичное можно использовать опи- санную выше схему (вычислить сумму произведений цифр числа на значения соответ- ствующих разрядов). Возьмем, например, число F8C)6: F х 256 = 15 х 256 = 3840 8 х 16 = 128 С х 1 = 12 х 1= 12 3980 (Не забывайте, что F в шестнадцатеричном виде равно 1510.) Преобразуя число FC,6 в двоичное представление, сначала переведем его в деся- тичное, а затем уже в двоичное: F х 16 = 15 х 16 = 240 С х 1 = 12 х 1= 12 +-------------------------- 252 Преобразование числа 25210 в двоичное представление показано в следующей таблице: Разряд 9 8 7 6 5 4 3 2 1 Степень 28 27 26 25 24 23 22 21 2° Значение 256 128 64 32 16 8 4 2 1 Разряд, соответствующий 256-ти, отсутствует. 1x128 = 128 . , 252-128 = 124 1х 64 = 64 . 124-64 = 60 1х 32 = 32 . 60-32 = 28 1х 16 = 16 . 28-16 = 12 1х 8 = 8 . 12-8 = 4 1х 4 = 4 . 4-4 = 0 Ох 2 = 0 Ох 1 = 0 124+60+28+12+4 = 252 Таким образом, получилось двоичное число 11111100. Теперь, представив это число как два набора из четырех цифр (Illi 1100), можно осуществить магическое превращение. Правый набор представляет собой число 1100. В десятичном выражении это число 12, а в шестнадцатеричном — число С (1*8 + 1*4 + 0*2 + 0*1). Левый набор (1111) в десятичном выражении — число 15, а с основанием 16 — число F. Итак, имеем: 1111 1100 F С Расположив два шестнадцатеричных числа рядом, получаем число FC, реальным значением которого будет 1111 11002. Этот быстрый метод преобразования всегда ра- ботает безотказно! Можно взять любое двоичное число любой длины, разбить его на группы по четыре разряда, перевести каждую группу в шестнадцатеричный вид, рас- положить эти цифры рядом и получить шестнадцатеричное число. Вот другой пример: 1011 0001 1101 0111 Столбцы будут иметь следующие значения: 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384 и 32768.
424 Часть VII. Приложения 1X1 1x2 1x4 0x8 1 х 16 0 х 32 1 х 64 1 х 128 1 х 256 0 х 512 О х 1024 О х 2048 1 х 4096 1 х 8192 О х 16384 1 х 32768 1 2 4 О 16 О 64 128 256 4 096 8 192 О 32 768 Всего: 45 527 О О О Для преобразования этого числа в шестнадцатеричное потребуется таблица шест- надцатеричных значений. 65535 4096 256 16 1 Исследуемое число меньше, чем 65536, поэтому начинать можно с четвертого столб- ца. Число 4096 (значение четвертого разряда) укладывается в числе 45 527 одиннадцать раз с остатком 471. В остатке 471 число 256 (значение третьего разряда) укладывается один раз с остатком 215. В остатке 215 число 16 (значение второго разряда) укладывается 13 раз с остатком 7. Таким образом, получаем шестнадцатеричное число B1D7. Проверим это: В (11) х 4096 = 45 056 1 х 256 = 256 D (13) х 16 = 208 7 х 1 = 7 Всего: 45 527 Для проверки ускоренного метода перевода возьмем двоичное число 1011000111010111, разделим его на группы по четыре знака: 1011 0001 1101 0111. За- тем каждая из четырех групп преобразуется в шестнадцатеричное число: 1011 = 1x1= 1 1x2= 2 0x4= 0 1x8= 8 Всего: 11 Hex: В 0001 = 1x1 = 1 0x2 = 0 0x4 = 0 0x8 = 0 Всего: 1 Hex: 1
Приложение А. Двоичные и шестнадцатеричные числа 425 1101 = 1x1= 1 0x2= 0 1x4= 4 1x8= 8 Всего: 13 Hex = D 0111 = 1x1= 1 1x2= 2 1x4= 4 0x8= 0 Всего: 7 Hex: 7 Шестнадцатеричный результат: B1D7
ПРИЛОЖЕНИЕ Б Глоссарий Это приложение содержит определения основных терминов, которые были ис- пользованы в книге. Термины сгруппированы по занятиям, в описании которых они встречаются в первый раз. Таким образом, встретив новый термин в главе, можно об- ратиться к этому разделу и узнать о нем немного больше. Час 1 Библиотека (library). Предоставляемая компилятором, приобретаемая отдельно или создаваемая самостоятельно коллекция готовых для компоновки файлов. Функция (function). Блок кода, который выполняет конкретную задачу, например, складывает два числа или отображает информацию на экране. Класс (class). Определение нового типа данных. Класс реализован как совокупность данных и функций для их обработки. Компилятор (compiler). Программное обеспечение, преобразующее понятный чело- веку текст программы в машинный код. В результате получается объектный файл, ко- торый впоследствии компонуется (см. компоновщик) в исполняемый. Компоновщик (linker). Программное обеспечение, создающее исполняемый файл из файлов объектного кода, созданных компилятором. Объектно-ориентированный (Object-Oriented— ОО). Подход программирования, являющийся следующим этапом после процедурного и структурного программирова- ния. Как и подразумевает его название, он позволяет воспользоваться достоинствами определенных в классах объектов. Зачастую этим термином злоупотребляют исключи- тельно в маркетинговых целях. ANSI. Американский национальный институт стандартов (American National Standards Institute), некоммерческая компания, контролирующая стандарты США. Большинство стран и некоторые регионы (например, Европейское сообщество) имеют подобные организации. Некоторые из таких организаций являются государственны- ми, однако институт ANSI таковым не является. Более подробная информация по этой теме содержится по адресу http:/ /www. ansi. org. ISO. Международная организация по стандартизации (International Organization for Standardization). Неправительственная организация, аналогичная ANSI, но в междуна- родных масштабах. Более подробная информация по этой теме содержится по адресу http://www.iso.org.
Приложение Б. Глоссарий 427 Час 2 Компиляция (compiling). Первый этап преобразования исходного кода в исполняе- мый файл. В результате компиляции получается объектный код, сохраняемый обычно в файле с расширением . obj. Компоновка (linking). Второй этап преобразования исходного кода в исполняемый файл. В результате компоновки объектные файлы объединяются вместе — и получает- ся исполняемая программа. Исполняемая программа (executable program). Программа, допускающая выполнение на установленной операционной системе. Интерпретатор (interpreter). Интерпретатор преобразует понятный человеку текст программы в машинный код во время выполнения программы. Процедурное программирование (procedural programming). Ряд действий, выполняе- мых с набором данных. Структурное программирование (structured programming). Систематический подход, позволяющий разделить программу на процедуры. Инкапсуляция (encapsulation). Создание самозамкнутых объектов. Сокрытие данных (data hiding). Сокрытие состояния класса в его закрытых пере- менных-членах. Наследование (inheritance). Создание нового типа, способного расширить возмож- ности предыдущего. Полиморфизм (polymorphism). Способность работать с набором производных типов так, как будто они принадлежат одному базовому типу. Препроцессор (preprocessor). Программа, запуск которой осуществляется перед за- пуском компилятора. Она обрабатывает строки кода, начинающиеся символом #. Комментарий (comment). Текст, который не воспринимается компилятором как подлежащий обработке и предназначенный для хранения заметок, сделанных разра- ботчиком. Сигнатура (signature). Имя функции и ее аргументы. Час 3 Переменная (variable). Именованная ячейка памяти, в который можно хранить значение. Оперативная память (RAM — Random Access Memory). Память произвольного доступа. Тип (type). Размер и характеристика объекта. Знаковый (signed). Тип переменной, способной содержать как отрицательные, так и положительные значения. Беззнаковый (unsigned). Тип переменной, способной содержать лишь положитель- ные значения. ASCII (American Standard Code for Information Interchange — Американский стан- дартный код для обмена информацией). Система кодировки символов, цифр и знаков пунктуации, используемая большинством компьютеров. Чувствительность к регистру (case sensitive). Символы в верхнем и нижнем регист- рах считаются разными символами (имя myVal не соответствует имени Myval). Ключевое слово typedef. Определение типа данных. Позволяет создать синоним для встроенного типа данных. Константа (constant). Ячейка для хранения данных, значение которых нельзя из- менить во время выполнения программы. Литеральная константа (literal constant). Значение, вставленное непосредственно в код программы, например, 35. Символьная константа (symbolic constant). Типизированное, именованное значе- ние, не подлежащее изменению, например, BoilingPoint. Перечисляемая константа (Enumerated constant). Именованный набор констант.
428 Часть VII. Приложения Час 4 Оператор (statement). Средство управления последовательностью выполнения выраже- ний. Они возвращают результаты вычислений или не делают ничего (пустые операторы). Непечатаемый символ (whitespace). Пробел, табуляция и символ новой строки. Блок кода (compound statement). Единый оператор, состоящий из нескольких опе- раторов, начинается открывающей фигурной скобкой (() и заканчивается закрываю шей фигурной скобкой (}). Выражение (expression). Любой оператор, который возвращает значение. Оператор (statement). Это символ, который заставляет компилятор выполнить оп- ределенное действие или не делать ничего (пустой оператор). Операнд (operand). Математический термин, означающий часть выражения (левую или правую), используемую оператором. Оператор присвоения (=) (assignment operator). Позволяет заменить значение опе- ранда, расположенного с левой стороны от знака равенства, значением, вычисляемым справа от него. L-значение (l value). Операнд, располагающийся слева от оператора. R-значение (r-value). Операнд, располагающийся справа от оператора. Оператор отношения (relational operator) используется для определения равенства или неравенства двух значений. Инкремент (increment). Увеличение значения на 1 (оператор ++). Декремент (decrement). Уменьшение значения на 1 (оператор --). Префиксный оператор (prefix operator). Префиксный оператор (++туАде) изменяет значение перед возвращением. Постфиксный оператор (postfix operator). Постфиксный оператор (туАде++) изме- няет значение после возвращения. Приоритет (precedence). Обуславливает порядок выполнения операторов. Час 5 Стек (stack). Специальная область оперативной памяти, выделенная для размеще- ния данных программы, необходимых каждой вызываемой функции. Она называется стеком потому, что представляет собой очередь типа “последним пришел — первым ушел"'. Последний поступивший в нее элемент извлекается первым. Объявление функции (function declaration). Позволяет сообщить компилятору имя, тип возвращаемого значения и параметры функции. Прототип (prototype). Объявление функции. Определение функции (function definition). Предоставляет компилятору исполняемый код функции. Список параметров функции (function parameter list). Перечень всех параметров и их типов, разделенных запятыми. Локальная переменная (local variable). Переменная, которая существует только внут- ри функции. Область видимости (scope). Область, в которой к переменной можно обращаться. Глобальная переменная (global variable). Переменная, доступная в любом месте про- граммы. Час 6 Итерация (iteration). Повторение одних и тех же действий определенное количест- во раз. Бесконечный цикл (infinite loop). Цикл, который выполняется бесконечно. Подоб- ной ситуации желательно избегать. Код "спагетти ” (spaghetti code). Беспорядочный, запутанный, трудно читаемый код.
Приложение Б. Глоссарий 429 Час 7 Клиент (client). Другой класс или функция, которая использует данный класс. Переменные-члены (member variable) и данные-члены (data member). Переменные класса. Данные-члены (data member). См. переменные-члены. Функция-член (member function) и метод (method). Функция класса. Метод (method). См. функция-член. Объект (object). Экземпляр класса. Открытый доступ (public access). Доступ открыт для методов всех классов. Закрытый доступ (private access). Доступ открыт только для методов данного класса и методов классов, производных от него. Метод доступа (accessor method). Метод, используемый для доступа к закрытым переменным-членам. Определение метода (method definition). Определение начинается именем класса, сопровождается двумя двоеточиями, именем функции и списком ее параметров. Стандартный конструктор (default constructor). Конструктор без параметров. Час 8 Постоянная функция-член (constant member function). Такая функция объявляет, что она не будет изменять никаких значений среди членов класса. Интерфейс (interface) (или интерфейс класса). Объявление данных и методов, к кото- рым можно обращаться другим классам и коду. Это не сам код, а лишь сообщение о том, как он используется. Подобная информация зачастую сохраняется в файле заго- ловка и подключается в использующий ее модуль. Реализация (implementation) (или реализация класса). Код и объявления данных внутри класса. Это тот код, к которому обращаются при помощи интерфейса. Обычно подобная информация хранится в файле с расширением .срр и компилируется в объ- ектный файл или библиотеку. Как правило, производитель предоставляет интерфейс (в виде файла заголовка) и откомпилированный код (в виде объектного файла или биб- лиотеки), а не исходный код класса, поскольку так можно предотвратить его изменение Час 9 Указатель (pointer). Переменная, содержащая адрес области в памяти компьютера. Косвенное обращение (indirection). Доступ к значению переменной по указателю на нее. Распределяемая память (heap). Пространство в памяти, оставшееся после создания сегмента программного кода, области глобальных имен и стека. Оно называется также динамической памятью (free store) и представляет собой источник памяти, динамиче- ски распределяемой для нужд программы. Час 10 Паразитный указатель (stray pointer), или зависший (dangling pointer). Так называют указатель, которому после выполнения оператора delete, освободившего участок па- мяти, не было присвоено другое значение. Это довольно распространенная и трудно обнаруживаемая ошибка, поскольку ее последствия (сбой при обращении к памяти), как правило, проявляются вовсе не там, где расположен оператор delete. Час 11 Ссылка (reference). Псевдоним объекта.
430 Часть VII. Приложения Час 12 Эффективность (efficiency). Весьма популярная тема, посвященная способам усовер- шенствования кода программ так, чтобы он выполнялся немного быстрее. При создании программ эти подходы можно и не учитывать, если только речь не идет о системах, ра- ботающих в реальном масштабе времени (например, системах управления медицинским оборудованием или ракетами). Во всех остальных случаях можно позволить компилято- ру самостоятельно позаботиться о подробностях повышения эффективности кода. Час 13 Поверхностное копирование (shallow сору). Создается точная копия значений всех переменных-членов одного объекта в другом. Глубокое копирование (deep сору). Переносит находящиеся в динамической памяти значения во вновь созданные участки. Час 14 Унарный оператор (unary operator). Такому оператору передают только один опе- ранд, например, типа а++. Парный оператор (binary operator). Оператор, использующий два операнда, напри- мер, а+Ь. Тройственный оператор (ternary operator). Этому оператору передают три операнда. В языке C++ существует только один тройственный оператор, ?. Он применяется следующим образом, а < b ? true : false; Результатом этого оператора будет значение true, если а меньше ь, и значение false в противном случае. Количество операндов (arity). Количество используемых оператором операндов. В языке C++ возможны унарные, бинарные и тройственные операторы. Час 15 Массив (array). Коллекция объектов одинакового типа. Размер массива (subscript). Количество элементов массива. Смещение (offset). Номер элемента в массиве. Например, к четвертому элементу массива myArray можно обратиться как к myArray[3];. Строка (string). Массив символов, завершающийся нулевым символом. Час 16 Упрощение (stubbing out). Функцию можно реализовать так, чтобы этого было дос- таточно для компиляции, а ее усовершенствование до полнофункциональной версии осуществить позже. Переопределение (overriding). Замена в производном классе реализации функции ба- зового класса. Переопределенный метод должен иметь тот же тип возвращаемого зна- чения и сигнатуру, что и у базового метода. Час 17 Виртуальный метод (virtual method). Одно из средств реализации полиморфизма (polymorphism) в языке C++. Это позволяет обращаться с объектами производных классов так, как будто они являются объектами базового. V-таблица (v-table). Внутренний механизм отслеживания виртуальных функций отдельных объектов.
Приложение Б. Глоссарий 431 Час 18 Абстрактный тип данных (Abstract Data Type — ADT). Это, скорее, понятие (как фор- ма), чем объект (как круг). Как и подразумевает его название, это абстрактная форма. Чистая виртуальная функция (pure virtual function). Виртуальная функция, которую сле- дует переопределить в производном классе, поскольку никакого кода она не содержит. Час 19 Связанный список (linked list). Структура данных, состоящая из узлов, способных поддерживать связь друг с другом. Односвязный список (singly linked list). Связанный список, в котором узлы указыва- ют только на следующей узел списка, но не на предыдущий. Двухсвязный список (doubly linked list). Связанный список, в котором узлы указы- вают и на следующий, и на предыдущий узлы списка. Дерево (tree). Более сложная структура, состоящая из узлов, которые способны ука- зывать в двух, трех и более направлениях. Час 20 friend. Ключевое слово, позволяющее предоставить другому классу доступ к за- крытым переменным-членам текущего класса. Статические переменные-члены (static member data) В отличие от большинства данных-членов класса, они не реплицируются для каждого созданного объекта. Суще- ствует только одна копия данных, к которым способны обращаться все объекты этого класса. Как правило, они используются для отслеживания количества созданных объек- тов или чего-нибудь еще, что применяется всеми объектами одного класса. Статическая функция-член (static member function). Подобно статическим данным- членам, такая функция существует в области видимости класса, а не индивидуального объекта. К статическим функциям-членам можно обращаться безотносительно опре- деленного объекта. Час 21 #def ine,. Директива которая задает подстановку строк. Термин (token). Строка символов. Час 22 Каскадный метод разработки (waterfall). Этот метод подразумевает, что каждый пре- дыдущий этап будет полностью завершен перед переходом к следующему. Здесь каждый этап отделен от остальных. Такой подход применим и к разработке программного обес- печения, и к любому проекту (включая проектирование дома и автомобиля). Моделирование (simulation). Компьютерная модель реальной системы. Концептуализация (conceptualization). Осознание основной идеи проекта программ- ного обеспечения. Прецедент (use case) Описание способов применения системы. Унифицированный язык моделирования (Unified Modeling Language — UML). Стан- дартные графические средства представления требований и проекта. Пространство проблем (problem space). Набор задач, которые должна решать разра- батываемая программа. Пространство решений (solution space). Набор возможных решений поставленных задач. Управляющая программа (driver program). Тестовая программа.
432 Часть VII. Приложения Час 23 Шаблон (template). Позволяет определить общий класс или метод. Для создания экземпляров конкретных классов необходимый тип следует передать как параметр. Создание экземпляра (instantiation). Создание объекта класса или типа по шаблону. Час 24 Исключение (exception). Объект, передаваемый из той области кода, где возникла проблема, в ту область, которая пытается с ней справиться.
Предметный указатель # «define, 335; 339; 431 #else, 336 «endif, 336 «ifdef, 336 «ifndef, 336 «include, 335 A Abstract Data Type, 431 Accessor function, 131 Accessor method, 123; 429 ADT, 283; 431 AND, 75 ANSI, 23; 426 API, 372 Application Programming Interface, 372 Argument, 44 Arity, 218, 430 Array, 226; 430 ASCII, 50; 422; 427 Assembler, 35 Assert(), 343; 356 Assignment operator, 428 в Binary operator, 275; 430 Breakpoint, 26 c Call stack, 405 Case sensitive, 51; 427 Class, 119, 426 Client, 119, 132; 429 Comment, 41; 427 Compiler, 23; 36; 426 Compile-time binding, 267 Compile-time error, 132 Compiling, 36; 427 Compound statement, 61; 428 Concatenation, 342 Conceptualization, 360; 431 Console Input, 40 Console Output, 40 Constant, 56; 427 Constant member function, 131; 429 Constructor, 126 D Dangling pointer, 429 Data hiding, 427 Data member, 119, 429 DEBUG, 336; 343 DEBUGLEVEL, 356 Decrement, 64; 428 Deep copy, 205; 430 Default constructor, 726; 429 Dereference, 148 Derivation, 245 Derived type, 38 Destructor, 726 Doubly linked list, 295; 431 Driver program, 375; 431 E Efficiency, 430 Encapsulation, 37; 119, 427 Endl, 351 Enum, 58 Enumerated constant, 58, 427 Event loop, 363 Exception, 400; 432 Executable program, 427 Expression, 61; 428 F False, 68 Free store, 429 Friend, 318 Function, 79, 426 declaration, 428
434 Часть VII. Приложения definition, 428 overloading, 92 parameter list, 80, 428 G Global variable, 85; 428 H Header file, 133 Heap, 153; 429 I IDE C++BuilderX, 25 Implementation, 123; 132; 429 Include file, 39 Increment, 64; 428 Indirection, 148; 429 Infinite loop, 104; 428 Inheritance, 38, 427 Instance, 121 Instantiation, 121; 384; 432 Interface, 133; 429 Interpreter, 36; 427 Invariant, 346 ISO, 23; 426 Iteration, 99, 428 L Late binding, 267 Library, 426 Linked list, 295; 431 Linker, 23; 36; 426 Linking, 36; 427 Literal constant, 57; 427 Local variable, 83; 428 Loop, 99 L-value, 428 L-значение, 63; 428 M Macro function, 339 Main(), 338 Member function, 119, 429 Member variable, 119, 429 Memory leak, 155 Method, 119, 429 Method definition, 429 N Namespace, 31 Node, 295 NOT, 75 Null, 238 Null pointer, 147 О Object, 37; 429 Object-Oriented, 426 Object-oriented programming, 37 Offset, 430 OO, 426 Operand, 62; 428 Operator, 62 OR, 75 Overriding, 256; 430 P Parameter, 44 Passing by value, 87 Pointer, 144; 147; 429 Polymorphism, 263; 427; 430 Postfix, 65 Postfix operator, 428 Precedence, 66; 428 Prefix, 65 Prefix operator, 428 Preprocessor, 427 Private access, 429 Problem space, 361; 431 Procedural programming, 36; 427 Prototype, 80, 428 Public access, 429 Pure virtual function, 283; 431 R RAM, 48, 427 Random Access Memory, 427 Reference, 171; 429 Relational operator, 68, 428 Reusability, 37 Rhs, 208 Runtime binding, 267 Runtime error, 132 R-value, 428 R-значение, 63; 428
Предметный указатель 435 S Scope, 85; 428 Shallow copy, 205; 430 Signature, 44; 256; 427 Signed, 49; 427 Simulation, 360; 431 Singly linked list, 295; 431 Solution space, 361; 431 Stack, 95; 428 Standard Template Library, 396 Statement, 60; 428 Static binding, 267 Static member data, 308, 431 Static member function, 310; 431 STL, 396 Stray pointer, 166; 429 String, 238, 430 Stringizing, 342 Structured programming, 36; 427 Stubbing out, 246; 430 Subscript, 226; 430 Symbolic constant, 57; 427 T Target, 171 Template, 383; 432 Ternary operator, 275; 430 Text string, 40 Token, 336; 431 Tree, 295; 431 True, 68 Type, 48, 118, 427 Typedef, 326 u UML, 361; 431 Unary operator, 215; 430 Unified Modeling Language, 361; 431 Unsigned, 49; 427 Use case, 360; 431 Value, 48 Variable, 47; 427 Virtual method, 264; 430 V-pointer, 267 V-table, 267; 430 V-таблица, 267; 430 V-указатель, 267 w Waterfell, 431 Whitespace, 60; 428 Wild pointer, 147 A Абстрактный тип данных, 283; 431 Адрес, 144; 149; 151 начала массива, 235 памяти, 47 Адресат, 171 Анализ, 360 Аргумент, 44 Аргумент функции, 86 Ассемблер, 35 Б Байт, 427 Беззнаковый тип, 427 Бесконечный цикл, 104; 428 Библиотека, 28, 426 iostream, 39; 53 STL, 396 внешняя, 39 функций для строковых операций, 239 шаблонов, 396 Бит, 427 Блок, 295 catch, 401; 405 try, 401; 404 Блок кода, 67; 428 В Ввод с консоли, 40 Верблюжья нотация, 51 Вершина стека, 95 Взаимосвязь имеет, 135 принадлежит, 244 является, 244 Виртуальный деструктор, 270 конструктор копий, 271 метод, 264; 430 Вложенность, 312 Внешняя библиотека, 39 Временная копия, 185
436 Часть VII. Приложения Время выполнения, 33 компиляции, 33 компоновки, 33 Встраиваемая реализация, 133 функция, 93 Вывод на консоль, 40 на экран, 40; 53 сообщения, 40 Вызов конструктора, 205 функции, 42; 79 Выражение, 61; 428 Выход из функции, 88 Г Гигабайт, 422 Глобальная область видимости, 85 Глобальная переменная, 86; 428 Глубокое копирование, 205; 430 д Данные-члены, 119; 429 Двухсвязный список, 295; 431 Декремент, 64, 428 Деление по модулю, 63 Деление целочисленное, 63 Дерево, 295; 431 Деструктор, 126; 163; 250 копий, 186 Динамическая память, 429 Динамически распределяемая память, 153 Директива «define, 335; 339; 431 «else, 336; 337 «endif, 336 «ifdef, 336 «ifdef DEBUG, 336 «ifndef, 336 «include, 80; 133; 135; 335 return, 88 Доступ, 413 к переменным-членам, 162 к членам класса, 121 Дружественная функция, 319 3 Заголовок функции, 82 Заключительный символ. 40 Закрытая статическая переменная, 311 Закрытый доступ, 429 Знаковый тип, 427 Значение, 48 Значение по умолчанию, 90; 202 И Идентификатор, 54 Иерархия наследования, 369 Имя массива, 235 переменной, 47 указателя, 148 Инварианта класса, 346 Индекс, 228 Инициализация массива, 228 многомерного массива, 231 объекта, 205 Инкапсуляция, 37; 119; 318, 427 Инкремент, 64; 428 Инструкция препроцессора, 39; 335 Интерпретатор, 36; 427 Интерфейс, 133; 180; 429 класса, 132; 283 прикладных программ, 372 Исключение, 155; 400, 432 Исполняемая программа, 427 Исполняемый файл, 335; 338 Истина, 68 Исходный код, 27; 36; 335; 338 текст программы, 27 файл, 27 Итерация, 99-, 428 К Каскадный метод разработки, 359; 431 Килобайт, 422 Класс, 28, 119; 426 String, 241 базовый, 245 дружественный, 318 производный, 245 Клиент, 119, 132; 429 классов, 180
Предметный указатель 437 Ключевое слово, 51 class, 120, 246; 384 const, 57; /3/; /66; 336; 414 delete, /55 else, 69 enum, 58 for, 108 friend, 431 include, 39 inline, 93; 133 new, 154 private, 248 protected, 248 public, 248 return, 88 template, 384 typedef, 53; 326; 427 unsigned, 56 using namespace, 104 virtual, 282 Код исходный, 36 объектный, 36 определения, 338 отладочный, 343 Количество операндов, 218, 430 Комментарий, 41; 45; 427 в стиле С, 41 в стиле C++, 41 Компилятор, 23; 36; 335; 426 командной строки, 28 Компиляция, 36; 427 Компоновка, 28, 36; 427 Компоновщик, 23; 36; 426 Конкатенация, 342 Константа, 56; 427 литеральная, 57; 427 перечисляемая, 58, 427 символьная, 57; 427 Конструктор, 126; 250 копий, 185; 205 стандартный, 126; 229 Контекстная помощь, 27 Контроль типов данных, 342 Контрольная точка, 26 Концептуализация, 360, 431 Копирование глубокое, 205; 430 поверхностное, 205; 430 Корневая иерархия, 368 Косвенное обращение, 148, 429 Круглые скобки, 67; 77 Л Лексема, 336 Литерал, 57 Ложь, 68 Локальная переменная, 83; 428 м Макрос, 339 assert(), 343; 414 встроенный, 343 Макрофункция, 339 Максимально допустимое значение, 55 Массив, 226; 430 инициализация, 228 многомерный, 230 указателей, 233; 235 Машинный код, 36 язык, 35 Мегабайт, 422 Метод, 119; 429 С1опе(), 271 lnvariants(), 346 виртуальный, 264; 430 доступа, 123; 429 постоянный, 131 Многократное использование, 37 Моделирование, 360, 431 н Наследование, 38, 427 Непечатаемый символ, 60, 428 О Область видимости, 85; 428 глобальная, 85 Область глобальной переменной, 153 Обработка исключений, 405 Объект, 37; 121; 429 cout, 39; 40 endl, 35/ Объектно-ориентированное программирование, 37 Объектно-ориентированный, 426 Объектный код, 36 файл, 28, 36
438 Часть VII. Приложения Объявление класса, 120, 132; 133; 338 массива в динамической памяти, 234 функции, 79; 428 Односвязный список, 295; 431 Операнд, 62; 428 адресный, 63 операционный, 63 Оперативная память, 48; 144, 427 Оператор, 60, 62; 428 #, 342 342 «include, 45 &, 144; 148, 171 *, 148 ., 121; 162; 229 [], 229 +, 215 ++, 64 =, 60, 217 ->, 162 break, 102; 114 case, 114 cin, 239 continue, 102 delete, 155; 160, 237; 331 do..while, 106 dynamic_cast, 278 endl, 53 for, 108 goto, 99 if, 69 new, 154; 234; 237 switch, 113; 411 throw, 416 while, 100 взятия адреса, 148 взятия в кавычки, 342 вывода, 40 выхода из функции, 44; 88 декремента, 64 индекса, 229 инкремента, 55; 64 конкатенации, 342 косвенного доступа, 148, 162 логический, 75 AND, 75 NOT, 75 OR, 75 математический, 63 области видимости, 329 обращения к адресу, 144 отношения, 68, 428 больше, 68 больше или равно, 68 меньше, 68 меньше или равно, 68 не равно, 68 равно, 68 парный, 215; 430 постфиксный, 428 преобразования типов, 223 префиксный, 428 присвоения, 52; 62; 217; 219; 428 с вычитанием, 64 с делением, 64 с делением по модулю, 64 с суммой, 64 с умножением, 64 равенства, 221 составной, 61 ссылки, 171 суммы, 215 точечный, 162; 229 тройственный, 215; 430 унарный, 215; 430 Операция, 60 Определение констант, 57 метода, 429 методов класса, 133 нескольких переменных, 52 объекта, 121 переменной, 50 типа, 53 функции, 79; 428 Освобождение динамической памяти, 163 Открытый доступ, 429 Отладочный код, 343 Отступ, 77 Ошибка выполнения, 132 компиляции, 32; 132 п Паразитный указатель, 166; 429 Параллельная иерархия, 368 Параметр, 44 Парный оператор, 275; 430 Перегрузка конструктора, 204 операторов, 211 функции-члена, 200 функций, 92
Предметный указатель 439 Передаваемое значение, 90 Передача по значению, 87 по ссылке, 176 указателей на функции, 324 Передварительная компиляция, 39 Переменная, 47; 427 глобальная, 86; 97 инициализация, 52 локальная, 83; 97 определение, 50 присвоение значения, 52 Переменные-члены, 119; 429 Переопределение, 430 функций, 256 Переполнение регистра, 55; 58 Перечисляемая константа, 58 Поверхностное копирование, 205; 430 Подключение файла, 39; 338 Позднее связывание, 267 Полиморфизм, 38; 263; 427; 430 классов, 38 функций, 38, 92 Полубайт, 421 Постоянная функция-член, 131; 429 Постоянный объект, 190 Постоянный указатель, 167; 190 Постфикс, 65 Постфиксный оператор, 428 Поток ввода-вывода, 39 Поток вывода, 40 Препроцессор, 39; 57; 335; 427 Префикс, 65 Префиксный оператор, 428 Прецедент, 360, 431 Признак конца строки, 238 Приоритет, 66; 77; 428 операторов отношения, 76 Программирование процедурное, 36 структурное, 36 Производный тип, 38 Происхождение, 245 Прокрутка стека, 405 Промежуточный файл, 335 Пространство имен, 31; 104 Пространство проблем, 361; 431 Пространство решений, 361; 431 Прототип, 79; 428 функции, 80, 180 Процедурное программирование, 427 Псевдоним, 53; 174 Р Раздел default, 114 Размер класса, 120 массива, 226, 430 объекта класса, 129 переменной, 49 Распределяемая память, 429 Реализация, 123; 132; 429 встраиваемая, 133 чистой виртуальной функции, 286 Регистр, 153 Регистр букв, 51 Резервирование памяти, 48 С Связанный список, 295; 431 Связывание во время выполнения, 267 во время компиляции, 267 позднее, 267 статическое, 267 Сегмент программного кода, 153 Сигнатура, 44; 256; 427 Символ ASCII, 50 Смещение, 430 Соглашения об именовании, 120 Создание экземпляра, 121; 384, 432 Сокрытие данных, 427 метода базового класса, 258 Сообщение об ошибке, 32; 33 Список двухсвязный, 295 дерево, 295 односвязный, 295 Список параметров функции, 80, 428 Среда разработки, 33 Ссылка, 171; 429 Ссылка на объект, 176 в динамической памяти, 194 Стандарт ANSI, 68 C++, 23 Стандартная библиотека шаблонов, 396 Стандартный конструктор, 126; 229; 429 Статическая функция-член, 310, 431 Статические данные-члены, 308 Статические переменные-члены, 431 Статическое связывание, 267
440 Часть VII. Приложения Стек, 95; 153; 428 вызовов, 405 Строка. 238; 430 подстановки, 335; 339 Структурное программирование, 427 т Текстовая подстановка, 57 Текстовая строка, 40 Текстовый редактор, 33 Тело функции, 82 Термин, 336; 431 Тип, 48; 118; 427 bool, 68 double, 55 float, 55 long, 54 short, 54 signed, 49, 56 unsigned, 49, 56 unsigned short int, 53 void, 43; 88 беззнаковый, 49, 56 вещественный, 50 возвращаемого значения, 40 знаковый, 49, 56 определение, 53 переменной, 48; 50 производный, 38 целое число, 40 Точечный оператор, 162 Тройственный оператор, 215; 430 У Удаление объекта, 160 Указатель, 144; 147; 149, 429 ptr, 331 this, 164; 169, 312 vptr, 267 вершины стека, 95 дикий, 147 на массив, 235 на постоянный объект, 188 на функции-члены, 329 на функцию, 319 неинициализированный, 147 паразитный, 166 постоянный, 167; 190 пустой, 147 Унарный оператор, 215; 430 Унифицированный язык моделирования, 361; 431 Управляющая программа, 375; 431 Управляющий символ, 146 Упрощение, 246; 430 Упрощенный вызов, 321 Уровень отладки, 352 Усечение до целого, 58 Утечка памяти, 155; 157 Ф Файл lostream.h, 32; 39 заголовка, 133; 180, 338 assert.hpp, 345 string.h, 240 исполняемый, 335; 338 кода, 133 объектный, 28 объявлений, 338 подключаемый, 39 промежуточный, 335 реализации, 338 Фигурные скобки, 72; 410 Функции-члены, 119 Функция, 28; 40. 42; 79, 426 freed, 158 main(), 40, 42; 79, 338 mallocO, 158 sizeof(), 49 strcpy(), 239 strncpyO, 240 аргумент, 44; 86 встраиваемая, 93 вызов, 42 выход, 88 доступа, 131 дружественная, 319 заголовок, 43; 82 значение по умолчанию. 90 объявление, 79, 428 определение, 79; 428 параметр, 44 передаваемое значение, 90 полиморфизм, 92 постоянная, 131 прототип, 79. 180 статическая, 310 тело, 44; 82 чистая виртуальная, 283; 431 Функция-член, 429
Предметный указатель 441 ц Цикл, 99 do..while, 106 for, 108 инициализация, 108 приращение, 108 счетчик, 108 условие, 108 while, 100 бесконечный, 104\ 428 вложенный, 112 Цикл разработки, 29\ 359 Цикл событий, 363 ч Чистая виртуальная функция, 283\ 431 Члены класса закрытые, 122 открытые, 122 Чувствительность к регистру, 57; 427 ш Шаблон, 383\ 432 э Экземпляр, 121 класса, 121 Элемент массива, 226 Эффективность, 430 Я Язык assembler, 35 C++, 23 машинный, 35
Научно-популярное издание Джесс Либерти, Дэвцц Хорват Освой самостоятельно C++ за 24 часа 4-е издание Литературный редактор Верстка Художественный редактор Корректоры Н.В. Саит-Аметова В. И. Бордюк В. Г. Павлютин Л.А. Гордиенко, О.В. Мишутина Издательский дом “Вильямс" 127055, г Москва, ул. Лесная, д. 43, стр. 1 Подписано в печать 22.01.2007. Формат 70x100/16. Гарнитура Times. Печать офсетная. Усл печ. л. 36,12. Уч -изд. л. 24,69. Тираж 3000 экз. Заказ № 3867. Отпечатано по технологии CtP в ОАО “Печатный двор” им. А. М. Горького 197110, Санкт-Петербург, Чкаловский пр., 15.
Отрывная карточка Приоритет и порядок выполнения операторов Операторы в верхей части таблицы имеют более высокий приоритет, чем операто- ры, расположенные ниже. Выражения в круглых скобках, находящиеся в самом верху таблицы, имеют более высокий приоритет, чем все остальные операторы. Унарные операторы плюс (+) и минус (-), находящиеся на втором уровне, облада- ют более высоким приоритетом, чем арифметические плюс и минус на уровне 5. Сим- вол & на уровне 2 — это оператор обращения по адресу, а символ & на уровне 9 — би- товый оператор and. Символ * на уровне 3 — это обращение к указателю, а символ * на уровне 5 — оператор умножения. Операторы ++ и -- на уровне 2 являются пост- фиксными, а на уровне 3 — префиксными. При отсутствии круглых скобок находящиеся на одном уровне операторы обраба- тываются согласно указанному для них порядку выполнения слева направо или спра- ва налево. Уровень Оператор Порядок выполнения 1 (Высший) ( ) : : Слева направо 2 . [ ] _> ++ __ typied keyword_typecast Слева направо Слева направо 3 *&!-++ — + - sizeof new delete Справа налево Справа налево 4 . * -> * Слева направо 5 * / % Слева направо 6 + — Слева направо 7 << » Слева направо 8 <<=>>= Слева направо 9 == ! = Слева направо 10 & Слева направо 11 Слева направо 12 I Слева направо 13 Слева направо 14 II Слева направо 15 ? ; Справа налево 16 = *= /= += -= %= <<= »= &= л= I = Справа налево 17 1 throw Слева направо 18(Низший) • Слева направо
Типы данных С++ Тип 16 битов 32 бита Диапазон unsigned short int 2 байта 2 байта 0 - 65,535 short int 2 байта 2 байта -32,768 - 32,767 unsigned long int 4 байта 4 байта 0 - 4,294,967,295 long int 4 байта 4 байта -2,147,483,648 - 2,147,483,647 int 2 байта 4 байта (16): -2,768 - 32,767; (32): -2,147,483,648 - 2,147,483,647 unsigned int 2 байта 4 байта (16): 0 - 65,535; (32): 0 - 4,294,967,295 char I байт 1 байт 256 символьных значений wchar_t 2 байта 2 байта 65,535 символьных значений bool 1 байт 1 байт True (Истина) или False (Ложь) float 4 байта 4 байта 1.2Е-38 - 3.4Е+38 double 8 байтов 8 байтов 2.2Е-308 - 1.8Е+308 long double 10 байтов 10 байтов 3.4Е-4932 - 1.1Е+4932 Ключевые слова С и С++ and (&&)' and_eq (&=)' asm auto bitand (&)' bitor ( |)1 bool' break case catch' char class' compl (-)' const const_cast' continue default delete' do double dynami c_c as t1 else enum explicit' export' extern false' float for friend' goto if inline1 int long mutable1 namespace new' not (!)' not_eq (!=)' operator' or (11)' or_eq (|=)1 private' protected' public' register reinterpret_cast' return short signed sizeof size_t static static_cast' struct switch template' this' throw' true' try' typedef typeid' typename' union uns igned using' virtual' void volatile' wchar_t while xor (~)' xor eq (''=)' 'Только в C++. Символы в круглых скобках являются синонимами ключевых слов.
Лицензионное соглашение Открыв этот пакет, читатель берет на себя обязательство не копировать и не рас- пространять данный CD в целом. Правила копирования и распространения отдельных программ, находящихся на CD, определяются законами о защите авторских прав. Авторские права на программу установки и код принадлежат издательству и авто- рам книги. Отдельные программы и другие элементы CD, принадлежащие другим ав- торам или обладателям авторских прав, защищены авторским правом и лицензией со- общества Open Source. Содержащееся здесь программное обеспечение предоставляется как есть, без ка- ких либо гарантий. Ни издатель, ни его торговые агенты или дистрибьюторы не несут никакой ответственности за предполагаемые или фактические убытки, являющиеся результатом применения этих программ. (Поскольку не все государства гарантируют соблюдение авторских прав, это обращение адресовано к читателю лично.) Файлы на CD имеют длинные имена, содержащие символы в разных регистрах, поэтому используемый привод CD должен поддерживать ре- жим чтения таких файлов. Что содержит прилагаемый CD Прилагаемый CD содержит программный продукт C++BuilderX, Personal Edition и все примеры, рассматриваемые в этой книге. Для установки на операционной системе Windows необходимо предпринять сле- дующее: 1. вставьте диск в дисковод CD-ROM; 2. дважды щелкните на пиктограмме Му Computer (Мой компьютер), расположен- ной на рабочем столе; 3. дважды щелкните на пиктограмме дисковода CD-ROM; 4. дважды щелкните на файле start.exe. Следуйте инструкциям мастера уста- новки. <И*и — . •стермом Если для дисковода CD-ROM разрешен автоматический запуск, то про- грамма start.exe сработает автоматически, когда диск CD окажется в дисководе.
Освой самостоятельно C++ за 24 часа, 4-е издание JMIIIIII Код 22862 48.90 ГОН ЧЕТВЕРТОЕ ИЗДАНИЕ Начните прямо сейчас! Узнайте, как: • научиться быстро создавать объектно- ориентированные программы на языке C++ овладеть такими основными концепциями языка C++, как функции, классы, массивы и указатели • изучить работу со связанными списками и шаблонами • овладеть приемами отладки программ и устранения ошибок в коде • изучить методы обработки ошибок и применения исключений узнать, как сделать код совместимым со стандартом ANSI, обеспечив его многократное применение Двадцать четыре практических занятия Научиться программировать на языке C++ можно всего за 24 занятия по одному часу. Простое и понятное изложение материала, его постепенное усложнение и учет знаний, полученных на предыдущих занятиях, позволит быстро освоить основные концепции языка C++. Примечания содержат полезную и интересную информацию Советы разъясняют сложные понятия Предупреждения предостерегают от возможных ошибок Расположенные в конце каждого занятия разделы помогут закрепить полученные знания о языке C++. По книгам автора Джесса Либерти язык C++ изучило более 250000 читателей Джесс Либерти — автор множества книг по разработке программного обеспечения включая ряд бестселлеров по C++ и NET Являясь президентом ассоциации Liberty Associates Inc. (http://www LibertyAssociates com), он занимается разработкой уникального программного обеспечения, консультациями и преподаванием. Дэвид Б. Хорват, ССР — старший консультант из Филадельфии штат Пенсильвания автор книги UNIX for the Mainframer, соавтор (в паре с Джессом Либерти) книг Teach Yourself C++ for Linux in 21 Days Unix Unleashed Red Hat Linux Unleashed Learn Shell Programming in 24 Hours, Linux Unleashed и Linux Programming Unleashed. Он также опубликовал множество статей в журналах. CD содержит: • C++BuilderX Personal Edition • исходный код всех рассматриваемых в книге примеров Категория: программирование Содержание; C++ Уровень: для начинающих пользователей ВИЛЬЯМС К4 sAms www.williamspublishing.com www.samspublishing.com ISBN 978 5-8459 0949-7