Текст
                    

Язык программирования C++ Вводный курс Четвертое издание
Primer Fourth Edition Stanley B. Lippman Josee Lajoie Barbara E. Moo A Addison-Wesley Upper Saddle River, NJ • Boston • Indianapolis • San Francisco • New York • Toronto • Montreal London • Munich • Paris • Madrid • Capetown • Sydney • Tokyo • Singapore • Mexico City
Язык программирования Вводный курс Четвертое издание Стенли Б. Липпман Жози Лажойе Барбара Му ВИЛЬЯМС ScanneJ by Digrol Издательский дом “Вильямс” Москва ♦ Санкт-Петербург ♦ Киев 2007
ББК 32.973.26-018.2.75 Л61 УДК 681.3.07 Издательский дом “Вильямс” Зав. редакцией С.Н. Тригуб Перевод с английского и редакция В.А. Коваленко По общим вопросам обращайтесь в Издательский дом “Вильямс” по адресу: info@williamspublishing.com, http: //www.williamspublishing.com 115419, Москва, а/я 783; 03150, Киев, а/я 152 Липпман, Стенли Б., Лажойе, Жози, Му, Барбара Э. Л61 Язык программирования C++. Вводный курс, 4-е издание.: Пер. с англ. — М.: ООО “И.Д. Вильямс”, 2007. — 896 с.: ил. — Парал. тит. англ. ISBN 5-8459-1121-4 (рус.) Нынешнее издание столь популярного вводного курса стандартного языка C++ было полностью переделано, реорганизовано и переписано так, чтобы помочь быстрее и эф- фективнее научиться программировать на этом языке. По мере развития языка C++, ав- тор старается вносить в последующие издания соответствующие изменения. Теперь стандартная библиотека C++ описана с самого начала, что позволяет читателю сразу приступить к созданию работоспособных программ, еще до изучения подробностей язы- ка. Здесь содержатся полезные советы, которые помогут облегчить создание программ, а также повысить их эффективность. Примеры, в которых используются возможности библиотек, позволяют продемонстрировать достоинства языка C++, а также наиболее эффективные приемы его применения. Как и в предыдущих изданиях, здесь обсуждают- ся фундаментальные концепции и методы языка C++, что делает книгу ценнейшим ре- сурсом даже для опытных программистов. ББК 32.973.26-018.2.75 Все названия программных продуктов являются зарегистрированными торговыми марками соответ- ствующих фирм. Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами, будь то электронные или механические, включая фото- копирование и запись на магнитный носитель, если на это нет письменного разрешения издательства Addison-Wesley Publishing Company, Inc. Authorized translation from the English language edition published by Addison-Wesley Publishing Com- pany, Inc., Copyright © 2005 Objectwrite Inc., Josee Lajoie and Barbara E. Moo All rights reserved. No part of this book shall be reproduced, stored in a retrieval system, or transmitted by any means, electronic, mechanical, photocopying, recording, or otherwise, without written permission from the publisher. No patent liability is assumed with respect to the use of the information contained herein. All terms mentioned in this book that are known to be trademarks or service marks have been appropri- ately capitalized. Russian language edition is published by Williams Publishing House according to the Agreement with R&I Enterprises International, Copyright © 2007. ISBN 5-8459-1121-4 (рус.) © Издательский дом “Вильямс”, 2007 ISBN 0-201-72148-1 (англ.) © Objectwrite Inc., Josee Lajoie and Barbara E. Moo, 2005
Оглавление Введение 19 Глава 1. Первые шаги 23 ЧАСТЬ I. Основы 53 Глава 2. Переменные и базовые типы 55 Глава 3. Библиотечные типы данных 101 Глава 4. Массивы и указатели 133 Глава 5. Выражения 171 Глава 6. Операторы 217 Глава 7. Функции 251 Глава 8. Библиотека ввода-вывода 309 ЧАСТЬ II. Контейнеры и алгоритмы 331 Глава 9. Последовательные контейнеры 333 Глава 10. Ассоциативные контейнеры 383 Глава 11. Общие алгоритмы 417 ЧАСТЬ III. Абстракция, классы и данные 455 Глава 12. Классы 457 Глава 13. Управление копированием 505 Глава 14. Перегрузка операторов и преобразования 535 ЧАСТЬ IV. Объектно-ориентированное и общее программирование 585 Глава 15. Объектно-ориентированное программирование 587 Глава 16. Шаблоны и общее программирование 655 ЧАСТЬ V. Дополнительные темы 717 Глава 17. Инструменты для крупномасштабных программ 719 Глава 18. Специализированные инструменты и технологии 783 Приложение А. Библиотека 839 Предметный указатель 875
Содержание Введение 19 ГЛАВА 1. Первые шаги 23 1.1. Создание простой программы на языке C++ 24 1.1.1. Компиляция и запуск программы 25 1.2. Первый взгляд на ввод-вывод 27 1.2.1. Стандартные объекты ввода и вывода 28 1.2.2. Программа, использующая библиотеку ввода-вывода 28 1.3. Несколько слов о комментариях 32 1.4. Средства управления 34 1.4.1. Оператор while 34 1.4.2. Оператор for 36 1.4.3. Оператор if 39 1.4.4. Ввод неизвестного количества данных 41 1.5. Введение в классы 42 1.5.1. Класс Sales_item 43 1.5.2. Первый взгляд на функции-члены 46 1.6. Программа на языке C++ 48 Резюме 49 Термины 50 ЧАСТЬ I. Основы 53 ГЛАВА 2. Переменные и базовые типы 55 2.1. Простые встроенные типы 56 2.1.1. Целочисленные типы 57 2.1.2. Типы с плавающей запятой 60 2.2. Литеральные константы 61 2.3. Переменные 65 2.3.1. Что такое переменная? 67 2.3.2. Имя переменной 68 2.3.3. Определение объектов 70 2.3.4. Правила инициализации переменных 73 2.3.5. Объявления и определения 75 2.3.6. Область видимости имен 76 2.3.7. Определение переменных по месту применения 78 2.4. Спецификатор const 78 2.5. Ссылки 81 2.6. Определение имен типов 83 2.7. Перечисления 84
Содержание 7 2.8. Типы классов 85 2.9. Создание собственных файлов заголовка 89 2.9.1. Разработка собственных заголовков 91 2.9.2. Кратко о препроцессоре 93 Резюме 95 Термины 96 ГЛАВА 3. Библиотечные типы данных 101 3.1. Пространства имен и объявления using 102 3.2. Библиотечный тип string 104 3.2.1. Определение и инициализация строк 104 3.2.2. Чтение и запись строк 105 3.2.3. Операции со строками 107 3.2.4. Работа с символами строки 112 3.3. Библиотечный тип vector 114 3.3.1. Определение и инициализация векторов 115 3.3.2. Операции с векторами 117 3.4. Знакомство с итераторами 120 3.4.1. Арифметические действия с итераторами 124 3.5. Библиотечный тип bitset 125 3.5.1. Определение и инициализация наборов битов 125 3.5.2. Операции с наборами битов 127 Резюме 130 Термины 130 ГЛАВА 4. Массивы и указатели 133 4.1. Массивы 134 4.1.1. Определение и инициализация массивов 134 4.1.2. Операции с массивами 137 4.2. Знакомство с указателями 138 4.2.1. Что такое указатель? 138 4.2.2. Определение и инициализация указателей 139 4.2.3. Операции с указателями 144 4.2.4. Использование указателей для доступа к элементам массива 146 4.2.5. Указатели и спецификатор const 151 4.3. Символьная строка в стиле С 154 4.3.1. Динамическое создание массивов 159 4.3.2. Взаимодействие со старым кодом 164 4.4. Многомерные массивы 165 4.4.1. Указатели и многомерные массивы 167 Резюме 168 Термины 168 ГЛАВА 5. Выражения 171 5.1. Арифметические операторы 173 5.2. Операторы отношения и логические операторы 176
8 Содержание 5.3. Побитовые операторы 179 5.3.1. Использование битовых наборов и целочисленных значений 181 5.3.2. Использование операторов сдвига для организации ввода и вывода 183 5.4. Оператор присвоения 184 5.4.1. Оператор присвоения имеет порядок выполнения справа налево 184 5.4.2. Оператор присвоения имеет низкий приоритет 185 5.4.3. Составные операторы присвоения 186 5.5. Операторы инкремента и декремента 187 5.6. Оператор стрелка (->) 189 5.7. Условный оператор 190 5.8. Функция sizeof() 191 5.9. Оператор запятая (,) 192 5.10. Вычисление составных выражений 193 5.10.1. Приоритет 193 5.10.2. Порядок 194 5.10.3. Порядок вычисления операндов 197 5.11. Операторы new и delete 199 5.12. Преобразование типов 2 04 5.12.1. Когда происходит неявное преобразование типов 205 5.12.2. Арифметические преобразования 205 5.12.3. Другие неявные преобразования 207 5.12.4. Явное преобразование 209 5.12.5. Когда может пригодиться приведение типов 209 5.12.6. Именованные операторы приведения 210 5.12.7. Приведение типов в старом стиле 212 Резюме 213 Термины 214 ГЛАВА 6. Операторы 217 6.1. Простые операторы 218 6.2. Операторы объявления 219 6.3. Составные операторы (блоки) 219 6.4. Операторная область видимости 220 6.5. Оператор if 221 6.5.1. Оператор if с разделом else 223 6.6. Оператор switch 225 6.6.1. Использование оператора switch 226 6.6.2. Порядок выполнения внутри оператора switch 227 6.6.3. Метка default 228 6.6.4. Выражение switch и метки case 229 6.6.5. Определение переменной внутри оператора switch 229 6.7. Оператор while 231 6.8. Оператор цикла for 233 6.8.1. Цикл for без частей заголовка 234 6.8.2. Несколько определений в заголовке цикла for 235 6.9. Оператор цикла do...while 236
Содержание 9 6.10. Оператор break 237 6.11. Оператор continue 239 6.12. Оператор goto 239 6.13. Блок try и обработка исключений 241 6.13.1. Оператор throw 242 6.13.2. Блок try 242 6.13.3. Стандартные исключения 245 6.14. Использование препроцессора для отладки 246 Резюме 248 Термины 249 ГЛАВА 7. Функции 251 7.1. Определение функций 251 7.1.1. Тип возвращаемого значения функции 253 7.1.2. Список параметров функции 254 7.2. Передача аргумента 256 7.2.1. Нессылочные параметры 256 7.2.2. Ссылочные параметры 258 7.2.3. Параметры типа векторов и других контейнеров 264 7.2.4. Параметры в виде массива 264 7.2.5. Манипулирование массивами, переданными в функции 267 7.2.6. Функция main(): обработка параметров командной строки 269 7.2.7. Функции с варьирующимися параметрами 270 7.3. Оператор return 271 7.3.1. Функции без возвращаемого значения 271 7.3.2. Функции, возвращающие значение 272 7.3.3. Рекурсия 276 7.4. Объявление функций 277 7.4.1. Значения параметров по умолчанию 278 7.5. Локальные объекты 280 7.5.1. Автоматические объекты 280 7.5.2. Статические локальные объекты 281 7.6. Встраиваемые функции 282 7.7. Функции-члены класса 284 7.7.1. Определение тела функции-члена 284 7.7.2. Определение функции-члена вне класса 287 7.7.3. Создание конструктора Sales_item() 288 7.7.4. Организация файлов кода классов 290 7.8. Перегруженные функции 291 7.8.1. Перегрузка и область видимости 293 7.8.2. Подбор функций и преобразование аргументов 295 7.8.3. Три этапа подбора перегруженной версии 296 7.8.4. Преобразование типов аргументов 299 7.9. Указатели на функции 302 Резюме 305 Термины 306
10 Содержание ГЛАВА 8. Библиотека ввода-вывода 309 8.1. Объектно-ориентированная библиотека 310 8.2. Значения состояния потока 314 8.3. У правление буфером вывода 317 8.4. Ввод и вывод в файл 319 8.4.1. Использование объектов файловых потоков 320 8.4.2. Режимы файла 323 8.4.3. Программа, открывающая и проверяющая файл 325 8.5. Строковые потоки 326 Резюме 329 Термины 329 ЧАСТЬ II. Контейнеры и алгоритмы 331 ГЛАВА 9. Последовательные контейнеры 333 9.1. Определение последовательного контейнера 334 9.1.1. Инициализация элементов контейнера 335 9.1.2. Ограничения типов элементов, которые может содержать контейнер 337 9.2. Итераторы и диапазоны итераторов 339 9.2.1. Диапазоны итераторов 341 9.2.2. Некоторые операции с контейнерами делают итераторы некорректными 343 9.3. Операции с последовательными контейнерами 343 9.3.1. Вспомогательные типы, определенные в классе контейнера 344 9.3.2. Функции-члены begin() и end() 345 9.3.3. Добавление элементов в последовательный контейнер 345 9.3.4. Операторы сравнения 349 9.3.5. Операции с размером контейнера 351 9.3.6. Доступ к элементам 352 9.3.7. Удаление элементов 353 9.3.8. Присвоение и функция swap() 356 9.4. Как увеличивается размер вектора 358 9.4.1. Функции-члены capacity() и reserve() 359 9.5. Как выбрать тип контейнера 361 9.6. Еще раз о строках 363 9.6.1. Дополнительные способы создания строк 365 9.6.2. Дополнительные способы изменения строк 367 9.6.3. Операции, специфические только для строк 369 9.6.4. Операции поиска строк 371 9.6.5. Сравнение строк 374 9.7. Адаптеры контейнеров 376 9.7.1. Адаптер stack 377 9.7.2. Очередь и приоритетная очередь 379 Резюме 380 Термины 380
Содержание 11 ГЛАВА 10. Ассоциативные контейнеры 383 10.1. Предварительные сведения: тип pair 384 10.2. Ассоциативные контейнеры 386 10.3. Тип тар 387 10.3.1. Определение карты 387 10.3.2. Типы, определенные в шаблоне тар 389 10.3.3. Добавление элементов карты 390 10.3.4. Индексация карты 390 10.3.5. Применение функции map::insert() 392 10.3.6. Поиск и возвращение элементов карты 394 10.3.7. Удаление элементов карты 396 10.3.8. Перебор элементов карты 396 10.3.9. Карта преобразования слов 397 10.4. Тип set 399 10.4.1. Определение и применение наборов 400 10.4.2. Создание набора, исключающего слова 401 10.5. Типы multimap и multiset 403 10.5.1. Добавление и удаление элементов 403 10.5.2. Поиск элементов в контейнерах multimap и multiset 404 10.6. Применение контейнеров: программа TextQuery 407 10.6.1. Проект программы 408 10.6.2. Класс TextQuery 409 10.6.3. Применение класса TextQuery 411 10.6.4. Создание функций-членов 413 Резюме 415 Термины 416 ГЛАВА 11. Общие алгоритмы 417 11.1. Краткий обзор 418 11.2. Первый взгляд на алгоритмы 421 11.2.1. Алгоритмы, только читающие элементы контейнера 421 11.2.2. Алгоритмы, записывающие элементы контейнера 424 11.2.3. Алгоритмы, переупорядочивающие элементы контейнера 427 11.3. Возвращаясь к итераторам 432 11.3.1. Итераторы вставки 432 11.3.2. Итераторы ввода-вывода 434 11.3.3. Реверсивные итераторы 439 11.3.4. Константные итераторы 442 11.3.5. Пять категорий итераторов 443 11.4. Структура общих алгоритмов 446 11.4.1. Параметрическая схема алгоритмов 446 11.4.2. Соглашения об именовании алгоритмов 448 11.5. Алгоритмы, специфические для контейнеров 449 Резюме 451 Термины 452
12 Содержание ЧАСТЬ III. Абстракция, классы и данные 455 ГЛАВА 12. Классы 457 12.1. Определение и объявление классов 458 12.1.1. Определение класса 458 12.1.2. Абстракция данных и инкапсуляция 460 12.1.3. Подробнее об определении классов 463 12.1.4. Объявление и определение класса 466 12.1.5. Объекты класса 467 12.2. Неявный указатель this 468 12.3. Область видимости класса 473 12.3.1. Поиск имен в области видимости класса 475 12.4. Конструкторы 480 12.4.1. Список инициализирующих значений конструктора 482 12.4.2. Аргументы по умолчанию и конструкторы 486 12.4.3. Стандартный конструктор 487 12.4.4. Неявное преобразование 490 12.4.5. Явная инициализация переменных-членов класса 492 12.5. Дружественные отношения 493 12.6. Статические члены класса 496 12.6.1. Статические функции-члены 498 12.6.2. Статические переменные-члены 498 Резюме 501 Термины 501 ГЛАВА 13. Управление копированием 505 13.1. Конструктор копий 506 13.1.1. Синтезируемый конструктор копий 509 13.1.2. Определение собственного конструктора копий 510 13.1.3. Предотвращение копирования 511 13.2. Оператор присвоения 512 13.3. Деструктор 514 13.4. Пример обработки сообщения 517 13.5. Работа с указателями 522 13.5.1. Определение классов интеллектуальных указателей 525 13.5.2. Определение классов подобных значению 530 Резюме 532 Термины 533 ГЛАВА 14. Перегрузка операторов и преобразования 535 14.1. Определение перегруженного оператора 536 14.1.1. Проект перегруженного оператора 540 14.2. Операторы ввода и вывода 543 14.2.1. Перегрузка оператора вывода « 543 14.2.2. Перегрузка оператора ввода » 545 14.3. Арифметические операторы и операторы отношения 548
Содержание 13 14.3.1. Операторы равенства 549 14.3.2. Операторы отношения 550 14.4. Операторы присвоения 550 14.5. Оператор индексирования 551 14.6. Операторы доступа к членам класса 553 14.7. Операторы инкремента и декремента 556 14.8. Оператор вызова функции объекта 560 14.8.1. Использование объектов функции с библиотечными алгоритмами 561 14.8.2. Библиотечные объекты функций 563 14.8.3. Адаптеры функций для объектов функций 565 14.9. Преобразования и типы классов 566 14.9.1. Зачем нужны функции преобразования 566 14.9.2. Операторы преобразования 567 14.9.3. Соответствие аргументов и преобразования 571 14.9.4. Поиск перегруженной функции и аргументы класса 575 14.9.5. Перегрузка, преобразования и операторы 579 Резюме 582 Термины 583 ЧАСТЬ IV. Объектно-ориентированное и общее программирование 585 ГЛАВА 15. Объектно-ориентированное программирование 587 15.1. Краткий обзор OOP 588 15.2. Определение базовых и производных классов 590 15.2.1. Определение базового класса 590 15.2.2. Защищенные члены 592 15.2.3. Производные классы 594 15.2.4. Виртуальные и другие функции-члены 597 15.2.5. Открытое, закрытое и защищенное наследование 602 15.2.6. Наследование и дружественные отношения 606 15.2.7. Наследование и статические члены 606 15.3. Преобразования и наследование 607 15.3.1. Преобразование производного класса в базовый 608 15.3.2. Преобразование из базового класса в производный 610 15.4. Конструкторы и функции управления копированием 611 15.4.1. Конструкторы и функции управления копированием базового класса 611 15.4.2. Конструкторы производного класса 612 15.4.3. Управление копированием и наследование 616 15.4.4. Виртуальные деструкторы 618 15.4.5. Виртуальность конструкторов и деструкторов 620 15.5. Область видимости класса при наследовании 621 15.5.1. Поиск имен осуществляется во время компиляции 622 15.5.2. Конфликт имен и наследование 622
14 Содержание 15.5.3. Область видимости и функции-члены 624 15.5.4. Виртуальные функции и область видимости 625 15.6. Чистые виртуальные функции 627 15.7. Контейнеры и наследование 628 15.8. Управляющие классы и наследование 629 15.8.1. Управляющий класс, подобный указателю 630 15.8.2. Клонирование неизвестного типа 633 15.8.3. Использование управляющего класса 634 15.9. Продолжение приложения TextQuery 639 15.9.1. Объектно-ориентированное решение 640 15.9.2. Управляющий класс, подобный значению 641 15.9.3. Класс Query_base 644 15.9.4. Управляющий класс Query 644 15.9.5. Производные классы 647 15.9.6. Виртуальные функции eval() 650 Резюме 652 Термины 653 ГЛАВА 16. Шаблоны и общее программирование 655 16.1. Определение шаблона 656 16.1.1. Определение шаблона функции 657 16.1.2. Определение шаблона класса 659 16.1.3. Параметры шаблона 660 16.1.4. Параметры типа шаблона 662 16.1.5. Параметры значения шаблона 664 16.1.6. Создание общих программ 665 16.2. Создание экземпляра 667 16.2.1. Дедукция аргумента шаблона 669 16.2.2. Явные аргументы шаблона функции 673 16.3. Модели компиляции шаблона 676 16.4. Члены шаблона класса 679 16.4.1. Функции-члены шаблона класса 682 16.4.2. Аргументы шаблона для параметров значения 686 16.4.3. Дружественные отношения в шаблонах класса 688 16.4.4. Объявление дружественных отношений между шаблонами Queue и Queueitem 690 16.4.5. Шаблоны-члены 692 16.4.6. Законченный класс Queue 696 16.4.7. Статические члены шаблонов класса 697 16.5. Общий управляющий класс 698 16.5.1. Определение управляющего класса 699 16.5.2. Применение управляющего класса 701 16.6. Специализация шаблона 703 16.6.1. Специализация шаблона функции 704 16.6.2. Специализация шаблона класса 706 16.6.3. Специализация членов, но не класса 709 16.6.4. Частичная специализация шаблона класса 710
Scannej Ьм Diqrbl Содержание 15 16.7. Перегрузка и шаблоны функций Резюме Термины 711 714 715 ЧАСТЬ V. Дополнительные темы 717 ГЛАВА 17. Инструменты для крупномасштабных программ 719 17.1. Обработка исключений 720 17.1.1. Передача исключения типа класса 721 17.1.2. Прокрутка стека 723 17.1.3. Обработка исключения 724 17.1.4. Повторная передача исключения 727 17.1.5. Обработчик для всех исключений 728 17.1.6. Блок try функции и конструкторы 729 17.1.7. Иерархия класса исключения 729 17.1.8. Автоматическое освобождение ресурсов 731 17.1.9. Класс auto_ptr 733 17.1.10. Спецификация исключений 739 17.1.11. Спецификация исключений указателя на функцию 743 17.2. Пространства имен 744 17.2.1. Определение пространств имен 744 17.2.2. Вложенные пространства имен 749 17.2.3. Неименованные пространства имен 750 17.2.4. Использование членов пространства имен 752 17.2.5. Классы, пространства имен и области видимости 756 17.2.6. Перегрузка и пространства имен 759 17.2.7. Пространства имен и шаблоны 762 17.3. Множественное и виртуальное наследование 763 17.3.1. Множественное наследование 763 17.3.2. Преобразования и несколько базовых классов 766 17.3.3. Управление копированием при множественном наследовании 768 17.3.4. Область видимости класса при множественном наследовании 769 17.3.5. Виртуальное наследование 772 17.3.6. Объявление виртуального базового класса 774 17.3.7. Семантика специальной инициализации 776 Резюме 779 Термины 780 ГЛАВА 18. Специализированные инструменты и технологии 783 18.1. Оптимизация распределения памяти 783 18.1.1. Резервирование памяти в языке C++ 784 18.1.2. Класс allocator 785 18.1.3. Функции operator new() и operator delete() 789 18.1.4. Размещающий оператор new 791 18.1.5. Явный вызов деструктора 793 18.1.6. Операторы new и delete, специфические для класса 794
16 Содержание 18.1.7. Базовый класс системы резервирования памяти 796 18.2. Идентификация типов времени выполнения 803 18.2.1. Оператор dynamic cast 803 18.2.2. Оператор typeid 806 18.2.3. Применение RTTI 808 18.2.4. Класс type info 810 18.3. Указатель на член класса 811 18.3.1. Объявление указателя на член класса 811 18.3.2. Применение указателя на член класса 814 18.4. Вложенные классы 817 18.4.1. Реализация вложенного класса 818 18.4.2. Поиск имен в области видимости вложенного класса 821 18.5. Объединение: экономный класс 823 18.6. Локальные классы 826 18.7. Возможности, снижающие переносимость 828 18.7.1. Битовые поля 828 18.7.2. Спецификатор volatile 830 18.7.3. Директивы компоновки: extern "С" 832 Резюме 835 Термины 836 ПРИЛОЖЕНИЕ А. Библиотека 839 А. 1. Имена и заголовки стандартной библиотеки 839 А.2. Краткий обзор алгоритмов 841 А.2.1. Алгоритмы поиска объекта 841 А.2.2. Другие алгоритмы, осуществляющие только чтение 843 А.2.3. Алгоритмы бинарного поиска 844 А.2.4. Алгоритмы записи в элементы контейнера 845 А.2.5. Алгоритмы сортировки и разделения 847 А.2.6. Общие функции изменения порядка 849 А.2.7. Алгоритмы перестановки 852 А.2.8. Алгоритмы набора для отсортированных последовательностей 853 А.2.9. Минимальные и максимальные значения 854 А.2.10. Числовые алгоритмы 854 А.З. Возвращаясь к библиотеке ввода-вывода 856 А.3.1. Флаги формата 856 А.3.2. Большинство манипуляторов изменяют флаг формата 857 А.3.3. Управление форматом вывода 858 А.3.4. Управление форматом ввода 865 А.3.5. Бесформатные операции ввода-вывода 866 А.3.6. Однобайтовые операторы 866 А.3.7. Многобайтовые операторы 868 А.3.8. Произвольный доступ к потоку 869 А.3.9. Чтение и запись в тот же файл 872 Предметный указатель 875
Посвящается Бет, благодаря которой стало возможным написание этой и всех остальных книг. Посвящается Дэниелю и Анне, для которых возможно практически все. — Стэнли Б. Липпман Посвящается Марку и маме, за их безграничную любовь и поддержку. — Жози Лажойе Посвящается Энди, научившему меня программированию и многому другому. — Барбара Му

Введение Четвертое издание книги Язык программирования C++. Вводный курс, является исчерпывающим учебником для начинающих программистов на языке C++. По- скольку это вводный курс, он содержит простое и понятное описание языка, допол- ненное многочисленными примерами и упражнениями. В отличие от большинства вводных курсов, данное издание содержит подробное описание языка, где основное внимание уделяется наиболее современным и самым эффективным методам про- граммирования. Используя предыдущие издания книги, язык C++ изучило множество програм- мистов. За истекшее время язык C++ претерпел существенные усовершенствования. В последние годы основное внимание уделялось улучшению производительности процессов времени выполнения, а также способам повышения эффективности рабо- ты программистов. Широкая распространенность и доступность стандартной биб- лиотеки позволила изучать и использовать язык C++ более эффективно, чем в про- шлом. В нынешнем издании вводного курса языка C++ эти новые возможности бы- ли учтены. Четвертое издание вводного курса языка C++ полностью реорганизовано и пере- писано с учетом современных стилей программирования на языке C++. Здесь ос- новное внимание сосредоточено на использовании стандартной библиотеки, инкап- сулирующей методы для низкоуровневого программирования. Теперь стандартная библиотека описана с самого начала, а примеры переделаны так, чтобы продемонст- рировать преимущества предоставляемых библиотекой средств. Порядок изложения тем также был изменен и рационализирован. Кроме перестройки структуры текста, книга была дополнена новыми элемента- ми, позволяющими сделать ее более понятной для читателя. Каждая глава заканчи- вается разделом “Резюме”, содержащим перечень наиболее важных тем, и разделом “Термины”, содержащим список использованных терминов. Эти разделы можно ис- пользовать как некую форму списка контрольных вопросов: если термин непонятен, имеет смысл перечитать соответствующую часть главы. При оформлении книги используются соглашения, общепринятые в компьютер- ной литературе. Новые термины в тексте выделяются курсивом. Чтобы обратить внимание чита- теля на отдельные участки текста, также применяется курсив. Текст программ, функций, переменных, URL Web-страниц и другой код пред- ставлен моноширинным шрифтом. Все, что придется вводить с клавиатуры, выделено полужирным моноширинным шрифтом. Знакоместо в описаниях синтаксиса выделено курсивом. Это указывает на необходимость заменить знакоместо фактическим именем переменной, па- раметром или другим элементом, который должен находиться на этом месте BINDSIZE= (максимальная ширина колонки) ★ (номер колонки).
20 Введение Пункты меню и названия диалоговых окон представлены следующим образом: Menu Option (Пункт меню). Исходный код дополнительных примеров книги доступен на Web-сайте по сле- дующему URL: http: //www. awprof essional. com/cpp_primer. Текст некоторых абзацев книги выделен специальным шрифтом. Это примеча- ния, советы и предостережения, которые помогут обратить внимание на наиболее важные моменты в изложении материала и избежать ошибок при работе. Примечания содержат полезную и интересную информацию, которая позволяет бо- лее подробно рассмотреть отдельные детали. Выделенная с помощью этой пиктограммы информация сделает программирование на языке C++ более эффективным. Предостережения содержат информацию, которая поможет избежать возможных неприятностей. Неизменным, по сравнению с прежними изданиями, осталось исчерпывающее описание языка C++ и намерение авторов предоставить полное и ясное руководство. При изучении языка, кроме описания его возможностей, весьма полезно рассмотреть ряд примеров, которые позволят продемонстрировать наилучшее способы его при- менения. Хотя знакомство с языком С (от которого происходит язык C++) не обяза- тельно, авторы подразумевают, что читатель знаком с общими принципами про- граммирования на современных языках, обладающих блочной структурой. Структура книги Эта книга представляет собой вводный курс языка C++, соответствующего меж- дународному стандарту и снабженного обширной библиотекой, являющейся частью этого стандарта. Мощь языка C++ обусловлена его способностью поддерживать аб- стракции при программировании. Эффективное обучение программированию на языке C++ подразумевает не только изучение нового синтаксиса и семантики. Ос- новное внимание следует уделить применению тех возможностей языка C++, кото- рые позволяют быстро создавать надежные приложения, по производительности вполне сравнимые с низкоуровневыми программами, написанными на языке С. Язык C++ обладает обширными возможностями, понять которые новичку до- вольно сложно. Можно формально выделить три составляющие современного языка C++. Низкоуровневый язык, в значительной степени унаследованный от языка С. Дополнительные возможности языка, позволяющие определять собственные ти- пы данных, организовывать крупномасштабные программы и системы.
Введение 21 Стандартная библиотека, которая использует эти дополнительные возможности для обеспечения набора структур данных и алгоритмов. В книгах по языку C++ материал излагается в основном в той же последователь- ности: сначала низкоуровневые детали, а затем дополнительные возможности языка. Стандартная библиотека в этом случае рассматривается только после описания язы- ка в целом. В результате зачастую читатели “тонут” в проблемах низкоуровневого программирования или сложностях определения типов и никак не могут осознать реальную мощь абстрактных подходов программирования. Само собой разумеется, читатели не могут получить достаточную квалификацию, чтобы перейти к созданию своих собственных абстракций. В этом издании преследуется совершенно иная цель. Здесь основы языка и стан- дартная библиотека рассматриваются вместе с самого начала. Это позволяет читате- лю научиться создавать реальные программы. И только после того, как он освоит основы использования библиотеки (а также научится создавать поддерживаемые ею абстрактные программы), имеет смысл переходить к тем возможностям языка C++, которые позволяют создавать собственные абстракции. В частях I, “Основы”, и II, “Контейнеры и алгоритмы”, рассматриваются основы языка, а также возможности библиотеки. Основное внимание уделено созданию программ на языке C++, а также использованию библиотечных абстракций. Мате- риал этой части книги должен быть хорошо известен большинству программистов на языке C++. Этот материал, кроме изучения основ языка C++, служит и другой немаловажной цели. Библиотечные средства — это самостоятельные абстрактные типы данных, созданные на основе языка C++. Библиотека создана при помощи тех же самых средств, которые доступны любому программисту на языке C++. Опыт ав- торов в обучении языку C++ свидетельствует, что, научившись сначала использо- вать хорошо проработанные абстрактные типы, читатели лучше понимают, как соз- давать собственные типы. Части III—V посвящены созданию собственных типов. В части III, “Абстракция, классы и данные”, описана основа языка C++: механизм классов. Механизм классов составляет основу создания собственных абстракций. Кроме того, классы являются основой объектно-ориентированного и общего программирования, рассматриваемо- го в части IV, “Объектно-ориентированное и общее программирование”. Вводный курс завершается частью V, “Дополнительные темы”, в которой рассматриваются дополнительные возможности, используемые при создании больших и сложных систем. Благодарности Как и в предыдущих изданиях этой книги, автор хотел бы выразить отдельную благодарность Бьёрну Страуструпу (Bjarne Stroustrup) за его неустанную работу над языком C++, а также дружбу с авторами на протяжении многих лет. Хотелось бы также поблагодарить Алекса Степанова (Alex Stepanov) за его объяснения, кото- рые подвели к теме контейнеров и алгоритмов, составляющих ядро стандартной библиотеки. И наконец, сердечная благодарность членам комитета по стандарту C++ за их упорную многолетнюю работу по утверждению и усовершенствованию стандарта языка C++.
22 Введение Авторы также выражают глубокую благодарность рецензентам, чьи коммента- рии, замечания и полезные советы помогли улучшить книгу. Спасибо Полу Абра- амсу (Paul Abrahams), Майклу Боллу (Michael Ball), Мэри Дейджфорд (Магу Dageforde), Полю Дюбуа (Paul DuBois), Мэтту Гринвуду (Matt Greenwood), Мэтью П. Джонсону (Matthew Р. Johnson), Эндрю Кенигу (Andrew Koenig), Невину Либеру (Nevin Liber), Биллу Локу (Bill Locke), Роберту Марри (Robert Murray), Филу Романик (Phil Romanik), Джастину Шоу (Justin Shaw), Виктору Штерну (Victor Shtern), Кловису Тондо (Clovis Tondo), Дэвиду Вандевурду (Daveed Vandevoorde) и Стиву Виноски (Steve Vinoski). Эта книга была набрана при помощи системы LaTeX и прилагаемых к ней паке- тов. Авторы выражают глубокую благодарность членам сообщества LaTeX, сделав- шим доступным такой мощный инструмент. Примеры книги были проверены на компиляторах GNU и Microsoft. Благодарим их разработчиков и всех тех, кто создал остальные компиляторы C++, сделав таким образом язык C++ столь популярным. И наконец, благодарим сотрудников издательства Addison-Wesley, которые кури- ровали процесс публикации этой книги. Дебби Лафферти (Debbie Lafferty) — пер- вый редактор, который инициировал это издание и сопровождал его с самого перво- го выпуска. Питер Гордон (Peter Gordon) — новый редактор проекта, чья настойчи- вость при модернизации и рационализации текста, надеемся, существенно его улучшили. Ким Бодихаймер (Kim Boedigheimer) контролировал график выполнения работ. Тиррелл Олбах (Tyrrell Albaugh), Джим Маркхэм (Jim Markham), Элизабет Райан (Elizabeth Ryan) кДжон Фаллер (John Fuller) помогали авторам на протяже- нии всего проекта. От издательства Вы, читатель этой книги, и есть главный ее критик и комментатор. Мы ценим ва- ше мнение и хотим знать, что было сделано нами правильно, что можно было сде- лать лучше и что бы еще вы хотели увидеть изданным нами. Нам интересно услы- шать и любые другие замечания, которые вам хотелось бы высказать авторам. Мы ждем ваших комментариев. Вы можете прислать письмо по электронной поч- те или просто посетить наш Web-сервер, оставив на нем свои замечания, — одним словом, любым удобным для вас способом дайте нам знать, нравится или нет вам эта книга, а также выскажите свое мнение о том, как сделать наши книги более подхо- дящими для вас. Посылая письмо или сообщение, не забудьте указать название книги и ее авторов, а также ваш e-mail. Мы внимательно ознакомимся с вашим мнением и обязательно учтем его при отборе и подготовке к изданию следующих книг. Наши координаты: E-mail: info@williamspublishing.com WWW: http: //www.williamspublishing. com
ГЛАВА 1 Первые шаги В ЭТОЙ ГЛАВЕ... 1.1. Создание простой программы на языке C++ 24 1.2. Первый взгляд на ввод-вывод 27 1.3. Несколько слов о комментариях 32 1.4. Средства управления 34 1.5. Введение в классы 42 1.6. Программа на языке C++ 48 Резюме 49 Термины 50 В этой главе описана большая часть базовых элементов языка C++: встроенные, библиотечные и классовые типы данных, а также переменные, выражения, операто- ры и функции. Кроме того, здесь кратко объясняется, как откомпилировать про- грамму и запустить ее на выполнение. Изучив эту главу и выполнив соответствующие упражнения, читатель будет спосо- бен создать, откомпилировать и запустить на выполнение простую программу. В по- следующих главах представленные здесь темы рассматриваются более подробно. Лучше всего изучать новый язык программирования в процессе создания про- грамм. В этой главе будет создана программа, которая способна решить простую за- дачу. Речь идет о вполне обычной задаче по обработке данных: книжный магазин хранит файл транзакций, каждая из записей которого соответствует проданной кни- ге. Каждая транзакция содержит ISBN (International Standard Book Number — меж- дународный стандартный номер книги), представляющий собой уникальный иден- тификатор, присваиваемый большинству книг, публикуемых во всем мире. Кроме ISBN, транзакция содержит количество проданных копий и цену. Каждая транзак- ция выглядит так: 0-201-70353-Х 4 24.99, где первый элемент представляет со- бой ISBN, второй — количество проданных книг, а последний — цену. Владелец книжного магазина периодически читает этот файл и вычисляет количество продан- ных экземпляров каждой книги, общий доход по ней и среднюю цену за экземпляр. Итак, предстоит создать программу, которая и осуществит эти вычисления. Однако прежде чем приступить к разработке данной программы, имеет смысл изучить основные возможности языка C++. Как минимум, необходимо выяснить, как написать, откомпилировать и запустить на выполнение простую программу. Но
24 Введение что же эта программа должна делать? Хотя окончательное решение еще не вырабо- тано, в ее задачи входит следующее. Определить переменные. Обеспечить ввод и вывод. Определить структуру для хранения данных, которыми предстоит манипу- лировать. Проверять, не существует ли двух записей с одинаковым ISBN. Использовать цикл для обработки каждой записи в файле транзакций. Давайте сначала изучим необходимые элементы языка C++, а затем выработаем решение по проблеме книжного магазина. 1.1. Создание простой программы на языке C++ Каждая программа на языке C++ содержит одну или несколько функций (function). Главная (обязательно присутствующая) функция носит имя main (). Функция состоит из последовательности операторов (statement), которые и выпол- няют необходимые действия. Операционная система запускает программу на вы- полнение вызывая функцию main (). Эта функция выполняет свои операторы и возвращает значение операционной системе. В приведенном ниже примере простая функция main () ничего не делает, только возвращает значение 0. int main() { return 0; } Операционная система использует возвращенное функцией main () значение, чтобы выяснить, нормально ли слаботала программа или произошел сбой. Возвра- щение значения 0 свидетельствует об успехе. Функция main () во многом уникальна, однако важнейшей ее особенностью яв- ляется то, что она должна существовать в каждой программе на языке C++, ведь именно ее (и только ее) операционная система вызывает явно. Определение функции main () осуществляется аналогично любой другой функ- ции. Определение функции содержит четыре элемента: возвращаемый тип (return type), имя функции (function name), заключенный в круглые скобки список парамет- ров (parameter list), который может быть пустым, и тело функции (function body). Функция main () может иметь лишь ограниченный набор параметров. В приведен- ном выше примере список параметров пуст. Более подробная информация о пара- метрах функции main () приведена в разделе 7.2.6 (стр. 269). Функция main () должна иметь возвращаемый тип int, который соответствует целым числам. Тип int — это встроенный тип (built-in type) данных. Встроенный тип определен в самом языке. Заключительная часть определения функции — это ее тело, представляющее со- бой блок (block) операторов, который начинается открывающей фигурной скоб- кой (curly brace) и завершается закрывающей фигурной скобкой.
Глава 1. Первые шаги 25 return 0; Единственным оператором этой программы является оператор return, который завершает код функции. Обратите внимание на точку с запятой в конце оператора return. Точкой с запятой отмечают конец большинства операторов языка C++. Его очень просто пропустить, и это / приводит к передаче компилятором непонятного сообщения об ошибке. Значение, переданное оператору return, например 0, и будет значением, воз- вращаемым функцией. Возвращенное значение должно либо иметь возвращаемый тип, либо допускать преобразование в него. В приведенном выше примере возвра- щаемый тип функции main () указан как int и значение 0 также имеет тип int. В большинстве операционных систем возвращаемое функцией main () значение используется как индикатор состояния. Возвращение значения 0 свидетельствует об успешном завершении выполнения функции main (). Любое другое значение, как правило, означает отказ, а само значение указывает на его причину. Каждая опера- ционная система имеет свой собственный способ использования сообщений, воз- вращаемых функцией main () ‘. 1.1.1. Компиляция и запуск программы Написанную программу необходимо откомпилировать. Способ компиляции программы зависит от используемой операционной системы и компилятора. Более подробная информация о работе каждого конкретного компилятора приведена в его документации. Большинство PC-ориентированных компиляторов обладают интегрированной средой разработки (Integrated Development Environment — IDE), которая объединя- ет компилятор с соответствующими средствами редактирования и отладки кода. Эти средства весьма удобны при разработке сложных программ, однако ими следует научиться пользоваться. Большинство таких систем обладают графическим пользо- вательским интерфейсом, позволяющим разработчику создавать, компилировать и запускать программы используя различные меню, панели и кнопки. Описание по- добных систем выходит за рамки этой книги. Большинство компиляторов, включая укомплектованные IDE, обладают интер- фейсом командной строки. Если читатель не очень хорошо знаком с IDE используе- мого компилятора, то, возможно, имеет смысл начать с применения более простого интерфейса командной строки. Это позволит избежать необходимости сначала изу- чать IDE, а затем сам язык. Соглашение об именовании файлов исходного кода Используется ли интерфейс командной строки, или IDE, большинство компиля- торов ожидает, что подлежащая компиляции программа будет сохранена в файле. Файл, содержащий написанный разработчиком текст программы, называют файлом 1 У операционной системы MS DOS, например, это команда ERRORLEVEL. См. коман- ду DOS IF. — Примеч.ред.
26 Введение исходного кода (source file). В большинстве систем имя файла исходного кода состо- ит из двух частей: имени файла (например progl) и расширения. В соответствии с соглашением об именовании, расширение указывает на принадлежность файла к программам. Расширение зачастую указывает также на то, для какого именно языка написана программа. Это позволит автоматически выбрать соответствующий ему компилятор. Для файлов исходного кода примеров этой книги используется расши- рение . сс, что соответствует программам на языке C++. Таким образом, сохраним текст разрабатываемой программы в файле progl. сс. Расширение файлов исходного кода программ зависит от используемого компи- лятора C++, поэтому существуют и другие соглашения. Рассмотрим пример. progl.схх progl.срр progl.ср progl.С Вызов компилятора GNU или Microsoft Конкретная команда, используемая для вызова компилятора C++, зависит от приме- няемой операционной системы и версии компилятора. Наибольшее распространение получили компилятор GNU и компилятор C++ из комплекта Microsoft \ isual Studio. По умолчанию для вызова компилятора GNU используется команда д++. $ д++ progl.сс -о progl Здесь $ — это системное приглашение к вводу. Цанная команда создает исполняемый файл progl или progl.exe, в зависимости от операционной системы. На операци- онной системе UNIX исполняемые файлы не имеют расширения, а в операционной системе Windows они имеют расширение .ехе. Насть -о progl — это аргумент компилятора и имя получаемого исполняемого файла. Если пропустить аргумент -о progl, компилятор создаст исполняемый файл по имени a.out (на системе UNIX , или а. ехе ( на Windows). , (ля вызова компилятора Microsoft используется команда cl. С:\directory> cl -GX progl.срр Здесь С: \directory> — это системное приглашение к вводу, a directory — имя те- кущего каталога. Команда вызова компилятора — это cl, a -GX, соответственно, пара- метр, который указывает на необходимость откомпилировать программу используя интерфейс командной строки. Компилятор Microsoft автоматически создает испол- няемый файл, имя которого соответствует имени файла исходного кода. Исполняе- мый файл получит расширение . ехе и имя, совпадающее с именем файла исходного кода. В данном случае получится исполняемый файл progl. ехе. Нолее подробная информация по этой теме содержится в руководстве программиста, прилагаемом к компилятору. Запуск компилятора из командной строки При использовании интерфейса командной строки, процесс компиляции, как правило, отображается в окне консоли (например в окне оболочки (на UNIX) или окне сеанса MS-DOS (на Windows)). Подразумевая, что исходный код функции main () находится в файле по имени progl. сс, его можно откомпилировать при по- мощи следующей команды. $ СС progl.сс
Глава 1. Первые шаги 27 Где СС — это имя компилятора, а $ — системное приглашение к вводу. В резуль- тате работы компилятора получится исполняемый файл, запустить который можно введя его имя. В данном случае компилятор создаст исполняемый файл по имени а. ехе, а компилятор для UNIX создал бы исполняемый файл по имени а. out. Что- бы запустить исполняемый файл, в командной строке, после приглашение к вводу, следует ввести его имя. $ а.ехе В результате только что откомпилированная программа будет запущена на вы- полнение. На операционной системе UNIX иногда необходимо также указать ката- лог, в котором находится файл, даже если он находится в текущем каталоге. В таком случае применяется следующая форма записи. $ ./а.ехе Символ “. ”, следующий за наклонной чертой, означает, что файл находится в те- кущем каталоге. Способ доступа к значению, возвращаемому из функции main (), зависит от ис- пользуемой операционной системы. В обеих операционных системах (UNIX и Windows) после выполнения программы можно ввести команду echo с соответст- вующим параметром. На UNIX для выяснения состояния выполненной программы применяется следующая команда. $ echo $ ? В операционной системе Windows для этого применяется следующая команда. С:\directory> echo %ERRORLEVEL% Упражнения раздела 1.1.1 Упражнение 1.1. Просмотрите документацию по используемому компилятору и выясните, какое соглашение об именовании файлов он использует. Откомпилируйте и запустите на выполнение программу, функция main () которой приведена на стр. 24. Упражнение 1.2. Измените код программы так, чтобы функция main() возвращала значение -1. Возвращение значения -1 зачастую свидетельствует о сбое при выполнении программы. Но опе- рационные системы реагируют на сообщение функции main () об отказе по-разному (если во- обще реагируют на него). Перекомпилируйте и повторно запустите программу, чтобы увидеть, как используемая операционная система реагирует на свидетельство об отказе функции main о. 1.2. Первый взгляд на ввод-вывод В самом языке C++ никаких операторов для ввода и вывода (Input/Output — Ю) нет, их предоставляет стандартная библиотека (standard library). Библиотека пре- доставляет обширный набор подобных средств, однако для большинства задач, включая примеры этой книги, вполне достаточно изучить лишь несколько фунда- ментальных концепций и простых операций. В большинстве примеров этой книги использована библиотека iostream, кото- рая предоставляет средства для форматированного ввода и вывода. Основу библио- теки iostream составляют два типа, istream и ©stream, которые представляют потоки, соответственно, ввода и вывода. Поток (stream) — это последовательность
28 Введение символов, записываемая или читаемая из устройства ввода-вывода некоторым спо- собом. Термин “поток” подразумевает, что символы поступают и передаются после- довательно, на протяжении определенного времени. 1.2.1. Стандартные объекты ввода и вывода В библиотеке определено четыре объекта ввода-вывода. Для осуществления ввода используется объект с in (произносится “си-ин”) типа istream. Этот объ- ект упоминают также как стандартный ввод (standard input). Для вывода исполь- зуется объект cout (произносится “си-аут”) типа ©stream. Его зачастую упоми- нают как стандартный вывод (standard output)2. В библиотеке определено еще два объекта типа ©stream — это сегг и clog (произносится “си-ерр” и “си-лог” соот- ветственно). Объект сегг, называемый также стандартной ошибкой (standard error), как правило, используется в программах для создания предупреждений и сообщений об ошибках. Объект clog обычно используется для создания инфор- мационных сообщений. Как правило, операционная система ассоциирует каждый из этих объектов с ок- ном, в котором выполняется программа. Так, при получении данных объектом с in они считываются из того окна, в котором выполняется программа. Аналогично, при выводе данных объектами cout, сегг или clog, они отображаются в том же окне. Большинство операционных систем позволяют переадресовывать потоки ввода и выхода данных. Применение переадресации позволяет ассоциировать эти пото- ки с определенными файлами. 1.2.2. Программа, использующая библиотеку ввода-вывода Ранее уже было продемонстрировано, как откомпилировать и запустить на вы- полнение простую программу, хотя она ничего и не делала. Однако в ходе решения поставленной задачи необходимо будет объединить записи с совпадающими ISBN, чтобы выяснить общее количество проданных книг. Таким образом, необходимо научиться осуществлять сложение. Используя библиотеку ввода-вывода можно модифицировать прежнюю про- грамму так, чтобы она запрашивала у пользователя два числа, а затем вычисляла их сумму и выводила на экран. #include <iostream> int main() { std::cout « "Enter two numbers:" << std::endl; int vl, v2; std::cin >> vl » v2; std::cout « "The sum of " « vl « " and " « v2 << " is " << vl + v2 « std::endl; return 0; ) 2 Стандартным устройством ввода является клавиатура, а стандартным устройством вывода — экран. — Примеч. ред.
Глава 1. Первые шаги 29 Вначале программа отображает на экране приглашение пользователю ввести два числа. Enter two numbers: Затем программа ожидает ввода. Предположим, пользователь ввел следующие два числа и нажал клавишу <Enter>. 3 7 В результате программа отобразит следующее сообщение. The sum of 3 and 7 is 10 Первая строка кода — это директива препроцессора (preprocessor directive), кото- рая указывает компилятору3 на необходимость включить в программу библиотеку ©stream. #include <iostream> Имя внутри угловых скобок — это заголовок (header). Каждая программа, которая использует средства, хранимые в библиотеке, должна подключить соответствующий заголовок. Директива #include должна быть написана в одной строке. То есть и за- головок, и слово #include должны находиться в той же строке кода. Директива #include должна располагаться вне тела функции. Как правило, все директивы #include программы располагают в начале файла исходного кода. Запись в поток Первый оператор в теле функции main () выполняет выражение (expression). В языке C++ выражение состоит из одного или нескольких операндов (operand) и, как правило, представляет собой оператор (operator). Чтобы отобразить подсказку на стандартном устройстве вывода, в этом выражении используется оператор выво- да (output operator) (оператор <<). std::cout « "Enter two numbers:" « std::endl; Здесь оператор вывода использован дважды. Каждый экземпляр оператора выво- да получает два операнда: левый операнд (объект ©stream) и правый операнд (подлежащее отображению значение). Оператор заносит операнд расположенный справа, в объект ©stream, который является его левым операндом. В языке C++ каждое выражение возвращает результат, который обычно является значением, полученным в ходе применения оператора к его операндам. В случае оператора вывода, результатом является значение его левого операнда. То есть зна- чением, возвращенным операцией вывода-вывода, является сам поток. Тот факт, что оператор возвращает свой левый операнд, позволяет связать вместе несколько запросов на вывод. Оператор, отображающий на экране приглашение пользователю ввести два числа, эквивалентен следующему. (std::cout « "Enter two numbers:") « std::endl; Поскольку часть (std::cout << "Enter two numbers:") возвращает свой левый операнд, std: : ©out, этот оператор эквивалентен следующему. 3 На самом деле препроцессору. Компилятор получит готовый промежуточный файл, в со- став которого войдет содержимое подключенной библиотеки. — Примеч. ред.
30 Введение std::cout « "Enter two numbers:"; std::cout « std::endl Слово endl — это специальное значение, называемое манипулятором (manipulator). При его записи в поток вывода происходит переход на новую строку и сброс буфера, связанного с данным устройством. Сброс буфера гарантирует, что записанные в по- ток вывода данные будут отображены на экране немедленно. Во время отладки программисты зачастую добавляют операторы вывода промежуточ- ных значений. Для таких операторов всегда следует применять сброс потока. Если это- го не сделать, оставшиеся в буфере вывода данные, в случае сбоя программы, могут ввести в заблуждение разработчика, неправильно засвидетельствовав место возник- новения проблемы. Использование имен из стандартной библиотеки Внимательный читатель, вероятно, обратил внимание на то, что в этой программе использована форма записи std: :cout и std: :endl, а не просто cout и endl. Префикс std: : означает, что имена cout и endl определены внутри пространства имен (namespace) по имени std. Пространства имен позволяют избежать вероятных конфликтов, причиной которых является совпадение имен, определенных в разных библиотеках. Поскольку определенные в стандартной библиотеке имена указаны как принадлежащие конкретному пространству имен, те же самые имена можно ис- пользовать и в собственных целях. Побочным эффектом применения пространств имен библиотек является то, что названия используемых пространств приходится указывать явно, например std. В записи std: : cout применяется оператор области видимости (scope operator) : :, позволяющий указать, что здесь используется имя cout, которое определено в про- странстве имен std. Как будет продемонстрировано в разделе 3.1 (стр. 102), сущест- вует способ, позволяющий программисту избежать частого использования подроб- ного синтаксиса. Чтение из потока Отобразив приглашение к вводу, необходимо организовать чтение введенных пользователем данных. Сначала следует определить две переменные (variable), в данном случае vl и v2, которые и будут содержать введенные данные. int vl, v2; Эти переменные определены как относящиеся к типу int, который является встроенным типом данных для целочисленных значений. Сейчас эти переменные не инициализированы, т.е. им не присвоено никаких исходных значений. Следователь- но, первым действием при их использовании должно быть присвоение значений (в данном случае чтение данных). Тот факт, что они не имеют исходного значения, со- вершенно не важен, это даже является преимуществом. Следующим является оператор чтения введенных пользователем данных. std::cin » vl » v2; Оператор ввода (input operator) (т.е. оператор >>) ведет себя аналогично оператору вывода. Его левым операндом является объект типа istream, а правым операндом — объект, заполняемый данными. Он читает значение из потока, представляемого объек-
Глава 1. Первые шаги 31 том типа istream, и сохраняет его в правом операнде. Подобно оператору вывода, оператор ввода возвращает в качестве результата свой левый операнд. Это позволяет объединить в одном операторе последовательность из нескольких запросов на ввод данных. Другими словами, эта операция эквивалентна следующей. std::cin » vl; std::cin >> v2; В результате операции ввода два значения будут считаны со стандартного уст- ройства ввода и сохранены в переменных vl и v2. Завершение программы Теперь осталось лишь вывести результат сложения на экран. std::cout « "The sum of " « vl « " and " « v2 « " is " « vl + v2 « std::endl; Хоть этот оператор и значительно длиннее оператора, отобразившего приглаше- ние к вводу, принципиально он ничем не отличается. Он передает значения каждого из своих операндов в поток стандартного устройства вывода. Здесь интересен тот факт, что не все операнды имеют одинаковый тип значений. Некоторые из них яв- ляются строковыми литералами (string literal), например "The sum of ", другие значения относятся к типу int, например vl и v2, а третьи представляют собой ре- зультат вычисления арифметического выражения vl + v2. В библиотеке iostream определены версии операторов ввода и вывода для всех встроенных типов данных. Как правило, в коде программы на языке C++ пробел можно заменить переходом на но- зз/F I вую строку> Однако это правило имеет два исключения: пробелы внутри строкового ли- терала нельзя заменить символом новой строки. Второе исключение: пробелы внутри -& директив препроцессора. с . . _ . Scanpeq pu Migro' Фундаментальная концепция. Инициализированные и неинициализированные переменные Инициализация (initialization) — важнейшая концепция языка C++, к которой еще не раз придется вернуться в этой книге. Инициализированными являются те переменные, значения которым присвоены при определении. Неинициализированным переменным исходные значения не присваи- ваются. Рассмотрим пример. int vail =0; // инициализирована int val2; //не инициализирована Исходное значение переменной присваивается почти всегда, но делать это не обяза- тельно. Когда известно, что первым де) гствием с переменной будет присвоение ей но- вого значения, нет никакой необходимости в назначении ей исходного значения. На- пример, в первой реальной программе книги (стр. 28) определены две неинициализи- рованные переменные, в которые немедленно считываются значения. Но если нет уверенности в том, что значение переменной будет присвоено, прежде чем она окажется использована для других целей, имеет смысл присвоить ей исходное значение при определении. Если нельзя гарантировать, что значение переменной бу- дет обнулено перед чтением, ее также следует инициализировать.
32 Введение Упражнения раздела 1.2.2 Упражнение 1.3. Напишите программу, которая выводит на стандартное устройство вывода фра- зу "Hello, World". Упражнение 1.4. Чтобы получить сумму двух чисел, предыдущая программа использовала встро- енный оператор сложения (+). Напишите программу, которая использует оператор умножения (*) для вычисления произведения двух чисел. Упражнение 1.5. В предыдущей программе весь вывод осуществлял один большой оператор. Пе- репишите программу так, чтобы для вывода на экран каждого операнда использовался отдельный оператор. Упражнение 1.6. Объясните, что делает следующий фрагмент кода. std::cout « "The sum of " « vl; « " and " << v2; << " is " « vl + v2 « std::endl; Допустим ли этот код? Если да, то почему? Если нет, то почему? 1.3. Несколько слов о комментариях Прежде чем перейти к более сложным программам, давайте рассмотрим коммен- тарии языка C++. Комментарий (comment) помогает человеку, читающему исход- ный текст программы, понять ее смысл. Как правило, комментарий используется для кратких заметок об используемом алгоритме, о назначении переменных или для до- полнительных разъяснений сложного фрагмента кода. Комментарии не увеличива- ют размер исполняемой программы, поскольку компилятор их игнорирует. В этой книге комментарии выделены курсивом, чтобы отличить их от обычного кода про- 1 граммы. Обычно выделение текста комментариев определяется возможностями исполь- зуемой среды разработки. В языке C++ существует два вида комментариев: для одной строки и парный. Однострочный комментарий начинается символом двойной наклонной черты (//). Все, что находится справа от этого символа и до конца текущей строки, является комментарием, игнорируемым компилятором. Второй тип комментария, заключенного в пару символов / * * /, унаследован от языка С. Такие комментарии начинаются символом / * и завершаются символом */. Все, что находится между символами / * и */, компилятор считает комментарием. #include <iostream> /* Пример функции main(). Читает два числа и отображает их сумму */ int main() { // Предлагает пользователю ввести два числа std::cout « "Enter two numbers:" « std::endl; v2; int vl, std: cin » vl >> v2 // инициализирована // не инициализирована std::cout "The sum of << vl « " and " << v2 is " << vl + v2 « std::endl; return 0;
Глава 1. Первые шаги 33 Внутри парного комментария могут располагаться любые символы, включая символ табуляции, пробел и символ новой строки. Парный комментарий может занимать несколько строк кода, но это не обязательно. Когда парный комментарий занимает несколько строк кода, имеет смысл указать визуально, что эти строки принадлежат многострочному комментарию. Применяемый в этой книге стиль предполагает использование для обозначения внутренних строк многострочного комментария символы звездочки. Таким образом, символ звездочки в начале стро- ки свидетельствует о ее принадлежности к многострочному комментарию (но это необязательно ). В программах обычно используются обе формы комментариев. Парные коммен- тарии, как правило, используют для многострочных объяснений4, а двойную на- клонную черту — для замечаний в той же строке, что и код. Слишком длинные комментарии могут ухудшить читабельность кода. Обычно блок комментариев помещают выше кода, к которому он относится. По мере изменения кода комментарии следует обновлять. Программисты пола- гают, что комментарии правдивы, и верят им, даже если остальная документация свидетельствует об ином. Некорректный комментарий хуже отсутствия коммента- рия, поскольку он может ввести читателя в заблуждение. Парный комментарий не допускает вложения Комментарий, который начинается символом /*, всегда завершается следую- щим символом * /. Поэтому один парный комментарий не может находиться внут- ри другого. Сообщение о подобной ошибке, выданное компилятором, как прави- ло, вызывает удивление. Попробуйте, например, откомпилировать следующую программу. #include <iostream> / * * парный комментарий /* */ не допускает вложения * под "не допускает вложения" следует понимать, что остальная часть * текста будет рассматриваться как программный код ★ I int main() { return 0; } При комментировании большого участка кода, который необходимо временно отключить, внутри может оказаться уже имевшийся ранее парный комментарий, что приведет к возникновению проблемы. Поэтому для временного отключения большого раздела кода имеет смысл вставлять символ двойной наклонной черты в начале каждой строки кода, которую необходимо игнорировать. Таким образом, можно не волноваться о том, что внутри закомментированного кода окажется пар- ный комментарий5. 4 А также для временного отключения больших фрагментов кода при отладке. — Примеч. ред. s Есть и другой способ — все “постоянные” комментарии писать после двойной наклонной черты, а для “временных” использовать парный комментарий. Так и вложений можно избежать, и упростить последующий поиск временно закомментированных участков. — Примеч. ред.
34 Введение Упражнения раздела 1.3 Упражнение 1.7. Откомпилируйте программу, которая содержит вложенный комментарий. Упражнение 1.8. Укажите, какой из следующих операторов вывода (если он есть) является допустимым. std::cout « std::cout « " */ "; std::cout « /* "*/” */ ; Откомпилируйте программу с этими тремя операторами и проверьте правильность своего ответа. Исправьте ошибки, сообщения о которых были получены. 1.4. Средства управления Операторы выполняются последовательно: сначала первый оператор функции, затем второй и т.д. Безусловно, при последовательном выполнении операторов много задач не решить (включая проблему книжного магазина). Для управления последовательностью выполнения операторов, все языки программирования пре- доставляют определенные средства. В этом разделе кратко рассматривается неко- торые из подобных средств управления языка C++. Более подробно эти операторы рассматривается в главе 6, “Операторы”. 1.4.1. Оператор while Оператор while организует итерационное (циклическое) выполнение операто- ров. Используя оператор while можно написать следующую программу, сумми- рующую числа от 1 до 10 включительно, ttinclude <iostream> int main() { int sum = 0, val = 1; // продолжать выполнение цикла, пока значение val II не превысит 10 while (val <= 10) { sum += val; // присвоить sum сумму val и sum ++val; // добавить 1 к val ) std::cout « “Sum of 1 to 10 inclusive is " « sum « std::endl; return 0; Будучи откомпилированной и запущенной на выполнение, эта программа ото- бразит на экране следующий результат. Sum of 1 to 10 inclusive is 55 Как и прежде, программа начинается с подключения заголовка iostream и оп- ределения функции main (). Внутри функции main () определены две переменные типа int — sum, которая будет содержать полученную сумму, и val, которая будет содержать каждое из значений от 1 до 10. Переменной sum присваивается исходное значение 0, а переменной val — исходное значение 1.
Глава 1. Первые шаги 35 Очень важное значение имеет оператор while. Он имеет следующий синтаксис. while (условие) тело_оператора_мЬИе; Оператор while циклически выполняет тело_ опера тора_ while, пока условие остается истинным. Условие (condition) представляет собой выражение, результат вычисления кото- рого подлежит проверке. Если полученное в результате значение отлично от нуля, условие считается истинным (true), а если значение равно нулю — ложным (false). Если условие истинно (выражение возвращает значение, отличное от нуля), выполняется тело_оператора_ы11Ие. После выполнения оператора условие проверяется снова. Если условие остается истинным, оператор выполняется снова. Таким образом, проверка условия и выполнение оператора продолжается до тех пор, пока условие не станет ложным. В этой программе использован следующий оператор while. // продолжать выполнение цикла, пока значение val не превысит 10 while (val <= 10) { sum += val; // присвоить sum сумму val и sum ++val; // добавить 1 к val ) В условии цикла while, для сравнения текущего значения переменной val и числа 10, использован оператор меньше или равно (<=). Пока значение переменной val меньше или равно 10, выполняется тело цикла while. В данном случае телом цикла while является блок, содержащий два оператора. { sum += val; // присвоить sum сумму val и sum ++val; // добавить 1 к val } Блок (block) — это последовательность операторов, заключенных в фигурные скобки. В языке C++ блок можно использовать в любом месте, где допустим один оператор. Первым в блоке является составной оператор присвоения (compound assignment operator), или оператор присвоения с суммой (+=). Этот оператор добав- ляет свой правый операнд к левому операнду. Это эквивалентно двум операторам: суммы и присвоения. sum = sum + val; // присвоить sum сумму val и sum Таким образом, первый оператор добавляет значение переменной val к текуще- му значению переменной sum и сохраняет результат в той же переменной sum. Следующее действие использует префиксный оператор инкремента (prefix increment operator) (++), который осуществляет приращение. ++val; // добавить 1 к val Оператор инкремента добавляет единицу к своему операнду. Запись ++ val эк- вивалентна выражению val = val + 1. После выполнения тела, цикл while снова проверяет условие продолжения. Если после нового увеличения значение переменной val все еще меньше или равно 10, тело цикла while выполняется снова. Проверка условия и выполнение тела цикла про- должится до тех пор, пока значение переменной val остается меньше или равно 10.
36 Введение Как только значение переменной val станет больше 10, происходит выход из цикла while и управление переходит к оператору, следующему за ним. В данном случае это оператор, отображающий результат на экране, за которым следует опера- тор return, завершающий функцию main () и саму программу. Фундаментальная концепция. Отступ и выравнивание при оформлении кода C++ Оформление исходного кода программ на языке C++ не имеет жестких правил, по- этому расположение фигурных скобок, отступ, выравнивание, комментарии и разрыв строк, как правило, никак не влияет на полученную в результате компиляции про- грамму. Например, фигурная скобка, обозначающая начало тела функции main (), может находиться на одно] i строке со словом main (как в этой книге), в начале сле- дующей строки или где нибудь дальше. Единственное требование — чтобы между ней и круглыми скобками, в которые заключен список параметров функции main (), не было никаких печатаемых или ^закомментированных символов. Хотя исходны!I код вполне можно оформлять по своему усмотрению, необходимо все же позаботиться о его удобочитаемости. Можно, например, написать всю функцию main () в одной длинной строке. Такая форма записи вполне допустима, но читать подобный код будет крайне неудобно. До сих пор не стихают бесконечные дебаты по поводу наилучшего способа оформле- ния кода программ на языках C++ и С. Авторы убеждены, что единственно правиль- ного с гиля не существует, все зависит от личных предпочтений разработчика. Однако в коде этой книги принято размещать фигурные скобки, которые разграничивают функции, в собственных строках, а выравнивание составных операторов ввода и выво- да осуществлять так, чтобы отступ операндов совпадал, как это сделано для оператора std: : cout в коде функции main () на стр. 28. Другие соглашения будут описаны по мере усложнения программ. Не забывайте, что существуют и другие способы оформления кода. При выборе стеля оформления учитывайте удобочитаемость кода, а выбрав сталь, придерживайтесь его неукоснительно на протяжении всей программы. 1.4.2. Оператор for В рассмотренном ранее цикле while, для управления количеством итераций ис- пользовалась переменная val. При каждой итерации значение переменной val про- верялось, а затем в теле цикла while оно увеличивалось. Применение для управления циклом переменной, подобной val, столь популяр- но, что было разработано второе средство управления — оператор for, который по- зволяет избавиться от кода, который увеличивает значение управляющей перемен- ной. Используя оператор for, код программы, суммирующей числа от 1 до 10, мож- но было бы переписать следующим образом. #include <iostream> int main() { int sum - 0; // сложить числа от 1 до 10 включительно
Глава 1. Первые шаги 37 for (int val = sum += val 1; val <= 10; ++val) // эквивалентно sum = sum + val std::cout "Sum of 1 to 10 inclusive is " sum « std::endl; return 0; } Переменная sum определена и инициализирована нулевым значением до цикла for. Переменная val, используемая только внутри цикла, определена как часть са- мого оператора for. Оператор for состоит из двух частей: заголовка и тела, for (int val = 1; val <- 10; ++val) sum += val; // эквивалентно sum = sum + val Заголовок задает количество циклов выполнения тела. Сам заголовок содержит три части: оператор_инициализации, условие и выражение. В данном случае оператор_ инициализации (int val = 1;) определяет переменную val типа int, а также присваивает ей исходное значение (1). В цикле for оператор инициализации вы- полняется только один раз. Условие (val <= 10;), где в данном случае сравнивается текущее значение пе- ременной val и числа 10, проверяется на каждом цикле. Пока значение переменной val меньше или равно 10, тело цикла for выполняется. Выражение выполняется только после тела цикла. В данном цикле for, выражением является префиксный оператор инкремента, который, как известно, увеличивает значение переменной val на единицу. После выполнения выражения цикл for снова проверяет условие. Если новое значение переменной val все еще меньше или равно 10, тело цикла for вы- полняется снова, а значение переменной val увеличивается. Так продолжается до тех пор, пока условие истинно. В данном цикле for тело осуществляет суммирование. sum += val; // эквивалентно sum = sum + val Здесь использован составной оператор присвоения, который складывает текущие значения переменных val и sum, а результат сохраняет снова в переменной sum. Таким образом, выполнение данного оператора for осуществляется в следую- щем порядке. 1. Создается переменная val и инициализируется значением 1. 2. Проверяется значение переменной val (меньше или равно 10). 3. Если значение переменной val меньше или равно 10, выполняется тело цикла for, осуществляющее сложение значений переменных val и sum. Если значение переменной val больше 10, цикл for завершается и управление переходит к оператору, следующему за ним. 4. Приращение значения переменной val. 5. Пока условие истинно, повторяются действия начиная с пункта 2.
38 Введение После выхода из цикла for, переменная val оказывается уже недоступной. То есть по завершении цикла использовать переменную val нельзя. Однако не все компиля- торы придерживаются этого правила. Компиляторы, появившиеся до разработки стандарта C++, оставляют переменные, оп- ределенные в заголовке цикла for, доступными вне тела самого цикла. Поэтому раз- работчики, привыкшие к устаревшему компилятору, могут быть неприятно удивлены этим нововведением при переходе к современному стандартному компилятору. Возвращаясь К КОМПИЛЯЦИИ Одной из задач компилятора является поиск ошибок в тексте программ. Компилятор, безусловно, не может оценить правильность работы программы, но вполне способен обнаружить ошибки в форме записи. Ниже приведены примеры ошибок, которые компилятор обнаруживает чаще всего. 1. Синтаксические ошибки. Речь идет о грамматических ошибках языка C++. Приведенный ниже код демонстрирует наиболее распространенные синтакси- ческие ошибки, снабженные комментариями, которые описывают их суть. / / ошибка: отсутствует ')' списка параметров функции main() int main ( { // ошибка: после endl используется двоеточие, не точка с запятой std::cout <„< "Read each file." << std::endl: // ошибка: отсутствуют кавычки вокруг строкового литерала std::coat << Update master, << std::endl; // ok: в этой строке ошибок нет std::cout << "Write new master," << std::endl; // ошибка: отсутствует ';1 после оператора return return О 2. Ошибки несовпадения типа. Каждый элемент данных языка C++ имеет тип. Значение 10, например, является целым числом. Слово ‘ привет” с парными кавычками — это строковый литерал. Примером ошибки несовпадения являет- ся передача строкового литерала функции, которая ожидает целочисленный аргумент. 3. Ошибки объявления. Каждое имя, используемое в программе на языке C++, должно быть вначале объявлено. Использование необъявленного имени обыч- но приводит к сообщению об ошибке. Типичными ошибками объявления явля- ется также отсутствие указания пространства имен, например std: :, при дос- тупе к имени, определенному в библиотеке, а также орфографические ошибки в именах идентификаторов. #include <iostream> int main() int vl, v2; std::cin >> v >> v2; // ошибка.- используется "v" вместе "vl” // cout не определен, должно быть std::cout cout << vl + v2 << std::endl; return 0,- Сообщение об ошибке содержит номер строки и краткое описание того, что компиля- тор считает неправильным. Исправлять ошибки имеет смысл в том порядке, в котором
Глава 1. Первые шаги 39 поступают сообщения о них. Зачастую одна ошибка приводит к появлению других, поэтому компилятор, как правило, сообщает о большем количестве ошибок, чем име- ется фактически. Целесообразно также перекомпилировать код после устранения ка- ждой ошибки пли небольшого количества вполне очевидных ошибок. Этот цикл из- вестен под названием "редактирование, компиляция, отладка” (edit-compile-debug). Упражнения раздела 1.4.2 Упражнение 1.9. Что делает следующий цикл for? Каково финальное значение переменной sum? int sum = 0; for (int i = -100; i <= 100; ++i) sum += i; Упражнение 1.10. Напишите программу, которая использует цикл for для суммирования чисел от 50 до 100. Затем перепишите программу с использованием цикла while. Упражнение 1.11. Напишите программу, которая используя цикл while отображает на экране числа от 10 до 0. Затем перепишите программу с использованием цикла for. Упражнение 1.12. Сравните циклы, использованные в двух предыдущих упражнениях. Каковы преимущества и недостатки каждого из них в разных случаях? Упражнение 1.13. Диагностические сообщения разных компиляторов выглядят по-разному. На- пишите программы, которые содержат ошибки, описанные на стр. 38. Изучите переданные компи- лятором сообщения, чтобы они были знакомы на случай их появления при компиляции более сложных программ. 1.4.3. Оператор if Логическим продолжением примера о суммировании значений от 1 до 10 является суммирование диапазона значений между двумя числами, которые ввел пользователь. Эти числа можно было бы использовать непосредственно в операторе цикла for, где первое введенное число используется в качестве нижней границы диапазона, а вто- рое — верхней. Но если пользователь первым введет большее число, эта стратегия по- терпит неудачу: выход из цикла for произойдет немедленно. Поэтому значения необ- ходимо переставить так, чтобы большее число стало верхней границей диапазона, а меньшее — нижней. Для этого необходимо выяснить, которое из чисел больше. Подобно большинству языков, C++ предоставляет оператор if, который обеспе- чивает выполнение операторов по условию. Используя оператор if, программу суммирования можно модернизировать следующим образом. #include <iostream> int main() { std::cout « "Enter two numbers:" « std::endl; int vl, v2; std::cin >> vl >> v2; // прочитать введенные данные II использовать меньшее число как нижнюю границу суммируемого II диапазона, а большее число — как верхнюю int lower, upper; if (vl <= v2) { lower - vl; upper = v2; } else {
40 Введение lower = v2; upper = vl; int sum - 0; // суммировать значения от нижней до верхней // границы включительно for (int val = lower; val <= upper; ++val) sum += val; // sum - sum + val std::cout << "Sum of " << lower << " to " << upper << " inclusive is " « sum << std::endl; return 0; Если откомпилировать и запустить эту программу на выполнение, а затем ввести числа 7 и 3, результат будет следующим. Sum of 3 to 7 inclusive is 25 Большая часть этого кода должна быть уже знакома читателю по предыдущим примерам. Вначале программа запрашивает у пользователя два числа и определяет четыре переменные типа int. Затем она читает со стандартного устройства ввода данные для переменных vl и v2. Единственным новым кодом здесь является опе- ратор if. // использовать меныпее число как нижнюю границу суммируемого // диапазона, а большее число — как верхнюю int lower, upper; if (vl <= v2) { lower = vl; upper - v2; } else { lower = v2; upper = vl; } В результате выполнения этого кода переменным upper и lower должны быть присвоены соответствующие значения. Здесь в условии оператора if проверяется, является ли значение переменной vl меньшим или равным значению переменной v2. Если это так, выполняется блок кода, следующий непосредственно за условием. Этот блок содержит два оператора, каждый из которых осуществляет присвоение. Первый оператор присваивает значение переменной vl переменной lower, а второй присваивает значение переменной v2 переменной upper. Если условие ложно, т.е. значение переменной vl больше значения переменной v2, выполняется оператор, следующий после оператора else. Это тоже блок кода, состоящий из двух операторов присвоения. Здесь значение переменной v2 присваи- вается переменной lower, а значение переменной vl — переменной upper. Упражнения раздела 1.4.3 Упражнение 1.14. Что произойдет, если в рассматриваемой здесь программе введенные значе- ния будут равны? Упражнение 1.15. Откомпилируйте и запустите на выполнение программу этого раздела, а затем введите два равных значения. Сравните полученный результат с предсказанным в предыдущем уп- ражнении. Объясните различие между тем, что произошло и тем, что было предсказано.
Глава 1. Первые шаги 41 Упражнение 1.16. Напишите программу, выводящую на экран большее из двух значений, введен- ных пользователем. Упражнение 1.17. Напишите программу, запрашивающую у пользователя несколько значений и отображающую сообщение о количестве отрицательных чисел среди них. 1.4.4. Ввод неизвестного количества данных Еще одно усовершенствование, которое можно внести в разрабатываемую про- грамму суммирования (стр. 34), — это обеспечение возможности ввода пользовате- лем целого набора суммируемых чисел. В данном случае, количество подлежащих суммированию чисел заранее не известно. Теперь введенные числа необходимо хра- нить до тех пор, пока ввод не будет закончен. По завершении ввода программа ото- бразит на стандартном устройстве вывода полученную сумму, ftinclude <iostream> int main() { int sum - 0, value; // читать данные до конца файла, вычислить сумму всех значений while (std::cin >> value) sum += value; // эквивалентно sum = sum + val std::cout << "Sum is: " << sum << std::endl; return 0; } Если ввести значения 3 4 5 6, будет получен результат Sum is: 18. Как обычно, код начинается с подключения заголовков необходимых библиотек. Первая строка функции main () определяет две переменные типа int по имени sum и value. Переменная value применяется для хранения чисел, вводимых внутри ус- ловия цикла while. while (std::cin >> value) Что же происходит при вычислении результата условия, которым является опе- рация ввода std: : с in >> value? Происходит чтение следующего числа со стан- дартного устройства ввода и его сохранение в переменной value. Как упоминалось в разделе 1.2.2 (стр. 28), оператор ввода возвращает свой левый операнд. Таким об- разом, в условии фактически проверяется объект std: : с in. Когда объект типа istream используется при проверке условия, результат зави- сит от состояния потока. Если поток допустим, т.е. ввод следующего значения еще возможен, это условие считается истинным. Объект типа istream переходит в не- допустимое состояние при достижении конца файла (end-of-file) или ввода недопус- тимых данных, например строки вместо числа. Недопустимое состояние объекта ти- па istream в условии свидетельствует о том, что оно ложно. Пока не достигнут конец файла (или не произошла ошибка ввода), условие оста- ется истинным и выполняется тело цикла while. Тело состоит из одного составного оператора присвоения, который добавляет к левому операнду правый.
42 Введение Ввод конца файла с клавиатуры Разные операционные системы используют для конца файла различные значения. Для ввода символа конца файла в операционной системе Windows, достаточно нажать комбинацию клавиш <Ctrl+z>, а на машине с операционной системой UNIX, включая Мас OS-Х, как правило, используется комбинация клавиш <Ctrl+d>. Как только условие цикла while становится ложным, его выполнение прекраща- ется и управление переходит к следующему оператору. Этот оператор выводит на экран значение переменной sum, сопровождаемое манипулятором endl, который обеспечивает переход на новую строку и сброс буфера, связанного с объектом cout. И наконец, выполняется оператор return, который, как правило, возвращает опе- рационной системе значение 0, свидетельствующее об успешном завершении про- граммы. Упражнения раздела 1.4.4 Упражнение 1.18. Напишите программу, которая запрашивает у пользователя два числа и выво- дит на стандартное устройство вывода все числа, которые находятся между ними. Упражнение 1.19. Что произойдет, если в программе предыдущего упражнения ввести числа 1000 и 2000? Переделайте программу так, чтобы она никогда не выводила более 10 чисел в одной строке. Упражнение 1.20. Напишите программу, суммирующую числа в указанном пользователем диапа- зоне, но без оператора if, который устанавливает верхнюю и нижнюю границы. Что произойдет, если числа 7 и 3 ввести именно в таком порядке? Запустите программу, введите числа 7 и 3. Со- ответствует ли результат ожидаемому? Если нет, повторно ознакомьтесь с разделами, посвящен- ными циклам for и while, чтобы выяснить, почему. 1.5. Введение в классы Прежде чем перейти к решению проблемы книжного магазина, следует соз- дать структуру, предназначенную для хранения данных транзакции. Для определе- ния собственных структур данных, язык C++ предоставляет классы (class). Меха- низм классов — это одна из наиболее важных возможностей языка C++. Фактиче- ски, основное внимание при проектировании приложения на языке C++ уделяют именно определению различных классов, которые ведут себя так же, как встроенные типы данных. Как уже упоминалось, библиотека предоставляет такие типы, как istream и ostream, которые определены в ней как классы (т.е., строго говоря, они не являются частью самого языка). Полное понимание механизма классов предполагает изучение большого количе- ства информации. К счастью, классы, написанные другими разработчиками, можно использовать, даже не умея их создавать самостоятельно. В этом разделе описан простой класс, который можно использовать при решении проблемы книжного ма- газина. Реализован этот класс будет в следующих главах, когда читатель больше уз-, нает о типах, выражениях, операторах и функциях, т.е. всех тех компонентах, кото- рые используются при определении классов.
Глава 1. Первые шаги 43 Чтобы использовать класс, необходимо знать следующее. 1. Каково его имя? 2. Где он определен? 3. Что он делает? Предположим, что класс для решения проблемы книжного магазина имеет имя Sales_item, а определен он в заголовке Sales_item. h. 1.5.1. Класс Sales_item Класс Sales_item предназначен для хранения ISBN, а также для отслеживания количества проданных экземпляров, полученной суммы и средней цены проданных книг. Не будем пока рассматривать, как эти данные сохраняются и вычисляются. Чтобы применить класс, необходимо знать, что он делает, а не как. Как уже было продемонстрировано на примере использования таких библиотеч- ных средств, как объекты ввода и вывода, в код необходимо подключить соответст- вующий заголовок. Аналогично, для собственных классов также необходим соответ- ствующий заголовок. Как правило, определение класса помещают в файл, который должна подключить любая программа, собирающаяся его использовать. Традиционно классы сохраняют в файлах, имена которых совпадают с именами классов, определенных в заголовке, и расширением .h (некоторые программисты используют расширения . Н, . hpp или . hxx). В отличие от IDE, компиляторы, как правило, не требовательны к именам файлов заголовка. Предположим, что исполь- зуемый класс определен в файле по имени Sales_item. h. Операции с объектами класса Sales_ltem Каждый класс является определением типа. Имя типа совпадает с именем класса. Следовательно, класс Sales_item определен как тип по имени Sales_ item. Подобно встроенным типам данных, вполне можно создать переменную ти- па класса. Рассмотрим пример. Sales_item item; Этот код создает объект item типа Sales_item. Как правило, об этом говорят так: создан “объект типа Sales_item” или “объект класса Sales_item”, или даже “экземпляр класса Sales_item”. Кроме создания переменных типа Sales_item, с его объектами можно выпол- нять следующие операции. Использовать оператор суммы (+), чтобы сложить два объекта класса Sales_ item. Использовать оператор ввода (>>), чтобы прочитать объект класса Sales_item. Использовать оператор вывода (<<), чтобы отобразить объект класса Sales_itern. Использовать оператор присвоения (=), чтобы присвоить один объект класса Sales_item другому. Обращаться к функции same_isbn(), чтобы выяснить, не относятся ли два объекта класса Sales itemK той же самой книге.
44 Введение Чтение и запись объектов класса Sales_i tern Теперь, когда известны операции, поддерживаемые этим классом, можно соз- дать несколько простых программ, использующих его. Программа, приведенная ниже, читает данные со стандартного устройства ввода, а затем использует их для создания объекта класса Sales_item, который впоследствии отображается на стандартном устройстве вывода, ttinclude <iostream> #include "Sales_item.h" int main() { Sales_item book; // прочитать ISBN, количество проданных экземпляров и цену std::cin » book; // записать ISBN, количество проданных экземпляров, // общую сумму и среднюю цену std::cout << book « std::endl; return 0; } Если ввести значения 0-201-70353-Х 4 24.99, будет получен результат 0-201-70353-Х 4 99.96 24.99. Результат свидетельствует о том, что было продано четыре экземпляра книги по 24.99 долл. Каждый. Общий доход за четыре проданных экземпляра, составил 99.96 долл., а средняя цена на книгу составила 24.99 долл. Код программы начинается двумя директивами #include, одна из которых име- ет новую форму. Заголовок iostream определен в стандартной библиотеке, а заго- ловок Sales_item— нет. Тип Sales_item был определен самим разработчиком. При использовании собственных заголовков, их помещают в кавычки ( " " ). Заголовки стандартной библиотеки заключают в угловые скобки (<>), а нестандартные заголовки — в двойные кавычки (""). Тело функции main () начинается с определения объекта по имени book, кото- рый будет содержать данные, считываемые со стандартного устройства ввода. Сле- дующий оператор осуществляет чтение в этот объект, а третий оператор выводит объект на стандартное устройство вывода, сопровождая его, как обычно, манипуля- тором endl, сбрасывающим буфер. Фундаментальная концепция. Определение поведения класса По мере усложнения программ, использующих класс Sal es_itern, необходимо учи- тывать важную особенность: все действия, которые можно осуществлять с объектами любого класса, должен определить его автор. То есть автор структуры данных Sales_item определяет, как поведет себя объект класса Sales_item при создании и что случится при суммировании объектов класса Sales_item или применении их операторов ввода, вывода и т.д.
Глава 1. Первые шаги 45 Как правило, к объектам класса применимы только те операции, которые определены в этом классе. Таким образом, к объектам класса Sales_item применяются только те операции, которые перечислены на стр. 43. Реализация этих операций рассматривается в разделе 7.7.3 (стр. 288). Суммирование объектов класса Sales_i tern Рассмотрим немного более интересный пример суммирования двух объектов класса Sales_item. #include <iostream> ttinclude "Sales_item.h" int main() { Sales_item iteml, item2; std::cin >> iteml » item2; // прочитать две транзакции std::cout « iteml + item2 « std::endl; // отобразить их сумму return 0; } Предположим, что введены следующие значения. 0-201-78345-Х 3 20.00 0-201-78345-Х 2 25.00 После выполнения будет получен следующий результат. 0-201-78345-Х 5 110 22 Программа начинается с подключения заголовков Sales_item и iostream. За- тем создаются два объекта класса Sales_item (iteml и item2), предназначенные для хранения двух подлежащих суммированию транзакций. Следующее выражение осуществляет сложение и отображает результат. Из списка допустимых операций (стр. 43) известно, что в результате сложения двух объектов класса Sales_item по- лучается новый объект, ISBN которого совпадает с ISBN операндов, а количество проданных экземпляров и выручка являются суммой соответствующих значений исходных объектов. Известно также, что ISBN слагаемых элементов обязательно должен совпадать. Обратите внимание, эта программа очень похожа на программу стр. 28: она чита- ет два элемента данных и отображает их сумму. Отличаются они лишь тем, что в первом случае суммируются два целых числа, а во втором — два объекта класса Sales_item. Кроме того, сама концепция “суммы” здесь различна. В случае с типом int получается обычная сумма — результат сложения двух числовых значений. В случае с объектами класса Sales_item используется концептуально новое поня- тие суммы — результат сложения соответствующих компонентов двух объектов класса Sales item. Упражнения раздела 1.5.1 Упражнение 1.21. Web-сайт http://www.awprofessionai.com/cpp_primer содер- жит в каталоге кода первой главы копию файла Saiesitem.h. Скопируйте этот файл в свой
46 Введение рабочий каталог. Напишите программу, циклически перебирающую набор транзакций проданных книг и отображающую их на стандартном устройстве вывода. Упражнение 1.22. Напишите программу, которая считывает два объекта класса Saies_item, обладающие одинаковыми ISBN, и вычисляет их сумму. Упражнение 1.23. Напишите программу, которая обеспечивает ввод нескольких транзакций с одинаковым ISBN. Отобразите сумму всех введенных транзакций. 1.5.2. Первый взгляд на функции-члены К сожалению, программа суммирования объектов класса Sales_item имеет серьезную проблему. Что может произойти, если введенные транзакции имеют раз- ные ISBN? В этом случае суммировать данные бессмысленно. Для решения этой проблемы имеет смысл сначала проверить, обладают ли оба объекта класса Sales_ item одинаковым ISBN. #include <iostream> #include "Sales_item.h" int main() Sales_item iteml, item2; std::cin » iteml » item2; // сначала проверить, представляют ли объекты iteml и item2 // одну и ту же книгу if (iteml.same_isbn(item2)) { std::cout « iteml + item2 « std::endl; return 0; // свидетельство успеха } else { std::cerr « "Data must refer to same ISBN" return -1; « std::endl; // свидетельство отказа Отличие этой программы от предыдущей заключается в том, что оператор про- верки условия i f способен осуществлять переход по связанному с ним оператору else. Прежде чем рассмотреть оператор if, напомним, что выполнение программы полностью зависит от истинности его условия. Если проверка пройдена, дальней- ший код отображает на экране сумму (как и в предыдущей программе) и завершается оператором return, который возвращает свидетельствующее об успехе значение 0. Если проверка не пройдена, выполняется блок кода, расположенный после операто- ра else, который отображает соответствующее сообщение и возвращает значение -1, свидетельствующее об ошибке. Что такое функция-член? Условие оператора if проверяет результат вызова функции -члена (member function) same_isbn () объекта iteml класса Sales_item. // сначала проверить, представляют ли объекты iteml и item2 // одну и ту же книгу if (iteml.same_isbn(item2)) {
Глава 1. Первые шаги 47 Функция-член — это функция, определенная внутри класса. Функции-члены на- зывают также методами (method) класса. Функцию-член определяют только один раз, внутри класса, однако использовать ее может каждый объект данного класса. Обратите внимание, допустимые для объ- екта операции являются функциями-членами его класса. Таким образом, они явля- ются членами каждого объекта, хотя определены только один раз. То есть функции- члены совместно используются всеми объектами одинакового типа6. Когда происходит вызов функции-члена, как правило, указывают объект, функ- ция которого должна сработать. Этот синтаксис подразумевает использование то- чечного оператора (dot operator) (.). iteml.same_isbn Здесь происходит обращение к члену same_isbn объекта iteml. Точечный опе- ратор обеспечивает доступ к правому операнду элемента, являющегося левым опе- рандом. Точечный оператор применяется только к объектам класса, т.е. левый опе- ранд должен быть объектом класса, а правый — членом этого класса. В отличие от большинства других операторов, правый операнд точечного (.) является не объектом или значением, а именем члена класса. Когда функция-член используется в качестве правого операнда точечного опера- тора, как правило, происходит ее вызов. Запуск кода функции-члена на выполнение осуществляется аналогично любой другой функции: после имени функции следует расположить оператор обращения (call operator) (() ). Оператор обращения — это пара круглых скобок, в которые заключен список передаваемых функции аргумен- тов (argument). Список аргументов может быть пуст. Функции same_isbn () передают один аргумент, который является другим объ- ектом класса Sales_item. iteml.same_isbn(item2) Здесь происходит обращение к функции-члену same_isbn () объекта iteml, которой в качестве аргумента передают объект item2. При вызове функция same_ isbn () сравнивает ISBN аргумента (item2) с ISBN объекта (iteml). Таким обра- зом, вполне возможно проверить, совпадают ли ISBN обоих объектов. Если ISBN объектов совпадают, выполняется код после оператора if, который отображает результат сложения двух объектов класса Sales_item. В противном случае, если ISBN не совпадают, выполняется блок операторов, расположенных по- сле оператора else, который отображает сообщение об ошибке и осуществляет вы- ход из программы с возвращением значения -1. Напомним, что выход из функции main () с возвращением значения, отличного от нуля, свидетельствует об ошибке. В данном случае возвращение значения -1 указывает на невозможность вычислить требуемый результат. 6 Образно говоря, класс — это чертеж чайника, объект — сам чайник, член класса — ручка чайника. Один раз нарисованная на чертеже ручка будет у каждого чайника. При “обращении” к ручке, необходимо обязательно указать, чему именно она принадлежит (для этого использу- ется точечный оператор). — Примеч. ред.
48 Введение Упражнения раздела 1.5.2 Упражнение 1.24. Напишите программу, которая читает несколько транзакций. Для каждой вновь прочитанной транзакции необходимо выяснить, соответствует ли ее ISBN предыдущей транзакции. Программа должна подсчитать количество транзакций для каждого ISBN. Проверьте программу введя несколько транзакций. Транзакции должны содержать разные ISBN, но записи по каждому ISBN должны быть сгруппированы вместе. 1.6. Программа на языке C++ Теперь все готово для решения проблемы книжного магазина: следует прочитать файл транзакций сбыта и создать отчет, где для каждой книги будет подсчитана об- щая выручка, средняя цена и количество проданных экземпляров. Предположим, что все транзакции сгруппированы по ISBN. Данная программа объединяет данные по каждому ISBN в объекте total (всего) класса Sales_item. Каждая транзакция, считанная со стандартного устройства ввода, будет сохранена во втором объекте класса Sales_item, по имени trans. Каждая вновь считанная транзакция сравнивается с объектом total класса Sales_item. Если их ISBN сов- падают, значение объекта total модифицируется. В противном случае значение объекта total выводится на экран, а затем, после присвоения объекту total значе- ния только что считанной транзакции, все повторяется. #include <iostream> ttinclude "Sales item.h" int main() { // объявить переменные для хранения суммы и данных // следующей записи Sales_item total, trans; // есть ли данные для обработки? if (std::cin » total) { // если да, прочитать транзакцию while (std::cin » trans) if (total.same_isbn(trans)) // совпадает: изменить суммарное количество total = total + trans; else { // не совпадает: отобразить и переприсвоить total std::cout « total « std::endl; total = trans; } // не забыть отобразить последнюю запись std::cout « total << std::endl; } else { // нет ввода! Предупредить пользователя std::cout << "No data?!" « std::endl; return -1; // свидетельство отказа } return 0; }
Глава 1. Первые шаги 49 Это наиболее сложная программа из рассмотренных на настоящий момент, однако все ее элементы уже знакомы. Как обычно, код начинается с подключения используе- мых заголовков: iostream (из библиотеки) и Sales_item. h (собственного). Внутри функции main () определены необходимые объекты: total (для сумми- рования данных по текущему ISBN) и trans (для хранения только что введенной транзакции). Сначала осуществляется чтение транзакции в переменную total, а также проверка успешности чтения. При отказе чтения данных (т.е. пользователь нажал клавишу <Entr>, не введя никаких данных), управление переходит к наибо- лее удаленному оператору else, код которого отображает сообщение, предупреж- дающее пользователя об отсутствии данных. Предположим, что запись введена успешно и управление перешло к коду, располо- женному непосредственно после условия оператора if. Первым является оператор while, который организует циклический перебор всех остальных записей. Подобно про- грамме на стр. 41, в условии цикла while со стандартного устройства ввода считывается очередное значение, ISBN которого затем проверяется на совпадение с текущим. В дан- ном случае считывается объект trans класса Sales_itern. Выполнение тела цикла while продолжается до тех пор, пока пользователь вводит допустимые записи. Тело цикла while содержит лишь оператор if, который проверяет, совпадают ли ISBN текущей и введенной записей. Если они совпадают, оба объекта суммиру- ются, а результат сохраняется в объекте total. Если ISBN не совпадают, храни- мое в объекте total значение отображается на экране, а затем ему присваивается значение объекта trans. После выполнения оператора if управление возвращается к условию оператора while, где считывается следующая транзакция. Так продол- жается до тех пор, пока не исчерпаются все записи. По завершении цикла while, содержащиеся в объекте total данные о послед- нем ISBN остаются неотображенными. Этот недостаток устраняется в последнем операторе блока кода, расположенного непосредственно после первого оператора if. Упражнения раздела 1.6 Упражнение 1.25. Используя доступный на Web-сайте (стр. 45) заголовок Saies_item.h, от- компилируйте и запустите на выполнение программу книжного магазина, рассматриваемую в этом разделе. Упражнение 1.26. В программе книжного магазина, при суммировании объектов trans и total, использован обычный оператор плюс, а не составной оператор суммы с присвоением. Почему не использован составной оператор? Резюме Эта глава содержит достаточно информации о языке C++, чтобы читатель смог написать, откомпилировать и запустить на выполнение простую программу. Здесь было описано, как определить функцию main (), присутствующую в любой программе на языке C++. Также было продемонстрировано, как определить переменные, организовать ввод и вывод данных, использовать операторы if, for и while. Глава завершается описанием фундаментального элемента языка C++: класса. Здесь было продемонстрировано создание и применение объек- тов классов. Определение собственных классов описано в следующих главах.
50 Введение Термины Аргумент (argument). Значение, передаваемое функции при вызове. Библиотечный тип (library type). Тип, определенный в стандартной библиотеке (напри- мер istream). Блок (block). Последовательность операторов, заключенных в фигурные скобки. Буфер (buffer). Область памяти, используемая для хранения данных. Средства ввода- вывода зачастую хранят вводимые и выводимые данные в буфере, работа которого никак не зависит от действий программы. Из буфера вывода, как правило, данные извлекаются явно, чтобы их сразу же можно было применить. Как правило, буфер объекта с in сбрасывается при обращении к объекту cout, а буфер объекта cout сбрасывается при завершении программы. Возвращаемый тип (return type). Тип возвращенного функцией значения. Встроенный тип (built-in type). Тип данных, определенный в самом языке (например int). Выражение (expression). Наименьшая единица вычислений. Выражение состоит из одно- го или нескольких операндов и оператора. Вычисление выражения определяет результат. На- пример, сложение целочисленных значений (i + j ) — это арифметическое выражение, ре- зультатом которого является целочисленное значение, равное сумме двух исходных значе- ний. Более подробная информация о выражениях приведена в главе 5, “Выражения”. Директива препроцессора (preprocessor directive). Инструкция препроцессору языка C++, например директива #include. Директивы препроцессора располагаются в одной строке. Более подробная информация о препроцессоре приведена в разделе 2.9.2 (стр. 93). Заголовок (header). Механизм, позволяющий сделать определения классов или других имен доступными в нескольких программах. Заголовок подключается в код программы при помощи директивы #include. Имя функции (function name). Имя, под которым функция известна и может быть вызвана. Класс (class). Механизм языка C++, позволяющий определить собственную структуру данных. Класс — это одна из фундаментальных особенностей языка C++. Библиотечные ти- пы, такие как istream и ©stream, на самом деле являются классами. Комментарий (comment). Текст в исходном коде, который игнорируется компилято- ром. Язык C++ поддерживает два вида комментариев: до конца строки и парный. Коммен- тарий в стиле языка С начинается символом / / и продолжается до конца строки. Все, что находится от символа / / и до конца строки, является комментарием. Парные комментарии начинаются символом / * и завершаются символом */. Все, что находится между ними, яв- ляется комментарием. Конец файла (end-of-file). Специфический для каждой операционной системы маркер, указывающий на завершение последовательности данных файла. Манипулятор (manipulator). Объект, который манипулирует непосредственно потоком ввода или вывода (например std: :endl). Более подробная информация о манипуляторах приведена в разделе А.3.1 (стр. 857). Метод (method). То же, что и функция-член. Неинициализированная переменная (uninitialized variable). Переменная, которая не имеет исходного значения. Когда для переменной, типом которой является класс, не указано никакого исходного значения, ее инициализация осуществляется так, как указано в определении класса. Перед использованием неинициализированной переменной необходимо присвоить значение. Неинициализированные переменные являются потенциальным источником ошибок. Объект сегг. Объект типа ostream, связанный с потоком стандартного устройства ото- бражения сообщений об ошибке, который зачастую совпадает с потоком стандартного уст- ройства вывода. По умолчанию запись в объект сегг не буферизируется. Как правило, объ- ект сегг используется для вывода сообщений об ошибках и других данных, не являющихся частью нормальной логики программы.
Глава 1. Первые шаги 51 Объект cin. Объект типа istream, используемый для чтения данных со стандартного устройства ввода. Объект clog. Объект типа ostream, связанный с потоком стандартного устройства ото- бражения сообщений об ошибке. По умолчанию запись в объект clog буферизируется. Как правило, объект clog используется для записи информации о ходе выполнения программы в файл журнала. Объект cout. Объект типа ostream, используемый для записи на стандартное устройст- во вывода. Обычно используется для вывода данных программы. Оператор (). Оператор обращения. Пара круглых скобок () после имени функции. При- водит к вызову функции. Передаваемые при вызове аргументы функции указывают внутри круглых скобок. Оператор (statement). Самый маленький независимый элемент программы на языке C++. Это аналогия предложения в человеческом языке. Операторы языка C++ обычно завершают- ся точкой с запятой. Оператор .. Точечный оператор. Получает два операнда: левый операнд — объект, пра- вый — имя члена класса этого объекта. Оператор обеспечивает доступ к члену класса имено- ванного объекта. Оператор : :. Оператор области видимости. Более подробная информация об области ви- димости приведена в главе 2, “Переменные и базовые типы”. Кроме всего прочего, оператор области видимости используется для доступа к элементам по именам в пространстве имен. Например, запись std: :cout указывает, что используемое имя cout определено в про- странстве имен std. Оператор ++. Оператор инкремента. Добавляет к операнду единицу. Например, выраже- ние++i эквивалентно выражению i = i + 1. Оператор +=. Составной оператор присвоения. Добавляет правый операнд к левому, а ре- зультат сохраняет в левом операнде. Например, выражение а += Ь эквивалентно выраже- нию а = а + Ь. Оператор «. Оператор вывода. Записывает правый операнд в поток вывода, указанный ле- вым операндом. Например, выражение cout << "hi" передаст слово "hi" на стандартное устройство вывода. Несколько операций вывода вполне можно объединить. Например, выраже- ние cout << "hi" << "bye" передаст на стандартное устройство вывода слово "hibye". Оператор =. Присваивает значение правого операнда левому. Оператор >>. Оператор ввода. Считывает в правый операнд данные из потока ввода, оп- ределенного левым операндом. Например, выражение cin >> i считывает следующее зна- чение со стандартного устройства ввода в переменную i. Несколько операций ввода вполне можно объединить. Например, выражение cin >> i >> j считывает данные сначала в пе- ременную i, а затем в переменную j. Оператор ! =. Не равно. Проверяет неравенство левого и правого операндов. Оператор ==. Равно. Проверяет, равен ли левый операнд правому. Оператор <. Меньше чем. Проверяет, меньше ли левый операнд, чем правый. Оператор >. Больше чем. Проверяет, больше ли левый операнд, чем правый. Оператор <=. Меньше или равно. Проверяет, меньше или равен левый операнд правому. Оператор >=. Больше или равно. Проверяет, больше или равен левый операнд правому. Оператор for. Управляющий оператор, который обеспечивает итерационное выполне- ние. Зачастую используется для перебора структур данных, а также для повторения вычисле- ний определенное количество раз. Оператор if. Управляющий оператор, обеспечивающий выполнение разных операторов на основании значения определенного условия. Если условие истинно (значение true), вы- полняется тело оператора if. В противном случае (значение false) управление переходит к оператору else.
52 Введение Оператор while. Управляющий оператор, обеспечивающий итерационное выполнение кода тела цикла, пока его условие остается истинным. Переменная (variable). Именованный объект. Пространство имен (namespace). Механизм применения имен, определенных в библиоте- ках. Применение пространств имен позволяет избежать случайных конфликтов имени. Име- на, определенные в стандартной библиотеке языка C++, находятся в пространстве имен std. Пространство имен std. Пространство имен, используемое стандартной библиоте- кой. Запись std: :cout указывает, что используемое имя cout определено в простран- стве имен std. Редактирование, компиляция, отладка (edit-compile-debug). Процесс, обеспечивающий правильное выполнение программы. Список параметров (parameter list). Часть определения функции. Список параметров оп- ределяет аргументы, применяемые при вызове функции. Список параметров может быть пуст. Стандартная библиотека (standard library). Коллекция типов и функций, которой должен обладать каждый компилятор языка C++. Библиотека предоставляет обширный набор средств, включая типы для работы с потоками ввода и вывода. Под “библиотекой” програм- мисты C++ подразумевают либо всю стандартную библиотеку, либо ее часть, библиотеку ти- пов. Например, когда программисты говорят о “библиотеке iostream”, они подразумевают ту часть стандартной библиотеки, в которой определен класс iostream. Стандартная ошибка (standard error). Поток вывода, предназначенный для передачи со- общения об ошибке. В оконных операционных системах, как правило, стандартный вывод и стандартная ошибка связаны с тем окном, в котором выполняется программа. Стандартный ввод (standard input). Поток ввода, который в оконных операционных сис- темах обычно связан с окном, в котором выполняется программа. Стандартный вывод (standard output). Поток вывода, который в оконных операционных системах обычно связан с окном, в котором выполняется программа. Строковый литерал (string literal). Последовательность символов, заключенных в двой- ные кавычки. Структура данных (data structure). Логическое объединение типов данных и операций, возможных для них. Тело функции (function body). Блок операторов, определяющий выполняемые функцией действия. Тип iostream. Библиотечный тип, обеспечивающий потоковый ввод и вывод. Тип istream. Библиотечный тип, обеспечивающий потоковый ввод. Тип ostream. Библиотечный тип, обеспечивающий потоковый вывод. Тип класса (class type). Тип, определенный классом. Имя типа совпадает с именем класса. Условие (condition). Выражение, результатом которого является логическое значение true (истина) или false (ложь). Нулевой результат арифметического выражения соответ- ствует значению false, а любой другой — значению true. Файл исходного кода (source file). Термин, используемый для описания файла, который содержит текст программы на языке C++. Фигурная скобка (curly brace). Фигурные скобки разграничивают блоки кода. Откры- вающая фигурная скобка ({) начинает блок, а закрывающая (}) завершает его. Функция (function). Именованный блок операторов. Функция main (). Функция, вызываемая операционной системой при запуске программы C++ на выполнение. Каждая программа должна иметь одну и только одну функцию по имени main (). Функция-член (member function). Операция, определенная классом. Как правило, функ- ции-члены применяются для работы с определенным объектом.
Основы В ЭТОЙ ЧАСТИ... Глава 2. Переменные и базовые типы Глава 3. Библиотечные типы данных Глава 4. Массивы и указатели Глава 5. Выражения Глава 6. Операторы Глава 7. Функции Глава 8. Библиотека ввода-вывода Все языки программирования имеют отличительные особенности, определяющие типы приложений, для которых данный язык подходит лучше всего, хотя большин- ство фундаментальных атрибутов у них одинаковы. По существу, все языки про- граммирования обладают следующими элементами. Встроенные типы данных (например целые числа, символы и т.д.). Выражения и операторы, позволяющие манипулировать значениями этих типов. Переменные, которые позволяют присваивать имена используемым объектам. Управляющие структуры, такие как if или while, которые обеспечивают ус- ловное и циклическое выполнение наборов действий. Функции, позволяющие обратиться к именованным модулям действий. Современные языки программирования, кроме этого базового набора, обладают еще двумя важными возможностями: дополнять язык собственными типами дан- ных, а также использовать библиотеки функций и типов данных, которые не встрое- ны в сам язык. В языке C++, как и в большинстве языков программирования, допустимые для объекта операции определяет его тип. То есть оператор будет допустимым или недо- пустимым в зависимости от типа используемого объекта. Некоторые языки, напри- мер Smalltalk и Python, проверяют используемые в выражениях типы во время вы- полнения программы. В отличие от них, язык C++ осуществляет контроль типов данных статически, т.е. соответствие типов проверяется во время компиляции. Как
54 Часть I. Основы следствие, компилятор требует сообщить ему тип каждого используемого в про- грамме имени, прежде чем оно будет применено. Язык C++ предоставляет набор встроенных типов данных, операторы для мани- пулирования ими и небольшой набор операторов для управления процессом выпол- нения программы. Эти элементы формируют алфавит, при помощи которого можно написать (и было написано) множество больших и сложных реальных систем. На этом базовом уровне язык C++ довольно прост. Его потрясающая мощь является ре- зультатом поддержки механизмов, которые позволяют программисту самостоятель- но определять новые структуры данных. Важнейшим компонентом языка C++ является класс, который позволяет про- граммистам определять свои собственные типы данных. В языке C++ такие типы иногда называют “типами класса”, чтобы отличить их от базовых типов, встроенных в сам язык. Некоторые языки программирования позволяют определять типы, кото- рые способны содержать только данные. Другие, подобно языку C++, позволяют оп- ределять типы, в состав которых можно включить операции, выполняемые с этими данными. Одна из главных задач проекта C++ заключалась в предоставлении про- граммистам возможности самостоятельно определять типы данных, использование которых будет столь же легким, как и использование встроенных типов данных. Стандартная библиотека языка C++ использует эту возможность для реализации обширного набора классов и связанных с ними функций. Первым шагом по овладению языком C++ является изучение его основ и библио- теки — такова тема части I, “Основы”. В главе 2, “Переменные и базовые типы”, рас- сматриваются встроенные типы данных, а также обсуждается механизм определения новых, собственных типов. В главе 3, “Библиотечные типы данных”, описаны два фундаментальных библиотечных типа: string (строка) и vector (вектор). Масси- вы, рассматриваемые в главе 4, “Массивы и указатели”, представляют собой низко- уровневую структуру данных, встроенную в язык C++ и множество других языков. Массивы подобны векторам, но использовать их гораздо сложнее. Главы 5-7 посвя- щены выражениям, операторам и функциям. Часть завершается главой 8, “Библио- тека ввода-вывода”, демонстрирующей наиболее важные средства библиотеки вво- да-вывода.
Переменные и базовые типы В ЭТОЙ ГЛАВЕ... 2.1. Простые встроенные типы 56 2.2. Литеральные константы 61 2.3. Переменные 65 2.4. Спецификатор const 78 2.5. Ссылки 81 2.6. Определение имен типов 83 2.7. Перечисления 84 2.8. Типы классов 85 2.9. Создание собственных файлов заголовка 89 Резюме 95 Термины 96 Типы данных — это фундамент любой программы. Они указывают, что именно представляют собой эти данные и какие операции с ними можно выполнять. В языке C++ определено несколько базовых типов: символы, целые числа, числа с плавающей запятой и т.д. Язык предоставляет также механизмы, позволяющие программисту определять собственные типы данных. В библиотеке эти механизмы использованы для определения более сложных типов, таких как символьные строки переменной длины, векторы и т.д. И наконец, существующие типы можно модифи- цировать и получать составные типы. В этой главе рассматриваются встроенные ти- пы данных и основы применения более сложных типов. Тип определяет, что представляют собой эти данных и какие операции с ними можно выполнять. Как уже было продемонстрировано в главе 1, “Первые шаги”, один и тот же оператор, например i = i + j ;, может означать совершенно разные вещи, в зависимо- сти от типов переменных i и j. Если i и j являются целыми числами, этот оператор представляет собой обычное арифметическое сложение значений. Но если i и j являют- ся объектами класса Sales_item, данный оператор суммирует их компоненты. Поддержка подобных типов в языке C++ весьма основательна: определение на- бора базовых типов и способов манипулирования ими встроены в сам язык. Язык предоставляет также набор средств, который позволяет разработчику определять собственные типы данных. В этой главе описание типов языка C++ начинается с
56 Часть I. Основы изучения встроенных типов данных, а также рассмотрения взаимосвязи между ти- пами и объектами. Здесь также продемонстрированы способы модификации сущест- вующих типов данных и создания собственных. 2.1. Простые встроенные типы В языке C++ определен набор арифметических типов (arithmetic type), перемен- ные которых предназначены для хранения целых чисел, чисел с плавающей запятой, отдельных символов и логических значений. Кроме того, существует специальный тип void (ничто), который не предназначен для хранения значений и применяться лишь при некоторых обстоятельствах. Тип void, как правило, используют для ука- зания типа возвращаемого значения функции, которая ничего не возвращает. Размер арифметических типов данных зависит от конкретного компьютера. Под размером типа данных подразумевают количество битов, необходимых для разме- щения в памяти переменной данного типа. Для каждого арифметического типа стан- дарт гарантирует минимальный размер, но это не запрещает компилятору использо- вать больший размер. На самом деле, практически все компиляторы используют для типа int несколько больший размер, чем минимальный. Список встроенных ариф- метических типов и их размеров приведен в табл. 2.1. Таблица 2.1. Арифметические типы языка C++ Тип Значение bool char wchar_t short long float double long double Логический тип (boolean) Символ Символ Unicode Короткое целое число Целое число Длинное целое число Число с плавающей запятой одинарной точности Число с плавающей запятой двойной точности Число с плавающей запятой повышенной точности Минимальный размер Не определен 8 битов 16 битов 16 битов 16 битов 32 бита 6 значащих цифр 10 значащих цифр 10 значащих цифр Поскольку максимальный и минимальный размер типов данных (в битах) зависит от кон- кретной машины, представляемые этими типами значения также являются машинно- зависимыми. Машинный уровень представления встроенных типов Встроенные типы языка C++ жестко связаны с их представлением в памяти компь- ютера. Компьютер хранит данные в виде последовательности битов, каждый из ко- торых может иметь значение 0 или 1. фрагмент данных в памяти может выглядеть следующим образом. 00011011011100010110010000111011 ...
Глава 2. Переменные и базовые типы 57 Данные на битовом уровне (в памяти) не имеют ни структуры, ни смысла. Другими словами, структуру данных в памяти можно представить как набор фрагмен- тов. Большинство компьютеров оперируют с памятью, разделенной на порции и содер- жащей определенное количество битов, как правило, кратное степеням числа 2. Обычно это порции по 8,16 или 32 бита, однако сейчас уже применяются порции по 64 и 128 би- тов. Хотя реальные размеры зависят от конкретной машины, обычно используются пор- ции по 8 битов, известные как байты (byte), по 32 бита (word), а также по 4 байта. У большинства компьютеров каждый байт в памяти пронумерован. Этот номер назы- вается адресом (address). Предположив, что машина оперирует с 8-битовыми байтами, 32-битовые числа в памяти можно было бы представить следующим образом. 736424 0 0 0 1 1 0 1 1 736425 0 1 1 1 0 0 0 1 736426 0 1 1 0 0 1 0 0 736427 0 0 1 1 1 0 1 1 Здесь адрес каждого байта представлен слева, а 8 битов байта — справа. При помощи адреса можно обратиться к любому из байтов, а также к набору из не- скольких байтов, начинающемуся с этого адреса. В этом случае говорят о доступе к байту по адресу 736424 или о байте, хранящемуся по адресу 736426. Можно сказать, что байт по адресу 736425 не равен 6ai (ту по адресу 73642 7. Чтобы присвоить значение по адресу 736425, необходимо знать тип подлежащего со- хранению значения. Когда тип известен, можно выяснить количество битов, необхо- димое для представления значения этого типа и интерпретации его битов. Если известно, что расположенный по адресу 736425 байт имеет тип “8-битовое беззнако- вое целое число”, этот байт является чистом 112. С другой стороны, если т от же байт имеет символьный тип из набора ISO LA TIN-1, он представляет собой символ “q”. В обоих слу- чаях речь шла о том же битс, но в зависимости от типа он интерпретировался по-разному. 2.1.1. Целочисленные типы Целые числа, символы и логические значения обобщенно называют целочислен- ными типами (integral type) или арифметическими типами (arithmetic type). Существует два символьных типа: char и wchar_t. Тип char гарантированно дос- таточен для хранения числового значения, соответствующего любому символу в базовом наборе символов компьютера. Как правило, символ (тип char) занимает один байт. Тип wchar_t используется в расширенном наборе символов (например, в Китае и Японии), где для обозначения каждого символа однобайтовый тип char не подходит. Типы short, int и long представляют целочисленные значения потенциально разных размеров. Как правило, тип short представляет числа, умещающиеся в по- ловине машинного типа word, тип int соответствует машинному типу word, а тип long занимает один или два размера типа word (на 32-битовых машинах, типы int и long имеют обычно одинаковый размер). Тип bool предназначен для логических значений true (истина) и false (ложь). Переменной типа bool можно присвоить значение любого из арифметических ти- пов. Значениям 0 переменной арифметического типа соответствует значению false логической переменной (тип bool), а любое другое значение (отличное от нуля) со- ответствует значению true.
58 Часть I. Основы Знаковые и беззнаковые типы Целочисленные типы, за исключением логического, могут быть знаковыми (signed) или беззнаковыми (unsigned). Как и следует из его названия, знаковый тип предназначен как для отрицательных, так и для положительных чисел (включая нуль), а беззнаковые типы — лишь для значений, больших или равных нулю. По умолчанию все целые числа (int, short и long) являются знаковыми. Что- бы получать беззнаковый тип, его следует определить как unsigned, например unsigned long. Тип unsigned int можно сокращенно указать как unsigned. То есть под типом unsigned, если не указано продолжение, подразумевается тип unsigned int. В отличие от других целочисленных типов, существует три разновидности типа char: простой тип char, signed char и unsigned char. Хоть и существует три версии типа char, представлять его можно только двумя способами: как signed char и unsigned char. Конкретная версия используемого типа char определя- ется компилятором. Как представляются значения целочисленных типов Все биты беззнакового типа используются для значения. Например, если беззна- ковый тип определен на машине, использующей 8 битов, переменная этого типа сможет содержать значения 0 до 255. Стандарт языка C++ не определяет способ представления знаковых чисел на би- товом уровне, поэтому каждый компилятор может самостоятельно выбирать способ представления знаковых типов. Способ представления может повлиять на диапазон значений, который способна содержать переменная знакового типа. Однако стандарт гарантируют, что 8-битовая знаковая переменная сможет содержать значения по крайней мере от -127 до 127, однако большинство реализаций позволяет содержать значения от -128 до 127. Самый распространенный способ представления знаковых целочисленных типов подразумевает использование одного из битов как знакового разряда. Когда знако- вый разряд содержит значение 1 — число отрицательное, а когда значение 0 — число положительное или 0. Присвоение значений целочисленных типов Тип объекта определяет значения, которые он может содержать. В связи с этим возникает вопрос: а что произойдет при попытке присвоить объекту значение, не входящее в диапазон, допустимый для данного типа?1 Ответ зависит от того, являет- ся ли тип знаковым или беззнаковым. Компилятор вынужден будет откорректировать эти значения так, чтобы они со- ответствовали диапазону. Для беззнакового типа компилятор оставит в переменной остаток от деления по модулю исходного числа на значение диапазона1 2. Например, 8-битовый объект типа unsigned char способен содержать значения от 0 до 255 включительно. Если присвоить значение, превышающее этот диапазон, компилятор фактически присвоит остаток от деления значения по модулю на 256. Например, при 1 То есть произойдет переполнение переменной. — Примеч. ред. 2 Коротко говоря, отсечет старшие биты, выходящие за границы диапазона. — Примеч. ред.
Глава 2. Переменные и базовые типы 59 попытке присвоить значение 336 8-битовый переменной типа unsigned char, фак- тически будет присвоено значение 80, поскольку результат деления по модулю чис- ла 336 на 256 равен 80. Для беззнаковых типов отрицательные значения всегда вне диапазона. Объект беззнакового типа не сможет содержать отрицательное значение. В некоторых язы- ках вполне допустимо присвоение отрицательного значения переменной беззнако- вого типа, но не в языке C++. В языке C++ присвоение отрицательного числа объекту беззнакового типа вполне до- пустимо, но в результате получится модуль отрицательного значения по размеру типа. Так, если присвоить значение -1 переменной типа unsigned char (8 битов), будет получено значение 255, что соответствует -1 по модулю 2563. Результат присвоения значения, превышающего допустимый диапазон, перемен- ной знакового типа зависит от применяемого компилятора. Большинство компиля- торов поступает со знаковыми переменными так, как с беззнаковыми. То есть они присваивают остаток деления по модулю на размер типа. Но это вовсе не гарантиру- ет, что так будут поступать все компиляторы для всех знаковых переменных. Совет. Используйте встроенные арифметические типы Количество целочисленных типов в языке C++ весьма обширно. Подобно языку С, язык C++ был разработан так, чтобы позволить программам, при необходимости, об- ращаться непосредственно к аппаратным средствам. Поэтому и целочисленные типы определены так. чтобы они соответствовали особенностям различных аппаратных средств. Большинство программистов, желая избежать этих сложностей, ограничива- ют количество фактически используемых ими типов. На практике целые числа в основном используются для подсчетов. Программы, на-, пример, должны подсчитывать количество элементов в таких структурах данных, как вектор и массив. Как будет продемонстрировано в главах 3, “Библиотечные типы дан- ных и 4, “Массивы и указатели”, в стандартной библиотеке уже определен набор ти- пов, специатьно предназначенных для действий с размерами объектов. При подсчете подобных элементов всегда лучше использовать соответствующий тип из библиотеки, поскольку он специально предназначен для этой цели. При подсчетах в других усло- виях обычно имеет смысл использовать беззнаковые значения. Отрицательные числа в этом случае, как правило, не нужны, а беззнаковый тип позволяет хранить значения, которые вдвое больше, чем знаковый. При целочисленных арифметических вычислениях тип short используют очень редко. Поскольку это приводит к возникновению загадочных ошибок. Причиной является при- своение переменной типа short значения, размер которого превышает максимально до- пустимый. Конкретный результат зависит от машины, но, как правило, переполнение переменной приводит к усечению числа, т.е. слишком большое положительное число превращается либо в самое маленькое, либо в самое большое отрицательное. По той же причине переменные типа char, несмотря на его принадлежность к целочисленным ти- пам, используют в основном для хранения символов, а не для вычислений. Тот факт, что 3 Другими словами, произойдет интерпретация знакового значения как беззнакового. — Примеч. ред.
60 Часть I. Основы тип char является знаковым на одних реализациях и беззнаковым на других, делает его применение при вычислениях весьма проблематичным. На большинстве машин для целочисленных вычисления вполне безопасно использо- вать тип int. С технической точки зрения тип int может занимать всего 16 битов, это слишком мало для большинства задач. Однако практически все современные машины используют для типа int 32 бита, как и для типа long. Затруднения в выборе исполь- зуемого типа (int или long) возникают на машинах с 32-битовым типом int и 64- битовым типом long. Здесь время выполнения арифметических операций с перемен- ными типа long может оказаться существенно большим, чем у аналогичных операций с 32-битовым типом int. В этом случае для принятия решения об использовании типа int или long потребуется детально разобраться в задачах программы и фактически необходимой производительности. Правильно выбрать тип для чисел с плавающей запятой значительно проще: здесь почти всегда имеет смысл использовать тип double. Потеря точности при использо- вании типа float может оказаться весьма существенной, в то время как разница в эффективности вычислений с использованием переменных двойной и одинарной точ- ности незначительна. Фактически, на некоторых машинах, операции с переменными двойной точности осуществляются быстрее, чем операции одинарной точности. Точ- ность, обеспечиваемая типом long double, как правило, не нужна и влечет за собой значительные дополнительные затраты времени при вычислениях. 2.1.2. Типы с плавающей запятой Типы float, double и long double предназначены для чисел с плавающей запятой одинарной, двойной и повышенной точности. Как правило, переменная типа float занимает 32 бита, переменная типа double — 64 бита, а переменная типа long double 96 или 128 битов. Размер определяет количество значащих цифр, ко- торое может иметь значение, хранимое в переменной типа с плавающей запятой. Для реальных программ тип float обычно недостаточно точен, поскольку обеспечи- вает лишь 6 значащих цифр. Тип double гарантирует по крайней мере 10 значащих цифр, а этого вполне достаточно для большинства вычислений. Упражнения раздела 2.1.2 Упражнение 2.1. В чем разница между значениями типа int, long и short? Упражнение 2.2. В чем разница между типами unsigned и signed? Упражнение 2.3. Если на данной машине переменная типа short занимает 16 битов, какое са- мое большое число может быть присвоено такой переменной? А переменной типа unsigned short? Упражнение 2.4. Какое значение будет содержать 16-битовая переменная типа unsigned short, если ей присвоить число 100 000? А если число 100 000 присвоить обычной 16-битовый переменной типа short? Упражнение 2.5. В чем разница между типами float и double?
Глава 2. Переменные и базовые типы 61 Упражнение 2.6. Какие типы данных следует использовать для основной суммы и взносов при вычислении выплат по закладной? Объясните, почему был выбран каждый тип. 2.2. Литеральные константы Значение 42 в коде программы называется литеральной константой (literal constant), поскольку значение задается в виде текста и его уже нельзя изменить. Ка- ждый литерал имеет тип. Например, 0 — int, а 3.14159 — double. Литералы до- пустимы только для встроенных типов, а для классов они невозможны. Следова- тельно, для библиотечных типов литералы неприменимы. Правила для целочисленных литералов Для записи целочисленного константного литерала можно использовать одну из трех форм: десятичное, восьмеричное или шестнадцатеричное число. Безусловно, реального (бинарного) значения эти формы записи не изменяют. Например, значе- ние 2 0 можно записать любым из трех следующих способов. 20 // десятичная форма 024 // восьмеричная форма 0x14 // шестнадцатеричная форма Целочисленные константные литералы начинающиеся с нуля (0), интерпрети- руются как восьмеричные, а начинающиеся с Ох или ОХ — как шестнадцатеричные. По умолчанию целочисленный константный литерал имеет тип int или long. Реальный тип зависит от значения литерала, если достаточно типа int, использует- ся он, а если нет —используется тип long. Добавив суффикс можно явно задать тип целочисленного константного литерала: long, unsigned или unsigned long. Чтобы задать константу типа long, непосредственно после значения следует указать суффикс L или 1 (символ L в верхнем или нижнем регистре). Имеет смысл использовать символ l (в верхнем регистре), поскольку символ 1 (в нижнем регистре) легко перепутать с цифрой 1. Аналогично, чтобы задать беззнаковый (unsigned) литерал, применяется суф- фикс U или и, а для литеральной константы типа unsigned long — L и и. Суффикс должен располагаться непосредственно после значения, без пробела. 128и /* unsigned */ 1024UL /* unsigned long ★/ IL /* long */ 8Lu /* unsigned long */ Литералы типа short не существуют. Правила для литералов с плавающей запятой Для записи литеральных констант с плавающей запятой можно использовать как обычную десятичную форму, так и экспоненциальную. В экспоненциальной форме записи используется символ Е или е. По умолчанию литералы с плавающей запятой имеют тип double. Для указания одинарной точности непосредственно после зна- чения располагают суффикс F или f. Точно так же указывают на повышенную точ-
62 Часть I. Основы ность при помощи суффикса L или 1 (использовать символ 1 не рекомендуется). Каждая пара литералов, представленных ниже, означает одинаковое значение. 3.14159F 3,14159E0f .OOlf 1E-3F 12.345L 1.2345E1L 0. ОеО Логические и символьные литералы Слова true и false — это литералы типа bool (логические литералы), bool test - false; Символьный литерал представляет собой печатаемый символ (printable character), заключенный в одинарные кавычки. 'а' '2' ''// пробел Такие литералы имеют тип char. Чтобы использовать символьный литерал типа wchar_t, непосредственно перед ним следует расположить префикс L. L'a' Управляющие последовательности для непечатаемых символов Некоторые chmj злы являются непечатаемыми (nonprintable character). Непеча- таемый символ никак не отображается ни на экране, ни при печати (например забой или переход на новую строку). Некоторые символы имеют в синтаксисе языка спе- циальное назначение, например одиночные и двойные кавычки, а также символ на- клонной черты влево. Для ввода непечатаемых и специальных символов использует- ся управляющая последовательность (escape sequence). Управляющая последова- тельность начинается символом наклонной черты влево. В языке C++ определены следующие управляющие последовательности. Новая строка (newline) Вертикальная табуляция (vertical tab) Возврат каретки (carriage return) Оповещение (alert) Вопросительный знак (question mark) Двойная кавычка (double quote) \п Горизонтальная табуляция (horizontal tab) \t \v Возврат на один символ (backspace) \b \г Прогон страницы (formfeed) \f \a Наклонная черта влево (backslash) \\ \ ? Одинарная кавычка (single quote) \' \ II В общем виде управляющая последовательность имеет следующую форму. \ооо Здесь ооо представляет собой последовательность из трех восьмеричных цифр. Значения восьмеричных цифр соответствуют числовым значениям символов. Ниже приведены примеры представления литеральных констант, соответствующих сим- волам набора ASCII. \7 (оповещение) \12 (новая строка) \40 (пробел) \0 (ноль) \062 ('2') \115 ('М') Символ, представляемый последовательностью ' \ 0 ', зачастую называют нуле- вым символом (null character). Он имеет специальное значение, как будет продемон- стрировано вскоре.
Глава 2. Переменные и базовые типы 63 Символ можно также записать используя шестнадцатеричную управляющую по- следовательность. \xddd Здесь после наклонной черты влево и символа х располагается одна или не- сколько шестнадцатеричных цифр. Символьные и строковые литералы Scanned by Digrol Все описанные до сих пор литералы принадлежали простым встроенным типам. Но существует еще один, более сложный литерал — строковый. Строковый лите- рал — это массив символов. Более подробная информация по этой теме приведена в разделе 4.3 (стр. 154). Константные строковые литералы способны содержать любое количество симво- лов, заключенных в двойные кавычки. Для непечатаемых символов применяются соответствующие управляющие последовательности. "Hello World!" // простой строковый литерал "" // пустой строковый литерал "\nCC\toptions\tfile. [сС] \п" // строковый литерал с символами // табуляции и новой строки В целях совместимости с языком С, компилятор дополняет строковые литералы языка C++ еще одним завершающим нулевым символом. 'А' // одинарные кавычки: символьный литерал Символьный литерал состоит из одного символа А, а указанный ниже строковый литерал представляет собой массив из двух символов: символа А и нулевого символа. "А" // двойные кавычки: строковый литерал Аналогично, символьный литерал Unicode имеет вид L' а', а строковый литерал Unicode, которому предшествует префикс L, соответственно L"a wide string literal". Строковый литерал Unicode является массивом константных символов Unicode и тоже завершается нулевым символом. Составные строковые литералы Два строковых литерала (или два строковых литерала Unicode), которые распо- ложены рядом и разделены только пробелами, символами табуляции или новой строки, объединяются в один новый строковый литерал. Это облегчает запись длин- ных строковых литералов, занимающих несколько отдельных строк. std::cout « "a multi-line " "string literal " "using concatenation" « std::endl; При выполнении этот оператор отобразит следующую строку, a multi-line string literal using concatenation Но что произойдет, если попытаться объединить обычный строковый литерал и строковый литерал Unicode? Рассмотрим пример. // Объединение простых строк и строк Unicode std::cout « "multi-line " L"literal " «std::endl;
64 Часть I. Основы Результат окажется неопределенным, т.е. стандарт никак не определяет поведе- ние компилятора при объединении двух разных типов. Программа может срабо- тать нормально, может зависнуть, а может отобразить всякий мусор. Кроме того, один и тот же код, при компиляции разными компиляторами, может вести себя по-разному. Совет. Не полагайтесь на неопределенное поведение Использование в программе неопределенного поведения недопустимо. Если програм- ма работает нормально, это просто случайность. Неопределенное поведение — эго од- на из тех ошибок, которые компилятор не может обнаружить, а следовательно, воз- никшие проблемы крайне сложно устранить. К сожалению, программы, которые характеризуются неопределенным поведением на некоторых компиляторах и при некоторых обстоятельствах, могут работать вполне нормально, не проявляя проблему. Но нет никаких гарантий, что та же программа, от- компилированная на другом компиляторе или даже следующей версии данного ком- пилятора, продолжит работать правильно. Нет даже гарантий того, что, нормально ра- ботая с одним набором данных, она будет нормально работать с другим. Поэтому полагаться на неопределенное поведение ни в коем случае нельзя. Аналогично, в программах нельзя полагаться на машинно-зависимое поведение, не стоит, например, надеяться на то, что переменная типа int имеет фиксированный за- ранее известны!' размер. Такие программы называют непереносимыми (nonportable). При переносе такой программы на другую машину, любой полагающийся на машин- но-зависимое поведение код, вероятней всего, сработает неправильно, поэтому его придется искать и исправлять. Поиск подобных проблем в ранее нормально работав- шей программе, мягко говоря, не самая приятная работа. Многострочные литералы Существует и более примитивный (но менее популярный) способ создания длинных строк, который основан на редко используемой особенности: поместив в конце строки символ наклонной черты влево можно указать, что следующая строка составляет с данной единое целое. Как утверждалось ранее, оформление исходного кода программ на языке C++ не имеет жестких правил. Речь шла о том, что существует лишь несколько мест, где пе- ренос строк недопустим. Одним из таких мест является середина строковых литера- лов. То есть строку нельзя разбивать посередине слова. Однако используя символ наклонной черты влево это правило можно обойти4. // Символ \ перед символом новой строки позволяет // игнорировать переход на новую строку Std::COU\ t << "Hi" « st\ d::endl; Это эквивалентно следующей строке кода. std::cout << "Hi" << std::endl; 4 Аналогично обходят правило, предписывающее располагать директивы препроцессора в одной строке. — Примеч. ред.
Глава 2. Переменные и базовые типы 65 Таким образом, это можно использовать для записи длинных строковых литералов. // многострочный литерал std::cout << "a multi-line \ string literal \ using a backslash" « std::endl; return 0; } Обратите внимание, символ наклонной черты влево должен располагаться в строке последним, после него не должно быть никаких комментариев или пробелов. Кроме того, все пробелы и символы табуляции в начале следующей строки окажутся частью литерала. Поэтому строки продолжения длинного литерала не имеют обыч- ного отступа. Упражнения раздела 2.2 Упражнение 2.7. Объясните различия между следующими литеральными константами. (а) ' а' , L1 а ' , " а" , L" а" (Ь) 10, 10u, 10L, 10uL, 012, OxC (с) 3.14, 3.14f, 3.14L Упражнение 2.8. Укажите тип каждой из этих литеральных констант. (а) -10 (b) -10и (с) -10. (d) -10е-2 Упражнение 2.9. Какая из приведенных ниже форм записи недопустима (если она есть)? (a) "Who goes with F\145rgus?\012" (b) 3.14elL (c) "two" L"some" (d) 1024f (e) 3.14UL (f) "multiple line comment" Упражнение 2.10. Напишите программу, которая при помощи управляющих последовательностей отображает на экране строку 2м и символ новой строки. Измените программу так, чтобы между символами 2 и м оказался символ табуляции. 2.3. Переменные Предположим, что число 2 необходимо возвести в степень 10. Не долго думая, эту задачу можно решить примерно следующим образом. ftinclude <iostream> int main() { // первое решение - не самое лучшее std::cout << "2 raised to the power of 10: std::cout << 2*2*2*2*2*2*2*2*2*2; std::cout << std::endl; return 0; Эта программа решает проблему, хотя и приходится несколько раз пересчитать литералы, чтобы точно удостовериться в 10 умножениях числа 2 на себя. Так или иначе, но программа дает правильный ответ — 1 024.
66 Часть I. Основы Но что если необходимо вычислить 2 в степени 17, а затем в степени 23? Изме- нять код программы каждый раз не очень удобно. Хуже того, при этом очень просто допустить ошибку. Изменяя такую программу, можно просчитаться в количестве операций умножения и получить неправильный ответ. В качестве альтернативы прямому решению “в лоб”, вычисление степеней числа 2 можно осуществлять в два этапа. 1. Используя именованные объекты организовать отображение на экране результа- та любого вычисления. 2. Используя средства управления организовать циклическое выполнение после- довательности операторов, пока условие остается истинным. Теперь рассмотрим второй, альтернативный способ возведения числа 2 в степень 10. #include <iostream> int main() { // локальные объекты типа int int value = 2; int pow = 10; int result - 1; // повторять вычисление, пока ent не станет равен pow for (int ent = 0; ent ! = pow; ++cnt) result *= value; // result = result * value; std::cout « value « " raised to the power of " << pow « ": \t" « result « std::endl; return 0; } Объекты value, pow, result и ent — это переменные (variable), которые позво- ляют хранить, изменять и отображать содержащиеся в них значения. Цикл for ор- ганизует многократное выполнение вычислений, пока значение переменной ent не станет равно значению переменной pow. Упражнения раздела 2.3 Упражнение 2.11. Напишите программу, которая запрашивает у пользователя два числа (осно- вание и степень), а затем отображает результат возведения основания в степень. Фундаментальная концепция. Строгая статическая типизация Язык C++ обладает строгим статическим контролем типов (statically typed) данных. Это означает, что проверка соответствия значений заявленным для них типам данных осуществляется во время комииляции. Сам процесс проверки называют контролем соответствия типов (type-cbecking) или типизацией (typing). В большинстве языков тип объекта определяет операции, которые с ним можно вы- полнять. Если тип объекта не предполагает выполнения определенной операции, вы- полнить ее не удастся. Проверка допустимости операций в языке C++ осуществляется во время компиляции. Записанное программистом в исходный код выражение проверяется компилятором на
Глава 2. Переменные и базовые типы 67 предмет соответствия типов использованных в выражении объектов и допустимых для них действий. Если проверка выявит ошибки, компилятор выдаст соответствую- щее сообщение, а исполняемый файл не будет создан. По мере усложнения рассматриваемых программ и используемых в них типов, пре- имущество строгого контроля соответствия типов при поиске ошибок в исходном ко- де, будет продемонстрировано со всей очевидностью. Следствием статической про- верки является то, что тип каждого используемого в программе объекта должен быть известен компилятору. Следовательно, тип переменной необходимо определить раньше, чем переменная данного типа будет использована. 2.3.1. Что такое переменная? Переменная представляет собой именованное хранилище, которым программа способна манипулировать. Для каждой переменной языка C++ указан тип, который определяет ее размер и способ размещения в памяти, а также диапазон допустимых для хранения значений и набор применимых операций. Программисты языка C++ используют термины переменная и объект как синонимы. L-значения и г-значения Более подробная информация о выражениях приведена в главе 5, “Выражения”, а пока отметим, что выражения языка C++ имеют две части. 1. L-значение (lvalue). Выражение, являющееся 1-значением, может располагаться как с левой, так и с правой стороны оператора присвоения. 2. R-значение (rvalue). Выражение, являющееся r-значением, может располагаться только с правой, но никак не с левой стороны оператора присвоения. Переменные, которые являются 1-значениями, могут располагаться как с левой, так и с правой стороны оператора присвоения, а числовые литералы, являющиеся г- значениями, подлежат лишь присвоению, их значение изменить нельзя. Предполо- жим, что определено несколько перемецных. int units_sold = 0; double sales_price = 0, total_revenue - 0; Код, приведенный ниже, содержит ошибки, которые проявятся во время компи- ляции. // ошибка: арифметическое выражение не является 1-значением units_sold * sales_price - total_revenue; // ошибка: литеральная константа не является 1-значением 0 = 1; Некоторые операторы, например оператор присвоения, требуют, чтобы один из его операндов был 1-значением. Поэтому 1-значения применяются значительно чаще, чем r-значения. Способ использования 1-значения определяет контекст, в котором оно присутствует в выражении. Рассмотрим пример. units_sold = units_sold + 1; В этом выражении переменная units_sold используется как операнд двух раз- ных операторов. Оператору + нужны только значения его операндов. Значением пе- ременной (value) называют текущее значение, хранимое в области памяти, принад-
68 Часть I. Основы лежащей данной переменной. Оператор сложения получает эти значения, а вычис- ленную сумму возвращает как результат. Переменная units_sold используется как левая сторона оператора присвоения (=). Оператор присвоения читает выражение с правой стороны и заносит его резуль- тат в переменную, указанную с левой стороны. В этом выражении результат сложе- ния сохраняется в области памяти, которая принадлежит переменной units_sold. Прежнее значение переменной units_sold при этом перезаписывается. В этой книге будут еще не раз продемонстрированы ситуации, когда использование 1 г- или 1-значений существенно влияет на поведение и (или) эффективность программ / у’’/ (в особенности при передаче и возвращении значений из функций). Упражнения раздела 2.3.1 Упражнение 2.12. В чем различите между I- и r-значениями; приведите примеры каждого из них. Упражнение 2.13. Назовите случай, где 1-значение необходимо. Терминология. Что такое объект/ Программисты языка C++ используют термин “объект” часто, и не всегда по делу. В самом общем определении, объект — это область в памяти, для которой указан тип. В более узком смысле, объект — это 1-значение, полученное в результате обработки выражения. Одни программисты используют термин ‘объект лишь для переменных и экземпля- ров классов. Другие используют его, чтобы различать именованные и неименованные объекты, когда речь идет о переменных. Третьи различают объекты и значения, ис- пользуя термин ‘объект" для тех данных, которые могут быть изменены программой, и термин ’значение' — для тех данных, которые предназначены только для чтения. В этой книге используется наиболее распространенное значение термина “объект”, т.е. область памяти, для которой указан тип. Здесь под объектом подразумеваются прак- тически все используемые в программе данные, независимо от того, имеют ли они встроенный тип или тип класса, являются ли они именованными или нет, предназна- чены только для чтения или допускают изменение. 2.3.2. Имя переменной Имя переменной, т.е. ее идентификатор (identifier), может состоять из букв, цифр и символов подчеркивания. Имя должно начинаться с буквы или символа подчер- кивания. Символы в верхнем и нижнем регистрах различаются, т.е. идентифика- торы языка C++ чувствительны к регистру. Ниже представлены четыре разных идентификатора. // объявление четырех разных переменных типа int int somename, someName, SomeName, SOMENAME; Язык C++ не налагает никаких ограничений на длину имен, однако для удобства чтения и записи кода не стоит делать их слишком длинными.
Глава 2. Переменные и базовые типы 69 Такое, например, имя нельзя считать удачным идентификатором. gosh_this_is_an_impossibly_long_name_to_type Ключевые слова языка C++ В языке C++ некоторый набор слов зарезервирован для внутреннего использова- ния. Такие слова называют ключевыми (keyword). Ключевые слова нельзя использо- вать в качестве идентификаторов. Полный список ключевых слов языка C++ приве- ден в табл. 2.2. Таблица 2.2. Ключевые слова языка C++ asm do if return try auto double inline short typedef bool dynamic_cast int signed typeid break else long sizeof typename case enum mutable static union catch explicit namespace static_cast unsigned char export new struct using class extern operator switch virtual const false private template void const_cast float protected this volatile continue for public throw wchar_t default friend register true while delete goto reinterpret_cast Язык C++ резервирует также несколько слов, применяемых в качестве альтерна- тивных имен некоторых операторов. Альтернативные имена предназначены для поддержки нестандартных наборов символов в операторах C++. Эти имена, пере- численные в табл. 2.3, также нельзя использовать в качестве идентификаторов. Таблица 2.3. Альтернативные имена операторов языка C++ and bitand compl not_eq or_eq xor_eq and_eq bitor not or xor Кроме ключевых слов, стандарт резервирует также набор идентификаторов для использования в библиотеке, поэтому пользовательские идентификаторы не могут содержать два последовательных символа подчеркивания, а также начинаться с сим- вола подчеркивания, непосредственно за которым следует прописная буква. Неко- торые идентификаторы, определенные вне функций, не могут начинаться с символа подчеркивания. Соглашения об именовании переменных Существует множество общепринятых соглашений для именования перемен- ных. Применение подобных соглашений может существенно улучшать удобочи- таемость кода.
70 Часть I. Основы Для имен переменных обычно используют символы в нижнем регистре. Напри- мер, index, а не Index или INDEX. Идентификаторам имеет смысл присваивать мнемонические, интуитивно по- нятные имена, которые дают некоторое представление об их назначении. На- пример: оп_1оап (при_ссуде) или salary (зарплата). Несколько слов в идентификаторе разделяют либо символом подчеркивания, либо прописными буквами в первых символах каждого слова. Например: student_ loan или studentLoan, но не student loan. Самым важным аспектом соглашения об именовании является его неукоснительное соблюдение. Упражнения раздела 2.3.2 Упражнение 2.14. Какие из приведенных ниже имен являются недопустимыми (если они есть)? Исправьте каждое недопустимое имя. (a) int double = 3.14159; (b) char (c) bool catch-22; (d) char l_or_2 = '1'; (e) float Float = 3.14f; 2.3.3. Определение объектов Ниже приведены операторы, определяющие пять переменных. int units_sold; double sales_price, std::string title; avg_price; Sales_item curr_book; Каждое определение начинается co спецификатора типа (type specifier), за кото- рым следует одно или несколько (разделяемых запятыми) имен. Завершает опре- деление точка с запятой. Спецификатор типа задает тип объекта: int, double, std: : string или Sales_item — все это названия типов. Типы int и double яв- ляются встроенными, тип std: : string — это тип, определенный в стандартной библиотеке, а тип Sales_item является пользовательским классом, использован- ным ранее в разделе 1.5 (стр. 42) и определенным в последующих главах. Тип задает объем, необходимый для хранения объекта в памяти, а также набор операций, кото- рые можно выполнять с этим объектом. В одном операторе можно определить несколько переменных. double salary, wage; // определены две переменные типа double int month, day, year; // определены три переменные типа int std::string address; // определена одна переменная типа std:'.string Инициализация Определение задает лишь тип переменной и ее имя. В определении объекту мож- но также присвоить исходное значение. Объект, которому присвоено значение при
Глава 2. Переменные и базовые типы 71 определении, называется инициализированным (initialized). Язык C++ поддерживает две формы инициализации переменных: инициализация копии (copy-initialization) и прямая инициализация (direct-initialization). Синтаксис инициализации копии под- разумевает использование знака равенства (=), а при прямой инициализации исход- ное значение помещают в круглые скобки. int ival (1024); int ival = 1024; // прямая инициализация // инициализация копии В обоих случаях переменная ival инициализируется значением 1024. Читателю может показаться не до конца понятным, почему речь идет о знаке равенства, а не об операторе присвоения. Дело в том, что в языке C++ это принципиально — ини- циализация не является присвоением. Инициализация осуществляется при создании переменной, т.е. именно запись в память исходного значения создает переменную. Присвоение подразумевает предварительное удаление текущего значения объекта и замену его новым. Большинство начинающих программистов языка C++ не до конца понимают, по- чему при инициализации переменных используется символ =. На первый взгляд инициализация выглядит как форма присвоения. Но в языке C++ инициализация и присвоение — совершенно разные операции. Эта концепция особенно туманна, по- скольку во многих других языках такое различие несущественно и может игнориро- ваться. Кроме того, даже в языке C++ это различие никак не проявляется до тех пор, пока не приходится создавать довольно сложные классы. Тем не менее, это фунда- ментальная концепция, к которой придется вернуться позже. Между инициализацией копий и прямой инициализацией объектов класса существует не- Ьзм!? 1 которое различие. Более подробная информация об этом различии приведена в главе 13, “Управление копированием”, а пока достаточно знать, что синтаксис прямой инициализа- '-Р? ции гибче и немного эффективней. Множественная инициализация Инициализировать объект встроенного типа можно только одним способом: полу- чить значение и скопировать его в только что созданный объект. Для встроенных ти- пов нет особого различия между прямой инициализацией и инициализацией копий. Но в случае объектов классов ситуация иная. Инициализация некоторых из них может быть осуществлена только при помощи прямой инициализации. Чтобы по- нять причину этого, необходимо немного подробней рассмотреть механизм инициа- лизации объекта класса. В каждом классе может быть определена одна или несколько специальных функ- ций-членов (раздел 1.2, стр. 27), которые задают способ инициализации переменной типа класса. Функции-члены, определяющие способ инициализации, называют кон- структорами (constructor). Подобно любой другой функции, конструктору можно передать несколько аргументов. В классе может быть определено несколько конст- рукторов, каждому из которых может быть передано различное количество аргумен- тов разного типа. Давайте рассмотрим класс string, более подробно описанный в главе 3, “Биб- лиотечные типы данных”. Тип string определен в библиотеке и предназначен для хранения символьных строк переменного размера. Для использования строк, в файл
72 Часть I. Основы исходного кода следует подключить заголовок класса string. Подобно типам вво- да-вывода, класс string определен в пространстве имен std. В классе string определено несколько конструкторов, что предоставляет не- сколько разных способов инициализации строк. Одним из них является инициали- зация строки как копии символьного строкового литерала. #include <string> // альтернативные способы инициализации строк из символьного // строкового литерала std::string titleA = "C++ Primer, 4th Ed."; std::string titleB("C++ Primer, 4th Ed."); В данном случае применима любая форма инициализации. Оба определения соз- дают объект класса string, исходным значением которого является копия передан- ного строкового литерала. Однако строку можно также инициализировать последовательностью, состоящей из заданного количества указанного символа. Приведенный ниже код создает стро- ку, содержащую последовательность из 10 символов 9. std::string all_nines (10, '9'); // all_nines = "9999999999" В данном случае единственным способом инициализации переменной а11_ nines является прямая форма. При инициализации последовательностью, копии применять нельзя. Инициализация нескольких переменных Когда при определении инициализируются две (или более) переменные, каждой из них можно назначить собственное значение. Для этого непосредственно после имени каждого объекта следует указать его значение. В одном определении могут находиться как инициализированные, так и неинициализированные переменные. Обе формы синтаксиса инициализации могут быть совмещены. #include <string> // ok: сначала определить и инициализировать salary, II чтобы использовать ее при вычислении wage double salary - 9999.99, wage(salary + 0.01); // ok: инициализированная и неинициализированная int interval, month = 8, day = 7, year = 1955; // ok: обе формы синтаксиса инициализации std::string title("C++ Primer, 4th Ed."), publisher = "A-W"; Объект может быть инициализирован результатом выражения любой сложности, включая значение, возвращаемое функцией. double price = 109.99, discount = 0.16; double sale_price = apply_discount(price, discount); Здесь функции apply_discount О передаются два значения типа double, и она возвращает значение типа double. В этом примере функции apply_discount () передаются переменные price и discount, а возвращаемое ей значение использу- ется при инициализации переменной sale_price.
Глава 2. Переменные и базовые типы 73 Упражнения раздела 2.3.3 Упражнение 2.15. В чем различие между следующими определениями (если они есть). int month = 9, day = 7; int month = 09, day = 07; Если одно из определений содержит ошибку, как ее исправить? Упражнение 2.16. Предположим, что функция calc () возвращает значение типа double. До- пустимы ли приведенные ниже определения? Исправьте ошибки, если они есть. (a) int саг = 1024, auto = 2048; (b) int ival = ival; (с) std::cin >> int input_value; (d) double salary = wage = 9999.99; (e) double calc = calc(); 2.3.4. Правила инициализации переменных Иногда, при определении переменной без инициализации, система способна ини- циализировать ее самостоятельно. Конкретное значение (если оно есть) зависит от типа переменной, а иногда и от момента определения. Инициализация переменных встроенного типа Будет ли инициализирована переменная встроенного типа автоматически, зави- сит от того, где именно она определена. Переменные, определенные вне тела какой- либо функции, инициализируются нулевым значением. Переменные встроенного типа, определенные внутри тела функции, не инициализируются. При использова- нии неинициализированной (uninitialized) переменной для чего-либо, отличного от присвоения ей значения, будет получен неопределенный результат’. Иногда подоб- ные ошибки очень трудно найти. Как уже упоминалось, ни в коем случае не стоит полагаться на неопределенное поведение. ЖХ Авторы настоятельно рекомендуют инициализировать каждый объект встроенного ~ типа. Не всегда необходимо инициализировать такие переменные, но лучше сделать /^омеиЭуем эт0> если нет уверенности в том, что от инициализации данной переменной вполне ~ можно отказаться. Внимание! Неинициализированные переменные — причина проблем во время выполнения Использование неинициализированного объекта — это наиболее распространенный вид трудно обнаруживаемых ошибок. Компилятор не обязан искать случаи использо- вания неинициализированных переменных, хотя некоторые выдают по этому поводу предупреждения. Однако все случаи использования неинициализированных перемен- ных никакой компилятор не обнаружит. Иногда (если повезет) неинициализированная переменная приводит к отказу сразу, при запуске программы. Обнаружив место, где происходит отказ, как правило, до- 5 То есть будет использовано случайное значение, находящееся в памяти по адресу, на ко- торый указывает переменная. — Примеч. ред.
74 Часть I. Основы вольно просто выяснить, что его причиной является неправильно инициализирован- ная переменная. Но иногда программа срабатывает, хотя результат получается ошибочным. Возможен даже худший вариант, когда на одной машине результаты получаются правильными, а на другой происходит сбой. Кроме того, добавление кода во вполне работоспособную программу в неподходящем месте, тоже может привести к внезапному возникновению проблем. Проблема заключается в том, что фактически неинициализированные переменные все же имеют значение. Размещая переменную в области памяти, компилятор считает расположенные там биты исходным значением переменной. Когда речь идет о цело- численном типе, любой набор битов интерпретируется как вполне законное значение, хотя оно вряд ли окажется тем, которое имел в виду программист. Поскольку значение допустимо, весьма маловероятно, что его применение приведет к аварийному отказу. Но что еще менее вероятно, так это получение правильного результата вычисления. Инициализация переменных типа класса В каждом классе определено, как именно объекты его типа могут быть инициали- зированы. Для этого в каждом классе определен один или несколько конструкторов (см. раздел 2.3.3 стр. 71). Известно, например, что класс string обладает по край- ней мере двумя конструкторами. Один из них позволяет инициализировать объект класса string при помощи символьного или строкового литерала, а другой — при помощи последовательности повторяющихся символов. В каждом классе можно также указать, что произойдет, если его переменная бу- дет определена, но не инициализирована. Для этого в классе определен специальный конструктор, известный как стандартный конструктор (default constructor). Этот конструктор называют стандартным потому, что его выполнение осуществляется ав- томатически, если осуществляющий инициализацию объекта конструктор не вызван в коде явно. Стандартный конструктор используется независимо от того, где именно определена переменная. Большинство классов имеет стандартный конструктор. Если класс имеет стан- дартный конструктор, его переменные можно определять без явной инициализации. Например, тип string имеет стандартный конструктор, что позволяет инициали- зировать его объект как пустую строку, т.е. строку без символов. std::string empty; // empty - пустая строка; empty - "" Некоторые классы не имеют стандартного конструктора. Для их объектов необ- ходима явная инициализация. Переменные таких типов без исходного значения оп- ределить нельзя. Упражнения раздела 2.3.4 Упражнение 2.17. Каковы исходные значения (если они есть) каждой из следующих переменных? std::string global_str; int global_int; int main() { int local_int; std::string local_str; return 0; }
Глава 2. Переменные и базовые типы 75 2.3.5. Объявления и определения Как будет продемонстрировано в разделе 2.9 (стр. 89), программы на языке C++ обычно состоят из нескольких файлов. Чтобы организовать доступ к той же пере- менной из нескольких файлов, в языке C++ различают объявления и определения. Определение (definition) переменной выделяет место для ее хранения, а также может назначить ей исходное значение. В программе должно быть одно и только од- но определение переменной. Объявление (declaration) оповещает программу об имени и типе переменной. Оп- ределение одновременно является и объявлением: при определении переменной объявляются ее имя и тип. Используя ключевое слово extern можно объявить имя переменной без ее определения. Объявление, которое не является определением, со- стоит из имени объекта и его типа, которому предшествуют ключевое слово extern, extern int i; // объявить, но не определять переменную i int i; // объявить и определить переменную i Объявление переменной внешней (external) не является определением, и память для ее хранения не выделяет. Фактически это свидетельствует о том, что определе- ние переменной находится в другом месте программы. Переменная в программе мо- жет быть объявлена несколько раз, но определена она может быть только один раз. Объявление может осуществить инициализацию только тогда, когда оно одно- временно является определением, поскольку только определение выделяет память. Область памяти при инициализации выделяется всегда. Поэтому если при объявле- нии осуществляется инициализация, происходит определение, даже когда объявле- ние помечено как extern. extern double pi = 3.1416; // определение Несмотря на применение ключевого слова extern, этот оператор определяет пе- ременную pi, осуществляя ее создание и инициализацию. Объявление внешней пе- ременной может включать инициализацию лишь вне функции. Поскольку объявление внешней переменной с инициализацией рассматривается как определение, любое последующее определение этой переменной будет ошибкой, extern double pi = 3.1416; // определение double pi; // ошибка: переопределение pi Аналогично, последующее повторное объявление переменной, которая уже была инициализирована, также является ошибкой. extern double pi = 3.1416; // определение extern double pi; // ok: объявление, а не определение extern double pi = 3.1416; // ошибка: переопределение pi Обсуждение различия между объявлением и определением может показаться из- лишним, однако на самом деле оно очень важно. ГВ языке C++ переменная должна быть определена обязательно и только один раз. Кроме 1 того, перед применением она должна быть определена или объявлена. Любая переменная, используемая в более чем одном файле, требует раздельно- го объявления и определения. В таких случаях один файл содержит определение
76 Часть I. Основы переменной, а другие файлы, в которых она используется, содержат объявления (но не определения). Упражнения раздела 2.3.5 Упражнение 2.18. Объясните назначение каждого из экземпляров переменной name. extern std::string name; std::string name("exercise 3.5a"); extern std::string name("exercise 3.5a"); 2.3.6. Область видимости имен Каждое имя в программе на языке C++ принадлежит вполне определенному объекту (например переменной, функции, типу и т.д.). Несмотря на это требование, имена в программах зачастую применяются многократно: одно и то же имя может использоваться в разных контекстах, от которых зависит конкретное назначение имени. Контекст, используемый для различения имен, называется областью видимо- сти (scope). Область видимости — это отсек программы. Имя может принадлежать разным объектам, находящимся в разных областях видимости. Большинство областей видимости в коде C++ разграничены фигурными скобка- ми. Фактически, имена видимы от точки их объявления до конца области видимо- сти, в которой они объявлены. Давайте вернемся к программе, которая уже обсужда- лась в разделе 1.4.2 на стр. 36. #include <iostream> int main() int sum = 0; // сложить числа от 1 до 10 включительно for (int val = 1; val <= 10; ++val) sum += val; // эквивалентно sum - sum + val std::cout "Sum of 1 to 10 inclusive is " sum std::endl; return 0; } В этой программе определены три собственных имени, а также использованы два имени из стандартной библиотеки. Здесь определена функция по имени main () и две переменные с именами sum и val. Имя main определено вне любых фигурных скобок и видимо во всей программе. Имена, определенные вне функций, имеют гло- бальную область видимости (global scope); они доступны в программе повсюду. Имя sum определено внутри области видимости функции main (). Она будет доступна внутри функции main (), но не вне ее. Переменная sum имеет локальную область видимости (local scope). Имя val немного интересней. Оно определено в области видимости оператора for (раздел 1.4.2, стр. 36). Это имя применимо только в самом операторе, но не в другом месте функции main (). Такую область видимости назы- вают операторной (statement scope).
Глава 2. Переменные и базовые типы 77 Вложенные области видимости в языке С++ Имена, определенные в глобальной области видимости, применимы и в локаль- ной области видимости; глобальные имена и имена, определенные для функции ло- кально, применимы внутри области видимости оператора и т.д. Во внутренней об- ласти видимости имена могут быть переопределены (redefined). Чтобы разобраться, какому именно объекту принадлежит имя, необходимо выяснить области видимо- сти, в которой имя определено. ttinclude <iostream> #include <string> /* Программа предназначена исключительно для демонстрации областей ★ видимости, поскольку использование в функции глобальной ★ переменной, а также определение одноименной локальной * переменной - это очень плохой стиль программирования * / std::string si = "hello"; // si имеет глобальную область видимости int main() std::string s2 = "world"; // s2 имеет локальную область // видимости II используя глобальную si отобразить "hello world" std::cout << si << " " << s2 << std::endl; int si - 42; // локальная si скрывает глобальную si II используя локальную si отобразить "42 world" std::cout << si << " " << s2 << std::endl; return 0; } В этой программе определены три переменные: глобальная переменная типа string по имени si, локальная переменная типа string по имени s2 и локальная переменная типа int по имени si. Определение локальной переменной si скрыва- ет глобальную переменную s 1. Переменные видимы начиная с точки их объявления. Таким образом, на момент выполнения первого оператора вывода, локальная переменная si еще не существует и не видима. Поэтому в данном выражении вывода под именем si подразумевается глобальная переменная si. В результате на экране отображаются слова “hello world”. Второй оператор вывода расположен после определения локальной пере- менной si. Теперь в области видимости находится локальная переменная si, кото- рая скрывает одноименную глобальную переменную. Поэтому второй оператор вы- вода, использующий локальную переменную si вместо глобальной, выводит на эк- ран слова “ 4 2 world”. Программы, подобные приведенной выше, невероятно сложны в отладке. Как правило, определять локальные переменные, имена которых совпадают с именем глобальной переменной, а также использовать глобальные переменные в функциях — это крайне неудачное решение. Для локальных и глобальных переменных желательно использовать разные имена. Более подробная информация о локальной и глобальной области видимости при- ведена в главе 7, “Функции”, а об операторной области видимости — в главе 6, “Операторы”. Язык C++ имеет еще два уровня областей видимости: область видимо- сти класса (class scope), рассматриваемая в главе 12, “Классы”, и область видимости пространства имен (namespace scope), рассматриваемая в разделе 17.2.
78 Часть I. Основы Упражнения раздела 2.3.6 Упражнение 2.19. Каково значение переменной j в следующей программе? int i = 42; int main() { int i = 100; int j = i; } Упражнение 2.20. Какие значения отобразит на экране следующий код? int i = 100, sum = 0; for (int i = 0; i != 10; ++i) sum += i; std::cout << i << " " << sum << std::endl; Упражнение 2.21. Допустим ли следующий код? int sum = 0; for (int i = 0; i != 10; ++i) sum += i; std::cout « "Sum from 0 to " << i << " is " << sum << std::endl; 2.3.7. Определение переменных по месту применения Фактически, определения и объявления переменных могут располагаться в лю- бом месте кода программы и даже в операторах, допускающих это. Единственное ус- ловие — переменная должна быть объявлена или определена до применения. Создавать объект имеет смысл ближе к точке его первого применения. Определение объекта рядом с местом его первого применения улучшает удобо- читаемость кода. В этом случае при поиске определения переменной, читатель не должен возвращаться к началу раздела кода. Кроме того, когда переменная опреде- ляется поближе к месту ее первого применения, ей зачастую проще назначить ре- альное исходное значение. При размещении объявлений следует выполнить одно условие: переменная ста- новится доступной начиная с места ее определения до конца блока включительно. Таким образом, переменная должна быть определена либо в наиболее удаленной об- ласти видимости, в которой она будет использоваться, либо перед ней. 2.4. Спецификатор const Приведенный ниже цикл for имеет две проблемы, и обе они связаны с использо- ванием литерала 512 в качестве верхней границы. for (int index = 0; index != 512; ++index) {
Глава 2. Переменные и базовые типы 79 Проблема первая — удобочитаемость. Что означает сравнение значения перемен- ной index с числом 512? Какой цикл осуществляется, т.е. что он делает 512 раз? (Подобные числа, в данном примере 512, называют магическими (magic number), по- скольку из контекста их смысл абсолютно не очевиден. Как будто число было взято фокусником прямо из воздуха.) Проблема вторая — последующая поддержка. Вообразите, что программа доста- точно велика и число 512 повторяется 100 раз. Предположим, что в 80 случаях число 512 используется для указания размера какого либо буфера, а в 20 других — для иных целей. Теперь, допустим, возникла необходимость увеличить размер буфера до 1024. Чтобы внести это изменение, придется осмотреть каждое из мест, где исполь- зуется число 512, и каждый раз выяснять, относится ли данное число 512 к размеру буфера или нет. Ошибка даже в одном случае сделает программу неработоспособной и потребует повторного осмотра каждого случая применения числа. Решением обеих проблем является применение объекта, инициализированного значением 512. int bufsize = 512; // размер буфера ввода for (int index = 0; index != bufSize; ++index) { } Выбрав подходящее по смыслу имя, например bufsize (размер буфера), код программы можно сделать более удобочитаемым. Теперь в условии проверяется объект, а не литеральная константа. index != bufsize Если этот размер придется теперь изменить, искать 80 случаев его применения уже не нужно. Достаточно откорректировать лишь одну строку, где инициализиру- ется переменная bufsize. Этот подход не только существенно проще, но и вероят- ность ошибок при нем значительно ниже. Определение константного объекта Существует, однако, еще одна серьезная проблема, связанная с определением пере- менной, предназначенной для хранения постоянного значения. Дело в том, что значе- ние переменной bufsize может быть случайно изменено. Для решения этой пробле- мы используется спецификатор const, который преобразует объект в константу, const int bufsize = 512; // размер буфера ввода Здесь определена константная (постоянная) переменная bufsize, которая ини- циализирована значением 512. Переменная bufsize все еще остается 1-значением (раздел 2.3.1, стр. 67), но теперь оно неизменно. Любая попытка записи в константу bufsize приведет к ошибке во время компиляции. bufsize =0; // ошибка: попытка записи в константный объект Поскольку значение объекта, объявленного константным, впоследствии изменить нельзя, его инициализация должна быть осуществлена при определении. const std::string hi = "hello!"; // ok: инициализация const int i, j =0; // ошибка: i - неинициализированная константа
80 Часть I. Основы По умолчанию константные объекты локальны для файла Неконстантная переменная, определенная в глобальной области видимости (раз- дел 2.3.6 стр. 76), доступна на протяжении всей программы. Таким образом, опреде- лив неконстантную переменную в одном файле (при условии, что было сделано со- ответствующее объявление), ее можно использовать и в другом файле. // файл file_l.cc int counter; // определение // файл file_2.cc extern int counter; // применение счетчика из файла file_l ++counter; // приращение счетчика, определенного // в файле file_l В отличие от других переменных, если противное не указано явно, объявленные в глобальной области видимости константы локальны для файла, в котором они оп- ределены. То есть она существует только в этом файле и не доступна из других. Чтобы сделать константный объект доступным на протяжении всей программы, применяется ключевое слово extern. // файл file_l.cc // определить и инициализировать константу, которая будет // доступна из других файлов extern const int bufsize = fcn(); // файл file_2.cc extern const int bufsize; // применение bufsize из файла file_l for (int index = 0; index != bufsize; ++index) Здесь константа bufsize определена и инициализирована в результате вызова функции fen () в файле file_l. се. Ключевое слово extern в определении кон- станты bufsize означает, что она применяются и в других файлах. Ее объявление в файле f ile_2 .се также использует ключевое слово extern. В данном случае слово extern означает, что постоянная bufsize уже определена, а следовательно, инициализация ей не нужна. В разделе 2.9.1 (стр. 92) будет продемонстрировано, почему константные объекты сделаны локальными для файла. Неконстантные переменные по умолчанию являются внешними (extern). Чтобы сделать константную переменную доступной для других файлов, ее необходимо явно определить как внешнюю (extern). Упражнения раздела 2.4 Упражнение 2.22. Следующий фрагмент кода вполне допустим, хотя стиль программирования ос- тавляет желать лучшего. Какую проблему он содержит? Как можно улучшить этот код? for (int i = 0; i < 100; ++i) // применение i Упражнение 2.23. Какой из приведенных ниже вариантов допустим? В каких случаях некоторые из вариантов недопустимы и почему? const int sz = ent; (c) cnt++; sz++;
Глава 2. Переменные и базовые типы 81 2.5. Ссылки Ссылка (reference) является альтернативным именем объекта. В реальных про- граммах ссылки используются в основном как формальные параметры функций. Бо- лее подробная информация о передаче параметров по ссылке приведена в разде- ле 7.2.2 (стр. 258), а сейчас рассмотрим применение ссылок как независимых объектов. Ссылка — это составной тип (compound type), имени которого в определении пред- шествует символ &. Составной тип — это тип, определенный в терминах другого типа. В случае ссылок каждый ссылочный тип “ссылается” на некий другой тип. Нельзя объя- вить ссылку на ссылочный тип, но можно создать ссылку на любой другой тип данных. Ссылка обязательно должна быть инициализирована объектом того же типа, что и сама ссылка. int ival - 1024; int SrefVal = ival; // ok: refVal ссылается на ival int &refVal2; // ошибка: ссылку следует инициализировать int &refVal3 =10; // ошибка: инициализировать следует объектом Ссылка является псевдонимом Поскольку ссылка является всего лишь другим именем объекта, с которым она связана, все операции со ссылкой фактически осуществляются с самим объектом. refVal += 2; Здесь к значению переменной ival, скрытой под псевдонимом refVal, добав- ляется 2. int ii = refVal; Аналогично, переменной ii присваивается значение переменной ival. После инициализации ссылки объектом, она остается привязанной к этому объекту на В протяжении всего своего существования. Переприсвоить ссылку другому объекту нельзя. Очень важно уяснить, что ссылка является лишь другим именем объекта. На са- мом деле, к переменной ival можно обратиться как по ее фактическому имени, так и по псевдониму refVal. Присвоение — это всего лишь одна из операций, поэтому результатом следующего действия будет присвоение значения 5 переменной ival. refVal = 5; Как следствие, ссылку необходимо инициализировать при определении, по- скольку инициализация — это единственный способ указать на объект, к которому относится ссылка. Определение нескольких ссылок В одном определении можно создать несколько ссылок. Каждому идентификато- ру, являющемуся ссылкой, должен предшествовать символ &. int i = 1024, i2 = 2048; int &r = i, r2 = i2; int i3 = 1024, &ri = i3; int &r3 = i3, &r4 = i2; // r - ссылка, r2 переменная типа int // определение объекта и ссылки на него // определение двух ссылок
82 Часть I. Основы Константные ссылки Константная ссылка (const reference) — это ссылка на константный объект. const int ival = 1024; const int &refVal = ival; // ok: и ссылка и объект - константы int &ref2 - ival; // ошибка: неконстантная ссылка на // константный объект По ссылке ref Val можно осуществлять чтение данных, но не запись. Таким об- разом, любое присвоение для ссылки refVal запрещено. Это ограничение имеет глубокий смысл: поскольку самому объекту ival нельзя присвоить новое значение, это должно быть невозможным и для ссылки ref Val, ведь в противном случае мож- но будет изменить значение объекта ival. По той же самой причине инициализация ссылки ref 2 объектом ival будет оши- бочной, ведь ref 2 является простой, неконстантной ссылкой (nonconst reference), которая вполне применима для изменения значения объекта, на который она указы- вает. Присвоение значения константе ival при помощи ссылки ref 2 привело бы к изменению значения константного объекта. Чтобы предотвратить такое измене- ние, назначать простой ссылке константный объект запрещается. Терминология. Константная ссылка — это ссылка на константу Программисты C++, как правило, используют термин “константная ссылка” (const reference), однако фактически речь идет о “ссылке на константу” (reference to const). Аналогично, под разговорным термином “неконстантная ссылка ’ (nonconst reference) подразумевается “ссылка на неконстантный тип ’ (reference to nonconst type). Эти тер- мины настолько общеприняты, что в этой книге используются именно они. Константная ссылка может быть инициализирована объектом любого типа, а также r-значением (раздел 2.3.1, стр. 67), например литеральной константой, int i = 42; // допустимо только для константных ссылок const int &r = 42; const int &:r2 = г + i; Но для неконстантных ссылок подобная инициализация недопустима. Попытка сделать это приведет к ошибке во время компиляции. Ее причина не очевидна, но вполне объяснима. Давайте рассмотрим, что происходит при попытке назначить ссылку объектам разного типа. double dval = 3.14; const int &ri = dval; Компилятор преобразует этот код в нечто вроде следующего. int temp = dval; // создать временную переменную типа int из // переменной типа double const int &ri = temp; // назначить ссылку ri этой временный / / переменной Если ссылка ri не является константной, ей вполне можно присвоить новое зна- чение. Это изменило бы не переменную dval, а переменную temp. Программист, ожи- дающий, что присвоение значения ссылке ri изменит значение переменной dval,
Глава 2. Переменные и базовые типы 83 будет весьма удивлен, когда изменения не произойдет. Разрешение назначать значе- ниям, требующим наличия временных объектов, только константных ссылок, позво- ляет полностью избежать подобных проблем, поскольку константная ссылка пред- назначена только для чтения. Неконстантная ссылка может быть назначена объекту только того же типа, что и ссылка. Константная ссылка может быть назначена r-значению или объекту, тип которого не сов- падает с типом ссылки. Упражнения раздела 2.5 Упражнение 2.24. Какие из следующих определений (если они есть) недопустимы? Почему? Как их исправить? (a) int ival = 1.01; (b) int &rvall = 1.01; (c) int &rval2 = ival; (d) const int &rval3 = 1; Упражнение 2.25. Какие из следующих присвоений (если они есть) являются недопустимыми (с учетом предыдущих определений)? Если таковые есть, объясните, почему. (a) rval2 = 3.14159; (b) rval2 = rval3; (с) ival = rval3; (d) rval3 = ival; Упражнение 2.26. В чем разница между определениями (а) и присвоениями (Ь)? Какие из них (если есть) недопустимы? (a) int ival =0; (b) ival = ri; const int &ri =0; ri = ival; Упражнение 2.27. Что отобразит на экране следующий код? 2.6. Определение имен типов Определение имен типов (typedef) позволяет задавать синонимы для названий типов данных. typedef double wages; typedef int exam_score; typedef wages salary; // wages - синоним double // exam_score - синоним int // indirect - синоним double Созданное имя можно использовать как спецификатор типа. wages hourly, weekly; exam score test_result; // аналог double hourly, weekly; // аналог int test_result; Определение имени типа начинается с ключевого слова typedef, за которым следует указание типа данных и идентификатора. Идентификатор, или вновь опре- деленное имя, не создает новый тип данных, а скорее является синонимом уже суще- ствующего типа данных. Вновь определенное имя можно использовать в программе везде, где допустимо использование имени типа данных. Синонимы имен типов используются, как правило, в следующих целях. Чтобы скрыть реализацию данного типа или подчеркнуть цель, для которой он используется.
84 Часть I. Основы Чтобы рационализировать сложное определение типа и сделать его понятней. Чтобы позволить использовать один тип в нескольких случаях, когда при созда- нии переменных, для ясности, применяются разные названия одного типа. 2.7. Перечисления Зачастую для некоего атрибута необходимо определить набор возможных значе- ний. Например, файл можно открыть одним из трех способов: для ввода (input), для вывода (output) и для добавления (append). Чтобы отслеживать состояние файла (т.е. цель, для которой он был открыт), с каждым соответствующим идентификато- ром можно связать уникальное постоянное число. Таким образом, можно написать следующий код. const const const input = 0; output = 1; append = 2; Хотя такой подход вполне работоспособен, он имеет существенный недостаток: эти значения никак не связаны. Перечисление (enumeration) предоставляет альтерна- тивный способ не только определения наборов целочисленных констант, но и их группировки. Определение и инициализация перечисления При определении перечисления используется ключевое слово enum, за которым следует необязательное имя перечисления и заключенный в фигурные скобки спи- сок перечислителей (enumerator), т.е. допустимых значений, разделенных запятыми. // input это 0, output - 1, a append - 2 enum open_modes {input, output, append}; По умолчанию первый перечислитель получает нулевое значение, а каждый по- следующий — значение, которое на единицу больше. Перечислители — это константные значения Одному или нескольким перечислителям можно присвоить конкретные значе- ния. Используемое для инициализации перечислителя значение должно быть кон- стантным выражением (constant expression). Константное выражение — это выра- жение, в результате вычисления которого компилятором во время компиляции получается значение целочисленного типа. Целочисленный константный литерал (integral literal constant) — это константное выражение, т.е. константный объект (раздел 2.4, стр. 78), который самостоятельно инициализируется результатом кон- стантного выражения. Например, можно создать следующее перечисление. // shape - 1, sphere - 2, cylinder - 3, polygon - 4 enum Forms {shape = 1, sphere, cylinder, polygon}; f В перечислении enum Forms перечислителю shape значение 1 было присвоено явно. Другие перечислители инициализированы неявно: sphere — значением 2, cylinder — значением 3, a polygon — значением 4.
Глава 2. Переменные и базовые типы 85 Значение перечисления необязательно должно быть уникальным. // point2d - 2, point2w - 3, point3d - 3, point3w - 4 enum Points { point2d = 2, point2w, point3d = 3, point3w }; В этом примере перечислитель point 2d явно инициализирован значением 2. Следующий перечислитель, point2w, инициализирован по умолчанию, т.е. ему присвоено значение, которое на единицу больше, чем у предыдущего. Таким обра- зом, перечислитель point 2 w инициализирован значением 3. Перечислитель point 3d явно инициализирован значением 3, а перечислитель point3w, снова по умолча- нию, инициализирован значением 4. Значение перечислителя изменить нельзя. Как следствие, сам перечислитель яв- ляется константным выражением и может применяться везде, где необходимо кон- стантное выражение. Каждое перечисление является уникальным типом данных Каждое ключевое слово enum определяет новый тип данных. Подобно любым другим типам, объекты типа Points можно определять и инициализировать, а затем использовать их различными способами. Объект типа перечисления может быть инициализирован или присвоен лишь либо в виде одного из его перечислителей, ли- бо другим объектом перечисления того же типа. Points pt3d = point3d; Points pt2w = 3; pt2w = polygon; pt2w = pt3d; // ok: point3d является перечислителем // типа Points II ошибка: pt2w инициализируется значением // типа int // ошибка: polygon не является // перечислителем типа Points // ok: оба объекта имеют тип /! перечисления Points Обратите внимание, что нельзя присвоить значение 3 объекту типа Points, даже при том, что 3 — это одно из значений перечислителя типа Points. 2.8. Типы классов Чтобы определить собственный тип данных, в языке C++ применяется класс (class). Класс позволяет определить данные, которые будет содержать объект его ти- па, а также операции, которые с объектом его типа можно выполнять. Все библио- течные типы, такие как string, istream и ©stream, определены как классы. Классы — основа языка C++. Описанию их поддержки в языке C++, а также опе- раций с использованием типов классов, посвящены части III, “Абстракция, классы и данные”, IV, “Объектно-ориентированное и общее программирование”, и V, “Дополнительные темы”. В главе 1, “Первые шаги”, для решения проблемы книжного магазина был ис- пользован тип Sales_item. Объекты типа Sales_item использовались для от- слеживания данных о сбыте книг по их ISBN. В этом разделе будет продемонстриро- вано, как определить простой класс, Sales_item.
86 Часть I. Основы Проектирование класса начинается с операций Каждый класс имеет интерфейс (interface) и реализацию (implementation). В ин- терфейсе указаны операции, которые может выполнять с объектом класса код, кото- рый его использует. Реализация обычно содержит необходимые классу данные, а также все функции, которые необходимы классу, но не предназначены для общего использования. Определение класса обычно начинают с разработки его интерфейса, т.е. выясне- ния операций, которые класс будет поддерживать. Исходя из этих операций можно выявить данные, которые потребуются классу для решения его задач, а также опре- делить все необходимые для этого функции. Поддерживаемые классом операции — это те действия, которые использовались в главе 1, “Первые шаги”. Сами операции были описаны в разделе 1.5.1 (стр. 43). Оператор суммы (+) позволяет сложить два объекта класса Sales_item. Операторы ввода (>>) и вывода (<<) позволяют читать и отображать объекты класса Sales_item. Оператор присвоения (=) позволяет присвоить один объект класса Sales_item другому. Функция same_isbn () позволяет выяснить, не относятся ли два объекта класса Sales itemK той же самой книге. К разработке этих операций придется вернуться в главах 7, “Функции”, и 14, “Пе- регрузка операторов и преобразования”, после того как будет описано определение функций и операторов. Несмотря на то, что реализовать эти функции пока невозмож- но, вполне можно выявить данные, которые понадобятся для осуществления этих опе- раций. Разрабатываемый класс Sales_item должен выполнять следующие операции. 1. Отслеживать количество проданных экземпляров по каждой книге. 2. Сообщать об общей выручке по каждой книге. 3. Вычислять среднюю цену, по которой продается каждая книга. Проанализировав этот список задач, можно придти к выводу, что для отслеживания количества проданных экземпляров книг понадобится переменная типа unsigned, а для отслеживания суммы выручки понадобится переменная типа double. Обладая этими данными, можно вычислить среднюю цену, разделив общий доход на количест- во проданных экземпляров. Поскольку необходимо знать, о которой именно из книг идет речь, для хранения ее ISBN понадобится также переменная типа string. Определение класса Sales_item Таким образом, необходим способ, позволяющий создать тип данных, который будет способен хранить эти три элемента данных и выполнять операции, использо- ванные в главе 1, “Первые шаги”. В языке C++ для создания такого типа данных ис- пользуется определение класса. class Sales_item { public: // операции, допустимые для объектов класса Sales_item, // будут указаны здесь
Глава 2. Переменные и базовые типы 87 private: std::string isbn; unsigned units_sold; double revenue; } ; Определение класса начинается с ключевого слова class, за которым следует идентификатор, обозначающий имя класса. Тело класса располагается внутри фи- гурных скобок. Определение класса завершается точкой с запятой после закрываю- щей фигурной скобки. Довольно распространенной ошибкой начинающих программистов является отсутствие точки с запятой в конце определения класса. В теле класса, которое может быть пустым, определяют данные и операции, кото- рые и составляют новый тип. Операции и данные, являющиеся частью класса, назы- вают членами класса (member). Операции называют также функциями-членами (member function) (раздел 1.2, стр. 27), а данные — данными-членами (data member) или переменными-членами. Класс может содержать любое количество разделов, помеченных маркерами дос- тупа (access label) public (открытый) или private (закрытый). Маркер доступа указывает, будет ли член класса доступен вне объекта. Код, использующий объект класса, может обращаться только к его открытым (public) членам. При определении классов создается новый тип данных. Имя класса — это имя нового типа. Назвав класс именем Sales_item, программист указывает, что Sales_item— это новый тип данных, переменные которого теперь можно созда- вать в программе. Каждый класс создает собственную область видимости (раздел 2.3.6, стр. 76). Та- ким образом, имена, присвоенные данным и операциям внутри тела класса, должны быть уникальными внутри самого класса, но могут совпадать с именами, определен- ными вне класса. Данные-члены класса Переменную-член класса определяют аналогично обычным переменным. Для нее необходимо указать тип и присвоить имя, точно так же, как и при определении обычной переменной. В данном случае класс имеет три переменные-члена: переменная isbn типа string, переменная units_sold типа unsigned и переменная revenue типа double. Переменные-члены класса определяют содержимое объектов этого класса. Когда объект класса Sales_item окажется создан, он будет содержать три пере- менные: типа string, типа unsigned и типа double. Но между определением обычных переменных и переменных-членов класса есть одно важное различие: члены класса при определении, как правило, нельзя инициа-
88 Часть I. Основы лизировать. При определении переменной-члена ей только присваивают имя и ука- зывают тип. Для инициализации переменных-членов, определенных внутри опреде- ления класса, применяются специальные функции-члены, называемые конструкто- рами (constructor) (раздел 2.3.3, стр. 71). Конструкторы класса Sales_item будут определены в разделе 7.7.3 на стр. 288. Маркеры доступа Маркеры доступа (access label) указывают, может ли код, который использует объект этого класса, обращаться к данному члену класса. Функции-члены класса мо- гут использовать любой член собственного класса независимо от его уровня доступа (access level). В определении класса маркеры доступа public и private могут ис- пользоваться несколько раз. Установленный уровень доступа остается текущим до тех пор, пока не встретится новый маркер доступа. В разделе public (открытые) определяют те члены класса, к которым можно об- ращаться в любой части программы. Обычно в раздел public помещают определе- ния тех операций, которые может выполнять любой код программы. К закрытым членам класса, объявленным в разделе private, внешний код, кото- рый не является частью класса, доступа не имеет. Сделав переменные-члены класса Sales item закрытыми, разработчик гарантировал, что внешний код, использую- щий объекты класса Sales_item, не сможет манипулировать ими непосредственно. Программы наподобие созданной в главе 1, “Первые шаги”, не смогут обратиться к закрытым членам класса. С объектами класса Sales_item можно выполнять за- данные для них операции, но непосредственно изменять их данные нельзя. Применение ключевого слова struct Язык C++ содержит ключевое слово struct, которое также применяется для оп- ределения классов6. Ключевое слово struct унаследовано из языка С. При определении класса с использованием ключевого слова class, все его чле- ны, определенные до первого маркера доступа, по умолчанию являются закрытыми, а при использовании ключевого слова struct те же члены будут открытыми. При- менение ключевых слов class и struct влияет только на начальный, заданный по умолчанию уровень доступа. Класс Sales_item можно было бы определить и следующим образом. struct Sales_item { // маркер public здесь не нужен, поскольку по умолчанию // функции объектов Sales_item будут открыты private: std::string isbn; unsigned units_sold; double revenue; }; Между первоначальным определением класса и данным существует лишь два различия: здесь вместо ключевого слова class применяется ключевое слово struct, а также отсутствует ключевое слово public, располагавшееся непосред- 6 На самом деле структур. Но в языке C+ + различия между структурами и классами не- значительны. — Примеч. ред.
Глава 2. Переменные и базовые типы 89 ственно после открывающейся фигурной скобки. Поскольку все члены структуры, если иное не указано явно, являются открытыми, нет никакой необходимости в мар- кере public. Единственное различие между классами, определенными при помощи ключевых слов class и struct, заключается в заданном по умолчанию уровне доступа. Члены струк- туры по умолчанию являются открытыми, а члены класса по умолчанию закрыты. Упражнения раздела 2.8 Упражнение 2.28. Откомпилируйте программу, приведенную ниже, и выясните, выдает ли исполь- зуемый компилятор сообщение об отсутствии точки с запятой после определения класса. class Foo { // пусто } // Обратите внимание: точки с запятой нет int main() { return 0; } Если появилось сообщение об ошибке, обратите на это внимание. Упражнение 2.29. В чем отличите между разделами public и private в определении класса. Упражнение 2.30. Подберите тип и определите переменные-члены классов, соответствующие следующим задачам. (а) Номер телефона (Ь) Адрес (с) Имя служащего или название компании (d) Студент университета 2.9. Создание собственных файлов заголовка Как упоминалось в разделе 1.5 (стр. 42), определения классов обычно располага- ют в файле заголовка (header file). Здесь будет продемонстрировано, как создать файл заголовка для класса Sales_item. Фактически, программы на языке C++ используют заголовки не только для хра- нения определения класса. Напомним, что каждое имя перед использованием следу- ет объявить или определить. Программы, которые рассматривались до сих пор, вы- полняли это требование, поскольку весь их код помещался в одном файле. Пока оп- ределение каждого объекта предшествует коду, в котором он используется, эта стратегия работает. Однако большинство программ не столь просты, чтобы их код мог находиться в одном файле. Программы, состоящие из нескольких файлов, нуж- даются в способе связи используемых имен и их объявлений. В языке C++ для этого используются файлы заголовка. Для обеспечения возможности разделить программу на несколько логических частей, язык C++ предоставляет технологию, известную как раздельная компиляция (separate compilation). Раздельная компиляция позволяет составлять программу из нескольких файлов. Для применения раздельной компиляции, поместим определе-
90 Часть I. Основы ние класса Sales_item в файл заголовка. Функции-члены класса Sales_item, определение которых продемонстрировано в разделе 7.7 (стр. 284), войдут в состав отдельного файла исходного кода. Код, использующий объекты класса Sales item, например функция main (), также находится в отдельных файлах исходного кода. В каждый из файлов исходного кода, использующего класс Sales item, следует подключить файл заголовка Sales item. h. Компиляция и компоновка нескольких файлов исходного кода Чтобы создать исполняемый файл, компилятор следует уведомить не только о том, где искать функцию main (), но и о том, где расположено определение функций- членов класса Sales_item. Предположим, что существует два файла: main, сс, ко- торый содержит определение функции main (), и Sales_item. сс, который содер- жит функции-члены класса Sales_item. Эти файлы можно откомпилировать сле- дующим образом. $ СС -с main.cc Sales_item.cc # обычно создает а.ехе # некоторые компиляторы создают а. out # передает исполняемые. код в файл main.exe $ СС -с main.cc Sales_item.сс -о main Здесь $ — это приглашение к вводу операционной системы, а # — символ начала ком- ментария командной строки. Теперь можно запустить исполняемый файл, который выполнит функцию main () программы. Если впоследствии изменения были внесены только в один из файлов исходного кода (. сс), зачастую имеет смысл перекомпилировать только его. Большинство компиля- торов позволяют компилировать каждый файл по отдельности. В результате обыч- но получается файл с расширением . о, свидетельствующем о том, что он содержит объектный код. Компилятор позволяет компоновать (link', объектные файлы вместе, чтобы полу- чить исполняемый файл. В операционной системе автора, где для вызова компиля- тора используется команда СС, данную программу можно откомпилировать сле- дующим образом. $ СС -с main.cc # создает main.о $ СС -с Sales_item.cc # создает Sales_item.o $ СС main.о Sales_item.o # обычно создает а.ехе; # некоторые компиляторы создают а.out # передает исполняемый код в файл main.exe $ СС main.о Sales_item.о -о main Чтобы угочнить, как именно компилировать и запускать программы, состоящие из не- скольких файлов исходного кода, на конкретном компиляторе, необходимо обратить- ся к руководству по его применению. Большинство компиляторов обладает параметром, применение которого позволяет включить обнаружение ошибок компилятором. Уточните этот параметр в руководстве по применению используемого компилятора.
Глава 2. Переменные и базовые типы 91 2.9.1. Разработка собственных заголовков Файлы заголовков являются централизованным хранилищем для связанных объявлений. Заголовки обычно содержат объявления классов, внешних (extern) переменных и функций (более подробная информация по этой теме приведена в разделе 7.4 (стр. 277)). В файлы, которые используют или определяют эти объекты, подключают соответствующий заголовок (или заголовки). Правильное применение файлов заголовка предоставляет два преимущества: все файлы гарантированно используют одинаковое объявление каждого объекта; если объявление требует изменений, модифицировать следует только заголовок. При разработке заголовков следует быть очень внимательным. Объединять в за- головке следует лишь те объявления, которые логически связаны вместе. Компиля- ция заголовка требует времени. В достаточно больших программах время компиля- ции может отказаться весьма существенным. - Чтобы сократить время компиляции, необходимое для обработки заголовка, некото- рые реализации языка C++ предоставляют предварительно скомпилированные фай- лы заголовков. Более подробная информация по этой теме приведена в справочнике по используемой версии компилятора C++. Заголовки предназначены для объявлений, а не для определений При создании заголовка не следует забывать о различии между определением, которое может быть только одним, и объявлением, которых может быть несколько (раздел 2.3.5, стр. 75). Операторы, приведенные ниже, являются определением, а сле- довательно, не должны располагаться в заголовке. extern int ival =10; // инициализация, а следовательно, double fica_rate; // определение // не внешняя, тоже определение Хотя переменная ival объявлена внешней (extern), здесь происходит ее ини- циализация, а следовательно, этот оператор является определением. Объявление пе- ременной f ica_rate также является определением, хоть инициализации оно и не имеет, ключевое слово extern здесь отсутствует. Включение любого из этих опре- делений в два или несколько файлов той же программы приведет к сообщению об ошибке во время компоновки, оповещающему о повторном определении. Поскольку заголовки подключаются в несколько файлов исходного кода, они не должны содержать определений переменных или функций. Правило, запрещающее помещать в заголовки определения, имеет три исключе- ния: классы, константные объекты, значения которых известны на момент компиля- ции, и встраиваемые (inline) функции (встраиваемые функции рассматриваются в разделе 7.6 (стр. 282)). Эти объекты могут быть определены в нескольких файлах исходного кода, если определения каждого файла совпадают.
92 Часть I. Основы Эти объекты определяют в заголовках, потому что для создания кода компилято- ру нужны не только объявления, но и определения. Например, чтобы создать код, который определяет или использует объекты класса, компилятор должен знать, ка- кие переменные-члены составляют этот класс. Он также должен знать, какие опера- ции можно выполнять с этими объектами. Необходимую информацию предоставля- ет определение класса. Ответ на вопрос, почему в заголовке определяют констант- ные объекты, потребует немного больше объяснений. Некоторые константные объекты определяют в заголовках Напомним, что по умолчанию константная переменная (раздел 2.4, стр. 80) ло- кальна для файла, в котором она определена. Как будет продемонстрировано далее, это сделано специально для того, чтобы позволить размещать определения кон- стантных переменных в файлах заголовка. При использовании языка C++ иногда необходимы константные выражения (раздел 2.7, стр. 84). Например, инициализация перечисления является константным выражением. Как будет продемонстрировано в последующих главах, бывают и дру- гие случаи, где константные выражения необходимы. Константное выражение — это выражение, которое компилятор может обрабо- тать во время компиляции. Константная переменная целочисленного типа может быть инициализирована в результате вычисления константного выражения. Но для того, чтобы компилятор смог вычислить результат константного выражения и инициализировать им константу, он должен его получить. Поэтому с целью ис- пользовать то же самое константное значение в нескольких файлах, и константа и инициализирующее ее выражение должны быть видимы в каждом файле. Для это- го их следует поместить в файл заголовка. Таким образом компилятор сможет увидеть и константу и инициализирующее ее выражение каждый раз, когда кон- станта используется. Однако может существовать только одно определение любой переменной в про- грамме на языке C++ (раздел 2.3.5, стр. 75). Определение выделяет область памяти для хранения переменной, и все ее пользователи должны обращаться именно к этой и только к этой области. Поскольку по умолчанию константные объекты локальны для файла, в котором они определены, их определения вполне можно помещать в файл заголовка. Для этого есть одна важная причина. При определении константы в файле заго- ловка, каждый файл исходного кода, в который он будет подключен, получит свою собственную константную переменную с тем же самым именем и значением. При инициализации константы константным выражением, такой подход гаран- тирует, что все переменные будут иметь одинаковое значение. Кроме того, практи- чески все компиляторы заменяют во время компиляции все случаи использования таких константных переменных соответствующим им константным выражением. Таким образом, для хранения константных переменных, инициализированных кон- стантными выражениями, практически никогда не будут использоваться никакие хранилища. Если константа инициализируется результатом неконстантного выражения, она не может быть определена в файле заголовка. Такая константа, как и любая другая переменная, должна быть определена и инициализирована в файле исходного кода.
Глава 2. Переменные и базовые типы 93 Внешнее объявление такой константы должно быть осуществлено в заголовке, что позволит нескольким файлам совместно использовать ее. Упражнения раздела 2.9.1 Упражнение 2.31. Укажите, какие из приведенных ниже операторов являются объявлениями, а ка- кие — определениями. Объясните, почему. (a) extern int ix = 1024; (b) int iy; (c) extern int iz; (d) extern const int &ri; Упражнение 2.32. Какие из приведенных ниже объявлений и определений следует поместить в заголовок, а какие — в файл исходного кода? Объясните, почему. (а) (Ь) (с) (d) Упражнение 2.33. Выясните, какие параметры используемый компилятор предоставляет для по- вышения уровня предупреждений. Перекомпилируйте приведенный ранее пример с использовани- ем этого параметра и посмотрите, появились ли дополнительные сообщения о проблемах. 2.9.2. Кратко о препроцессоре Теперь, зная, что именно следует помещать в файлы заголовков, настало время создать их на практике. Как уже было продемонстрировано, для подключения заго- ловка в файл исходного кода, используется директива #include. Но чтобы нау- читься создавать собственные заголовки, о работе директивы #include необходимо знать несколько больше. Директива #include предназначена для препроцессора (preprocessor) C++. Препроцессор обрабатывает исходный текст программы перед передачей его компилятору. Язык C++ унаследовал довольно развитой препро- цессор от языка С. Современные программы используют препроцессор весьма ограниченно. Директиве #include передают одни аргумент: имя заголовка. Препроцессор за- меняет каждую директиву #include содержимым указанного файла. Собственные заголовки сохраняются в файлах, а системные заголовки, для повышения эффек- тивности, могут быть сохранены в формате, специфическом для применяемого ком- пилятора. Но независимо от формы, в которой сохранен заголовок, он обычно со- держит определение класса, а также объявления переменных и функций, необходи- мых для обеспечения раздельной компиляции. Заголовки зачастую нуждаются в других заголовках В заголовки зачастую подключают другие заголовки. Элементы, определения ко- торых содержит заголовок, иногда нуждаются в средствах из других заголовков. На- пример, в заголовок, содержащий определение класса Sale si tern, следует под- ключить библиотеку string. Поскольку в состав класса Sales_item входит пере- менная-член типа string, он должен иметь доступ к заголовку string.
94 Часть I. Основы Подключение других заголовков столь популярно, что один и тот же заголовок нередко оказывается подключен в файл исходного кода несколько раз. Например, программа, использующая заголовок Sales_item, тоже может использовать биб- лиотеку string. Разработчик, использующий класс Sales_item, не знает (да и не должен знать), что его заголовок уже подключил библиотеку string. В этом случае заголовок string окажется подключен дважды: один раз в самой программе, а вто- рой раз в составе заголовка Sales_item. Следовательно, файлы заголовка необходимо разрабатывать так, чтобы они мог- ли быть подключены в один файл исходного только один раз. Это гарантирует, что неоднократное подключение файла заголовка не приведет к повторному определе- нию классов и объектов, указанных в нем. Наиболее распространенный способ обезопасить используемые препроцессором заголовки подразумевает применение защиты заголовка (header guard). Защита предотвращает обработку препроцессором содержимого файла заголовка, если такой заголовок уже был обнаружен. Как избежать нескольких подключений Прежде чем создавать собственный заголовок, необходимо изучить некоторые дополнительные средства препроцессора. Препроцессор позволяет определять соб- ственные переменные. Имена, используемые для переменных препроцессора, должны быть уникальны на про- тяжении всей программы. Все имена в программе, совпадающие с именами пере- менных препроцессора, будут рассматриваться именно как имена переменных пре- процессора. Чтобы избежать конфликта имен, в именах переменных препроцессора обычно используют только прописные буквы. Переменная препроцессора имеет два состояния: она либо уже определена, либо еще не определена. Существуют также директивы препроцессора, которые позво- ляют определять и проверять состояние переменных препроцессора. Директива #def ine получает имя и определяет его как имя переменной препроцессора. Дирек- тива #ifndef проверяет, не была ли еще указанная переменная препроцессора оп- ределена. Если нет, выполняется код, расположенный после директивы ttifndef и до директивы #endif. Используя эти средства можно принять меры для защиты заголовка от неодно- кратных подключений, ttifndef SALESITEM_H ttdefine SALESITEM_H // здесь расположено определение класса Sales_item // и связанных с ним функций #endif Условное выражение #ifndef SALESITEM_H проверяет, не была ли определена переменная препроцессора SALES1ТЕМ_Н. Если переменная препроцессора SALESITEM_H не была определена, выполняются строки кода, расположенные после директивы #ifndef и до директивы #endif. И наоборот, если переменная SALESITEM_H уже была определена, содержимое директивы #if ndef игнорируются.
Глава 2. Переменные и базовые типы 95 Чтобы гарантировать одноразовую обработку содержимого заголовка, данный файл начинается директивой #ifndef, тело которой в первый раз сработает, по- скольку переменная препроцессора SALESITEM_H еще не будет определена. Сле- дующий оператор определяет переменную препроцессора SALES 1ТЕМ_Н. Таким об- разом, если этот файл будет подключен во второй раз, директива #ifndef выяснит, что переменная препроцессора SALES 1ТЕМ_Н уже определена, и пропустит осталь- ную часть файла заголовка. В результате повторной обработки содержимого заго- ловка удается избежать. /ШГ Заголовки должны иметь защиту даже в том случае, если они не подключают другие заголовки. Защита заголовка по сути очень проста и позволяет избежать загадочных Хкомен<)уем сообщений об ошибке компилятора, если заголовок впоследствии все же окажется 1 подключенным несколько раз. Этот подход прекрасно срабатывает в случае, если используемые имена констант препроцессора ни в одном из заголовков не совпадают. Вероятность проблем с одно- именными переменными препроцессора можно существенно снизить, если в их име- нах использовать имя класса, определенного внутри заголовка. Программа может иметь только один класс по имени Sales_item. Использовав это имя в составе имени файла заголовка и переменной препроцессора, можно существенно снизить вероятность их совпадения с именами другого файла и переменной. Использование собственных заголовков Директива #include применяется в одной из двух следующих форм. ttinclude <standard_header> #include "my_file.h" Когда имя заголовка заключено в угловые скобки (<>), предполагается, что это стандартный заголовок. Компилятор будет искать его в заранее указанных местах, список которых может быть изменен при помощи переменной среды окружения path, задающей пути поиска файлов, а также в местах, указанных соответствующим параметром командной строки. Разные компиляторы используют разные методы поиска, поэтому более подробную информацию по этой теме следует получить у коллег, использующих данную версию компилятора, или в документации по компи- лятору. В двойные кавычки заключают имена несистемных заголовков. Поиск фай- лов несистемных заголовков обычно начинается в каталоге, содержащем файл ис- ходного кода. Резюме Типы являются основой всего программирования на языке C++. Каждый тип задает объем памяти, необходимый для хранения его объекта, а также опера- ции, которые с ним можно выполнять. Язык C++ предоставляет набор встроенных типов, та- ких как int и char. Свойства этих типов жестко привязаны к их представлению на аппарат- ных средствах машины. Типы могут быть константными и неконстантными; константный объект должен быть инициализирован при объявлении, поскольку впоследствии изменить его значение невоз- можно. Кроме того, можно определять составные типы и ссылки. Ссылка позволяет при-
96 Часть I. Основы своить объекту альтернативное имя. Составной тип — это тип, определенный в терминах другого типа. Используя механизм классов, язык позволяет определять собственные типы. Стандартная библиотека использует именно классы, чтобы предоставить набор высокоуровневых абстрак- ций, таких как объекты ввода-вывода и тип string. Язык C++ обладает статическим контролем типов данных: переменные и функции следу- ет объявлять ранее, чем они будут использованы. Переменная может быть объявлена не- сколько раз, но определена только один раз. Инициализировать переменные при их определе- нии имеет смысл почти всегда. Термины L-значение (lvalue). Значение, которое может находиться слева от знака присвоения. L-значение не является константным, его можно изменить. R-значение (rvalue). Значение, применяемое с правой (но не с левой) стороны оператора присвоения. R-значение допускает лишь чтение, но не изменение. Адрес (address). Номер байта в памяти, начиная с которого располагается объект. Арифметический тип (arithmetic type). Арифметические типы предназначены для чисел: целых и с плавающей запятой. Существует три типа чисел с плавающей запятой: long double, double и float. Они предназначены для значений повышенной, двойной и одинарной точности. Почти всегда имеет смысл использовать тип double. Тип float гаран- тированно имеет только шесть значащих цифр, а этого слишком мало для большинства вы- числений. К целочисленным типам относятся: bool, char, wchar_t, short, int и long. Це- лочисленные типы могут быть знаковыми (signed) и беззнаковыми (unsigned). Для ариф- метических вычислений типы short и char почти никогда не применяют. Беззнаковые типы используют при подсчете. Переменная типа bool (логическая) способна содержать только два значения: true или false. Тип whcar_t предназначен для символов Unicode, а тип char для обычных 8-битовых символов, таких как в наборах Latin-1 или ASCII. Байт (byte). Как правило, самый маленький адресуемый блок памяти. На большинстве машин байт составляет 8 битов. Беззнаковый тип (unsigned). Целочисленный тип данных, переменные которого способны хранить значения больше или равные нулю. Время выполнения (run time). Так называется время, в течение которого программа вы- полняется. Глобальная область видимости (global scope). Область видимости, внешняя для всех ос- тальных областей видимости. Заголовок (header). Механизм создания определений класса и других объявлений, дос- тупных в нескольких файлах исходного кода. Пользовательские заголовки сохраняются как файлы. Системные заголовки могут быть сохранены как файлы или в некотором другом фор- мате, специфическом для конкретной системы. Закрытый член (private member). Член класса, который недоступен коду, использующему класс. Защита заголовка (header guard). Переменная препроцессора, предназначенная для предотвращения неоднократного подключения содержимого заголовка в один файл исход- ного кода. Знаковый тип (signed). Целочисленный тип данных, переменные которого способны хра- нить отрицательные и положительные числа, включая нуль. Идентификатор (identifier). Имя (name). Идентификатор — это непустая последователь- ность букв, цифр и знаков подчеркивания, которая не должна начинаться с цифры. Иденти-
Глава 2. Переменные и базовые типы 97 фикаторы чувствительны к регистру: символы в верхнем и нижнем регистре считаются раз- ными. Идентификаторами не могут быть ключевые слова языка C++. Идентификаторы не могут содержать два смежных символа подчеркивания и не могут начинаться с символа под- черкивания, сопровождаемого прописной буквой. Инициализация копии (copy-initialization). Форма инициализации, при которой исполь- зуется символ =, указывающий, что переменная должна быть инициализирована как копия инициализатора. Инициализация переменной (variable initialization). Термин, используемый для описания назначения исходного значения переменной или элементам массива. Инициализация не яв- ляется присвоением значения. Инициализацию объектов классов осуществляет стандартный конструктор. Если стандартного конструктора нет, во время компиляции произойдет ошибка: объект потребует явной инициализации. Инициализация встроенных типов зависит от об- ласти видимости. Объекты, определенные в глобальной области видимости, инициализиру- ются нулевым значением. Объекты определенные в локальной области видимости, не ини- циализируются и имеют неопределенные значения. Инициализированная переменная (initialized variable). Переменная, которая имеет ис- ходное значение. Исходное значение может быть назначено переменной при определении. Как правило, переменные имеет смысл инициализировать. Интерфейс (interface). Операции, допустимые для типа. В грамотно разработанных клас- сах интерфейс отделен от реализации. Интерфейс определен как открытая (public) часть класса, а реализация — как закрытая (private). Переменные-члены обычно являются ча- стью реализации, а функции-члены — частью интерфейса. Открытые функции-члены (интер- фейс) определяют операции, доступные для пользователей этого класса, а закрытые функ- ции-члены (реализация) предназначены для необходимых классу внутренних операций, но не для общего доступа извне. Класс (class). Механизм определения типов данных языка C++. Для определения класса используется ключевое слово class или struct. Классы могут содержать данные и функ- ции-члены. Члены могут быть открытыми (public) или закрытыми (private). Как прави- ло, функции-члены, задающие операции, допустимые для объектов данного типа, объявляют открытыми, а переменные-члены и функции, используемые в реализации класса, — закрыты- ми. По умолчанию члены класса, определенного с использованием ключевого слова class, являются закрытыми, а члены класса, определенного с использованием ключевого слова struct, — открытыми. Ключевое слово struct. Применятся для определения класса (структуры). По умолча- нию члены структуры являются открытыми (public), пока не указано иное. Ключевое слово typedef. Позволяет создать синоним для некоего типа данных. Для это- го применяется следующая форма записи. typedef тип синоним; Здесь определен синоним (другое имя) типа по имени тип. Компоновка (link). Этап компиляции, в результате которого объединяется несколько объектных файлов и создается исполняемая программа. На этапе компоновки выясняются зависимости между файлами, такие как взаимосвязь между вызовом функции в одном файле и ее определением в другом файле. Константная ссылка (const reference). Ссылка, которая может быть связана с констант- ным объектом, неконстантным объектом или г-значением. Константная ссылка не может быть использована для изменения содержимого объекта, псевдонимом которого она является. Константное выражение (constant expression). Выражение, значение которого может быть вычислено во время компиляции.
98 Часть I. Основы Конструктор (constructor). Специальная функция-член, используемая для инициализа- ции создаваемого объекта. Конструктор предназначен для инициализации переменных- членов объекта необходимыми исходными значениями. Контроль соответствия типов (type-checking). Термин, используемый для описания про- цесса проверки компилятором объектов на соответствие с определением их типа. Литеральная константа (literal constant). Значение, такое как число, символ или строка символов. Это значение не может быть изменено. Символьные литералы заключают в оди- нарные кавычки, а строковые литералы в двойные. Локальная область видимости (local scope). Термин, используемый для описания области видимости функции и вложенных областей видимости внутри функции. Магическое число (magic number). Числовой литерал, значение которого используется в программе, но назначение его не очевидно (как будто создано по волшебству). Маркер доступа (access label). Член класса может быть определен как private (зак- рытый). Это запретит доступ к нему из кода, который использует объект данного типа. Член может быть также определен как public (открытый). Это сделает его доступным из кода программы. Массив (array). Структура данных, содержащая коллекцию неименованных объектов, к которым можно обращаться по индексу. В этой главе продемонстрировано использование символьных массивов для хранения строковых литералов. Более подробно массивы рассмат- риваются в главе 4, “Массивы и указатели”. Неинициализированная переменная (uninitialized variable). Такая переменная не имеет исходного значения. Это вовсе не значит, что она имеет нулевое, или пустое, значение, на са- мом деле она имеет случайное значение, биты которого располагались в памяти ранее. Не- инициализированные переменные — обычный источник ошибок. Неконстантная ссылка (nonconst reference). Ссылка, которая может быть связана только с неконстантным 1-значением того же самого типа, что и ссылка. Неконстантная ссылка по- зволяет изменять значение объекта, псевдонимом которого она является. Неопределенное поведение (undefined behavior). Случай, для которого стандарт языка не определяет однозначных правил. В таких случаях компилятор может поступать как угодно. Нельзя полагаться на неопределенный режим, поскольку он является причиной ошибок во время выполнения программы. Непечатаемый символ (nonprintable character). Символ, не имеющий видимого представ- ления, например символ возврата на один символ, символ новой строки и т.д. Область видимости (scope). Часть программы, в которой имена имеют смысл. Язык С++ имеет несколько уровней областей видимости. • Глобальная (global) — имена, определенные вне остальных областей видимости. • Класса (class) — имена, определенные классом. • Пространства имен (namespace) — имена, определенные внутри пространства имен. • Локальная (local) — имена, определенные внутри функции. • Блока (block) — имена, определенные внутри блока операторов, т.е. внутри пары фи- гурных скобок. • Операторная (statement) — имена, определенные внутри условия оператора, такого как if, for или while. Области видимости могут быть сложенными. Например, имена, объявленные в глобаль- ной области видимости, доступны и в функции, и в операторной области видимости. Объект (object). Область памяти, которая имеет тип. Переменная — это объект, который имеет имя.
Глава 2. Переменные и базовые типы 99 Объявление (declaration). Уведомление о существовании переменной, функции или типа, определяемых в другом месте программы. Некоторые объявления одновременно являются определениями. Только определение выделяет память для хранения переменной. Переменная может быть объявлена как внешняя, с ключевым словом extern. Никакие имена не могут быть использованы, пока они не определены или не объявлены. Определение (definition). Выделяет область в памяти для хранения данных переменной и (необязательно) инициализирует ее значение. Никакие имена не могут быть использованы, пока они не определены или не объявлены. Открытый член (public member). Член класса, который применим любой частью программы. Переменная-член (data member). Элемент данных, которые составляют объект. Как пра- вило, переменные-члены должны оставаться закрытыми. Перечисление (enumeration). Тип, группирующий набор именованных целочисленных констант. Перечислитель (enumerator). Именованный член перечисления. Каждый перечислитель инициализируется константным целочисленным значением. Перечислители могут быть ис- пользованы там, где необходимы целочисленные константные выражения, например, при оп- ределении размерности массива. Препроцессор (preprocessor). Препроцессор — это программа, автоматически запускаемая перед компилятором C++. Препроцессор унаследован от языка С, необходимость использо- вания его возможностей в языке C++ существенно меньше. Одним из важных средств пре- процессора, использование которого остается актуальным, является директива #include, позволяющая подключать заголовки в текст программы. Прямая инициализация (direct-initialization). Форма инициализации, при которой разде- ляемый запятыми список значений помещают внутрь пары круглых скобок. Раздельная компиляция (separate compilation). Возможность разделить программу на не- сколько отдельных файлов исходного кода. Реализация (implementation). Как правило, закрытые (private) члены класса, опреде- ляющие данные и все операции, которые не предназначены для доступа из кода, использую- щего класс. Классы istream и ostream, например, управляют буфером ввода-вывода, кото- рый является частью их реализации и не предоставляется для непосредственного доступа пользователей этих классов. Составной тип (compound type). Тип, определенный в терминах другого типа. В главе 4, “Массивы и указатели”, рассматриваются два дополнительных составных типа: указатели и массивы. Спецификатор типа (type specifier). Часть определения или объявления, указывающая тип создаваемой переменной. Ссылка (reference). Псевдоним объекта. Определяется следующим образом. тип ^.идентификатор = объект; Здесь определен идентификатор, являющийся альтернативным именем объекта. Любая операция с идентифика тором интерпретируется как операция с объектом. Стандартный конструктор (default constructor). Конструктор, используемый в отсутствии явной инициализации объекта класса. Например, стандартный конструктор класса string инициализирует новый объект класса string пустой строкой. Другой конструктор класса string инициализирует строку набором символов. Статическая типизация (statically typed). Термин, используемый для описания контроля соответствия типов данных во время компиляции. Такие языки, как C++, проверяют во время компиляции допустимость используемых в выражениях типов. Тип void. Специальный тип данных, который не имеет никакого значения и не допускает никаких операций. Переменную типа void определить нельзя. Как правило, тип void ис- пользуется при указании возвращаемого типа функции, которая ничего не возвращает.
100 Часть I. Основы Тип word. Специфический для каждой машины размер блока памяти, применяемый при целочисленных вычислениях. Обычно размер типа word достаточно велик, чтобы содержать адрес. На 32-битовой машине тип word обычно занимает 4 байта. Управляющая последовательность (escape sequence). Альтернативный механизм пред- ставления символов. Обычно используется для представления непечатаемых символов, таких как символ новой строки или табуляции. Управляющая последовательность состоит из сим- вола наклонной черты влево, сопровождаемой символом, восьмеричным числом из трех цифр или шестнадцатеричным числом. Встроенные управляющие последовательности языка С++ перечислены в разделе 2.2 на стр. 62. Управляющие последовательности могут быть исполь- зованы как символьные литералы (заключены в одинарные кавычки) или как часть лите- ральной строки (заключены в двойные кавычки). Целочисленный тип (integral type). См. арифметический тип. Член класса (class member). Данные или операции, являющиеся частью класса.
ГЛАВА 3 Библиотечные типы данных В ЭТОЙ ГЛАВЕ... 3.1. Пространства имен и объявления using 102 3.2. Библиотечный тип string 104 3.3. Библиотечный тип vector 114 3.4. Знакомство с итераторами 120 3.5. Библиотечный тип bitset 125 Резюме 130 Термины 130 Кроме базовых типов, описанных в главе 2, “Переменные и базовые типы”, биб- лиотека языка C++ предоставляет богатое разнообразие абстрактных типов данных. Из наиболее важных библиотечных типов следует упомянуть классы string и vector, которые определяют символьные строки переменного размера и коллекции соответственно. С классами string и vector связан сопутствующий тип, извест- ный как итератор (iterator). Он используется для доступа к символам строк и эле- ментам векторов. Эти библиотечные типы являются абстракциями более простых базовых типов, массивов и указателей, которые являются частью языка. Еще один библиотечный тип, bitset, предоставляет абстрактный способ управ- ления коллекцией битов. Этот класс предоставляет более удобный способ работы с битами данных, чем обеспечивают встроенные побитовые операторы для значений целочисленных типов. В этой главе рассматриваются такие библиотечные типы, как vector, string и bitset. Следующая глава посвящена массивам и указателям, а глава 5, “Выражения”, встроенным побитовым операторам. Все типы, описанные в главе 2, “Переменные и базовые типы”, относились к низ- коуровневым типам данных, они представляют такие абстракции, как числа или символы, и определены в терминах их машинного представления. Кроме типов, определенных в самом языке C++, стандартная библиотека предостав- ляет набор дополнительных высокоуровневых абстрактных типов данных (abstract data type). Высокоуровневыми эти библиотечные типы называют потому, что они отражают более сложные концепции, а абстрактными — потому что при их исполь- зовании можно не заботиться о том, как эти типы представлены. Достаточно лишь знать, какие операции они выполняют.
102 Часть I. Основы Двумя наиболее важными библиотечными типами являются string и vector. Класс string позволяет использовать символьные строки переменной длины, а класс vector — хранить наборы объектов одинакового типа. Эти классы очень важ- ны, поскольку они обладают многими преимуществами перед базовыми типами, оп- ределенными в языке. В главе 4, “Массивы и указатели”, описаны аналогичные базо- вые конструкции языка, которые хоть и подобны, но менее гибки и подвержены ошибкам, чем библиотечные типы string и vector. Еще одним библиотечным типом, обеспечивающим удобство и эффективность работы, является класс bitset. Этот класс позволяет работать со значениями как с коллекцией битов, обеспечивая более простые способы выполнения побитовых опе- раций, чем позволяют встроенные побитовые операторы, рассматриваемые в разде- ле 5.3 на стр. 179. Однако прежде чем переходить к изучению библиотечных типов, имеет смысл рассмотреть механизм, который упрощает доступ к именам, определенным в биб- лиотеке. 3.1. Пространства имен и объявления using До сих пор имена из стандартной библиотеки упоминались в программах явно, т.е. перед каждым из них было указано имя пространства имен std. Например, при чтении со стандартного устройств ввода применялась форма записи std: : с in. Здесь использован оператор области видимости : : (раздел 1.2.2, стр. 30). Он означает, что имя, указанное в правом операнде оператора, следует искать в области видимости указанной в левом операнде. Таким образом, код std: :cin означает, что исполь- зуемое имя с in определено в пространстве имен std. При частом использовании библиотечных имен такая форма записи может оказаться чересчур громоздкой. К счастью, существуют и более простые способы применения членов пространств имен. В этом разделе описан самый надежный механизм: объявление using (using declaration). Другие способы, позволяющие упростить использование имен из дру- гих пространств, рассматриваются в разделе 17.2 (стр. 744). Объявление using позволяет получать доступ к именам из другого пространства имен без указания префикса имя_пространства_имен: :. Объявление using имеет следующий формат. using пространствоимен:-.имя; После того как объявление using было сделано один раз, к указанному в нем имени можно обращаться без указания пространства имен. ttinclude <string> ttinclude <iostream> // объявление using свидетельствует о намерении использовать // указанные имена именно из пространства имен std using std::cin; using std::string; int main() { string s; // ok: теперь string - это синоним std:-.string cin >> s; // ok: теперь cin - это синоним std::cin cout << s; // ошибка: объявления using нет; здесь нужно указать // полное имя
Глава 3. Библиотечные типы данных 103 std::cout s; // ок: явно указано применение cout из // пространства имен std Использование имени из пространства имен без уточнения версии в объявлении using является ошибкой, хотя некоторые компиляторы могут оказаться не в со- стоянии обнаружить ее. Для каждого имени необходимо индивидуальное объявление using Объявление using применяется только к одному элементу пространства имен. Это позволяет жестко задавать имена, используемые в каждой программе. Поэтому если необходимо использовать несколько имен из пространства имен std (или лю- бого другого), объявление using понадобится для каждого из них. Например, про- грамму суммирования (стр. 28) можно переписать следующим образом. #include <iostream> // объявления using для имен из стандартной библиотеки using std::cin; using std::cout; using std::endl; int main() { cout << "Enter two numbers:" << endl; int vl, v2; cin >> vl >> v2; cout << "The sum of " << vl " and " is v2 vl + v2 endl ; return 0; Объявления using для имен cin, cout и endl означают, что их можно теперь использовать без префикса std: :, что делает код проще и читабельней. Начиная с этого момента подразумевается, что код примеров снабжен объявле- ниями using для имен из стандартной библиотеки. Таким образом, в тексте приме- ров кода будет применяться форма записи cin, а не std: : cin. Чтобы сократить код примеров, необходимые при компиляции объявления using здесь не приводятся. Аналогично, в примерах кода больше не будут отображаться необходимые при ком- пиляции директивы #include. В табл. А.1 (стр. 841) приложения А, “Библиотека”, перечислены библиотечные имена и соответствующие им заголовки стандартной библиотеки, которые были использованы в этой книге. Не забывайте, что перед компиляцией примеров этой книги в их код следует добавлять соответствующие директивы #include и объявления using. ^i/ма^ Определения классов, использующих типы из стандартной библиотеки Однако внутри файлов заголовка всегда необходимо использовать только полно- стью определенные имена библиотечных типов. Дело в том, что препроцессор просто копирует содержимое заголовка в текст программы. Таким образом, при подключении
104 Часть I. Основы файла заголовка, его содержимое без изменений войдет в состав файла программы. Следовательно, размещение объявления using внутри файла заголовка эквива- лентно размещению того же объявления using в каждой использующей его про- грамме независимо от того, нужно это или нет. Общепринятой практикой является определение в заголовках только того, что абсо- лютно необходимо. комеидуем Упражнения раздела 3.1 Упражнение 3.1. Перепишите программу раздела 2.3 (стр. 65), вычисляющую результат возведе- ния заданного числа в указанную степень так, чтобы используя соответствующие объявления using обеспечить доступ к библиотечным именам без префикса std::. 3.2. Библиотечный тип string Тип string предназначен для символьных строк переменной длины. Библиоте- ка отвечает за операции управления памятью, необходимые для хранения наборов символов, а также за разнообразные вспомогательные операции. Подобно любым библиотечным типам, использующие класс string программы должны сначала подключить соответствующий заголовок. Такие программы будут более компактными, если в их начало включить также соответствующее объявление using. #include <string> using std::string; 3.2.1. Определение и инициализация строк Таблица 3.1. Способы инициализации объекта класса string string si; string s2(si); string s3("value"); string s4 (n, 'c'); Стандартный конструктор; si — пустая строка Инициализация строки s2 как копии строки si Инициализация строки s3 как копии строкового литерала Инициализация строки s4 последовательностью из п символов с Внимание! Библиотечный тип string и строковые литералы По исторически сложившимся причинам, а также в целях совместимости с языком С, символьные строковые литералы имеют тип, отличный от типа string стандартной библиотеки. Этот факт может ввести в заблуждение, поэтому его следует учитывать при использовании строкового литерала и класса string. Библиотечный тип string обладает несколькими конструкторами (раздел 2.3.3, стр. 71). Конструктор — это специальная функция-член, которая позволяет инициа- лизировать объекты данного класса. Список наиболее часто используемых конст-
Глава 3. Библиотечные типы данных 105 рукторов класса string приведен в табл. 3.1. Когда объект не инициализируется явно, по умолчанию используется стандартный конструктор (раздел 2.3.4, стр. 74). Упражнения раздела 3.2.1 Упражнение 3.2. Что такое стандартный конструктор? Упражнение 3.3. Назовите три способа инициализации объекта класса string. Упражнение 3.4. Каковы значения переменных s и s2? string s; int main() { string s2; 3.2.2. Чтение и запись строк Как было продемонстрировано в главе 1, “Первые шаги”, для чтения и записи значений встроенных типов данных (таких как int, double и т.д.) используется библиотека iostream. Таким образом, используя библиотеки iostream и string можно организовать чтение и запись строк при помощи стандартных операторов ввода и вывода. // Обратите внимание, перед компиляцией этот код следует дополнить // директивами ^include и объявлениями using int main() { string s; // пустая строка cin >> s; // чтение разделяемой пробелами строки в s cout << s << endl; // запись s на стандартное устройство вывода return 0; Эта программа начинается с определения переменной s типа string. 1 >> S; // чтение разделяемой пробелами строки в s Следующая строка (см. выше) читает данные со стандартного устройства ввода и со- храняет их в переменной s. Оператор ввода строк осуществляет следующие действия. Читает и отбрасывает все предваряющие непечатаемые символы (например про- белы, символы новой строки и табуляции). Затем читает значащие символы, пока не встретится следующий непечатаемый символ. Таким образом, если ввести “ Hello World! ” (обратите внимание на пред- варяющие и завершающие пробелы), фактически будет получено значение “Hello” без пробелов. Операторы ввода и вывода строк ведут себя аналогично операторам встроенных типов. В частности, они возвращают как результат свой левый операнд. Таким обра- зом, операторы чтения или записи можно объединять в цепочки. string si, s2; cin >> si >> s2; // сначала прочитать в переменную si, cout // а затем в переменную s2 « endl; // отобразить обе строки
106 Часть I. Основы Если в этой версии программы осуществить предыдущий ввод, вывод будет следующим. Hello World! Чтобы откомпилировать эту программу, необходимо добавить соответствующие дирек- тивы #inciude (для библиотек iostream и string), а также объявления using для всех используемых библиотечных имен: string, cin, cout и endl. Начиная с этого места подразумевается, что в код программ следует включить необхо- димые директивы #include и объявления using. Чтение неопределенного количества строк Подобно операторам ввода, читающим встроенные типы, оператор ввода класса string возвращает поток, из которого он осуществляет чтение. Следовательно, оператор ввода класса string можно использовать в условии продолжения, точно так же, как это было сделано при чтении целых чисел в программе на стр. 41. Приве- денная ниже программа читает набор строк со стандартного устройства ввода и по- строчно записывает прочитанное на стандартное устройство вывода. int main() { string word; // читать до конца файла, писать каждое слово с новой строки while (cin >> word) } В данном случае для чтения строк используется оператор ввода. Этот оператор возвращает поток istream, из которого он читает данные, для проверки в условии оператора while, который в результате выполняет чтение до тех пор, пока поток до- пустим. Поток остается допустимым до тех пор, пока не будет введен символ конца файла или недопустимое значение. В теле цикла while прочитанное в условии зна- чение выводится на стандартное устройство вывода. Выход из цикла (и из программы) происходит при вводе символа конца файла. Применение функции get line () для чтения целой строки Класс string обладает дополнительный вспомогательной функцией ввода-вывода get line (). Функции get line () передают поток ввода и строку. Она читает всю введенную строку из потока и сохраняет ее в переданной строковой переменной, без символа новой строки. В отличие от оператора ввода, функция get line () не игно- рирует предваряющие пробелы. Однако каждый раз, когда функция getline () встречает символ новой строкой, даже если это первый введенный символ, она оста- навливает чтение из буфера ввода и завершает работу. В результате, если первым окажется символ новой строки, переданная строковая переменная будет пуста. Функция getline () возвращает переданный ей аргумент типа istream, чтобы подобно оператору ввода использовать его при проверке условий. Например, преды- дущую программу можно переписать так, чтобы она вместо одного слова читала и выводила целую строку, int main() string line;
Глава 3. Библиотечные типы данных 107 // читать строку до конца файла while (getline(cin, line)) cout << line « endl; return 0; Поскольку переменная line не будет содержать символа новой строки, для по- строчной записи результата его придется ввести принудительно. Для этого, как обычно, используется манипулятор endl, который, кроме перевода строки, осуще- ствляет сброс буфера вывода. Символ новой строки, который вынуждает функцию getline () завершить работу, от- брасывается и в строковой переменной не сохраняется. Упражнения раздела 3.2.2 Упражнение 3.5. Напишите программу, читающую со стандартного устройства ввода целые стро- ки. Измените программу так, чтобы она читала отдельные слова. Упражнение 3.6. Объясните, как поступают с символами предваряющих пробелов при чтении данных в строковую переменную, когда используется функция getline (). 3.2.3. Операции со строками Список наиболее часто используемых операций со строками приведен в табл. 3.2. Таблица 3.2. Строковые операции s. empty () Возвращает значение true, если строка s пуста. В противном случае возвращает значение false s. size () Возвращает количество символов в строке s s [п] Возвращает символ номер п в строке s; нумерация начинается с О si + s2 Возвращает строку, состоящую из содержимого строк si и s2 si = s2 Заменяет символы строки si копией содержимого s2 vi == v2 Возвращает значение true, если строки vl и v2 совпадают. В противном случае возвращает значение false ! =, <, <=,> и >= Имеют обычное назначение Строковые операции size () и empty () Длина строки равна количеству ее символов. Именно это значение и возвращает функция size (). int main() { string st("The expense of spirit\n"); cout << "The size of " << st << "is " << st.size() << " characters, including the newline" « endl; return 0; }
108 Часть I. Основы Если откомпилировать и запустить на выполнение эту программу, результат будет следующим. The size of The expense of spirit is 22 characters, including the newline Зачастую необходимо выяснить, не является ли строка пустой. Для этого можно сравнить ее размер с нулем, if (st.size() == 0 ) / / ok: пуста. В данном случае фактическое количество символов в строке не важно, главное знать, не равно ли оно нулю. Ответ на тот же вопрос можно получить и проще — ис- пользуя функцию-член empty (). if (st.empty()) // ok: пуста Функция empty () возвращает логическое значение true (раздел 2.1, стр. 56), если строка не содержит никаких символов, и значение false — в противном случае. Тип string::size_type Вполне логично ожидать, что функция size () возвращает значение типа int, а учитывая совет на стр. 59 (раздел 2.1.1), вероятней всего, типа unsigned. Но вместо этого функция size () возвращает значение типа string: :size_type. Этот тип требует более подробных объяснений. В классе string (и нескольких других библиотечных типах) определены вспо- могательные типы данных. Эти вспомогательные типы позволяют использовать библиотечные типы машинно-независимым способом. Тип size_type — это один из таких вспомогательны типов. Он определен как синоним типа unsigned, unsigned int или unsigned long, т.е. как гарантированно большой, чтобы содержать раз- мер любой строки. Чтобы воспользоваться типом size_type, определенным в классе string, применяется оператор области видимости (: :), указывающий на то, что имя size_type определено в классе string. Любая переменная, используемая для сохранения результата обращения к функции size () класса string, должна иметь тип string: :size_type. Значение возвращаемое функцией size () ни в коем случае нельзя присваивать переменной типа int. Хотя точный размер типа string: :size_type неизвестен, можно с уверенно- стью сказать, что это беззнаковый тип (раздел 2.1.1, стр. 57). Как известно, беззнаковая версия любого типа способна содержать положительные значения вдвое большего раз- мера, чем знаковая версия того же типа. Таким образом, размер самой большой строки может оказаться вдвое больше значения, предельно допустимого для типа int. Еще одна проблема, связанная с применением типа int, заключается в том, что на некоторых машинах размер типа int слишком мал, чтобы содержать размер строк даже вполне реальной длинны. Например, если машина имеет 16-битовый тип int, то самая большая строка могла бы иметь размер 32 767 символов. Размер объекта класса string, в который загружают содержимое файла, может легко превысить это число. Поэтому самым надежным способом хранения размеров строк является ис- пользование специального типа string: : size_type.
Глава 3. Библиотечные типы данных 109 Операторы сравнения класса string В классе string определено несколько операторов для сравнения двух строковых значений. Каждый из этих операторов работает, сравнивая символы каждой строки. При сравнении строк учитывается регистр символов. То есть символы в верхнем и ниж- || нем регистре считаются разными. На большинстве компьютеров прописные буквы распо- лагаются раньше строчных, поскольку им соответствует меньшее битовое число. Оператор равенства (==) сравнивает две строки и возвращает значение true, ес- ли они равны. Две строки считаются равными, если они имеют одинаковую длину и содержат одинаковые символы. Существует также оператор неравенства (! =), воз- вращающий значение true, если строки не равны. Операторы сравнения <, <=, > и >= проверяют, не является ли одна строка мень- ше, меньше или равна, больше и больше или равна другой. small = "small"; //si - копия big // false string big = "big", string si = big; if (big == small) // true, они равны или big меньше si Эти операторы сравнивают строки используя ту же стратегию, т.е. посимвольно с учетом регистра. Если две строки имеют разные длины и каждый символ короткой строки равен соответствующему символу длинной строки, короткая строка меньше длинной. Если символы двух строк отличаются, результат сравнения определит первый несовпадающий символ. Давайте рассмотрим несколько строк. string substr = "Hello"; string phrase = "Hello World"; string slang = "Hiya"; Здесь строковая переменная substr содержит значение, меньшее, чем строковая переменная phrase, а строковая переменная slang содержит значение, которое больше, чем строковые переменные substr и phrase. Присвоение строк Как правило, библиотечные типы столь же просты в применении, как и встроенные. Поэтому большинство библиотечных типов поддерживают присвоение. Строки не яв- ляются исключением, один объект класса string вполне можно присвоить другому. // stl - пустая строка, st2 - копия литерала string stl, st2 = "The expense of spirit"; stl = st2; // замена содержимого stl копией st2 После присвоения переменная stl содержит копию символов переменной st2. Большинство библиотечных реализаций класса string сопряжены с некоторы- ми проблемами при выполнении таких операций, как присвоение, но следует заме- тить, что концептуально присвоение требует довольно большого количества дейст-
110 Часть I. Основы вий. Сюда относится удаление хранилища, содержащего символы переменной stl, создание хранилища для копии символов переменной st2 и, собственно, копирова- ние символов переменной st2 в это новое хранилище1. Сложение двух строк Сложение строк называется конкатенацией (concatenation). Конкатенация позволяет состыковать две или несколько строк при помощи оператора плюс (+) или составного оператора присвоения (+=) (раздел 1.4.1 стр. 34). Давайте рассмотрим две строки, string si("hello, "); string s2("world\n"); Эти строки можно сложить и создать третью строку следующим образом, string s3 = si + s2; // s3 содержит hello, world\n Чтобы сумму строк s2 и si поместить в переменную si, можно использовать оператор +=. si += s2; // эквивалентно si = si + s2 Сложение строк и символьных строковых литералов Строковые переменные si и s2 складываются непосредственно по их значениям. Тот же результат можно получить при конкатенации объектов класса string и строковых литералов, string si("hello"); string s2("world"); string s3 = si + ", " + s2 + "\n"; При смешанном сложении строк и строковых литералов, по крайней мере один из операндов каждого оператора + должен быть объектом класса string. string si = "hello"; string s2 = "world"; string s3 = si + ", "; // ok: сложение строки и литерала string s4 = "hello" + ", "; // ошибка: нет строкового операнда string s5 = si + ", " + "world"; // ok: каждый + имеет string s6 "hello" + // строковый операнд + s2; // ошибка: нельзя сложить // строковые литералы Инициализация переменных s3 и s4 осуществляет только одну операцию. В дан- ном случае инициализация переменной s3 вполне корректна, ведь здесь осуществ- ляется конкатенация объекта класса string и строкового литерала. Инициализация переменной s4 недопустима, поскольку здесь осуществляется пытка сложить два строковых литерала. Инициализация переменной s5, как ни странно, вполне допустима. Здесь конка- тенация срабатывает аналогично цепочке выражений ввода или вывода (раздел 1.2, стр. 27). Оператор суммы библиотечного типа string возвращает тип string. Та- ким образом, часть выражения si + ", ", при инициализации переменной s5, возвращает тип string, который вполне может осуществить конкатенацию с лите- ралом "world\n". Это эквивалентно следующей форме записи. 1И наконец, переприсвоение новому хранилищу имени stl. — Примеч.ред.
Глава 3. Библиотечные типы данных 111 string tmp = si + II II . s5 = tmp + "world"; // ok: + имеет строковый операнд II ok: + имеет строковый операнд С другой стороны, инициализация переменной s6 недопустима. Можно заме- тить, что в первой части выражения происходит сложение двух строковых литера- лов. Это ошибка, поэтому весь оператор считается недопустимым. Выборка символов из строки Для доступа к отдельным символам строки тип string предоставляет оператор индексирования ([]). Оператору индексирования передают значение типа size_type, указывающее номер требуемого символа. Значение, передаваемое оператору индек- сирования, называют индексом (subscript или index). Индексирование строк начинается с нуля. То есть если переменная s типа string не пуста, s[o] соответствует первому символу строки, s[i] — второму, а s[s.size() - 1]—последнему. Использование индекса вне этого диапазона является серьезной ошибкой. Используя оператор индексирования можно, например, отобразить каждый сим- вол строки в отдельной строке. string str("some string"); for (string::size_type ix = cout « str[ix] « endl 0; ix != str.size(); При каждой итерации цикла происходит выборка следующего символа из строки str и его вывод на стандартное устройство перед символом новой строки. Оператор индексирования возвращает L-значение Напомним, что переменная представляет собой 1-значение (раздел 2.3.1, стр. 67), т.е. она может располагаться с левой стороны от оператора присвоения. Подобно пере- менной, значение возвращенное оператором индексирования является 1-значением. Следовательно, индексирование применимо с обеих сторон оператора присвоения. Следующий цикл присваивает звездочку каждому символу строки str. for (string::size_type ix = 0; ix str[ix] = '*'; != str.size(); ++ix) Вычисление значений индекса Любое выражение, результатом которого является целочисленное значение, при- менимо в качестве индекса. Например, если someval и someotherval являются целочисленными переменными, следующая запись вполне допустима, str[someotherval * someval] = someval; Хотя в качестве индекса применим любой целочисленный тип, фактически ин- декс имеет тип string: : size type (некий беззнаковый тип). Тип string: :size_type используется для индекса по тем же причинам, что и для возвращаемого значения функции size О. Используемая при индексировании строки переменная должна быть способна содержать число, соответствующее коли- честву символов строки.
112 Часть I. Основы Ответственность за не превышение индексом диапазона при индексировании строки, несет сам разработчик. Диапазон индексирования составляют числа от нуля до размера строки минус один. Использование для индекса переменной типа string: :size_type (или другого беззнакового типа) гарантирует, что его значе- ние не будет меньшим, чем нуль. Остается лишь организовать проверку того, что индекс всегда будет меньше размера строки. Проверку значения индекса библиотека не обеспечивает. Применение индекса, значе- ние которого находится вне диапазона, приводит во время выполнения программы к неопредсказуемым последствиям (как правило, фатальным) и является серьезной ошибкой. 3.2.4. Работа с символами строки Зачастую приходится работать с индивидуальными символами строки. Напри- мер, может понадобиться выяснить, является ли определенный символ пробелом, буквой или цифрой. Список функций, применимых к отдельным символам строки (и любым другим символьным значениям) приведен в таб. 3.3. Эти функции опреде- лены в заголовке cctype. Как правило, эти функции проверяют переданный им символ и возвращают зна- чение типа int, соответствующее логическому значению. То есть каждая из функ- ций возвращает нуль, если проверка не пройдена, и любое другое, отличное от нуля, значение — в противном случае. Таблица 3.3. Функции cctype isalnum(c) isalpha(с) iscntrl(с) isdigit(с) i sgraph(c) islower(c) isprint(c) ispunct(c) isspace (c) isupper(c) isxdigit(c) tolower(c) toupper(c) Возвращает значение true, если с является буквой или цифрой Возвращает значение true, если с — буква Возвращает значение true, если с — управляющий символ Возвращает значение true, если с — цифра Возвращает значение true, если с — не пробел, а печатаемый символ Возвращает значение true, если с — символ в нижнем регистре Возвращает значение true, если с — печатаемый символ Возвращает значение true, если с — знак пунктуации Возвращает значение true, если с — пробел Возвращает значение true, если с — символ в верхнем регистре Возвращает значение true, если с — шестнадцатеричная цифра Если с — прописная буква, возвращает ее эквивалент в нижнем регистре, а в против- ном случае возвращает символ с неизменным Если с — строчная буква, возвращает ее эквивалент в верхнем регистре, а в противном случае возвращает символ с неизменным
Глава 3. Библиотечные типы данных 113 Эти функции считают печатаемыми такие символы, которые имеют видимое графическое представление, а к непечатаемым относятся пробел, табуляция, верти- кальная табуляция, возврат каретки, новая строка и прогон страницы. Знак пунк- туации — это отображаемый символ, который не является цифрой, буквой или непе- чатаемым символом (таким как пробел). Эти функции можно использовать, например, для вывода на экран информации о количестве знаков пунктуации во введенной строке. string s("Hello World!!!"); string::size_type punct_cnt = 0; // подсчитать количество знаков пунктуации в строке s for (string::size_type index = 0; index != s.sizeO; ++index) if (ispunct(s[index])) ++punct_cnt; cout << punct_cnt << " punctuation characters in " << s « endl; Результат ее выполнения будет следующим. 3 punctuation characters in Hello World!!! Совет. Используйте заголовки библиотек языка С в версии для языка C++ Кроме средств, определенных специально для языка C++, его библиотека содержит и библиотеку языка С. Заголовок cctype обеспечивает доступ к библиотечным функ- циям языка С, определенным в файле заголовка С по имени с type. h. Стандартные имена заголовков языка С используют формат имя. h. В версиях анало- гичных заголовков для языка C++, из имен удалено расширение . h и добавлен пре- фикс с. Таким образом, имена заголовков версии для языка C++ имеют формат симя. Символ с указывает, что заголовок происходит от заголовка библиотеки С. Следова- тельно, заголовок cctype имеет то же содержимое, что и ctype. h. но в формате, со- ответствующем программам языка C++. В частности, имена, определенные в заголов- ках симя, принадлежат пространству имен std, а имена, определенные в заголовках версии . h, — нет. Как правило, в программах на языке C++ имеет смысл использовать версии заголовков в формате симяг, а не имя.Ь. Таким образом, будут автоматически использованы имена из стандартной библиотеки, принадлежащие пространству имен std. Использование за- головка . h возлагает на программиста дополнительную заботу по отслеживанию, какие из библиотечных имен унаследованы от языка С, а какие принадлежат языку C++. Функции tolower () и toupper () возвращают не логическое значение, а передан- ный им символ, неизменный или с измененным регистром. Чтобы поменять регистр символа на нижний, функцию tolower () можно использовать следующим образом. // преобразовать символы строки s for (string::size_type index = 0; s[index] = tolower(s[index]); в нижний регистр index != s.sizeO; ++index) cout << s << endl; Результат выполнения этого кода будет следующим. hello world!!!
114 Часть I. Основы Упражнения раздела 3.2.4 Упражнение 3.7. Напишите программу, которая читает две строки и уведомляет, равны ли они, а если нет, какая из них больше. Измените программу так, чтобы она указывала, имеют ли строки одинаковую длину, и если нет, то какая из них длинней. Упражнение 3.8. Напишите программу, способную читать строки со стандартного устройства ввода и соединять их в одну большую строку. Отобразите полученную строку. Измените программу так, чтобы отделить соседние введенные строки пробелами. Упражнение 3.9. Что делает следующая программа? Действительно ли она допустима? Если нет, то почему? string S; cout << s [0] << endl; Упражнение 3.10. Напишите программу поиска в строке знаков пунктуации. Программа должна позволить ввести символьную строку, содержащую знаки пунктуации, и вывести ту же строку, но уже без знаков пунктуации. 3.3. Библиотечный тип vector Вектор (vector) — это коллекция объектов одинакового типа, каждому из которых присвоен целочисленный индекс. Подобно типу string, всю “заботу” по манипулиро- ванию памятью и хранению элементов типа vector, берет на себя библиотека. Вектор называют также контейнером (container), поскольку он содержит другие объекты. Все объекты в контейнере должны иметь одинаковый тип. Более подробная информация о контейнерах приведена в главе 9, “Последовательные контейнеры”. Чтобы использовать вектор, необходимо подключить соответствующий заголо- вок. В примерах этой книги подразумевается, что в начале кода сделано следующее объявление using и подключен соответствующий заголовок. Вектор является шаблоном класса (class template). Шаблоны позволяют создать одно определение класса или функции, которые впоследствии можно применить для ряда типов. Таким образом, можно определить вектор, содержащий строки, или век- тор, содержащий целочисленные переменные либо даже объекты собственного клас- са, такого как Sales_item. Более подробная информация о собственных шаблонах классов приведена в главе 16, “Шаблоны и общее программирование”. К счастью, чтобы использовать шаблоны, вовсе не обязательно уметь их создавать. Чтобы объявить объект типа, созданного из шаблона класса, ему необходимо предоставить дополнительную информацию. Характер этой информации зависит от шаблона. В случае вектора, необходимо указать, объекты какого типа он будет со- держать. Чтобы указать тип, его имя следует поместить в угловые скобки, распола- гающиеся после имени шаблона. vector<int> ivec; // ivec содержит объекты типа int vector<Sales_item> Sales_vec; // содержит Sales_item Подобно определению любой другой переменной, здесь указан тип и список из одной или нескольких переменных. В первом из этих определений (vector<int>)
Глава 3. Библиотечные типы данных 115 создан вектор ivec для хранения объектов типа int, а во втором определен вектор Sales_vec, способный хранить объекты класса Sales_item. Часть vector — это не тип, а шаблон, который можно использовать для определения вектора, способного хранить наборы любых типов. Тип, указанный в определении векто- ра, позволяет задать тип хранимых в нем элементов. Следовательно, типом является vector<int> И vector<string>. 3.3.1. Определение и инициализация векторов В классе vector определено несколько конструкторов (раздел 2.3.3, стр. 71), ко- торые можно использовать при определении и инициализации объектов векторов. Эти конструкторы перечислены в табл. 3.4. Таблица 3.4. Способы инициализации векторов vector<T> vlВектор, содержащий объекты типа т. Стандартный конструктор vl пуст vector<T> v2 (vl); Вектор v2 — копия вектора Vl vector<T> v3 (n, i); Вектор v3 содержит n элементов co значением i vector<T> v4 (n); Вектор v4 содержит n копий самостоятельно инициализированного объекта Создание определенного количества элементов При создании вектора, если он не пуст, его элементы должны быть инициализи- рованы допустимыми значениями. Когда один вектор копируется в другой, каждый элемент в новом векторе будет инициализирован копией соответствующего элемен- та исходного вектора. При этом оба вектора должны быть предназначены для хране- ния элементов одинакового типа. vector<int> ivecl; vector<int> ivec2(ivecl); vector<string> svec(ivecl); // ivecl содержит объекты типа int /! ok: копирование элементов ivecl в ivec2 // ошибка: svec содержит строки, // а не целые числа Вектор можно инициализировать набором из определенного количества элемен- тов, обладающих указанным значением. Для указания количества элементов вектора конструктор использует счетчик, а также значение, которое должен содержать каж- дый из элементов. vector<int> ivec4(10, vector<string> svec(10, "hi!"); -1) ; / / 10 элементов, инициализируемых II значением -1 // 10 строк, инициализируемых II значением "hi!" Фундаментальная концепция. Размер вектора увеличивается динамически Главным преимуществом векторов (и других библиотечных контейнеров), является высокая эффективность добавления в них элементов во время работы. Для этого век- тор обеспечен возможностью динамически изменять свой размер по мере добавления в него элементов.
116 Часть I. Основы Как будет продемонстрировано в главе 4, “Массивы и указатели”, подобное поведение резко контрастирует с возможностями встроенных массивов языка С и большинства других языков. В частности, читатели, которые знакомы с языками С или Java, могли бы предположить, что вектор имеет фиксированное количество элементов, а его раз- мер имеет смысл выбирать с некоторым запасом от ожидаемого. Однако фактически имеет место противоположный случай по причинам, рассматриваемым в главе 9, “Последовательные контейнеры”. Хотя количество элементов вектора можно задать заранее, как правило, удобнеей соз- давать пустой вектор и добавлять в него элементы по мере надобности. Инициализирующее значение Когда инициализирующий элемент не указан, библиотека самостоятельно созда- ет значение, инициализирующее элемент. Это созданное библиотекой инициализи- рующее значение (value initialized) используется для инициализации каждого эле- мента в контейнере. Фактически используемое значение зависит от типа хранимых в векторе элементов. Если элементы вектора имеют встроенный тип, например int, библиотека ини- циализирует их значением 0. vector<string> fvec(10); // 10 элементов, инициализированных / / зна чением О Если хранимые в векторе элементы являются объектами класса (например строками) и для них определены собственные конструкторы, для создания ини- циализирующего значения элемента библиотека использует стандартный конст- руктор класса. vector<string> svec(10); // 10 элементов, инициализированных // пустой строкой Как будет продемонстрировано в главе 12, “Классы”, некоторые классы, в которых опре- I делены собственные конструкторы, не определен стандартный конструктор. Вектор тако- го типа нельзя инициализировать указав только размер, приходится также указывать и начальное значение элемента. Поэтому существует третья возможность, когда класс элемента может не иметь соответствующего конструктора. В данном случае библиотека все равно создает объ- ект, инициализированный значением, но использует для этого значение каждого конкретного объекта элемента. Упражнения раздела 3.3.1 Упражнение 3.11. Какое из следующих определений векторов (если оно есть) является ошибочным? (a) vector< vector<int> > ivec; (b) vector<string> svec = ivec; (c) vector<string> svec(10, "null");
Глава 3. Библиотечные типы данных 117 Упражнение 3.12. Сколько элементов содержится в каждом из следующих векторов? Каковы зна- чения их элементов? (a) vector<int> ivecl; (b) vector<int> ivec2(10); (c) vector<int> ivec3(10, 42); (d) vector<string> svecl; (e) vector<string> svec2(10); (f) vector<string> svec3(10, "hello"); 3.3.2. Операции с векторами Библиотека vector обеспечивает несколько операций с векторами, большинство из которых подобны операциям со строками. Список важнейших операций с векто- рами приведен в табл. 3.5. Таблица 3.5. Операции с векторами v. empty () Возвращает значение true, если вектор v пуст. В противном случае возвращает значение false v. size () Возвращает количество элементов вектора v v. push_back (t) Добавляет элемент со значением t в конец вектора v v [п] Возвращает элемент номер п вектора v vi = v2 Заменяет элементы вектора vi копией элементов вектора v2 vi == v2 Возвращает значение true, если векторы vl и v2 равны ! =, <, <=, Имеют обычное назначение > и >= Размер вектора Функции empty () и size () класса vector аналогичны одноименным функ- циям класса string (раздел 3.2.3, стр. 107). Функция size() возвращает значение типа size_type, определенное для соответствующего типа vector. Чтобы использовать тип size_type, необходимо указать тип, для которого он опреде- лен. Для типа vector всегда необходимо указывать тип хранимого элемента. vector<int>::size_type vector::size_type Добавление элементов в вектор Функции push_back () передают значение элемента, добавляемого в конец вектора. // читать слова со стандартного устройства ввода и сохранять // их как элементы вектора string word; vector<string> text; // пустой вектор while (cin >> word) { text.push_back(word); // добавить слово в вектор text }
118 Часть I. Основы Этот цикл последовательно читает строки со стандартного устройства ввода и добавляет их в конец вектора. Вектор t ext определен как изначально пустой. При каждой итерации цикла в вектор добавляется новый элемент, значением которого является слово, прочитанное со стандартного устройства ввода. По завершении цик- ла элементы вектора text будут содержать все прочитанные слова. Индексирование вектора Хранимые в векторе объекты не именованы. Обращаться к ним можно лишь по позиции в векторе. Для этого используется оператор индексирования. Индексиро- вание вектора подобно индексированию строк (раздел 3.2.3, стр. 111). Оператор индексирования вектора получает значение индекса и возвращает эле- мент, соответствующий этой позиции. Элементы вектора пронумерованы начиная с 0. В приведенном ниже примере цикл for используется для обнуления всех эле- ментов вектора ivec. // обнулить все элементы вектора for (vector<int>::size_type ix = 0; ix != ivec.size(); ++ix) ivec[ix] = 0; Подобно оператору индексирования строки, векторный оператор индексиро- вания возвращает 1-значение, поэтому сделанное в теле цикла присвоение впол- не законно. Также аналогично строкам, для индекса используется векторный тип size_type. Даже если вектор ivec пуст, цикл for сработает правильно. Если вектор ivec пуст, функция size () вернет значение о и выражение проверки цикла for сравнит ите- ратор ix со значением о. Поскольку вначале итератор ix содержит значение о, ре- зультат проверки на первом же цикле окажется отрицательным и его тело не будет вы- полнено ни разу. Фундаментальная концепция. Старое доброе программирование Программисты, перешедшие на язык C++ с языка С или Java, могли бы быть удивле- ны тем, что в данном цикле при сравнении индекса с размером вектора использован оператор ! =, а не <. Программистов С, вероятно, удивит также то, что вызов функции size () происходит непосредственно в операторе for, а не перед ним с запоминанием результирующего значения в переменной. Протраммисты языка C++ предпочитают использовать в циклах оператор !=, а не < исключительно по привычке. В данном случае никакой разницы между ними нет. В данном случае вызов функции size() в операторе for вместо запоминания ре- зультата ее выполнения также особого значения не имеет, но является хорошей при- вычкой. В языке C++ размер таких структур, как вектор, может изменяться динамиче- ски. Данный цикл только читает элементы; но не добавляет их. Однако другой цикл вполне может менять количество элементов. В таком случае, цикл использующий за- ранее сохраненное значение размера, будет некорректен. Поэтому проверять текущий размер имеет смысл на каждой итерации цикла. Как будет продемонстрировано в главе 7, “Функции”, функции языка C++ можно объявить встраиваемыми. В этом случае компилятор встроит содержимое такой функции непосредственно в код по месту вызова, а не будет соз; авать механизм фак-
Глава 3. Библиотечные типы данных 119 тического вызова функции. Крошечные библиотечные функции, такие как size О, почти наверняка имеет смысл объявлять встраиваемыми, поэтому ожидаемые допол- нительные затраты времени на ее выполнение при каждом цикле окажутся несущест- венными. Индексирование не добавляет элементов Программисты, плохо знакомые с языком C++, иногда полагают, что индексиро- вание вектора позволяет добавлять в него элементы, но это не так. vector<int> ivec; // пустой вектор for (vector<int>::size_type ix = 0; ix != 10; ++ix) ivec [ix] = ix; // катастрофа: ivec не имеет элементов В этом коде предполагалось добавить 10 новых элементов в вектор ivec, при- сваивая им значения от 0 до 9. Однако вектор ivec остается пустым, поскольку при помощи индексирования можно обращаться только к уже существующим элементам. Правильно такой цикл можно создать следующим образом, for (vector<int>::size_type ix = 0; ix != 10; ++ix) ivec.push_back(ix); // ok: добавляет новый элемент co значением ix Присвоение значения элементу при помощи индекса не создает новых элементов век- тора, для этого элемент должен уже существовать. Внимание! Индексировать можно лишь существующие элементы! Очень важно понять, что оператор индексирования ( [] ) можно использовать для дос- тупа только к фактически существующим элементам. Рассмотрим пример. vector<int> ivec; // пустой вектор cout << ivec [0]; // Ошибка: ivec не имеет элементов! vector<int> ivec2(10); // вектор из 10 элементов cout << ivec [10] ; // Ошибка: ivec имеет элементы О...9 Попытка обращения во время выполнения программы к несуществующему элементу является серьезной ошибкой. Подобно большинству подобных ошибок, практически ни одна из реализаций компилятора не обнаруживает их. Результат выполнения такой программы непредсказуем, однако почти наверняка такая программа правильно рабо- тать не будет. Это предостережение применимо к любым случаям индексирования, включая строки и, как будет продемонстрировано вскоре, встроенные массивы. Попытка индексирования несуществующих элементов, к сожалению, является весьма распространенной и грубой ошибкой программирования. Так называемая ошибка пе- реполнения буфера (buffer overflow) — результат индексирования несуществующих элементов. Такие ошибки являются наиболее распространенной причиной проблем защиты приложений.
120 Часть I. Основы Упражнения раздела 3.3.2 Упражнение 3.13. Прочитайте в вектор набор целых чисел. Вычислите и отобразите сумму каж- дой пары смежных элементов в векторе. Если количество элементов нечетно, сообщите пользова- телю об этом и отобразите значение последнего элемента без суммирования. Измените програм- му так, чтобы она отобразила сумму первого и последнего элементов, затем сумму второго и предпоследнего и т.д. Упражнение 3.14. Прочитайте некоторый текст, сохраняя каждое введенное слово как отдельный элемент вектора. Преобразуйте символы каждого слова в прописные. Отобразите преобразован- ные элементы вектора, выводя по восемь слов в строке. Упражнение 3.15. Допустима ли следующая программа? Если нет, то как ее исправить? vector<int> ivec; ivec[0] = 42; Упражнение 3.16. Укажите три способа создания вектора и добавления в него 10 элементов, каждый из которых содержит значение 4 2. Существует ли для этого предпочтительный способ и почему? 3.4. Знакомство с итераторами Кроме индексирования, для доступа к элементам вектора библиотека предостав- ляет еще один способ — итератор (iterator). Итератор — это тип, позволяющий об- ращаться к хранимым в контейнере элементам, перемещаясь от одного к другому. В библиотеке тип итератора определен для каждого из стандартных контейнеров, включая вектор. Итераторы являются более распространенным средством, чем ин- дексирование: для всех библиотечных контейнеров определены типы итераторов, но лишь некоторые из них поддерживают индексирование. Поскольку итераторами об- ладают все контейнеры, современные программисты языка C++ предпочитают ис- пользовать для доступа к элементам именно итераторы, а не индексы, даже если данный тип контейнера (например вектор) индексирование поддерживает. Более подробная информация о работе с итераторами приведена в главе 11, “Общие алгоритмы”, но использовать их можно уже на данном этапе, не до конца понимая все подробности. Контейнерный тип iterator В каждом из классов контейнеров, например в классе vector, определен его соб- ственный тип итератора. vector<int>::iterator iter; В этом операторе определена переменная iter, тип iterator которой определен в векторе vector<int>. В каждом библиотечном классе контейнера определен член по имени iterator, который является синонимом его фактического типа итератора. Терминология. Итератор и тип iterator На первый взгляд терминология, связанная с итераторами, не до конца понятна. Час- тично это связано с тем, что термин итератор (iterator) используется для описания двух понятий, собственно итератора и специального типа iterator, определенного в классе контейнера, например vector<int>.
Глава 3. Библиотечные типы данных 121 Важно понять, что концептуально итератор служит для перебора коллекции элемен- тов определенного типа, а тип iterator предоставляет некий набор действий. Эти действия позволяют перемещаться между элементами контейнера и обращаться к их значениям. В каждом контейнерном классе определен его собственный тип iterator, который применяется при организации доступа к элементам, хранимым в контейнере. Таким образом, для каждого контейнера определен тип по имени iterator, обеспечиваю- щий действия концептуального итератора. Функции begin () и end () В классе каждого контейнера определены две функции, begin () и end (), кото- рые возвращают итератор. Итератор, возвращаемый функцией begin (), позволяет обратиться к первому элементу контейнера, если он есть. vector<int>::iterator iter = ivec.begin(); Этот оператор инициализирует итератор iter значением, возвращенным функ- цией begin () вектора ivec. С учетом того, что вектор не пуст, в результате по- добной инициализации итератор iter позволит обратиться к тому же элементу, что и ivec [0]. Итератор, возвращенный функцией end (), позиционирует итератор на элемент, следующий за последним. Иногда говорят, что он указывает на конец вектора, но если воспользоваться им, обращение произойдет к несуществующему элементу вне вектора. Если вектор пуст, функции begin () и end () возвращают одинаковый итератор. Итератор, возвращенный функцией end (), указывает на фактически несуществующий /За!л^ | элемент вектора. Он используется как граница при переборе элементов вектора. Обращение к значению и инкремент векторных итераторов Операции с переменными типа iterator позволяют получить доступ к элемен- ту, на который указывает итератор, а также переместить итератор с одного эле- мента на другой. Для доступа к элементу, на который указывает итератор, типы iterator пре- доставляет оператор обращения к значению (dereference operator) или ссылки (*). *iter = 0; Оператор обращения к значению возвращает элемент, на который итератор ука- зывает в настоящее время. Предположим, что итератор iter указывает на первый элемент вектора, в этом случае *iter будет тем же элементом, что и ivec [0]. В ре- зультате выполнения выражения, приведенного выше, первому элементу вектора будет присвоено значение 0. Чтобы переместить итератор на следующий элемент в контейнере, используется оператор инкремента (increment) (++), или приращения (раздел 1.4.1, стр. 35). Логи- чески, приращение итератора подобно инкременту целочисленной переменной. В случае с целочисленной переменной, ее значение увеличивается на единицу, а в случае с итератором, он перемещается на одну позицию вперед. Так, если итератор
122 Часть I. Основы iter указывает на первый элемент вектора, после оператора ++iter он будет ука- зывать на второй. Поскольку возвращаемый функцией end () итератор не указывает ни на один из суще- ствующих элементов, его нельзя ни прирастить, ни обратиться по нему к значению. Другие операции с итераторами С итераторами можно выполнить еще две весьма полезные операции, сравнить их используя операторы == и ! =. Итераторы равны, если они указывают на тот же элемент, и не равны в противном случае. Программа, использующая итераторы Предположим, что каждый из элементов вектора ivec типа vector<int> необ- ходимо обнулить. Для этого можно использовать индексирование. // обнулить все элементы вектора ivec В этой программе для перебора элементов вектора ivec использован цикл for. Непосредственно в цикле for определен индекс, значение которого увеличивается при каждой итерации. Код тела цикла for присваивает каждому элементу вектора ivec значение 0. С использованием итератора этот цикл можно переписать следующим образом. // эквивалентный цикл, обнуляющий вектор при помощи итератора for (vector<int>::iterator iter = ivec.begin(); iter != ivec.end(); ++iter) *iter =0; // присвоение значения 0 элементу, // на который указывает итератор Цикл for начинается, как правило, с определения и инициализации итератора iter, чтобы получить возможность обратиться к первому элементу вектора ivec. В условии цикла for проверяется неравенство итератора iter со значением, воз- вращаемым функцией end (). При каждом проходе цикла осуществляется инкре- мент итератора iter. Таким образом, цикл for перебирает все элементы вектора ivec начиная с первого и до последнего. В конце итератор iter указывает на по- следний элемент вектора ivec. После обработки последнего элемента и приращения итератора iter, он станет равен значению, возвращаемому функцией end (), и цикл прекратится. Выражение в теле цикла for использует для доступа к значению текущего эле- мента оператор обращения к значению (*). Подобно оператору индексирования, значение, возвращаемое этим оператором является 1-значением. Поэтому здесь же можно использовать оператор присвоения, чтобы присвоить элементу значение или изменить его. В результате выполнения этого цикла, каждому элементу вектора ivec будет присвоено значение 0. Рассмотрев этот код подробней, можно заметить, что он выполняет те же дейст- вия, что и предыдущая версия, в которой использовано индексирование. Здесь так же перебираются и обнуляются все элементы вектора от первого до последнего.
Глава 3. Библиотечные типы данных 123 Эта программа, подобно представленной на стр. 118, безошибочно сработает и с пустым I вектором. Если вектор ivec пуст, возвращенный функцией begin () итератор не ука- ^9 зывает ни на один из существующих элементов, поскольку таковых еще нет. В этом слу- чае итератор, возвращенный функцией begin (), совпадает с возвращенным функцией end () и, после проверки условия, цикл for немедленно прекращается. Тип const iterator В предыдущей программе для изменения текущего значения вектора использо- вался итератор vector: : iterator. В каждом контейнерном классе определен также тип по имени const_iterator, который используется только при чтении, но не записи значений в элементы контейнера. При обращении с использованием обычного итератора, получается неконстант- ная ссылка на элемент (раздел 2.5, стр. 82), а при использовании типа const_ iterator — ссылка на константный объект (раздел 2.4, стр. 78). Это аналогично любой константной переменной, непозволяющий изменять свое значение. Предположим, например, что необходимо перебрать все элементы вектора text типа vector<string>. Для этого можно предпринять следующее. // использовать const_iterator, поскольку изменять элементы не нужно for (vector<string>::const_iterator iter = text.begin(); iter != text.end(); ++iter) cout << *iter « endl; // отобразить каждый элемент // вектора text Этот цикл подобен предыдущему, за исключением того, что значения итератору не присваиваются, а выводятся на экран. Поскольку итератор используется для чтения, а не для записи, он объявлен как const_iterator. При обращении к константному итера- тору возвращается константное значение. Присвоить значение такому элементу нельзя. for (vector<string>::const_iterator iter = text.begin(); iter != text.end(); ++ iter) *iter = " // ошибка: *iter является константой Итератор типа const_iterator позволяет изменять значение самого итерато- ра, но не элемента, на который он указывает. К такому итератору можно применять операторы инкремента и обращения к значению, но не оператор присвоения (не пу- тать с инициализацией). vector<int> nums(lO); // nums не константен const vector<int>::iterator cit = nums.begin(); *cit =1; // ok: cit позволяет инициализировать элемент ++cit; // ошибка: нельзя изменить значение cit Константный итератор может быть использован с константным или неконстантным вектором, поскольку он не позволяет записывать данные в элемент. Итератор являю- щийся константой практически бесполезен: после инициализации его можно использо- вать для чтения и записи данных в элемент, но сменить этот элемент на другой нельзя. const vector<int> nines(10, 9); // нельзя будет изменять элементы // вектора nines // ошибка: итератор cit2 позволяет изменять элемент, на который он II указывает, а вектор nines является константным const vector<int>::iterator cit2 - nines.begin(); // ok: итератор it не позволяет изменять значение элемента, на II который он указывает, поэтому с константным вектором он // вполне применим
124 Часть I. Основы vector<int>::const_iterator it = nines.begin(); *it = 10; // ошибка: *it является константой ++it; // ok: итератор it не является константой, поэтому его // значение вполне можно изменять I/ итератор, не позволяющий записывать элементы vector<int>::const_iterator // итератор, значение которого нельзя изменить const vector<int>::iterator Упражнения раздела 3.4 Упражнение 3.17. Переделайте код упражнения из раздела 3.3.2 (стр. 120) так, чтобы для досту- па к элементам вектора вместо индексирования использовался итератор. Упражнение 3.18. Напишите программу, где создается вектор из 10 элементов. При помощи ите- ратора присвойте каждому элементу значение, которое вдвое больше его текущего значения. Упражнение 3.19. Проверьте предыдущую программу, отобразив хранимые в векторе значения. Упражнение 3.20. Объясните, какой итератор использован в предыдущих программах и почему. Упражнение 3.21. Когда имеет смысл использовать константный итератор? Когда имеет смысл использовать итератор типа const_iterator. Объясните различие между ними. 3.4.1. Арифметические действия с итераторами Кроме оператора инкремента, который переводит итератор на один элемент впе- ред, итераторы векторов (и многих других библиотечных контейнеров) поддержи- вают и другие арифметические операции. К этим операциям, называемым арифме- тическими действиями с итераторами, относят следующие. К итератору можно прибавить или вычесть из него целочисленное значение. В результате получается новый итератор, который указывает на п элементов вперед (при сложении) или назад (при вычитании) от исходного значения ите- ратора iter. Результат сложения и вычитания должен указывать на элемент вектора, к которому относится итератор iter, или на один из его концов. Типом добавляемого или вычитаемого значения, как правило, является size_type век- тора или dif f erence_type (см. ниже). iterl - iter2 Вычисление разницы между двумя итераторами, результатом которого является знаковое целочисленное значение типа dif ference_type. Этот тип, подобно типу size_type, определен в классе вектора. Тип dif f erence_type является знаковым потому, что результатом вычитания может оказаться отрицательное число. Этот тип является гарантированно большим, чтобы содержать разницу между двумя любыми итераторами. Оба итератора, iterl и iter2, должны принадлежать к одному вектору. Арифметические действия с итераторами можно использовать для перемещения итератора на определенное количество элементов. Например, середину вектора можно найти следующим образом.
Глава 3. Библиотечные типы данных 125 vector<int>::iterator mid = (vi.begin() + vi.sizeO) / 2; Этот код инициализирует итератор mid так, чтобы он указывал на элемент, бли- жайший к середине вектора ivec. Это вычисление существенно эффективней спе- циального участка кода, который увеличивая значение итератора на единицу пере- местит его на серединный элемент. Любая операция, которая изменяет размер вектора, делает существующие итераторы недопустимыми. Например, после вызова функции push_back () полагаться на значение уже существующего итератора больше нельзя. Упражнения раздела 3.4.1 Упражнение 3.22. Что получится, если вычислить итератор mid следующим образом. vector<int>::iterator mid = (vi.begin() + vi.end()) / 2; 3.5. Библиотечный тип bit set Иногда в программах приходится иметь дело с упорядоченными наборами битов. Каждый бит может содержать значение 0 или 1. Использование битов — это самый компактный способ хранения значений в формате да или нет (иногда называемых флагами). Предоставляя класс bit set (набор битов), стандартная библиотека су- щественно облегчает работу с битами. Чтобы использовать класс bitset, в про- грамму необходимо подключить его файл заголовка. В примерах также подразуме- вается, что в коде сделано соответствующее объявление using std: : bitset. #include <bitset using std::bitset; 3.5.1. Определение и инициализация наборов битов Список конструкторов типа bit set приведен в табл. 3.6. Подобно вектору, тип bitset является шаблоном класса. В отличие от вектора, объекты типа bitset различаются по размеру, а не по типу. При определении набора битов в угловых скобках указывают количество битов, которые будет содержать набор. bitset<32> bitvec; // 32 бита, все нули Размер должен быть указан константным выражением (раздел 2.7, стр. 84). Это может быть целочисленный константный литерал, как здесь, или константный объ- ект целочисленного типа, инициализированный константным значением. Приведенный выше оператор определяет bitvec как набор битов, который со- держит 32 бита. Подобно элементам вектора, биты набора данных не именованы, а обращение к ним осуществляется по позиции. Биты пронумерованы начиная с нуля. Таким образом, биты набора bitvec пронумерованы от 0 до 31. Биты, расположен- ные ближе к 0, называют младшими битами (low-order), а расположенные ближе к 31 — старшими битами (high-order).
126 Часть I. Основы Таблица 3.6. Способы инициализации объектов класса bitset bitset<n> Ь; Набор ь содержит п битов, каждый из которых содержит значение о bitset<n> b(u); Набор ь является копией значения и типа unsigned long bitset<n> b (s); Набор b является копией битов, содержащихся в строке s bitset<n> ь (s, pos, n); Набор b является копией битов из п символов строки s начиная с позиции pos Инициализация набора битов беззнаковым значением Когда значение типа unsigned long инициализирует набор битов, оно рас- сматривается как битовая схема (bit pattern). Биты в наборе битов являются копией этой схемы. Если размер набора битов больше количества битов, расположенных в переменной типа unsigned long, остающиеся старшие биты будут заполнены нулями. Если размер набора битов меньше количества битов инициализирующего значения, будут использованы лишь младшие биты инициализирующего значе- ния, а старшие биты окажутся отброшены. На машине с 32-битовым типом unsigned long, шестнадцатеричное значение Oxf f f f представляется последовательностью из 16 битов, заполненных единицами, с последующими нулями до конца размера. (Каждая цифра Oxf имеет битовое пред- ставление 1111.) Набор битов можно инициализировать значением Oxffff сле- дующим образом. // набор bitvecl меньше инициализирующего значения bitset<16> bitvecl(Oxffff); // биты 0...15 заполнены единицами /I размер набора bitvec2 и инициализирующего значения bitset<32> bitvec2(Oxffff); // биты 0...15 заполнены II a 16... 31 - нулями совпадают единицами, // на 32-битовой машине биты 0...31 инициализированы числом Oxffff bitset<128> bitvec3(Oxffff); // биты от 32 до 127 заполнены нулями Во всех трех случаях биты от 0 до 15 инициализированы единицами. Для набора битов bitvecl старшие биты инициализирующего значения отброшены; набор би- тов bitvecl имеет меньше битов, чем тип unsigned long. Набор битов bitvec2 имеет тот же размер, что и тип unsigned long, поэтому используются все биты инициализирующего объекта. Размер набора битов bitvec3 больше, чем у типа unsigned long, поэтому его старшие биты, более 31, инициализированы нулями. Инициализация набора битов из строки При инициализации набора битов из строки, строка представляется как битовая схема. Биты строки считываются справа налево. string strval("1100"); bitset<32> bitvec4(strval); Битовая схема набора bitvec4 имеет второй и третий биты в состоянии 1, а ос- тальные в состоянии 0. Когда строка содержит меньше символов, чем размер набора битов, для старших битов используется значение нуль.
Глава 3. Библиотечные типы данных 127 Соглашения о нумерации строк и наборов битов инверсно взаимосвязаны: самый правый Ц символ строки (обладающий самым большим значением индекса) используется для ини- ’4 циализации самого младшего бита в наборе битов (бит с индексом 0). При инициализа- ции набора битов из строки очень важно помнить об этом различии. В качестве исходного значения для набора битов можно использовать не всю строку, а ее часть. string str("1111111000000011001101"); bitset<32> bitvec5(str, 5, 4); // 4 бита, начиная от str[5], 1100 bitset<32> bitvec6(str, str.size() - 4); // использовать 4 // последних символа Здесь набор битов bitvecS инициализирован частью строки str, начинающей- ся с символа str [5] и распространяющейся на четыре следующие позиции. Как обычно при инициализации, набор битов bitvecS заполняется значениями под- строки из 4 символов начиная с пятого, т.е. значением 1100, а биты в остальных по- зициях заполняются нулями. В третьей строке кода использован подход, позволяю- щий применить символы от указанной позиции до конца строки. В данном случае, для инициализации четырех младших битов набора bitvecS, используются симво- лы строки str начиная с четвертого от конца. Остальные биты набора bitvec6 инициализируются нулями. Эти случаи инициализации представлены на рис. 3.1. str bitvecS bitvec6 (Следующий элемент после последнего) Str[5] (Первый элемент) bitvec5[4] bitvec5[0] str.size() - 4 Рис. 3.1. Инициализация набора битое из строки 00000001 101 bitvec6[4] bitvec6[0] 3.5.2. Операции с наборами битов В классе bitset определены несколько функций табл. 3.7, обеспечивающих вы- полнение с набором битов таких операций, как проверка состояния и установка од- ного или нескольких битов. Таблица 3.7. Операции с наборами битов Scanned bu Digrol b.any() b.попе() b.count() b.size () b [pos] Все ли биты набора b установлены? Нет ли в наборе ь установленных битов? Количество установленных битов в наборе ь Количество битов в наборе b Доступ к биту номер pos в наборе ь
128 Часть I. Основы Окончание табл. 3.7 b. test(pos) b.any() b. set () b.set(pos) b.reset() b.reset(pos) b.flip() b.flip(pos) Установлен ли бит номер pos в наборе ь? Все ли биты набора b установлены? Устанавливает все биты в наборе ь Устанавливает бит номер pos в наборе ь Сбрасывает все биты в наборе ь Сбрасывает бит номер pos в наборе ь Изменяет состояние всех битов в наборе ь Изменяет состояние бита номер pos в наборе ь b. to_ulong () Возвращает число типа unsigned long с теми же битами, что и в наборе ь os << b Передает биты набора ь в поток os Проверка набора битов в целом Функция any () возвращает значение true, если один или несколько битов набора установлены, т.е. находятся в состоянии 1. Функция попе (), наоборот, возвращает значение true, если все биты объекта сброшены, т.е. находятся в состоянии 0. bitset<32> bitvec; // 32 бита, все нули bool is_set = bitvec.any(); // false, все биты - нули bool is_not_set = bitvec.none(); // true, все биты - нули Если необходимо узнать, сколько битов установлено, можно воспользоваться функцией count (), которая возвращает количество установленных битов. size_t bits_set = bitvec.count(); // возвращает количество // установленных битов Функция count () возвращает библиотечный тип size_t, который определен в заголовке cstddef библиотеки С. Версия ее заголовка для языка C++ имеет имя stddef .h. Это машинно-зависимый беззнаковый тип, который гарантированно ве- лик, чтобы содержать размер объекта в памяти. Функция size (), подобно одноименной функции векторов и строк, возвращает общее количество битов в наборе. Возвращаемое значение имеет тип size_t. size_t sz = bitvec.size(); // возвращает число 32 Доступ к битам в наборе битов Оператор индексирования позволяет читать и записывать значения битов в по- зиции указанные индексом. Его можно также использовать для проверки или уста- новки значения определенного бита. // присвоить 1 каждому биту диапазона for (int index = 0; index != 32; index += 2) bitvec[index] = 1; Этот цикл установит 32 первых бита набора bitvec. Чтобы проверить или установить определенное битовое значение, совместно с опе- ратором индексирования можно использовать функции set (), test () и reset ().
Глава 3. Библиотечные типы данных 129 // аналогичный цикл, использующий функцию set() for (int index = 0; index != 32; index += 2) bitvec.set(index); Чтобы выяснить, установлен ли определенный бит, можно либо воспользо- ваться функцией test (), либо проверить значение, возвращаемое оператором индексирования, if (bitvec.test(i)) // bitvec[i] установлен !/ аналогичная проверка при помощи индексирования if (bitvec[i]) // bitvec[i] установлен Результатом проверки возвращенного оператором индексирования значения бу- дет true, если бит установлен (1), или false — если бит сброшен (0). Установка значений набора битов в целом Функции set() nresetO также можно использовать для установки или сбро- са всех битов набора объекта соответственно. bitvec.reset(); // сбросить все биты в 0 bitvec.set(); // установить все биты в 1 Функция f 1 ip () инвертирует значение отдельного бита или всего набора битов. bitvec.flip(0); // инвертирует значение первого бита bitvec[0].flip(); // тоже инвертирует первый бит bitvec.flip(); // инвертирует значения всех битов Получение значения из набора битов Функция to_ulong () возвращает значение типа unsigned long, которое со- держит ту же битовую схему, что и набор битов. Эту функцию можно использовать только в том случае, если размер набора битов меньше или равен размеру типа unsigned long. unsigned long ulong = bitvec3.to_ulong(); cout « "ulong = " << ulong « endl; Функция to_ulong () была разработана в языке С для передачи содержимого из набора битов еще до появления стандарта языка C++. Если набор содержит больше битов, чем помещается в переменную типа unsigned long, во время выполнения будет передано исключение. Знакомство с исключениями начинается в разделе 6.13 (стр. 241), а более подробная информация о них приведена в разделе 17.1 (стр. 720). Отображение битов Для отображения битовой схемы, содержащейся в наборе битов, можно восполь- зоваться оператором вывода (<<). bitset<32> bitvec2 (Oxffff); // биты 0...15 установлены в 1, а II биты 16...31 сброшены в О cout « "bitvec2: " « bitvec2 « endl; В результате на экране будет отображено. bitvec2: 00000000000000001111111111111111
130 Часть I. Основы Использование побитовых операторов Класс bitset поддерживает также встроенные побитовые операторы (bitwise operator). Как определено в языке, эти операторы применимы к целочисленным опе- рандам. Они выполняют операции, подобные операциям с набором данных, описан- ным в этом разделе. Более подробная информация об этих операторах приведена в разделе 5.3 (стр. 179). Упражнения раздела 3.5.2 Упражнение 3.23. Объясните, какую битовую схему содержит каждый из следующих наборов битов. (a) bitset<64> bitvec(32); (b) bitset<32> bv(1010101); (c) string bstr; cin » bstr; bitset<8>bv(bstr); Упражнение 3.24. Предположим, что существует последовательность 1, 2, 3, 5, 8,13, 21. Инициа- лизируйте набор bitset<32> так, чтобы в каждой из позиций, указанной числом этой последо- вательности, бит был установлен (1). В качестве альтернативы создайте пустой набор битов и на- пишите небольшую программу, устанавливающую каждый из соответствующих битов. Резюме В библиотеке определено несколько высокоуровневых абстрактных типов данных, вклю- чая строки и векторы. Класс string предоставляет символьные строки переменной длины, а шаблон vector позволяет создавать коллекции объектов одинакового типа. Итераторы обеспечивают косвенный доступ к объектам, хранимым в контейнере. Ите- раторы применимы для доступа и перемещения между элементами строк и векторов. В следующей главе рассматриваются массивы и указатели, являющиеся встроенными ти- пами языка. Эти типы являются низкоуровневыми аналогами библиотечных векторов и строк. Как правило, предпочтительней использовать библиотечные классы, а не встроенные. Термины Абстрактный тип данных (abstract data type). Тип, представление которого скрыто. Ис- пользуя абстрактный тип, достаточно знать только то, какие операции он поддерживает. Арифметические действия с итераторами (iterator arithmetic). Арифметические опера- ции, которые можно применять к некоторым, но не всем, типам итераторов. Добавление и вы- читание целого числа из итератора приводит к изменению позиции итератора на соответст- вующее количество элементов вперед или назад от исходного. Вычитание двух итераторов позволяет вычислить дистанцию между ними. Арифметические действия допустимы лишь для итераторов, относящихся к элементам того же контейнера. Заголовок cctype. Унаследованный от языка С заголовок, который содержит определе- ния функций для проверки символьных значений. Список наиболее популярных из них при- веден в табл. 3.3 на стр. 112. Индекс (index). Значение, используемое в операторе индексирования для указания эле- мента, возвращаемого из строки или вектора. Инициализирующее значение (value initialized). Используется, когда при инициализации контейнера указано только количество элементов контейнера, без явного указания значений. Элементы инициализируются копией значения, созданного компилятором. Если контейнер
Глава 3. Библиотечные типы данных 131 предназначен для встроенного типа, в элементы копируется нулевое значение. Для классов инициализирующее значение создает стандартный конструктор класса. Элементы контейне- ра, являющиеся объектами класса, могут быть инициализированы только тогда, когда класс имеет стандартный конструктор. Итератор после конца (off-the-end iterator). Итератор, возвращаемый функцией end (). Он указывает не на последний существующий элемент контейнера, а на позицию за его кон- цом, т.е. на несуществующий элемент. Класс bitset (набор битов). Определенный в стандартной библиотеке класс, объект ко- торого содержит коллекцию битов и позволяет выполнять с ним операции по проверке и ус- тановке значений. Контейнер (container). Тип, объекты которого способны содержать коллекцию объектов определенного типа. Младшие биты (low-order). Биты набора, обладающие самыми маленькими индексами. Объявление using. Позволяет сделать имя, определенное в пространстве имен, доступ- ным непосредственно в коде. using пространствоимен: -.имя; Теперь имя можно использовать без префикса пространствоимен: :. Оператор *. Для итераторов определен оператор обращения к значению, позволяющий получить объект, на который указывает итератор. Этот оператор возвращает 1-значение, по- этому его можно использовать как левый операнд присвоения. Присвоение значения резуль- тату выполнения этого оператора приводит к присвоению нового значения соответствующе- му элементу. Оператор : :. Оператор области видимости. Находит имя его правого операнда в области видимости, указанной левым операндом. Используется для доступа к именам из пространства имен, например std:: cout, где имя cout принадлежит пространству имен std. Аналогично он используется и для доступа к именам определенным в классе, например string: : size_type, где тип size_type определен в классе string. Оператор [ ]. Перегруженный оператор индексирования, определенный для строк, векторов и наборов битов. Он получает два операнда: левый (имя объекта) и правый (индекс). Оператор выбирает элемент, позиция которого указана индексом. Нумерация элементов при индексиро- вании начинается с нуля, т.е. первым является элемент номер 0, а последним — элемент номер obj . size () -1. Индексирование возвращает 1-значение, поэтому его можно использовать как левый операнд присвоения. В результате новое значение будет присвоено элементу, указанному по индексу. Оператор ++. Для итераторов некоторых типов определен оператор инкремента, который “добавляет единицу”, перемещая итератор на следующий элемент. Оператор <<. Для библиотечных типов string и bitset определен оператор вывода. Строковый оператор вывода выводит на стандартное устройство вывода символы строки, а битовый — битовую схему. Оператор >>. Для библиотечных типов string и bitset определен оператор ввода. Строковый оператор ввода читает разграниченные пробелами последовательности символов и сохраняет их в строковой переменной, указанной правым операндом. Оператор ввода для набора битов читает битовую последовательность и записывает ее в набор битов, указанный правым операндом. Старшие биты (high-order). Биты набора, обладающие самыми большими индексами. Тип dif ference_type. Определенный в классе вектора знаковый целочисленный тип, переменная которого способна содержать дистанцию между двумя любыми итераторами. Тип iterator (итератор). Тип, используемый при переборе элементов контейнера и об- ращении к ним.
132 Часть I. Основы Тип size_t. Машинно-зависимый беззнаковый целочисленный тип, определенный в за- головке cstddef. Является достаточно большим, чтобы содержать размер самого большого возможного массива. Тип size_type. Определенный для классов строк и векторов беззнаковый тип, перемен- ные которого достаточно велики, чтобы содержать размер любой строки или вектора. Функция empty (). Функция, определенная в строковых и векторных классах. Она воз- вращает логическое значение (типа bool), которое указывает, имеются ли в строке символы или элементы в векторе. Возвращает значение true, если размер нулевой, или значение false в противном случае. Функция getline (). Определенная в заголовке string функция, которой передают по- ток istream и строковую переменную. Функция читает данные из потока до тех пор, пока не встретится символ новой строки, а прочитанное сохраняет в строковой переменной. Функция возвращает поток istream. Символ новой строки в прочитанных данных отбрасывается. Функция push_back (). Определенная в классе вектора функция, которая добавляет элементы в его конец. Функция size (). Определенная в библиотечных классах строк, векторов и наборов би- тов функция, которая возвращает количество символов, элементов или битов соответственно. Строковые и векторные функции возвращают значение типа size_type. Например, функ- ция size () класса string возвращает значение типа string:: size_type. Функция size () класса bitset возвращает значение типа size_t. Шаблон класса (class template). Проект, согласно которому может быть создано множест- во специализированных классов. Чтобы применить шаблон класса, необходимо указать фак- тический класс или используемое значение (или значения). Например, vector — это шаб- лон, объекты классов которого способны содержать объекты указанного типа. При создании вектора необходимо указать, объекты какого именно типа будет содержать данный вектор. Вектор, объявленный как vectore int >, способен содержать целые числа, а вектор, объяв- ленный как vector<string>, — строки и т.д.
Массивы и указатели В ЭТОЙ ГЛАВЕ... 4.1. Массивы 4.2. Знакомство с указателями 4.3. Символьная строка в стиле С 4.4. Многомерные массивы Резюме Термины 134 138 154 165 168 168 В языке C++ определено два низкоуровневых составных типа: массивы и указате- ли, которые подобны векторам и итераторам. Подобно векторам, массив содержит коллекцию объектов некоего типа. Но в отличие от векторов, массивы имеют фикси- рованный размер, т.е. после создания массивов добавлять в них новые элементы нель- зя. Подобно итераторам, указатели применяются для доступа к элементам массива. В современных программах на языке C++ почти всегда имеет смысл использо- вать векторы и итераторы, а не низкоуровневые массивы и указатели. В хорошо про- работанных программах массивы и указатели используются только внутри реализа- ции класса, где необходима высокая скорость работы. Массив — это встроенная структура данных языка, подобная библиотечному век- тору. Как и вектор, массив является контейнером для объектов одинакового типа. Хранимые объекты не являются именованными, а обращение к ним осуществляется по позиции в массиве. По сравнению с векторами, массивы обладают рядом существенных недостатков: они имеют фиксированный размер, а также не предоставляют никаких средств, по- зволяющих выяснить их размер. У массивов нет ни функции size (), ни функции push_back (), позволяющей автоматически добавлять новые элементы. Когда раз- мер массива приходится изменять, программист вынужден создавать новый массив, большего размера, и копировать в него элементы из предыдущего массива. Программы, использующие встроенные массивы, сложней в отладке и более подвержены 5д.fW I ошибкам, чем программы, использующие стандартные векторы. До появления стандартной библиотеки, в программах на языке C++ для хране- ния коллекций объектов приходилось использовать тяжелые в применении массивы.
134 Часть I. Основы В современных программах вместо массивов практически всегда имеет смысл ис- пользовать векторы. Применение массивов следует ограничить внутренними орга- низационными задачами программ, где высокая производительность весьма сущест- венна, а векторы не могут ее обеспечить. Однако в настоящее время существует большое количество кода C++, в котором используются массивы. Следовательно, все программисты C++ должны уметь работать с массивами. 4.1. Массивы Массив (array) — это составной тип (раздел 2.5, стр. 81), при объявлении которого используют спецификатор типа, идентификатор и размерность. Спецификатор типа (type specifier) задает тип хранимых в массиве элементов, а размерность указывает их количество. В качестве спецификатора типа можно использовать как встроенные типы данных, так и классы. За исключением ссылок, здесь может быть также использован любой составной тип. Массивов ссылок не существует. 4.1.1. Определение и инициализация массивов Размерность (dimension) должна быть константным выражением (раздел 2.7, стр. 84), результат которого больше или равен единице. Константное выражение (constant expression) — это любое выражение, использующее только целочисленные константные литералы, перечислители (раздел 2.7, стр. 84) или константные объекты целочисленного типа, которые сами инициализированы результатом константных выражений. Неконстантная переменная, или константная переменная, значение ко- торой неизвестно до момента запуска программы, не может быть использована для определения размерности массива. Размерность указывают внутри пары квадратных скобок ( [ ] ). // и buf_size и max_files являются константами const unsigned buf_size = 512, max_files - 20; int staff_size = 27; // не константа const unsigned sz = get_size(); // константное значение, не II известное до момента выполнения char input_buffer[buf_size]; // ok: константная переменная string fileTable[max_files + 1]; // ok: константное выражение double salaries[staff_size]; // ошибка: неконстантная переменная int test_scores[get_size()]; // ошибка: неконстантное выражение int vals[sz]; // ошибка: до момента выполнения // размер неизвестен Хотя переменная staf f_size инициализирована литеральной константой, сама она является неконстантным объектом. Ее значение окажется известно только во время выполнения, поэтому для размерности массива оно неприменимо. Несмотря на то, что размер является константным объектом, его значение так же неизвестно, пока во время выполнения не будет вызвана функция get_size (). Следовательно, как размерность его использовать нельзя. С другой стороны, следующее выражение считается константным, поскольку переменная max_f iles является константной. max_files + 1 Результат этого выражения (21) может быть вычислен во время компиляции.
Глава 4. Массивы и указатели 135 Явная инициализация элементов массива При определении массива, для его элементов можно предоставить разделяемый запятыми список инициализирующих значений. Этот список следует заключить в фигурные скобки. const unsigned array_size = 3; int ia[array_size] = {0, 1, 2}; Если инициализирующие значения для элементов не предоставлены, они ини- циализируются таким же образом, как и переменные (раздел 2.3.4, стр. 73). Элементы встроенного типа, определенного вне тела функции, инициализиру- ются нулевыми значениями. Элементы встроенного типа, определенного внутри тела функции, не инициали- зируются. Независимо от того, где определен массив, если типом его элементов является класс, инициализацию элементов осуществляет стандартный конструктор этого класса (если он есть). Если класс не имеет стандартного конструктора, элементы следует инициализировать явно. Если элементы локального массива встроенного типа не инициализировать явно, они ос- танутся неинициализированными. При использовании таких элементов для любых целей, кроме присвоения значений, результат окажется непредсказуемым. При явной инициализации массива, значение его размера не указывают. Компи- лятор самостоятельно вычислит размер массива исходя из количества элементов, int ia [ ] = {0, 1, 2}; // массив размером в 3 элемента Если размер массива определен, количество предоставляемых значений не долж- но его превышать. Если размер больше количества предоставленных значений, про- исходит инициализация лишь первых элементов, а остальные инициализируются либо нулевым значением, если элементы имеют встроенный тип, либо стандартным конструктором, если их типом является класс. const unsigned array_size = 5; // Эквивалент ia = {0, 1, 2, 0, 0} // ia[3] и ia[4] по умолчанию инициализируются нулем int ia[array_size] = {0, 1, 2}; // Эквивалент str_arr = {"hi", "bye", ""} / / от str_arr [2] до str_arr[4] по умолчанию инициализируются // пустой строкой string str_arr[array_size] = {"hi", "bye"}; Особенности символьных массивов Символьный массив может быть инициализирован либо списком разделяемых запятыми символьных литералов, заключенных в фигурные скобки, либо строковым литералом. Обратите внимание, эти две формы не эквивалентны. Напомним, что строковый литерал (раздел 2.2, стр. 63) содержит дополнительный нулевой завер- шающий символ. Когда символьный массив создается из строкового литерала, нуле- вой символ также добавляется в массив.
136 Часть I. Основы char cal [] // без нулевого символа char са2 [] = {'С, ' + ' \ 0' }; // нулевой символ введен явно char саЗ [] // нулевой завершающий символ // добавляется автоматически Массив cal имеет размер 3 элемента, а массивы са2 и саЗ — 4. При инициали- зации массива символов литералом, не следует забывать о завершающем нулевом символе. Например, следующий код приведет к ошибке во время компиляции. const char ch3 [6] = "Daniel"; // ошибка: Daniel содержит 7 элементов Здесь литерал содержит 6 видимых символов, но для массива необходим размер в 7 элементов: 6 — чтобы содержать литерал, и 1 — для нулевого символа. Массив не допускает ни копирования, ни присвоения В отличие от вектора, инициализировать массив копией другого массива нельзя. Кроме того, один массив нельзя присвоить другому. int ia[] - {0, 1, 2}; // ok: целочисленный массив int ia2 [] (ia); // ошибка: нельзя инициализировать II один массив другим int main() { const unsigned array_size = 3; int ia3 [array_size]; // ok: y_size]; // ok: но элементы неинициализированы! II ошибка: нельзя присвоить один массив другому return 0; Некоторые модифицированные компиляторы позволяют присваивать массивы. Если предполагается компилировать программу на нескольких компиляторах, желательно избегать таких нестандартных подходов, как присвоение массивов, поскольку на дру- гих компиляторах это может не сработать. Внимание! Массивы имеют Фиксированный размер В отличие от векторов, массивы не имеют функции push_back (), позволяющей до- бавлять новые элементы в его конец. После определения, добавлять в массив новые элементы нельзя. Если в массив все же необходимо добавлять элементы, придется самостоятельно соз- давать механизм управления хранением. Сначала необходимо создать новый массив большего размера, а затем скопировать в него элементы прежнего. Более подробно этот вопрос обсуждается в разделе 4.3.1 (стр. 159). Упражнения раздела 4.1.1 Упражнение 4.1. Предположим, что функция get_size() не получает никаких аргументов и возвращает значение типа int. Какие из следующих определений недопустимы и почему? unsigned buf_size = 1024; (a) int ia[buf_size]; (b) int ia[get_size()]; (c) int ia[4 * 7 - 14]; (d) char st [11] - "fundamental";
Глава 4. Массивы и указатели 137 Упражнение 4.2. Какие значения содержатся в следующих массивах? string sa[10]; int ia [ 10 ]; int main() { string sa2[10] ; int ia2[10] ; } Упражнение 4.3. Какие из следующих определений ошибочны (если они есть)? (a) int ia[7] = { 0, 1, 1, 2, 3, 5, 8 }; (b) vector<int> ivec = { 0, 1, 1, 2, 3, 5, 8 }; (c) int ia2[ ] = ial; (d) int ia3[ ] = ivec; Упражнение 4.4. Как можно инициализировать некоторые или все элементы массива? Упражнение 4.5. Перечислите недостатки массивов по сравнению с векторами. 4.1.2. Операции с массивами К элементам массива, подобно элементам вектора, можно обращаться при помо- щи оператора индексирования (раздел 3.3.2, стр. 118). Подобно элементам вектора, элементы массива пронумерованы начиная с 0. У массива из десяти элементов ин- дексы имеют значения от 0 до 9, а не от 1 до 10. Типом индекса вектора является vector: : size_type. При индексировании мас- сива, для индекса целесообразнее использовать тип size_t (раздел 3.5.2, стр. 128). В следующем примере Цикл for перебирает 10 элементов массива, присваивая каждому из них значение, равное индексу. int main() const size_t array_size = 10; int ia[array_size]; // 10 неинициализированных элементов // типа int II перебрать массив и присвоить каждому элементу значение // его индекса for (size_t ix = 0; ix != array_size; ++ix) Используя подобный цикл можно скопировать один массив в другой. int main() const size_t array_size = 7; int ial[] = { 0, 1, 2, 3, 4, 5, 6 }; int ia2[array_size]; // локальный неинициализированный массив // скопировать элементы массива ial в массив ia2 for (size_t ix = 0; ix != array_size; ++ix) ia2[ix] = ial[ix]; return 0;
138 Часть I. Основы Проверка значений индекса Подобно строкам и векторам, программист должен гарантировать, что значение индекса останется в допустимом диапазоне. Если не организовать в коде проверку допустимости индекса, нормально отком- пилирована будет даже такая программа, которая содержит столь грубую ошибку, как выход индекса за границы массива. Наиболее распространенной проблемой в приложении является переполнение бу- фера (buffer overflow). Эта ошибка происходит в случае, когда индекс не проверяется и происходит обращение к элементу вне пределов массива или другой подобной структуры данных. Упражнения раздела 4.1.2 Упражнение 4.6. В этом фрагменте кода предполагалось присвоить каждому элементу массива значение его индекса. Однако в код вкралось несколько ошибок индексации. Укажите их. const size_t array_size = 10; int ia[array_size]; for (size_t ix = 1; ix <= array_size; ++ix) ia[ix] = ix; Упражнение 4.7. Напишите код, позволяющий присвоить один массив другому. Измените код так, чтобы в нем использовались векторы. Как можно присвоить один вектор другому? Упражнение 4.8. Напишите программу, проверяющую равенство двух массивов. Напишите по- добную программу, но сравнивающую два вектора. Упражнение 4.9. Напишите программу, в которой определен массив из 10 элементов типа int. Присвойте каждому элементу значение, совпадающее с его позицией в массиве. 4.2. Знакомство с указателями Подобно тому, как вектор можно перебрать используя индексирование или ите- ратор, массив можно перебрать используя индексирование или указатель. Указатель (pointer) — это составной тип, переменная которого указывает на объект другого ти- па. Указатели можно использовать в качестве итераторов для массива, поскольку указатель способен указывать на элемент массива. Операторы инкремента и обра- щения к значению, примененные к указателю указывающему на элемент массива, ведут себя так же, как и при применении к итератору. При обращении к значению указателя возвращается объект, на который указывает указатель. При инкременте указатель перемещается на следующий элемент массива. Однако прежде чем при- ступать к созданию программы, использующей указатели, необходимо узнать о них немного больше. 4.2.1. Что такое указатель? Новичкам зачастую трудно понять назначение указателей. А связанные с указа- телями ошибки могут при отладке ввести в заблуждение даже опытных программи- стов. Однако подобно тому, как раньше указатели являлись важнейшей частью
Глава 4. Массивы и указатели 139 большинства программ С, так и ныне они остаются важным элементом многих про- грамм на языке C++. Концептуально указатели очень просты: они указывают на объект1. Подобно ите- ратору, указатель предоставляет косвенный доступ к объекту, на который он указы- вает. Однако конструкция указателя существенно проще. Итераторы используются только для доступа к элементам контейнера, а указатели, в отличие от них, приме- няются для ссылки на отдельные объекты. Другими словами, указатель — это переменная специального типа, способная со- держать адрес области в памяти. Указателю может быть присвоен адрес размещен- ного в памяти объекта. string s("hello world"); string *sp = &s; // указатель sp содержит адрес строки s Во втором операторе определен указатель sp на тип string, который был сразу инициализирован адресом строковой переменной s. Звездочка (*) в операторе опре- деления имени (*sp) означает, что речь идет об указателе по имени sp. Оператор & является оператором обращения к адресу (address-of), называемым также операто- ром взятия адреса. Он возвращает значение (адрес), в результате ссылки на которое (при помощи оператора обращения к значению) получается исходный объект. Опе- ратор обращения к адресу можно применять только для 1-значений (раздел 2.3.1, стр. 67). Поскольку переменная является 1-значением, вполне возможно получить ее адрес. Аналогично, операторы обращения к значению и индексирования, применен- ные к вектору, строке или встроенному массиву, возвращают 1-значения, а значит, оператор обращения к адресу применим к результату операторов индексирования и обращения к значению. В результате получится адрес определенного элемента. Совет. Избегайте использования массивов и указателей Именно в указателях и массивах особенно часто возникают ошибки. Часть проблем носит концептуальный характер: указатели используются для манипулирования па- мятью на низком уровне, и в них очень часто возникают арифметические ошибки. Проблемы возникают также из-за синтаксических ошибок, особенно при объявлении. Большинстве программ можно написать и без использования массивов и указателей. В < овременных программах на языке C++ вместо них следует использовать векторы и итераторы, а вместо строк в стиле языка С, представляющих собой массив симво- лов, — класс string. 4.2.2. Определение и инициализация указателей Каждый указатель имеет тип. Тип указателя определяет тип объектов, на которые он может указывать. Указатель на тип int, например, может указывать только на объект типа int. * Фактически указатель представляет собой именованную переменную, содержащую адрес первого байта объекта, размещенного в памяти. Поскольку размещенные в памяти объекты имеют разный размер, для вычисления конечного бита значения необходимо знать его тип, ведь именно тип и определяет размер переменной. Указателем является любая переменная (именованная область в памяти, содержащая адрес другой области), однако синтаксис дос- тупа к ее значению гораздо проще. — Примеч. ред.
140 Часть I. Основы Определение переменных-указателей Для указания на то, что данный идентификатор является указателем, в объявле- нии используется символ звездочки (*). vector<int> string double *pvec; *ipl, *ip2; *pstring; *dp; // pvec может указывать на veckorcint II ipl и ip2 могут указывать на int II pstring может указывать на string II dp может указывать на double Чтобы разобраться в объявлении указателя, его следует читать справа налево. Читая определение указателя pstring справа налево, можно заметить следующее, string *pstring; Эта часть определяет, что pstring является указателем, который может указы- вать на объекты типа string. Далее аналогично. int *ipl, *ip2; // ipl и ip2 могут указывать на int В этом определении указано, что ip2 и ipl будут указателями на переменные типа int. Символ * позволяет объявить в одном операторе объект и указатель того же типа, double dp, *dp2; // dp2 является указателем, a dp объектом. // Оба типа double Здесь dp2 определен как указатель, a dp как объект, но оба имеют тип double. Различные стили объявления указателей Символ * можно отделить от идентификатора пробелом. Следующая запись вполне допустима. string* ps; // допустимо, но может ввести в заблуждение Здесь объявлен указатель ps на тип string. Это определение может ввести в заблуждение, поскольку можно ошибочно пред- положить, что типом является string* и все определенные здесь переменные будут указателями на тип string. Но это не так. string* psi, ps2; // psi является указателем типа string, // a ps2 переменной типа string Здесь объявлен указатель psi и обычная строковая переменная ps2. Если в од- ном определении необходимо создать два указателя на одинаковый тип, символ * следует повторить для каждого идентификатора. string* psi, *ps2; // psi и ps2 являются указателями типа string Несколько объявлений указателей не всегда понятны Существует два общепринятых стиля объявления нескольких указателей одина- кового типа. Один стиль подразумевает объявление каждого имени в отдельном операторе. Здесь символ * можно помещать после типа, чтобы подчеркнуть, что объ- является именно указатель, string* psi; string* ps2;
Глава 4. Массивы и указатели 141 Другой стиль подразумевает размещение нескольких объявлений в одном опера- торе, а символ * располагается перед идентификатором, подчеркивая, что каждый объект является указателем. string *psl, *ps2; Как и в отношении всех остальных вопросов, связанных со стилем, единственно пра- вильного ответа здесь нет. Но очень важно, выбрав подходящий стиль, жестко при- держиваться именно его. В этой книге используется второй стиль, где символ * располагается рядом с именем переменной-указателя. Допустимые состояния указателя Допустимый указатель может находиться в одном из трех состояний: он может содержать адрес определенного объекта, адрес следующего объекта или нуль. Указа- тель, содержащий нулевое значение, не указывает ни на что. Неинициализирован- ный указатель, которому пока не присвоено значение, недопустим. Все приведенные ниже определения и присвоения допустимы. int ival = 1024; int *pi = 0; / / pi инициализирован адресом, не указывающим ни на / / что int *pi2 = &ival; // pi2 инициализирован адресом ival int *pi3; // допустимо но опасно, pi3 неинициализирован pi = pi2; // pi и pi2 содержат адреса одного объекта, т.е. ival pi2 - 0; // pi2 теперь не содержит адреса объекта Избегайте неинициализированных указателей Неинициализированные указатели — наиболее распространенная причина ошибок во время выполнения программы. Подобно любой другой переменной, иногда случайно используются неинициали- зированные указатели. Это практически всегда приводит к аварийному завершению выполнения программы. Однако несмотря на факт аварийного завершения, найти место использования неинициализированного указателя крайне сложно. В большинстве компиляторов, при использовании неинициализированного ука- зателя, будет применена случайная область памяти, адрес которой составит случай- ный набор битов, оказавшихся в указателе на момент его создания. Этот случайно созданный адрес и будет использован для чтения и сохранения данных. При выпол- нении программы это почти неизбежно приведет к аварийному отказу. Если это возможно, не определяйте указатель до создания объекта, на который он должен указывать. Это избавит от необходимости создавать неинициализированный -НчомеиЭуем указатель. ' Если приходится определять указатель отдельно от объекта, на который он должен указывать, инициализируйте указатель нулевым значением. Дело в том, что нулевой указатель вполне может быть обнаружен, а следовательно, сразу станет ясно, что он не указывает ни на что.
142 Часть I. Основы Обнаружить место использования неинициализированного указателя очень слож- но, поскольку невозможно отличить допустимый адрес от адреса, сформированного из битов, которые находились в памяти на момент создания указателя. Поэтому настоя- тельно рекомендуется инициализировать все переменные, особенно указатели. Ограничения на инициализацию и присвоение указателей Существует только четыре вида значений, которые могут быть использованы при инициализации и присвоении указателей. 1. Константное выражение (раздел 2.7, стр. 84) со значением 0 (например целочис- ленная константа, нулевое значение, которой известно на момент компиляции, или литеральная константа 0). 2. Адрес объекта соответствующего типа. 3. Адрес объекта, следующего после существующего объекта. 4. Другой допустимый указатель того же типа2. Указателю запрещено присваивать содержимое переменной типа int, даже если им является 0. Это должен быть литерал 0 или константа, значение 0 которой из- вестно уже на момент компиляции. int ival; int zero = 0; const int c_ival = 0; int *pi = ival; // ошибка: pi инициализируется значением ival pi = zero; // ошибка: pi присвоено значение zero pi = c_ival; // ok: c_ival - константа co значением 0, II известным на момент компиляции pi = 0; // ok: прямая инициализация литеральной II константой 0 Кроме литерала 0 и константы со значением 0, известным на момент компиля- ции, можно использовать также средство, которое язык C++ унаследовал от языка С. В заголовке cstdlib определена переменная препроцессора (раздел 2.9.2, стр. 93) по имени NULL, которой соответствует значение 0. Когда в коде используется пере- менная препроцессора, перед компиляцией она автоматически заменяется своим значением. Следовательно, инициализация указателя переменной NULL эквивалент- на его инициализации значением о. // cstdlib определяет NULL как 0 int *pi = NULL; // ok: эквивалент int *pi = 0; Подобно любым переменным препроцессора (раздел 2.9.2, стр. 94), имя NULL нельзя использовать для собственных переменных. Переменные препроцессора определены не в пространстве имен std, а следовательно имя null не эквивалентно имени Std: : NULL. Кроме двух исключений, рассматриваемых в разделах 4.2.5 и 15.3, инициализи- ровать указатель (или присвоить ему значение) можно лишь адресом или содержи- мым другого указателя того же типа. 2 Фактически два вида, допустимый адрес или 0. — Примеч. ред.
Глава 4. Массивы и указатели 143 double dval; double *pd - &dval; // ok: инициализация адресом переменной // типа double double *pd2 = pd; // ok: инициализация содержимым указателя на II переменную типа double int *pi = pd; // ошибка: у pi и pd разный тип pi = &dval; // ошибка: попытка присвоить адрес типа double // указателю типа int Причина, по которой типы указателей должны совпадать, обусловлена тем, что тип используемого указателя определяет тип объекта, на который он указывает. Указатели используются для косвенного обращения к объекту. Допустимость опе- раций, выполняемых с указателем, выясняется на основании его типа: указатель на тип int рассматривается как объект, на который он указывает, т.е. объект типа int. Если фактически этот указатель будет хранить адрес объекта некоего другого типа, например double, любая выполненная с таким указателем операция приве- дет к ошибке. Указатель типа void Tun void — это специальный тип, указатель такого типа может содержать адрес любого объекта. double obj = 3.14; double *pd = &obj; // ok: указатель типа void может содержать адрес любого типа данных void *pv = &obj; // obj может быть объектом любого типа pv = pd; // pd может быть указателем любого типа Тип void в определении указателя означает, что он предназначен для хранения адреса объекта, но тип объекта остается неизвестным. Однако с указателем типа void можно осуществлять лишь ограниченное количе- ство действий: его можно сравнивать с другим указателем, можно передавать или возвращать из функции, а также присваивать другому указателю типа void. Его нельзя использовать для работы с объектом, адрес которого он содержит. Но адрес, хранимый в указателе типа void, вполне можно получить, как будет продемонстри- ровано в разделе 5.12.4 (стр. 209). Упражнения раздела 4.2.2 Упражнение 4.10. Объясните, почему следует отдать предпочтение первой форме объявления указателя. int *ip; // хороший подход int* ip; // допустимо, но нежелательно Упражнение 4.11. Объясните, что означает каждое из следующих определений. Укажите, есть ли среди них недопустимые и почему. (a) int* ip; (b) string s, *sp = 0; (c) int i; double* dp = &i; (d) int* ip, ip2; (e) const int i = 0, *p = i (f) string *p = NULL; ip; * sp - 0; 0, * P = i;
144 Часть I. Основы Упражнение 4.12. Предположим, что существует указатель р. Можно ли выяснить, содержит он адрес допустимого объекта или нет? Если да, то как? Если нет, то почему? Упражнение 4.13. Почему инициализация первого указателя допустима, а второго — нет? int i = 42; void *р = &i; long *lp = &i; 4.2.3. Операции с указателями Указатели обеспечивают косвенное манипулирование объектами, на которые они указывают. Для доступа к объекту можно воспользоваться оператором обращения к значению указателя. Обращение к значению указателя подобно обращению к значе- нию итератора (раздел 3.4, стр. 121). Оператор обращения к значению (*) возвращает объект, адрес которого содержит указатель. string s("hello world"); string *sp = &s; // sp содержит адрес строки s cout « *sp; // отображает hello world При обращении к значению указателя sp получается значение строки s, которое и передается оператору вывода. Таким образом, оператор << отображает содержимое переменной s, т.е. hello world. В результате обращения к значению получается 1-значение Оператор обращения к значению возвращает 1-значение объекта, адрес которого содержит указатель. Поэтому полученное в результате значение объекта вполне можно изменить. * sp = " goodbye"; / / теперь содержимое переменной s изменено Поскольку осуществляется обращение к значению указателя sp, содержащего адрес переменной s, фактически значение присваивается переменной s. Указателю sp вполне можно присвоить адрес другого однотипного объекта. В ре- зультате указатель sp будет указывать на другой объект. string s2 sp = &s2; = "some value"; // теперь sp указывает на переменную s2 Для изменения хранимого в указателе адреса, достаточно просто присвоить его, без обращения к значению, на которое он указывает. Фундаментальная концепция. Присвоение значения указателю щи присвоение значения при помощи указателя Для новичков разница между присвоением значения указателю или присвоением зна- чения при помощи указателя может оказаться не до конца понятной. Очень важно уяснить, что если левым операндом является результат обращения к значению, изме- нено будет само значение, адрес которого хранит указатель. Если обращения к значе- нию нет, изменяется сам указатель. Схематически это представлено на рис. 4.1.
Глава 4. Массивы и указатели 145 string s1 ("some value"); string *sp1 = &s1; string s2("another"); string *sp2 = &s2; // присвоение при помощи sp1 // изменилось значение s1 *sp1 = "a new value"; sp1 s1 ► some value s2 ► another s1 ► a new value // присвоение sp1 spl // sp1 указывает на другой объект sp1 = sp2; s2______ > another Puc. 4.1. Присвоение значения указателю и присвое- ние значения при помощи указателя Сравнение указателей и ссылок Хотя и ссылки (reference) и указатели используются для косвенного доступа к другому значению, между ними есть два важных различия. Первое заключается в том, что ссылка всегда относится к объекту: ссылка без объекта — это весьма грубая ошибка. Второе заключается в поведении при присвоении: присвоение значения ссылке изменяет сам объект, а не переприсваивает ссылку другому объекту. Будучи инициализированной, ссылка всегда указывает на тот же объект. Давайте рассмотрим два фрагмента кода. В первом один указатель присваивается другому. int ival = 1024, ival2 = 2048; int *pi = &ival, *pi2 = &ival2; pi = pi2; // теперь pi указывает на ival2 После присвоения указателю pi адреса переменной ival, сам объект ival оста- ется неизменным. Оператор присвоения изменяет значение указателя pi, присваи- вая ему адрес другого объекта. Теперь рассмотрим подобную программу, в которой осуществляется присвоение двух ссылок. &ri2 = ival2; приевоение ival значения ival2 Оператор присвоения изменяет значение переменной ival, на которую указыва- ет ссылка ri, а не саму ссылку. После присвоения, обе ссылки продолжают отно- ситься к своим исходным объектам, а значения объектов становятся одинаковыми. Указатели на указатели Указатели являются самостоятельными объектами в памяти, а следовательно, они имеют адреса, которые можно сохранять в указателях, int ival = 1024; int *pi = &ival; // pi указывает на переменную типа int int **ppi = &pi; // ppi указывает на указатель типа int Здесь приведен пример указателя на указатель. При объявлении указателя на указатель используется двойная звездочка **. Графически эти объекты представле- ны на рис. 4.2.
146 Часть I. Основы PPi Pi ival 1024 Рис. 4.2. Указатель на указатель Как обычно, обращение к значению указателя ppi возвращает объект, адрес ко- торого хранит указатель. В данном случае это указатель на переменную типа int. int *pi2 = *ppi; // ppi указывает на указатель Д,ля обращения к значению переменной ival, необходимо два обращения к зна- чению указателя ppi. cout « "The value of ival\n" « "direct value: " « ival « "\n" « "indirect value: " « *pi « "\n" « "doubly indirect value: " « **ppi « endl; Эта программа отображает значение переменной ival тремя разными способа- ми. Сначала при помощи непосредственного обращения к переменной. Затем при помощи указателя pi типа int, и наконец, в результате двойного обращения к зна- чению указателя ppi. Упражнения раздела 4.2.3 Упражнение 4.14. Напишите код изменяющий значение указателя. Напишите код, изменяющий значение, адрес которого хранит указатель. Упражнение 4.15. Укажите принципиальные различия между указателями и ссылками. Упражнение 4.16. Что выполняет следующий код? int i = 42, j = 1024; int *pl = &i, *p2 = &j; *p2 = *pl * *p2; *pl *= *pl; 4.2.4. Использование указателей для доступа к элементам массива В языке C++ указатели тесно связаны с массивами. В частности, используемое в выражении имя массива автоматически преобразуется в указатель на его первый элемент. int ia[] = {0,2,4,6,8}; int *ip = ia; // ip указывает на ia[0] Если указатель необходимо переместить на другой элемент массива, можно воспользоваться оператором индексирования для поиска необходимого элемента, а затем применить оператор обращения к адресу, чтобы поместить полученный адрес в указатель. ip = &ia[4]; // ip указывает на последний элемент в ia
Глава 4. Массивы и указатели 147 Арифметические операции с указателями Вместо получения адреса значения, возвращаемого в результате обращения к элементу массива по индексу, можно воспользоваться арифметическими операциями с указателями (pointer arithmetic). Арифметические операции с указателями осуще- ствляются аналогично арифметическим операциям с итераторами (раздел 3.4.1, стр. 124) и имеют те же ограничения. При помощи арифметических операций над указателями (добавляя или вычитая целочисленные значения из указателя) можно вычислять указатели на другие элементы массива. ip = ia; // ok: ip указывает на ia[0] int *ip2 = ip + 4; // ok: ip2 указывает на ia[4], II последний элемент массива ia Когда к указателю ip добавляется значение 4, вычисляется новый указатель, ко- торый содержит адрес элемента массива на четыре позиции далее от исходного. В результате добавления (или вычитания) целочисленного значения к указателю получается новый указатель, который содержит адрес элемента, отстоящего от ис- ходного на соответствующее количество позиций вперед (или назад). |Г^7>Ъ|к Арифметические операции над указателями допустимы только тогда, когда исходный и ДзлЙ j получаемый в результате вычислений указатели содержат адреса элементов одного мас- сива и не ВЬ|ХОДЯТ за его пределы. Если существует указатель на объект, можно вычис- лить также и адрес позиции, расположенной непосредственно после него. Для этого дос- таточно добавить к указателю единицу. При условии, что массив ia содержит 4 элемента, добавление к ia числа 10 при- ведет к ошибке. // ошибка: ia содержит только 4 элемента, ia +10 - недопустимый / / адрес int *ip3 = ia + 10; Два указателя можно также вычитать друг из друга, если они содержат адреса элементов, которые принадлежат одному массиву и не выходят за его пределы. ptrdiff_t n = ip2 - ip; // ok: дистанция между указателями В результате получится дистанция между двумя позициями, адреса которых со- держат указатели, при этом каждая единица соответствует одному объекту. Резуль- тат вычитания двух указателей имеет библиотечный mun ptrdif f_t. Подобно типу size_t, тип ptrdif f_t является специфическим для каждой машины типом, определенным в заголовке cstddef. Тип size_t является беззнаковым, а тип ptrdif f_t — знаковым и целочисленным. Различия между этими типами определяются способом их применения: тип size_t используется для хранения размера массива, которому всегда соответству- ют положительные значения. Тип ptrdif f_t является гарантированно большим, чтобы содержать разницу между любыми двумя указателями одного массива, кото- рая вполне может быть отрицательной. Например, если вычесть ip2 из ip, резуль- татом будет - 4. К указателю всегда можно добавить (или вычесть) нуль, что оставляет указатель неизменным. Но интересней всего то, что указатель, содержащий нулевое значение, вполне допустим, и к нему можно добавить нуль. В результате получится другой ука-
148 Часть I. Основы затель, инициализированный нулевым значением. Можно даже вычесть два указателя, содержащие нулевое значение, и получить в результате другой нулевой указатель. Взаимосвязь между обращением к значению и арифметическими операциями с указателями Результат сложения целочисленного значения и указателя тоже является указа- телем. К значению полученного в результате указателя можно обратиться непосред- ственно, без предварительного присвоения другому указателю. int last = *(ia + 4); // ок: инициализировать last числом 8, I/ значением ia[4] Это выражение вычисляет адрес четвертого элемента массива ia и обращается по полученному адресу к его значению. Это эквивалентно записи ia [4 ]. Очень важно заключить в круглые скобки оператор суммы. last = *ia + 4; // ok: last =4, эквивалент ia[O] + 4 Здесь переменной last присваивается сумма значения, полученного при обращении к указателю ia3, и значения 4. Круглые скобки необходимы, поскольку приоритет (precedence) оператора сум- мы ниже, чем у оператора обращения к значению. Более подробная информация о приоритете приведена в разделе 5.10.1 (стр. 193), а пока лишь заметим, что приори- тет определяет, какие из операторов в выражениях выполняются в первую очередь, а какие — во вторую. Оператор обращения к значению имеет более высокий приори- тет, чем оператор суммы. Операторы более высокого приоритета выполняются раньше операторов с более низким приоритетом. Без круглых скобок оператор обращения к значению исполь- зовал бы в качестве операнда массив ia. Впоследствии к полученному в результате значению массива ia будет добавлено значение 4. Используя круглые скобки можно изменить порядок выполнения операторов. Так, выражение (ia + 4) будет рассматриваться как единый операнд. Здесь вы- числяется адрес элемента на четыре позиции больше, чем был у ia. Значение, хра- нящееся по этому новому адресу, и будет возвращено. Индексирование и указатели Как уже упоминалось, при использовании в выражении имени массива, фактиче- ски применяется указатель на первый элемент массива. Этот факт имеет ряд послед- ствий, которые будут рассматриваться по мере необходимости. Одним из последствий является то, что при индексировании массива фактически используется указатель, int ia[] = {0,2,4,б,8}; int i = ia[0]; // ia[0] указывает на первый элемент массива ia Как можно заметить, в выражении ia [0] используется имя массива. Фактически при индексировании массива используется указатель на элемент в массиве. Опера- тор индексирования можно использовать для создания любого указателя, пока он указывает на элемент в массиве. 3 Помните, что имена массивов являются указателями на их первый элемент. — Примеч. ред.
Глава 4. Массивы и указатели 149 int к = р [ - 2]; // ок: р указывает на элемент заданный индексом 2 / / ок: р[1] эквивалентно * (р + 1) , // р[1] - тот же самый элемент, что и ia[3] // ok: р[-2] - тот же самый элемент, что и ia[O] Вычисление указателя на элемент за пределами массива При использовании вектора, функция end () возвращает итератор, который ука- зывает на следующий элемент за пределами вектора. Этот итератор зачастую ис- пользуется в условии выхода из цикла, который перебирает элементы вектора. Ана- логично можно вычислить значение указателя “после конца” (off-the-end). const size_t arr_size = int arr[arr_size] = {1, int *p = arr; int *p2 = p + arr_size; 5; 2, 3, 4, 5}; // ok: p указывает на arr[0] II ok: p2 указывает на элемент за пределами // массива arr. Использовать осторожно, ни // в коем случае не обращаться к значению! В данном случае указатель р инициализируется адресом первого элемента масси- ва arr. Затем вычисляется указатель на один элемент за пределами массива arr. Для этого к значению указателя р добавляется размер массива arr. Таким образом, в результате добавления числа 5 к указателю р получится адрес пятого элемента ти- па int начиная с исходного. Другими словами, указатель, полученный в результате выражения р + 5, указывает на элемент, который должен был бы располагаться за пределами массива arr. Вычислить адрес на один элемент за пределами массива или объекта вполне допустимо. 1 Не допустимо обращаться к значению такого указателя. Не допустимо также вычислять адрес элемента, расположенного больше, чем на один элемент за пределами или перед началом массива. Вычисленный и сохраненный в указателе р2 адрес очень похож на итератор, воз- вращенный функцией end () при операциях с векторами. Такой итератор, на эле- мент расположенный после последнего элемента вектора, нельзя использовать для доступа к значению, но можно применять при сравнении с другим итератором при переборе элементов вектора. Аналогично, значение, полученное при вычислении указателя р2, применимо только для сравнения со значениями других указателей или как операнд в арифметических операциях с указателями. Если попытаться обра- титься к значению, адрес которого содержит указатель р2, вероятнее всего, будет получено некое случайное значение. Большинство компиляторов вполне нормально интерпретирует результат обращения к значению указателя р2 как число типа int, использовав для него случайный набор битов, расположенных в памяти после по- следнего элемента массива arr. Отображение элементов массива Теперь все готово для создания программы, использующей указатели. const size_t arr_sz = 5; int int_arr[arr_sz] = { 0, 1, 2, 3, 4 }; // pbegin указывает на первый element, // a pend - на следующий после последнего
150 Часть I. Основы for (int *pbegin = int_arr, *pend = int_arr + arr_sz; pbegin != pend; ++pbegin) cout « *pbegin << ' '; // отобразить текущий элемент В этой программе использована еще одна возможность цикла for: опера тор_ инициализации (раздел 1.4.2, стр. 37), которая позволяет определить несколько пе- ременных одинакового типа. В данном случае определено два указателя типа int по имени pbegin и pend. Эти указатели используются для перебора массива. Подобно другим встроенным типам, массивы не имеют функций-членов. Следовательно, функций-членов begin () и end () массивы также не имеют. Вместо этого приходится самостоятель- но позиционировать указатели так, чтобы обозначить первый элемент и следующий после последнего. Так и было сделано при инициализации этих двух указателей. Указатель pbegin инициализирован адресом первого элемента массива int_arr, а указатель pend — адресом несуществующего элемента массива, непосредственно после последнего, как показано на рис. 4.3. остальная память pend pbegin Рис. 43. Адреса указателей pbegin и pend Указатель pend служит ограничителем цикла for, останавливая перебор эле- ментов. При каждой итерации цикл for увеличивает указатель pbegin на едини- цу, организуя его переход на следующий элемент. На первом цикле указатель pbegin содержит адрес первого элемента, на втором — второго и т.д. После обра- ботки последнего элемента массива, значение указателя pbegin увеличивается еще раз и становится равным pend. Таким образом организован перебор всех эле- ментов массива. Указатели — итераторы для массивов Внимательный читатель, вероятно, заметил, что предыдущая программа очень похожа на программу, перебирающую и отображающую содержимое вектора строк на стр. 123. // аналогичный цикл, использующий итератор для обнуления всех // элементов вектора ivec for (vector<int>::iterator iter = ivec.begin(); iter != ivec.endO; ++iter) *iter =0; // обнулить элемент, на который указывает итератор iter В цикле этой программы итератор использован так же, как и указатель в про- грамме, выводящей содержимое массива. Это сходство — отнюдь не совпадение. Фактически массив встроенного типа обладает большинством свойств библиотечно- го контейнера, а используемые с массивами указатели очень похожи на итераторы. Более подробная информация о контейнерах и итераторах приведена в части II, “Контейнеры и алгоритмы”.
Глава 4. Массивы и указатели 151 Упражнения раздела 4.2.4 Упражнение 4.17. Что выполняет следующий оператор, если указатели pi и р2 содержат адреса элементов одного массива? pl += р2 - pl; Существуют ли такие значения указателей pi или р2, которые сделают этот код недопустимым? Упражнение 4.18. Напишите программу, которая использует указатели для обнуления элементов массива целых чисел. 4.2.5. Указатели и спецификатор const Существует два способа применения спецификатора const (раздел 2.4, стр. 78) с указателями: указатель может содержать адрес константного объекта и указатель сам может быть константой. В этом разделе обсуждаются оба вида указателей. Указатель на константный объект Рассматриваемые до сих пор указатели применялись для изменения значений объектов, на которые они указывают. Но если указатель содержит адрес константно- го объекта, ему следует запретить изменять сам объект. Для решения этой проблемы в языке C++ применяются указатели на константу (pointer to const). const double *cptr; // cptr может указывать на переменную типа // double, которая является константой Здесь cptr — это указатель на объект типа const double. Спецификатор const относится к типу объекта, на который указывает указатель cptr, а не к само- му указателю cptr непосредственно. То есть сам указатель cptr константным не является. Инициализировать его не обязательно, поскольку значение такому указа- телю можно присвоить в любой момент. Но вот что сделать нельзя, так это исполь- зовать указатель cptr для изменения значения, на которое он указывает. *cptr =42; // ошибка: *cptr должен быть константой Попытка присвоения адреса константного объекта обычному неконстантному указателю также приведет к ошибке во время компиляции. const double pi = 3.14; double *ptr = &pi; // ошибка: ptr обычный указатель const double *cptr = &pi; // ok: cptr указатель на константу Для хранения адреса константного объекта нельзя использовать указатель типа void (раздел 4.2.2, стр. 143). Вместо него следует использовать mun const void, способный содержать адрес константного объекта. const int universe = 42; const void *cpv = &universe; // ok: cpv является константой void *pv = ^universe; // ошибка: universe - константа Указателю на константный объект может быть присвоен адрес неконстантного объекта. double dval = 3.14; // переменная dval типа double. Ее значение cptr = &dval; // может быть изменено // ок: но изменить значение переменной dval / / при помощи указателя cptr нельзя
152 Часть I. Основы Хотя переменная dval не константна, любая попытка изменения ее значения при помощи указателя cptr приводит к ошибке во время компиляции, ведь при объяв- лении указателя cptr было указано, что он не будет изменять значение, на которое указывает. Тот факт, что это все же случилось, свидетельствует об ошибке. Для изменения объекта нельзя использовать указатель на константу, хранящий его адрес. 1 Но если указатель содержит адрес неконстантного объекта, с ним можно осуществлять некоторые действия, которые могут изменить объект. Возможность изменения значения, адрес которого содержит константный указа- тель, требует дополнительного рассмотрения. dval = 3.14159; // dvalis - не константа *cptr = 3.14159; // ошибка: cptr - указатель на константу double *ptr - &dval; // ok: ptг указывает на непостоянную II переменную типа double *ptr = 2.72; // ok: pt г является обычным указателем cout « *cptr; // ok: отображает 2.72 В данном случае cptr определен как указатель на константу, но фактически ука- зывает на неконстантный объект. Несмотря на то, что объект, адрес которого содер- жит указатель, не константен, использовать указатель cptr для изменения значения этого объекта нельзя. По существу, для указателя cptr не имеет значения, является ли объект, на который он указывает, константой или нет. Он считает константами все объекты, адрес которых хранит. Когда указатель на константу содеряйгг адрес неконстанты, значение объекта могло бы быть изменено: в конце концов, ведь это значение не является константой. Значение может быть переприсвоено либо непосредственно, либо, как в данном случае косвенно при помощи другого, обычного неконстантного указателя. Важно помнить, что нет ника- кой гарантии неизменности объекта, адрес которого хранит указатель на константу. jAhi Указатели на константу имеет смысл рассматривать как указатели, которые “пола- гают”, что указывают на константу. В реальных программах указатели на константу, как правило, используют для формальных параметров функций. Определение параметра как указателя на кон- станту является гарантией того, что передаваемый в функцию фактический объект не будет изменен при помощи данного параметра. Константный указатель Кроме указателей на константу, существуют также константные указатели (const pointer), т.е. указатели, содержимое которых изменять нельзя. int errNumb = 0; int *const curErr = &errNumb; // curErr - константный указатель Читая это определение справа налево, можно заметить, что curErr является константным указателем на объект типа int. Подобно любой другой константе, из- менить значение такого указателя нельзя, т.е. ему нельзя присвоить адрес другого объекта. Любая попытка присвоения значения константному указателю (даже того же самого) приведет к ошибке во время компиляции. curErr = curErr; // ошибка: curErr - константный указатель
Глава 4. Массивы и указатели 153 Как и любую другую константу, константный указатель следует инициализиро- вать при создании. Тот факт, что указатель сам является константой, вовсе не означает, что его нель- зя использовать для изменения значения, на которое он указывает. Возможность из- менять значение, адрес которого содержит указатель, зависит исключительно от ти- па объекта, на который он указывает. Например, указатель curErr содержит адрес обычной неконстантной переменной типа int. Поэтому указатель curErr вполне можно использовать для изменения значения переменной errNumb. if (*curErr) { errorHandler(); *curErr = 0; // ok: обнулить значение объекта, адрес которого // содержит указатель curErr } Константный указатель на константный объект Можно также создать константный указатель на константный объект, const double pi = 3.14159; // указатель pi_р является константным и указывает на // константный объект pi_p tr is const and points to a const object const double *const pi_ptr - &pi; В данном случае не может быть изменен ни адрес, хранимый в указателе pi_ptr, ни значение объекта, на который он указывает. Это определение можно прочитать справа налево так: pi__ptr — константный указатель на объект типа double, кото- рый является константой. Указатели и определения типов Использование указателей при определении типов (раздел 2.6, стр. 83) зачастую приводит к удивительным результатам. Подобные вопросы возникают неизбежно. Чем, например, является следующий тип cstr? typedef string *pstring; const pstring cstr; Простой ответ — указателем на константу pstring. Более серьезный вопрос: ка- ков тип объекта, адрес которого содержит указатель на константу pstring? Боль- шинство программистов ответят, что фактическим типом будет следующий. const string *cstr; // неверная интерпретация const pstring cstr To есть константа pstring является указателем на константу типа string, но это неправильно. Будет ошибкой полагать, что ключевое слово typedef осуществляет лишь тек- стовую подстановку. При объявлении константы типа pstring, модификатор const изменит тип pstring, который является указателем. Следовательно, в этом опреде- лении cstr объявлен как константный указатель на тип string. Предыдущее оп- ределение эквивалентно следующему. // cstr является константным указателем на тип string string *const cstr; // эквивалент const pstring cstr
154 Часть I. Основы Совет. Сложные объявления типов констант Частично проблемы при чтении объявлений констант возникают из за того, что клю- чевое слово const может располагаться до или после указания типа. string const si; // si и s2 имеют одинаковый тип, const string s2; // и обе строки являются константами Тот факт, что ключевое слово const при записи определения константы с использо- ванием ключевого слова typedef может предшествовать указанию типа, способен ввести в заблуждение относительно фактически определяемого типа. string s; typedef string *pstring; const pstring cstrl = &s; // этот способ записи малопонятен pstring const cstr2 = &s; // во всех трех объявлениях создается // одинаковый тип. Все они являются string *const cstr3 = &s; // константными указателями на тип string Расположение ключевого слова const после имени pstring упрощает понимание при чтении объявления справа налево. Теперь вполне очевидно, что cstr2 является константой типа pstring, который в свою очередь является константным указателем на тип string. К сожалению, большинство читателей программ на языке C++ ожидает увидеть клю- чевое слово const перед типом. Поэтому имеет смысл помещать ключевое слово const вначале, в силу сложившихся традиций. Но чтобы объявление стало более по- нятным, ключевое слово const можно поместить после типа 4.3. Символьная строка в стиле С Хотя язык C++ поддерживает строки в стиле С, использовать их в программах C++ не следует. Строки в стиле С — на удивление богатый источник разнообразных ошибок и наиболее распространенная причина проблем защиты. В разделе 2.2 (стр. 63), при рассмотрении строковых литералов, утверждалось, что типом строкового литерала является массив константных символов. Рассматривая эту тему подробнее, можно сделать вывод, что типом строкового литерала являет- ся const char. Строковый литерал — это экземпляр более общей конструкции, ко- торую язык C++ унаследовал от языка С: символьной строки в стиле С (C-style character string). Фактически строка в стиле С не является типом данных языка С или C++, на самом деле она является массивом символов с нулевым символом в конце. char cal [] = { ' С ' , ' + ’ '+1}; // без нулевого символа в конце // не может быть строкой в стиле С '\0'}; // нуль указан явно II завершающий нуль добавлен автоматически // завершающий нуль добавлен автоматически / / указатель на первый элемент массива, /I но не на строку в стиле С I/ указатель на первый элемент символьного // массива с нулевым символом в конце
Глава 4. Массивы и указатели 155 Ни cal ни cpl не являются строками в стиле С: cal является символьным мас- сивом, но без нулевого символа в конце. Следовательно, указатель cpl, содержит адрес массива cal, не имеющего нулевого символа в конце. В остальных случаях объявлены строки в стиле С (напомним, что имя массива рассматривается как указа- тель на первый элемент массива). Таким образом, са2 и саЗ являются указателями на первые элементы соответствующих массивов. Упражнения раздела 4.3 Упражнение 4.19. Объясните смысл следующих пяти определений. Укажите, какие из них недо- пустимы, (a) int i; (b) const int ic; (c) const int *pic; (d) int *const cpi; (e) const int *const epic; Упражнение 4.20. Какие из следующих инициализаций недопустимы? Объясните, почему. (a) int i = -1; (b) const int ic = i; (c) const int *pic = &ic; (d) int *const cpi = &ic; (e) const int *const epic = &ic; Упражнение 4.21. На основании определения предыдущего упражнения укажите, какие из сле- дующих присвоений являются недопустимыми? Объясните, почему. (d) pic = epic; (е) epic = &ic; (f) ic = *cpic; Использование строк в стиле С Манипулирование строками в стиле С осуществляется при помощи константных указателей на тип char. Как правило, для перебора строки в стиле С используют арифметические операции над указателями. При этом указатель увеличивают и про- веряют, не достиг ли он завершающего нулевого символа. const char *ср = "some value"; while (*cp) { // место, где с *ср осуществляются действия ++ср; В условии цикла while осуществляется обращение к значению указателя ср типа const char. Полученный в результате символ проверяется как логическое значение true или false. Значению true соответствует любой символ, за ис- ключением нулевого. Таким образом, цикл продолжается до тех пор, пока не встретится нулевой символ, завершающий массив, адрес которого содержит указа- тель ср. В теле цикла while осуществляются необходимые действия, а затем про- исходит приращение указателя ср, в результате чего он перемещается на следую- щий символ в массиве.
156 Часть I. Основы Этот цикл приведет к серьезной ошибке, если в конце массива, адрес которого содер- жит указатель ср, нет нулевого символа. В этом случае цикл будет читать символы до тех пор, пока где-нибудь в памяти не встретится нулевой символ. Строковые функции из библиотеки С Стандартная библиотека языка С предоставляет набор функций для работы со строками в стиле С (табл. 4.1). Чтобы воспользоваться этими функциями, необхо- димо подключить соответствующий файл заголовка. #include <cstring> Это версия для языка C++ заголовка string. h библиотеки С. Эти функции никак не проверяют передаваемые им строковые параметры. Переданный этим функциям указатель (указатели) должен быть ненулевым и содержать адрес начального символа массива с нулевым символом в конце. Неко- торые из этих функций записывают данные в переданную им строку. Этот результи- рующий массив должен быть достаточно большим, чтобы содержать все возвращае- мые функцией символы. Проверять размер выходной строки программисту придет- ся самостоятельно. Таблица 4.1. Функции для символьных строк в стиле С strlen(s) strcmp(sl, s2) strcat(si, s2) strcpy(sl, s2) strncat(sl, s2, n) stmcpy(sl, s2, n) Возвращает длину строки s без учета нулевого символа Проверяет равенство строк si и s2. Возвращает о, если si == s2, положи- тельное значение, если si > s2, и отрицательное значение, если si < s2 Добавляет строку s2 к si. Результат возвращает в строку si Копирует строку s2 в строку si. Результат возвращает в строку si Добавляет п символов из строки s2 в строку si. Результат возвращает в строку s1 Копирует п символов из строки s2 в строку si. Результат возвращает в строку s1 При сравнении строк библиотечного типа используются обычные операторы от- ношения. Эти же операторы можно использовать и для сравнения указателей на строки в стиле С, но результат будет совершенно иной, ведь фактически сравнива- ются адреса строк, а не их содержимое. if (cpl < ср2) // сравниваются адреса, а не значения Предположим, что указатели cpl и ср2 содержат адреса элементов одного массива. Теперь адрес в указателе cpl можно сравнивать с адресом в указателе ср2. Если ука- затели не относятся к одному массиву, результат их сравнения не имеет смысла.
Глава 4. Массивы и указатели 157 Для сравнения строк используется функция st гетр (), а возвращаемый ей ре- зультат сравнения можно интерпретировать. const char *cpl = "A string example"; const char *cp2 = "A different string"; int i = strcmp(cpl, cp2); i = strcmp(cp2, cpl); i = strcmp(cpl, cpl); // i положительное / / i отрицательное // i содержит нуль Функция stremp () может возвращать три вида значений: 0, если строки равны; положительное или отрицательное число, в зависимости от того, больше первая строка второй или меньше. Никогда не забывайте о нулевом завершающем символе При использовании библиотечных функций строк в стиле С, ни в коем случае нельзя забывать о нулевом символе в конце. char са[] = {'С, ' + // без нулевого символа в конце cout « strlen(ca) << endl; // ошибка: са не имеет нулевого // символа в конце В данном случае са — это массив символов, но без нулевого символа в конце. Ре- зультат его применения непредсказуем. Функция strlenO полагает, что сможет найти нулевой символ в конце переданного ей аргумента. Вероятней всего, в резуль- тате приведенного выше обращения функция strlen () будет перебирать память за пределами массива с а до тех пор, пока не встретится нулевой символ. В любом слу- чае, функция strlen () возвратит неправильное значение. За размер принимающей строки отвечает вызывающая функция Массив, передаваемый как первый аргумент функциям strcat () и strepy (), должен быть достаточно большим, чтобы содержать созданную ими строку. Приве- денный ниже код хоть и вполне обычен, но потенциально является причиной серь- езной ошибки. // Опасность: что случится, если ошибиться при расчете размера II массива largeStr? char largeStr[16 + 18 + 2]; // должно содержать cpl, пробел и ср2 strepy (largeStr, cpl); // копирует cpl в largeStr strcat (largeStr, " "); // добавляет пробел в конец largeStr strcat (largeStr, cp2); // добавляет cp2 в largeStr // отображает полученную в результате новую строку cout « largeStr « endl; Проблема в том, что при вычислении размера, необходимого для массива largeStr, легко ошибиться. Аналогично, если изменить размеры строк cpl или ср2, вычисленный ранее размер массива largeStr окажется неверен. К сожале- нию, фрагменты кода, подобные приведенному выше, широко распространены и за- частую приводят к серьезным проблемам. При использовании строк в стиле С применяйте функцию strn() При использовании строк в стиле С, как правило, предпочтительней использо- вать функции strncat () и strnepy (), а не функции strcat () и strepy (). char largeStr[16 + 18 + 2]; // должно содержать cpl, пробел и ср2 strnepy (largeStr, cpl, 17); // размер для копии, включая нуль
158 Часть I. Основы strncat (largeStr, " ", 2); // педантизм, но привычка хорошая strncat (largeStr, ср2 , 19); // добавляет более 18 символов, плюс // нулевой символ Для этих версий характерно правильное вычисление значения, управляющего количеством копируемых символов. В частности, при копировании или конкатена- ции необходимо всегда учитывать нулевой символ. Поэтому при каждом обращении к массиву largeStr необходимо выделять пространство для нулевого символа. Да- вайте подробно рассмотрим каждое из этих обращений. Обращение к функции strncpyO предполагает копирование 17 символов из массива cpl плюс нулевой символ. Остальные элементы предназначены для ну- левого символа, необходимого для правильного завершения массива largeStr. После обращения к функции strncpy (), вызов функции strlen () для масси- ва largeStr укажет, что его размер составляет 16 символов. Не забывайте, что функция strlen () вычисляет количество символов строки в стиле С без учета нулевого символа. Обращение к функции strncat О предполагает копирование двух символов: пробела и нулевого символа, завершающего строковый литерал. После этого об- ращения функция strlen () вернет для массива largeStr значение 17. Нуле- вой символ, который завершал массив largeStr, оказался перезаписан добав- ленным пробелом, а новый нулевой символ добавлен после него. При добавлении массива ср2 во втором обращении, также требуется скопиро- вать все символы массива ср 2, включая нулевой символ. После этого обращения функция strlen () должна бы вернуть для массива largeStr значение 35: 16 символов массива cpl, 18 — ср2 и 1 для пробела, разделяющего две строки. Однако размер массива largeStr составляет 36 символов. Эти операции безопасней, чем более простые версии, которым не передают раз- мер в качестве аргумента, если размер рассчитан правильно. При попытке скопиро- вать или объединить в строке больше символов, чем помещается в выходной массив, произойдет переполнение массива. Если копируемая или конкатенируемая строка больше указанного размера, новая версия окажется усечена. Усечение безопасней переполнения массива, но тоже является ошибкой. По возможности используйте библиотечный тип string Ни одна из этих проблем не имеет значения, если используются строки библио- теки C++. string largeStr = largeStr += " " ; largeStr += cp2; cpl; // инициализировать largeStr как копию cpl II добавить пробел в конец largeStr /! добавить ср2 в конец largeStr Здесь управление памятью при изменении размера любых строк, осуществляется библиотекой, а не разработчиком. Для большинства приложений безопасней и эффективней использовать библиотечные строки, а не строки в стиле С.
Глава 4. Массивы и указатели 159 Упражнения раздела 4.3 Упражнение 4.22. Объясните различия между двумя следующими циклами while. const char *ср = "hello"; int ent; while (cp) { ++cnt; ++cp; } while (*cp) { ++cnt; ++cp; } Упражнение 4.23. Что выполняет следующая программа? const char са[] = {’h', 'е', '1', '1', 'о'}; const char *ср = са; while (*ср) { cout « *ср « endl; ++ср; ) Упражнение 4.24. Объясните различия между функциями strepy () и strnepy (). Каковы преимущества и недостатки каждой из них? Упражнение 4.25. Напишите программу, которая сравнивает две строки. Напишите программу, которая сравнивает две символьные строки в стиле С. Упражнение 4.26. Напишите программу, которая читает две строки типа string со стандартно- го устройства ввода. Можно ли написать программу, которая читает со стандартного устройства ввода символьные строки в стиле С? 4.3.1. Динамическое создание массивов Переменная типа массива имеет три важных ограничения: она имеет фиксиро- ванный размер, этот размер должен быть известен на момент компиляции, и нако- нец, массив существует только до конца блока, в котором он был определен. Для ре- альных программ такие ограничения, как правило, неприемлемы, в них требуется создавать массивы динамически (dynamically), во время выполнения. Хотя все мас- сивы имеют фиксированный размер, у динамически создаваемых массивов он неиз- вестен на момент компиляции, поскольку он должен быть вычислен (и обычно вы- числяется) во время выполнения. В отличие от переменной типа массива, динамиче- ский массив продолжает существовать до тех пор, пока он не будет освобожден явно. При запуске каждая программа получает некий объем памяти, пул (pool), кото- рый она может использовать для хранения объектов, созданных в динамически рас- пределяемой памяти (dynamically allocated object). Этот пул доступной памяти на- зывают также динамической памятью (free store) программы или распределяемой памятью (heap). Для распределения (allocate), или резервирования, области в дина- мической памяти программы на языке С использовали функцию та 11 ос (), а для ее освобождения (free) — функцию free (). В языке C++ для этого используются опе- раторы new и delete. Определение динамического массива При определении переменной типа массива, указывают тип, имя и размерность, а при создании динамического массива определяют тип и размер, но не указывают его имя. Вместо него оператор new возвращает указатель на первый элемент в только что созданном массиве.
160 Часть I. Основы int *pia = new int [10]; // массив из 10 неинициализированных // целых чисел В данном случае оператор new создает массив из десяти целых чисел и возвраща- ет указатель на первый элемент в этом массиве, который и используется для ини- циализации указателя pi а. Оператор new получает тип и (необязательно) размерность массива внутри квад- ратных скобок. Размерность может быть указана выражением любой сложности. При создании массива, оператор new возвращает указатель на его первый элемент. В динамической памяти создаются неименованные объекты, обратиться к которым можно лишь косвенно, при помощи адреса. Инициализация динамического массива Для инициализации каждого элемента, при создании массива из объектов класса, используется их стандартный конструктор (раздел 2.3.4, стр. 73). Если массив со- держит элементы встроенного типа, они не инициализируются. string *psa = new string [10]; // массив из 10 пустых строк int *pia = new int [10]; // массив из 10 неинициализированных II целых чисел В каждом из этих выражений создается массив из 10 объектов. В первом случае — это строки. После резервирования области памяти, предназначенной для хранения объектов, у каждого из элементов массива срабатывает стандартный конструктор класса string. Во втором случае элементы имеют встроенный тип. Память для де- сяти целых чисел выделяется, но сами элементы не инициализируются. В качестве альтернативы, элементы можно инициализировать значением, распо- ложив после размера массива пустую пару круглых скобок (раздел 3.3.1, стр. 116). int *pia2 = new int [10](); // массив из 10 целых чисел Круглые скобки — это указание для компилятора инициализировать массив зна- чением, которым в данном случае является 0. Элементы динамического массива могут быть инициализированы только стандартным значением для типа элемента. Элементы не могут быть инициализированы индивидуаль- ными значениями, как это делается для элементов обычного массива. Динамические массивы константных объектов Если в динамической памяти создается массив константных объектов встроенно- го типа, их следует инициализировать, поскольку впоследствии нельзя будет изме- нить их значения. Единственный способ инициализации таких элементов подразу- мевает применение инициализирующего значения массива. // ошибка: неинициализированный константный массив const int *pci_bad = new const int[100]; // ok: использование для константного массива I/ инициализирующего значения const int *pci_ok = new const int[100](); Можно создать константный массив, элементы которого являются объектами класса, обладающего стандартным конструктором.
Глава 4. Массивы и указатели 161 // ок: массив из 100 пустых строк const string *pcs = new const string[100]; В данном случае для инициализации элементов массива использован стандарт- ный конструктор. Безусловно, как только будут созданы элементы, изменить их окажется невоз- можно, а следовательно, такие массивы не очень полезны. Создать пустой динамический массив вполне возможно Как правило, создавать массивы динамически приходится потому, что размер массива на момент компиляции неизвестен. Чтобы выяснить размер массива, а затем создать и перебрать его элементы, можно применить следующий код. size_t n = get_size(); // функция get_size() возвращает количество / / необходимых элементов int* р = new int[n]; for (int* q = p; q != p + n; ++q) / * обработка элементов массива * /; Интересный вопрос: что произойдет, если функция get_size () возвратит зна- чение 0? Не произойдет ничего страшного, код сработает прекрасно. Язык С++ вполне допускает обращение к оператору new для создания массива нулевого разме- ра, даже при том, что создать обычный массив нулевого размера невозможно. char arr[0]; // ошибка: нельзя определить массив нулевой длины char *ср = new char[0]; // ok: но к значению ср нельзя обратиться Оператор new, используемый для создания массива нулевого размера, возвраща- ет вполне допустимый, отличный от нуля указатель. Этот указатель будет отличать- ся от любого другого указателя, возвращенного оператором new, но обратиться к его значению нельзя, поскольку он не указывает ни на какой элемент. Однако такой указатель можно сравнивать, а следовательно, он применим в цикле, подобном при- веденному выше. К такому указателю вполне допустимо также добавлять (или вы- читать) нуль, а также вычитать указатель из себя самого (в результате все равно по- лучится нуль). В таком гипотетическом цикле, если обращение к функции get_size () возвра- тит значение 0, выполнение оператора new к ошибке не приведет. Однако поскольку массив пуст, указатель р не будет содержать адреса элемента. Поскольку переменная п содержит нуль, цикл for сравнит указатели q и р, а эти указатели равны, ведь ука- затель q был инициализирован значением указателя р, в результате условие цикла for не выполняется, а тело игнорируется. Освобождение динамической памяти Выделенную область памяти в конечном счете приходится освобождать. В про- тивном случае свободная память постепенно окажется исчерпана. Когда массив больше не нужен, занимаемую им область следует освободить явно и вернуть ее в пул в динамической памяти. Для этого к указателю, содержащему адрес массива, применяется оператор delete [ ]. delete [] pia; Это выражение освобождает динамически распределяемую память, занимаемую массивом, адрес которого содержит указатель pia. Пара пустых квадратных скобок
162 Часть I. Основы после ключевого слова delete необходима, она означает, что указатель содержит адрес массива, а не одного элемента в динамической памяти. Если пропустить пару пустых квадратных скобок, произойдет серьезная ошибка, кото- рую компилятор, вероятнее всего, не заметит, а следовательно, она проявится во вре- мя выполнения. Наименее серьезным последствием пропуска пары квадратных скобок при осво- бождении массива, будет освобождение во время выполнения не всей занятой мас- сивом памяти, что приведет к ее утечке (memory leak). Однако на некоторых систе- мах или при использовании элементов некоторых типов возможны гораздо более серьезные проблемы. Поэтому при удалении указателей на массивы очень важно не забывать о квадратных скобках. Различие между строками в стиле С и строками библиотечного типа string Две следующие программы иллюстрируют различия в использовании символьных строк в стиле С и строк библиотечного типа string языка C++. Версия с использова- нием типа string короче, проще и менее подвержена ошибкам. // реализация с использованием символьных строк в стиле С const char *рс = "a very long literal string"; const size_t len = strlen(pc +1 ); // размер для размещения // проверка эффективности размещения в памяти и копирования строк for (size_t ix = 0; ix != 1000000; ++ix) { char *pc2 = new char[len + 1]; // выделить область strcpy(pc2, pc); // создать копию if (strcmp(pc2, pc)) // использовать новую строку delete [] рс2; // не делать ничего // освободить память // реализация с использованием типа string string str("a very long literal string"); // проверка эффективности размещения e памяти и копирования строк for (int ix = 0; ix != 1000000; ++ix) { string str2 = str; // создать копию, размещается автоматически if (str != str2) // использовать новую строку // не делать ничего // str2 освобождается автоматически Более подробно эти программы рассматриваются в упражнениях раздела 4.3.1 (стр. 163). Использование динамических массивов Наиболее распространенной причиной использования динамических массивов является отсутствие сведений о его размере на момент компиляции. Например, во время выполнения программы указатели типа char зачастую используют для доступа
Глава 4. Массивы и указатели 163 к нескольким строкам в стиле С. Используемая для хранения различных строк па- мять обычно резервируется динамически (во время выполнения программы) на ос- новании длины сохраняемой строки. Этот подход значительно надежней создания массива фиксированного размера. В случае правильного вычисления необходимого размера, во время выполнения можно больше не беспокоиться о том, что строка пе- реполнит массив заранее заданного размера. Предположим, существуют следующие строки в стиле С. const char *noerr = "success"; const char *errl89 - "Error: a function declaration must " "specify a function return type!"; Во время выполнения может возникнуть необходимость скопировать одну из этих строк в новый символьный массив. Во время выполнения его размер можно вычислить следующим образом. const char *errorTxt; if (errorFound) errorTxt = errl89; else errorTxt = noerr; // не забывайте прибавить 1 для нулевого завершающего символа int dimension - strlen(errorTxt) + 1; char *errMsg = new char[dimension]; // скопировать текст сообщения об ошибке в errMsg strnepy(errMsg, errorTxt, dimension); Напомним, что функция strlen () возвращает длину строки без учета нулевого символа. Поэтому к возвращаемой ей длине строки следует добавить единицу соот- ветствующую завершающему нулевому символу. Упражнения раздела 4.3.1 Упражнение 4.27. Предположим, что указатель ра был определен в следующем выражении с ис- пользованием оператора new. Как удалить указатель ра? int *ра = new int[10]; Упражнение 4.28. Напишите программу, читающую со стандартного устройства ввода значения типа int и создающую из них вектор. Создайте массив того же размера, что и вектор, а затем скопируйте элементы вектора в массив. Упражнение 4.29. Вернемся к двум фрагментам кода, представленным на стр. 162. Объясните, что они делают? Почему, как правило, реализация с использованием класса string выполняется значительно бы- стрее, чем с использованием функций для строк в стиле С. Относительные значения среднего времени выполнения этих фрагментов на компьютере примерно пятилетней давности следующие: user 0.47 # класс string user 2.55 # символьная строка в стиле С Ожидаем ли этот результат? Как его объяснить? Упражнение 4.30. Напишите программу, конкатенирующую два литерала из строк в стиле С и по- мещающую результат в строку стиля С. Напишите программу, конкатенирующую две строки биб- лиотечного типа string, имеющие те же значения, что и литералы из предыдущей программы.
164 Часть I. Основы 4.3.2. Взаимодействие со старым кодом Существует достаточно много программ на языке C++, которые были написаны до появления стандартной библиотеки, а следовательно, типы vector и string в них еще не используются. Кроме того, довольно много программ C++ взаимодейст- вуют с уже существующими программами на языке С, которые не могут использо- вать библиотеку C++. Таким образом, не так уж и редки ситуации, когда программа, написанная на современном языке C++, вынуждена взаимодействовать с кодом, ис- пользующим массивы и (или) символьные строки в стиле С. Стандартная библиоте- ка предоставляет вспомогательные средства, облегчающие решение этих задач. Совместное использование строк типа string и строк в стиле С Как упоминалось в разделе 3.2.1 (стр. 104), переменную типа string можно инициализировать строковым литералом. string st3( "Hello World"); // st3 содержит Hello World Поскольку строка в стиле С имеет практически тот же тип, что и строковый лите- рал с нулевым символом в конце, ее можно использовать везде, где применим стро- ковый литерал. Строку типа string можно инициализировать строкой в стиле С или присво- ить ее. Строку в стиле С можно использовать в качестве операнда при сложении строк типа string, а также как правый операнд в составном операторе присвоения. Возможности для обратных действий не предоставляются, т.е. не существует простого способа использования строки библиотечного типа string, когда необхо- дима строка в стиле С. Например, невозможно инициализировать указатель типа char строкой типа string. char *str = st2; // ошибка во время компиляции Для этого можно воспользоваться функцией c_str () класса string. char *str = st2.c_str(); // почти правильно, но не совсем Имя функции c_str () означает, что она возвращает символьную строку в стиле С. Буквально оно означает представление строки в стиле С, т.е. возвращение указа- теля на начало символьного массива с нулевым символом в конце, который содер- жит те же самые данные, что и строка типа string. Такая инициализация неверна потому, что функция c_str () возвратит указа- тель на массив констант типа char, а это предотвращает возможность изменения элементов массива. Правильная инициализация представлена ниже. const char *str = st2.c_str(); // ok Возвращенный функцией c_str() массив не является гарантированно допустимым. j Любое последующее действие со строкой st2, которое способно изменить ее значение, iw может сделать массив недопустимым. Если в программе необходим последующий доступ к данным, возвращенный функцией c_str () массив следует скопировать.
Глава 4. Массивы и указатели 165 Использование массива для инициализации вектора Как утверждалось ранее (стр. 136), невозможно инициализировать массив значе- нием другого массива. Вместо этого, значения элементов первого массива следует явно скопировать в элементы второго. Исходя из этого может показаться достаточно странной возможность использования массива для инициализации вектора, хотя са- ма форма инициализации на первый взгляд не столь проста. Для инициализации вектора значениями массива, необходимо определить адрес его первого элемента и элемента, следующего за последним. const size_t arr_size = б; int int_arr[arr_size] = {0, 1, 2, 3, 4, 5}; // вектор ivec содержит 6 элементов, каждый из которых является II копией соответствующего элемента массива int_arr vector<int> ivec(int_arr, int_arr + arr_size); Два указателя, переданные вектору ivec при инициализации, задают диапазон его значений. Второй указатель содержит адрес элемента, следующего за последним копи- руемым. Диапазон отмеченных элементов может также представлять часть массива. // скопировать 3 элемента: vector<int> ivec(int_arr + int_arr[l], int_arr[2] и int_arr[3] 1, int_arr + 4); В результате вектор ivec инициализируется тремя элементами, значения кото- рых являются копиями значений элементов массива int_arr [1], int_arr [2] и int_arr[3]. Упражнения раздела 4.3.2 Упражнение 4.31. Напишите программу, которая читает со стандартного устройства ввода строку в символьный массив. Объясните, как программа справляется со вводимыми данными переменного размера. Проверьте программу введя строку данных, размер которой превышает размер массива. Упражнение 4.32. Напишите программу, инициализирующую вектор значениями массива це- лых чисел. Упражнение 4.33. Напишите программу, копирующую значения целочисленного вектора в мас- сив целых чисел. Упражнение 4.34. Напишите программу, читающую строки в вектор. Скопируйте этот вектор в массив указателей на тип char. Для каждого элемента вектора создайте новый символьный массив и скопируйте данные из элемента вектора в этот символьный массив. Затем поместите указатель на символьный массив в массив символьных указателей. Упражнение 4.35. Отобразите содержимое вектора и массива, созданного в предыдущем упраж- нении. После отображения не забудьте удалить символьные массивы. 4.4. Многомерные массивы Строго говоря, никаких многомерных массивов (multidimensioned array) в языке C++ нет. Фактически многомерный массив является массивом массивов. I/ массив из 3 элементов, каждый из которых // содержит 4 элемента типа int int ia [3] [4] ; При использовании многомерных массивов следует учитывать этот факт.
166 Часть I. Основы Массив, элементами которого являются массивы, называют двумерным. Каждая размерность обладает собственным индексом. ia[2] [3] // доступ к последнему элементу массива в последнем ряду Первую размерность зачастую называют рядом (row), а вторую — столбцом (column). В языке C++ нет никакого ограничения на количество используемых ин- дексов (размерностей). То есть вполне можно создать массив, чьи элементы являют- ся массивами элементов, которые являются массивами и т.д. Инициализация элементов многомерного массива Подобно любым массивам, элементы многомерного массива можно инициализи- ровать списком значений в фигурных скобках для каждого ряда. int ia [3] [4] = { /* 3 элемента, каждый является массивом из 4 элементов */ {О, 1, 2, 3} , /* инициализирующие значения для ряда 0 */ {4, 5, 6, 7} , /* инициализирующие значения для ряда 1 */ {8, 9, 10, 11} /* инициализирующие значения для ряда 2 */ } ; Вложенные фигурные скобки, означающие следующий ряд, необязательны. Сле- дующий код эквивалентен предыдущему, хотя и менее понятен. // аналогичная инициализация, но без необязательных вложенных II фигурных скобок для каждого ряда int ia[3][4] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; Как и в случае с одномерным массивом, инициализировать можно не все элемен- ты. Можно, например, инициализировать только первые элементы каждого ряда. // явная инициализация только нулевых элементов каждого ряда int ia[3][4] = {{ 0 } , { 4 } , { 8 }}; Значения остальных элементов зависят от их типа (см. стр. 135). // явная инициализация ряда О int ix[3] [4] = {0, 3, 6, 9} ; Этот код инициализирует элементы лишь первого ряда (с индексом 0). Осталь- ные элементы неявно инициализируются значением 0. Индексация многомерных массивов Индексация многомерного массива требует индексирования по каждой размер- ности. Например, следующая пара вложенных циклов for инициализирует двумер- ный массив. // для каждого ряда for (size_t i = 0; i != rowSize; + + i) // для каждого столбца внутри ряда for (size_t j = 0; j != colSize; ++j) // инициализация по указанной индексом позиции ia[i][j] = i * colSize + j; Чтобы получить доступ к определенному элементу двумерного массива, следу- ет указать индекс и столбца, и ряда. Индекс ряда указывает внутренний массив, а индекс столбца — элемент в нем. Понимание этого поможет вычислить значение индексов необходимых элементов многомерных массивов при доступе и инициа- лизации.
Глава 4. Массивы и указатели 167 Если в выражении указан только один индекс, использован будет элемент, яв- ляющийся внутренним массивом (рядом) с заданным индексом. Таким образом, за- пись ia [2] означает массив в последнем ряду массива ia. То есть здесь доступ осущест- вляется не к определенному элементу массива, а непосредственно к одному из массивов. 4.4.1. Указатели и многомерные массивы Подобно любым другим массивам, имя многомерного массива автоматически преобразуется в указатель на первый элемент. При определении указателя на многомерный массив, важно не забывать, что фактически многомерный массив является массивом массивов. Поскольку многомерный массив в действительности является массивом масси- вов, тип указателя, к которому приводится имя массива, имеет тип первого внутрен- него массива. Хотя концептуально это понятно, синтаксис объявления такого указа- теля не так уж и прост. // массив из 3 массивов на 4 элемента типа int // ip - указатель на массив 4 элементов типа int // ia[2] - массив из 4 элементов типа int Указатель на массив определяют аналогично самому массиву: сначала объявляют тип элемента, затем его имя и размер. Отличие в том, что имя является указателем, поэтому перед ним следует разместить символ звездочки (*). Определение указате- ля ip можно прочитать так: указатель *ip типа int [4] или так: ip — указатель на массив из четырех элементов типа int. Круглые скобки в этом объявлении абсолютно необходимы. int *ip[4]; // массив указателей на тип int int (*ip)[4]; // указатель на массив из 4 элементов типа int Определение типа упрощает указатели на многомерные массивы Применение ключевого слова typedef (раздел 2.6, стр. 83) может упростить соз- дание указателей на элементы многомерных массивов, а также запись и чтение данных. Определение типа для элемента массива ia можно записать следующим образом, typedef int int_array[4]; int_array *ip = ia; Полученный тип можно использовать для отображения элементов массива ia следующим образом. for (int_array *р = ia; р != ia + 3; ++р) for (int *q = *p; q != *p + 4; ++q) cout << *q << endl; Внешний цикл for сначала инициализирует указатель р так, чтобы он указывал на первый массив в массиве ia. Этот цикл продолжается, пока не будут обработаны все три ряда в массиве ia. В результате инкремента, ++р, указатель р переводится на следующий ряд (следующий элемент массива ia).
168 Часть I. Основы Внутренний цикл for фактически выбирает целочисленные значения, хранимые во внутренних массивах. Он начинается с создания указателя q, содержащего адрес первого элемента в массиве, на который указывает указатель р. При обращении к значению указателя р возвращается массив из четырех целых чисел. Как обычно, при использовании массива его имя автоматически преобразуется в указатель на его первый элемент. В данном случае первый элемент имеет тип int и именно на него указывает указатель q. Внутренний цикл for выполняется до тех пор, пока не будет обработан каждый элемент внутреннего массива. Чтобы получать указатель за пре- делами внутреннего массива, необходимо снова обратиться к указателю р и полу- чить указатель на первый элемент в этом массиве. К этому указателю добавляется 4, чтобы обработать четыре элемента в каждом внутреннем массиве. Упражнения раздела 4.4.1 Упражнение 4.36. Перепишите программу так, чтобы она отображала содержимое массива ia без использования определения типа для указателя во внешнем цикле. Резюме В этой главе обсуждались массивы и указатели. Их функциональные возможности подоб- ны возможностям библиотечных типов vector или string и их итераторам. О векторах го- ворят, что они гибче и проще в управлении, чем массивы. Аналогично, строки библиотечного типа string имеют больше преимуществ по сравнению со строками в стиле С, которые реа- лизованы в виде символьных массивов, содержащих в конце нулевой символ. Итераторы и указатели обеспечивают косвенный доступ к объектам. Итераторы исполь- зуются не только для доступа, но и для перемещения между элементами в векторах. Указате- ли предоставляют подобные возможности (доступ и перемещение между элементами масси- ва). Хотя указатели концептуально проще, их практическое применение несколько сложнее. Указатели и массивы могут понадобиться для решения некоторых низкоуровневых задач, но, как правило, их применения желательно избегать, поскольку они могут стать причиной многих ошибок и проблем. Вместо встроенных в язык низкоуровневых массивов и указателей предпочтительнее использовать библиотечные типы. Это особенно актуально для строк в стиле С, являющихся символьными массивами с нулевым символом в конце. В современных программах на языке C++ не следует использовать строки в стиле С. Термины Арифметические операции с указателями (pointer arithmetic). Арифметические опера- ции, допустимые для указателей. К указателю может быть добавлено (или вычтено) значение целочисленного типа, что приводит к переходу указателя на соответствующее количество элементов вперед (или назад) от исходного. Результатом вычитания двух указателей являет- ся дистанция между двумя элементами. Арифметические операции допустимы только для тех указателей, которые содержат адреса элементов того же массива. Динамическая память (free store). Пул памяти, доступный программе для хранения объ- ектов создаваемых динамически. Динамически созданный объект(ИупагтсаПу allocated object). Объект, который создан в динамической памяти программы. Такие объекты существуют до тех пор, пока они не будут удалены из динамической памяти явно.
Глава 4. Массивы и указатели 169 Оператор &. Оператор обращения к адресу получает один аргумент, который должен быть 1-значением. Возвращает адрес объекта в памяти. Оператор *. Оператор обращения к значению, адрес которого содержит указатель. Этот оператор возвращает 1-значение, что позволяет использовать его в операторе присвоения с любой стороны, а следовательно, изменять хранимое значение. Оператор []. Оператор индексирования получает два операнда: указатель на элемент массива и индекс. Его результат — элемент массива, адрес которого содержит указатель, а но- мер задан индексом. Нумерация индексов начинается с нуля, т.е. первый элемент массива имеет индекс 0, и последний — размер массива минус 1. Оператор индексирования возвраща- ет 1-значение, что позволяет использовать его в операторе присвоения с любой стороны, а следовательно, изменять хранимое значение Оператор ++. Оператор инкремента, используемый с указателем, перемещает его на сле- дующий элемент массива. Оператор delete. Освобождает участок памяти, зарезервированный оператором new. delete [] р; Здесь р является указателем на первый элемент массива в динамической памяти. Пара квадратных скобок обязательна, она указывает компилятору на необходимость освободить память, занимаемую всем массивом, а не только одним элементом, на который указывает ука- затель р. В программах C++ оператор delete аналогичен функции free () из библиотеки С. Оператор new. Резервирует область в динамической памяти. Для создания в динамиче- ской памяти массива из п элементов используется следующий синтаксис. new тип[п]; В результате получится массив, содержащий п элементов типа тип. Оператор new воз- вращает указатель на первый элемент массива. Программы C++ используют оператор new вместо функции malloc () библиотеки С. Приоритет (precedence). Приоритет определяет порядок выполнения операторов в выра- жении. Размер (dimension). Размер массива. Распределяемая память (heap). То же, что и динамическая память. Расширение компилятора (compiler extension). Дополнительный компонент языка, при- лагаемый к некоторым компиляторам. Код, ориентирующийся на применение расширений компилятора, может не подлежать переносу на другие компиляторы. Составной тип (compound type). Тип данных, определенный в терминах другого типа. Со- ставными типами являются массивы, указатели и ссылки. Строка в стиле С (C-style string). В программах на языке C++ следует использовать биб- лиотечный тип string, а не строки в стиле С, которые могут стать причиной ошибок. Боль- шинство недостатков в системе защиты сетевых приложений связаны с ошибками, причиной которых являются именно массивы и строки в стиле С. Тип const void *. Тип указателя, способного содержать адрес константы любого типа. См. void *. Тип ptrdif f_t. Машинно-зависимый знаковый целочисленный тип, определенный в за- головке cstddef. Является достаточно большим, чтобы содержать разницу между двумя указателями в самом большом массиве. Тип size_t. Машинно-зависимый беззнаковый целочисленный тип, определенный в за- головке cstddef. Является достаточно большим, чтобы содержать размер самого большого массива. Тип void *. Тип указателя, способного содержать адрес неконстантного объекта любого типа. Для указателей типа void разрешен ограниченный набор операций. Они могут быть пе- реданы или возвращены из функций, их можно сравнивать с другими указателями. Но обра- щаться к их значениям нельзя. Указатель (pointer). Объект, содержащий адрес другого объекта.

Выражения В ЭТОЙ ГЛАВЕ... 5.1. Арифметические операторы 173 5.2. Операторы отношения и логические операторы 176 5.3. Побитовые операторы 179 5.4. Оператор присвоения 184 5.5. Операторы инкремента и декремента 187 5.6. Оператор стрелка ( - >) 189 5.7. Условный оператор 190 5.8. Функция sizeof () 191 5.9. Оператор запятая (, ) 192 5.10. Вычисление составных выражений 193 5.11. Операторы new и delete 199 5.12. Преобразование типов 204 Резюме 213 Термины 214 Язык C++ предоставляет богатый набор операторов, а также определяет их на- значение и применение к операндам встроенного типа. Он позволяет также опреде- лять действие операторов, когда они применяются в объектах классов. Эта возмож- ность известна как перегрузка оператора, она используется в библиотеке для опре- деления операторов, которые применяются к библиотечным типам. В этой главе основное внимание уделяется определению операторов и их приме- нению к операндам встроенных типов. Здесь также рассматриваются некоторые из операторов, определенных в библиотеке. Определение собственных перегруженных операторов рассматривается в главе 14, “Перегрузка операторов и преобразования”. Выражение состоит из одного или нескольких операндов (operand), которые объ- единены операторами (operator). Самая простая форма выражения (expression) со- стоит из одиночной литеральной константы или переменной. Более сложные выра- жения формируются из оператора и одного или нескольких операндов. Каждое выражение возвращает результат (result). В выражении без оператора результатом является сам операнд, например литеральная константа или пере- менная. Когда объект используется в контексте, требующем значения, он обраба-
172 Часть I. Основы тывается так, чтобы объект вернул хранимое в нем значение. Например, если ival является объектом типа int, его можно использовать в условии выражения if сле- дующим образом. if (ival) // обработать ival как значение в условии Условие истинно, если значение переменной ival не равно нулю, а в противном случае условие ложно. Результат выражений, использующих операторы, определяется исходя из приме- нения каждого оператора к соответствующему операнду (операндам). Исключением является случай, когда результатом выражения является r-значение (раздел 2.3.1, стр. 67). Такой результат можно прочитать, но нельзя присвоить. Значение оператора определяет операция, которую он выполняет, а тип результата зави- сит от типов его операндов. Пока неизвестен тип каждого операнда, невозможно узнать, что именно означает данное конкретное выражение. Следующее выражение могло бы означать целочис- ленное сложение, конкатенацию строк, сложение чисел с плавающей запятой или что-нибудь совсем иное. i + j Результат выражения зависит от типов переменных i и j. Существуют унарные операторы (unary operator) и парные операторы (binary operator). Унарные операторы, такие как обращение к адресу (&) и обращение к зна- чению (*), воздействуют на один операнд. Парные операторы, такие как сложение (+) и вычитание (-), воздействуют на два операнда. Существует также (всего один) тройственный оператор (ternary operator), который использует три операнда. Более подробно он рассматривается в разделе 5.7 (стр. 190). Некоторые символы (symbol), например *, используются для обозначения как унарных, так и парных операторов. Символ * используется для обозначения опера- тора обращения к значению (унарный) и оператора умножения (парный). По спосо- бу применения они столь непохожи, что их можно рассматривать как два разных символа. Таким образом, то, будет ли символ унарным или парным оператором, оп- ределяется контекстом, в котором используется символ оператора. Операторы предъявляют требования к типам своих операндов. Требования к ти- пам операндов встроенных или составных операторов учтены в самом языке. На- пример, оператор обращения к значению, примененный к объекту встроенного типа, требует, чтобы его операнд имел тип указателя. Попытка применения оператора об- ращения к значению для объекта любого другого встроенного или составного типа приведет к ошибке. Парные операторы, применяемые к операндам встроенных или составных типов, как правило, требуют, чтобы операнды имели одинаковый тип или типы, который может быть преобразован в общий тип. Более подробно преобразование типов рас- сматривается в разделе 5.12 (стр. 204). Хотя правила преобразования могут быть до- вольно сложными, само преобразование вполне логично. Например, целое число
Глава 5. Выражения 173 можно преобразовать в число с плавающей запятой и наоборот, но преобразовывать указатель в число с плавающей запятой невозможно. Чтобы лучше понять порядок выполнения выражений с несколькими оператора- ми, следует рассмотреть концепцию приоритета (precedence) операторов. Напри- мер, в следующем выражении используется сложение, умножение и деление. 5 + 10 * 20 / 2; Результат этого выражения зависит от того, как операнды будут сгруппированы при выполнении операторов. Например, операндами оператора * могли бы быть числа 10 и 20, либо числа 10 и 20 / 2, либо 15 и 20, либо 15 и 20 / 2. Группи- ровку операторов и их операндов определяют правила приоритета. В языке C++ ре- зультатом этого выражения будет 105, умножение 10 на 20, деление результата на 2, и наконец, добавление 5. Однако знания правил группировки операндов и операторов не всегда достаточ- но для достижения необходимого результата. Может также понадобиться выяснить последовательность обработки операндов каждым оператором. Для каждого кон- кретного оператора существует порядок выполнения, который позволяет сделать предположение о последовательности обработки операндов, т.е. существует возмож- ность предусмотреть, будет ли левый операнд обработан перед правым или нет. Большинство операторов не гарантируют определенного порядка выполнения. Бо- лее подробно эти вопросы рассматриваются в разделе 5.10 (стр. 193). 5.1. Арифметические операторы Таблица 5.1. Арифметические операторы Оператор Действие Применение Унарный плюс Унарный минус Умножение Деление Остаток Сложение Вычитание + выражение - выражение выражение * выражение / выражение % выражение + выражение - выражение выражение выражение выражение выражение Если не указано иное, эти операторы применимы ко всем арифметическим типам, представленным в табл. 2.1 (стр. 56), а также к любым другим типам, которые могут быть преобразованы в арифметический тип. Операторы в таблице сгруппированы по их приоритетам: унарные операторы имеют более высокий приоритет, далее следуют операторы умножения и деления, а затем парные операторы сложения и вычитания. Операторы с более высоким при- оритетом выполняются прежде операторов с более низким приоритетом. При равен- стве приоритетов операторы выполняются в последовательности слева направо.
174 Часть I. Основы Рассмотрим применение приоритета и последовательность выполнения к сле- дующему выражению. 5 + 10 * 20 / 2; Как можно заметить, операндами оператора умножения (*) являются 10 и 20. Результат этого выражения и число 2 являются операндами оператора деления (/). Результат деления и число 5 — операнды оператора суммы (+). Назначение унарного оператора вычитания вполне очевидно, он меняет знак сво- его операнда на обратный. int i = 1024; int k - -i; // меняет значение операнда на отрицательное Унарный оператор суммы возвращает сам операнд, не внося никаких изменений. Внимание! Переполнение переменной и другие арифметические особенности Результат вычисления некоторых арифметических выражений недопустим. Некото- рые выражения недопустимы чисто математически: например деление на нуль. Недо- пустимость других связана с природой компьютеров, например переполнение пере- менной из-за значения, превышающего размер хранящей его переменной. Например, в компьютере, на котором переменная типа short занимает 16 битов, мак- симальным значением типа short может быть 32767. В таком случае следующий код приведет к переполнению переменной. // максимальное значение, если тип short занимает 16 битов short short_value = 32767; short xval = 1; // это вычисление приведет к переполнению short_value += ival; cout << "short_value: " << short_value << endl; Представление знакового значения 32768 требует 17 битов, но доступно только 16. На большинстве систем нет никаких средств, позволяющих предупредить о возможности переполнения переменной ни во время компиляции, ни во время выполнения. Факти- ческое значение, которое окажется в переменной short _value, у разных машин бу- дет разным. На машине автора программа отобразила следующий результат. short_value: -32768 Здесь произошло переполнение переменной: предназначенный для знака разряд со- держал значение 0, но был заменен на 1, что привело к появлению отрицательного значения. Поскольку размер арифметических типов ограничен, переполнение пере- менной вполне вероятно при некоторых вычислениях. Соблюдение рекомендаций, представленных в разделе 2.1.1 (стр. 59), поможет избежать подобных проблем. Парные операторы + и - могут быть также применены к значениям указате- лей. Использование этих операторов с указателями было описано в разделе 4.2.4 (стр. 147). Назначение арифметических операторов +, -, * и / вполне очевидно: это сложе- ние, вычитание, умножение и деление. Результатом деления целых чисел является целое число. Получаемая в результате деления дробная часть отбрасывается.
Глава 5. Выражения 175 int ivall = 21/6; // целочисленный результат получается при // усечении остатка int ival2 = 21/7; // без остатка получается целочисленное значение Обе переменные, ivall и ival2, инициализируются значением 3. Оператор % известен как остаток (remainder) или оператор деления по модулю (modulus). Он позволяет вычислить остаток от деления левого операнда на правый. Этот оператор применим только к целочисленным типам: bool, char, short, int, long и их беззнаковым версиям. int ival = 42; double dval = 3.14; ival % 12; // ok: возвращает 6 ival % dval; // ошибка: операнд с плавающей запятой Когда при делении (/) или делении по модулю (%) оба операнда положительны, результат положителен (или нуль). Если оба операнда отрицательны, результат де- ления положителен (или нуль), а результат деления по модулю отрицателен (или нуль). Если отрицателен только один операнд, знак результата обоих операторов за- висит от особенностей машины. 21 21 -21 21 21 21 -21 21 // ок: результат -5 / / зависит от машины: результат 1 или -4 // ок: результат 3 // ок: результат 3 II ок: результат 2 // зависит от машины: результат -4 или -5 Когда отрицателен только один операнд, знак результата оператора деления по модулю может быть взят как у числителя, так и у знаменателя. На машине, где опе- ратор деления по модулю ориентируется на знак числителя, значение деления усе- кается до нуля. Если оператор деления по модулю ориентируется на знак знамена- теля, результат деления усекается до минус бесконечности. Упражнения раздела 5.1 Упражнение 5.1. Введите следующее выражение и попробуйте указать, как оно будет обрабаты- ваться. Проверьте ответ, отобразив результат на экране. 12 / 3 * 4 + 5 * 15 + 24 % 4 / 2 Упражнение 5.2. Предскажите результат следующих выражений и укажите, какие из результатов (если они есть) зависят от машины. 21/5 21/5 21 % 5 21 % 4 Упражнение 5.3. Напишите выражение, позволяющее выяснить, является ли значение типа int четным или нечетным. Упражнение 5.4. Объясните значение термина “переполнение переменной”. Представьте три примера выражений, которые могут привести к переполнению.
176 Часть I. Основы 5.2. Операторы отношения и логические операторы Таблица 5.2. Операторы отношения и логические операторы Оператор Действие Применение ! Логическое NOT < Меньше < = Меньше или равно > Больше >= Больше или равно == Равно ! = Не равно && Логическое AND I I Логическое OR !выражение выражение < выражение выражение <= выражение выражение > выражение выражение >= выражение выражение = = выражение выражение ! = выражение выражение && выражение выражение | | выражение Каждый из этих операторов возвращает значение типа bool. Операторам отношения и логическим операторам передают арифметические операнды или операнды типа указателей, а возвращают они логические значения (тип bool). Логические операторы AND и OR Логические операторы рассматривают свои операнды как условия (раздел 1.4.1, стр. 34). Операнд расценивается как значение false, если его результат равен нулю, и как значение true — в противном случае. Результатом оператора AND (&&) будет значение true только в том случае, если оба его операнда возвращают значение true. Логический оператор OR ( | | ) возвращает значение true, если любой из его операндов расценивается как значение true. выражение! && выражение2 выражение! I I выражение2 // Логическое AND // Логическое OR Здесь значение выражения выражение2 проверяется только в том случае, если результата выражения выражение! недостаточно для определения результата. Дру- гими словами, результат выражения выражение2 вычисляется лишь в следующих случаях. В логическом операторе AND выражение выражение! вернуло значение true. Если выражение выражение! вернуло значение false, оператор AND вернет значение false в любом случае, независимо от того, какое значение вернет вы- ражение выражение2. Но если выражение выражение! вернуло значение true, вычислять значение выражения выражение2 имеет смысл, поскольку ес- ли оно вернет значение false, результат оператора AND может измениться.
Глава 5. Выражения 177 В логическом операторе OR выражение выражение! вернуло значение false. Лишь в этом случае результат оператора OR зависит от результата выражения выражение 2. Логические операторы AND и OR всегда обрабатывают свой левый операнд раньше пра- вого. Правый операнд обрабатывается только тогда, когда левый операнд не определил результат однозначно. Такой способ обработки значений зачастую называют вычислени- ем по сокращенной схеме (short-circuit evaluation). Одним из наиболее интересных способов использования логического оператора AND является использование оператора выражение! для проверки условия, кото- рое могло бы сделать вычисление результата выражения выражение2 небезопас- ным. Предположим, например, что строка s типа string содержит предложение, символы первого слова которого следует перевести в верхний регистр. Это можно сделать таким образом. string s (" Expressions in C++ are composed...'1); string: : iterator it = s.beginO; // преобразовать первое слово строки s в верхней регистр while (it != s.end() && !isspace(*it)) { *it = toupper(*it); // функция toupper() // рассматривается в разделе 3.2.4 ++it ; } В данном случае в условии цикла while объединена проверка двух выражений. Сначала проверяется, не достиг ли итератор i t конца строки. Если нет, итератор i t используется для обращения к символу строки s. Но это происходит только в том случае, если правый операнд пройдет проверку. Проверка гарантирует, что итератор it указывает на реально существующий символ. Цикл заканчивается, когда встре- чается пробел или конец строки (если пробелов в строке s нет). Логический оператор NOT Логический оператор NOT (!) рассматривают свой операнд как условие. Он воз- вращает результат, который имеет логическое значение, противоположное его опе- ранду. Если операнд отличен от нуля, оператор ! возвращает значение false. На- пример, применив логический оператор NOT к значению, возвращаемому функцией empty (), можно выяснить, существует ли текущий элемент вектора. // присвоить значение первого элемента вектора vec, // если он существует, переменной х int х = 0; if (!vec.empty()) х = *vec.begin(); Выражение Ivec.empty () вернет значение true, если обращение к функции empty () вернет значение false. Операторы отношения не следует объединять Операторы отношения (<, <=, >, <=) имеют порядок выполнения слева направо. Их результатом является логические значения (тип bool). Если объединить эти операторы вместе, можно получить весьма неожиданный результат.
178 Часть I. Основы // Упс! Это условие вовсе не является проверкой трех значений на / / неравенство if (i < j < к) {/*...*/} На самом деле, это выражение возвратит значение true, если значение перемен- ной к больше единицы! Дело в том, что левый операнд второго оператора <, который и определяет результат true или false всего оператора, сравнивает целочисленное значение переменной к с результатом первого оператора <, который способен вер- нуть лишь значение 0 или 1. Чтобы это выражение на самом деле проверяло нера- венство трех значений, его следует переписать следующим образом. if (i < j && j < к) {/*...*/} Проверка равенства и логические литералы Как будет продемонстрировано в разделе 5.12.2 (стр. 206), значение типа bool может быть преобразовано в значение любого арифметического типа (значение false преобразуется в 0, а значение true — в 1). Поскольку логическое значение true преобразуется в единицу, использовать литерал true при проверке равенства практически никогда не имеет смысла. if (val == true) { /* ... */ } Переменная val может иметь тип bool, а может иметь тип, преобразуемый в тип bool. Если переменная val имеет тип bool, предыдущее условие можно перепи- сать следующим образом, if (val) { /* ... */ } Такая форма записи короче и понятней (хотя считается, что на начальном этапе изучения языка такой сокращенный синтаксис может несколько озадачить). Когда переменная val не является логической, ее сравнение со значением true эквивалентно следующей записи. if (val == 1) { /* Это весьма отличается от прежней формы. // условие выполняется, если переменная val содержит II любое значение, отличное от нуля if (val) { /* ... */ } Здесь любое отличное от нуля значение переменной val расценивается как true. При явном сравнении со значением true, фактически проверяется равенство лишь со значением 1. Упражнения раздела 5.2 Упражнение 5.5. Объясните, когда используются логические операторы AND, OR, а также опера- тор равенства. Упражнение 5.6. Объясните поведение следующего условия цикла while. char *ср - "Hello World"; while (ср && *cp)
Глава 5. Выражения 179 Упражнение 5.7. Напишите условие цикла while, который читает целые числа со стандартного устройства ввода и останавливается, когда встречает значение 42. Упражнение 5.8. Напишите выражение, которое проверяет четыре значения: а, Ь, с и d. Значе- ние а должно быть больше чем ь, которое должно быть больше чем с, которое должно быть больше чем d. 5.3. Побитовые операторы Таблица 5.3. Побитовые операторы Оператор Действие Побитовое NOT Сдвиг влево Сдвиг вправо Побитовое AND Побитовое XOR Побитовое OR Применение ~ выражение выражение! << выражение2 выражение! >> выражение2 выражение! & выражение2 выражение! * выражение2 выражение! | выражение2 Побитовым операторам (bitwise operator) передают операнды целочисленного типа. Эти операторы обрабатывают операнды как коллекции битов, выполняя проверку и установку отдельных битов. Эти операторы применимы также к набо- рам битов (раздел 3.5, стр. 125), которые они обрабатывают аналогично целочис- ленным операндам. Побитовые операторы применимы как к знаковым, так и к беззнаковым целым числам. Обработка знакового разряда отрицательных значений при побитовых опе- рациях зачастую является машинно-зависимой. Поэтому такие операции нормально выполняются на одних машинах, а на других могут сработать иначе. Поскольку нет никаких гарантий однозначного выполнения побитовых операторов со знаковыми переменными на разных машинах, настоятельно рекомендуется исполь- зовать в них только беззнаковую версию целочисленных значений. В приведенных ниже примерах подразумевается, что тип unsigned char имеет 8 битов. Побитовый оператор NOT (~) подобен функции f lip () класса bitset (раз- дел 3.5.2 стр. 129): он создает новое значение, инвертировав биты операнда. Каждый бит, содержащий значение 1, будет содержать значение 0, а каждый бит, содержа- щий 0, получит значение 1, как показано на рис. 5.1. unsigned char bits = 0227; bits = -bits; 1 0 0 1 0 1 1 1 0 1 1 0 1 0 0 0 Рис. 5.1. Побитовый оператор NOT
180 Часть I. Основы Операторы < < и > > являются побитовыми операторами сдвига. Правый операнд этих операторов указывает, на сколько битов следует сдвинуть биты левого операн- да. Возвращаемое ими значение является копией левого операнда, биты которого сдвинуты на количество позиций, указанное правым операндом. Биты могут быть перемещены влево (<<) или вправо (>>), при этом вышедшие за пределы биты от- брасываются, как можно заметить на рис. 5.2. unsigned char bits = 1; bits « 1; // сдвиг влево bits « 2; // сдвиг влево bits » 3; // сдвиг вправо Рис. 5.2. Операторы побитового сдвига Оператор сдвига влево (<<) добавляет нулевые биты справа, а оператор сдвига вправо (>>) — слева, если операнд не имеет знака. Если операнд имеет знаковый тип, в зависимости от конкретной реализации, в знаковый разряд может быть либо вставлена его копия, либо значение 0. Правый операнд не должен быть отрицатель- ным значением, а также он не должен превышать количества битов в левом операн- де. В противном случае результат операции окажется непредсказуемым. Побитовый оператор AND (&) получает два целочисленных операнда. Каждая битовая позиция результата получит значение 1, если соответствующие биты обоих операндов содержат значение 1; в противном случае результат получит значение 0. Нередко побитовый оператор AND (&) путают с логическим оператором AND (&&) (раздел 5.2, стр. 176). Аналогично, побитовый оператор OR (|) иногда путают с логиче- ским оператором OR (| |). На рис. 5.3 проиллюстрирован результат применения побитового оператора AND к двум значениям типа unsigned char, инициализированным восьмеричным литералом. Рис. 5.3. Побитовый оператор AND Побитовый оператор XOR (А), т.е. исключающее или, также получает два цело- численных операнда. Каждая битовая позиция результата получит значение 1, если только один (а не оба) соответствующий бит операнда содержит значение 1; в про- тивном случае результат получит значение 0, как продемонстрировано на рис. 5.4. result = Ь1 Л Ь2; 1 1 0 0 1 0 1 0 Рис. 5.4. Побитовый оператор XOR
Глава 5. Выражения 181 Побитовый оператор OR ( |) получает два целочисленных операнда. Каждая бито- вая позиция результата получит значение 1, если один или оба соответствующих бита операнда содержат значение 1; в противном случае результат получит значение О, как продемонстрировано на рис. 5.5. result = Ь1 | Ь2; 1 1 1 0 1 1 1 1 Рис. 5.5. Побитовый оператор OR 5.3.1. Использование битовых наборов и целочисленных значений Как утверждалось ранее, для низкоуровневых побитовых операций более прием- лем класс bit set, чем целочисленные значения. Давайте на простом примере рас- смотрим, как можно решить аналогичную проблему используя класс bit set и це- лочисленные значения. Предположим, что каждый класс насчитывает по 30 учени- ков. Каждую неделю в классе происходит контрольный опрос. Ответ ученика на вопрос, правильный или неправильный, заносится в один бит результата опроса. Для сохранения результатов контрольных опросов можно использовать либо объект класса bit set, либо целочисленное значение. bitset<30> bitset_quizl; // решение с использованием класса bitset unsigned long int_quizl =0; // моделируемая коллекция битов В случае использования набора битов, определен объект bitset_quizl, размер которого точно соответствует необходимому (количеству учеников). По умолчанию значением каждого бита является нуль. При использовании встроенного типа, для хранения результатов опроса определена переменная int_quizl типа unsigned long, которая на любой машине будет иметь по крайней мере 32 бита. И наконец, переменная int_quizl явно инициализируется значением 0, чтобы все ее биты со- держали нулевые значения. Преподаватель должен иметь возможность устанавливать и проверять значения отдельных битов. Предположим, например, что ученик номер 27 ответил правильно. В этом случае необходимо установить соответствующий бит (присвоить значение 1). bitset_quizl.set(27); // указать, что ученик 27 ответил правильно int_quizl |= 1UL<<27; // указать, что ученик 27 ответил правильно В случае применения класса bit set это можно сделать непосредственно, уста- новив необходимый бит при помощи функции set (). В случае применения пере- менной типа unsigned long, все немного сложней и требует дополнительных объ- яснений. Чтобы установить определенный бит, применяется оператор OR и другое целое число, в котором установлен только один этот бит. То есть необходимо еще одно значение типа unsigned long, где бит номер 27 содержит единицу, а все ос- тальные — нули. Такое значение можно получать используя оператор сдвига влево и целочисленную константу 1. 1UL << 27; // создает значение только с одним установленным // битом в позиции 27
182 Часть I. Основы Теперь применим полученное значение в побитовом операторе OR к переменной int_quizl, в результате чего все ее биты останутся неизменными, за исключением бита номер 27. Этот бит окажется установленным. Используемый составной опера- тор (раздел 1.4.1, стр. 34) присвоит результат оператора OR переменной int_quizl. Этот оператор, | =, выполняется точно так же, как и оператор +=. Это эквивалентно следующей записи. // следующее присвоение эквивалентно int_quizl 1= 1UL « 27; int_quizl = int_quizl I 1UL << 27; Предположим, учитель выяснил, что ученик номер 27 при ответе пользовался шпаргалкой, а следовательно, зачета не заслуживает. Теперь учитель должен сбро- сить бит номер 27. bitset_quizl.reset(27); // ученик 27 ответил неправильно int_quizl &= ~(1UL<<27); // ученик 27 ответил неправильно И снова версия набора битов проще. Функция reset () сбрасывает (обнуляет) бит. В случае применения переменной типа unsigned long необходимо действие, обратное установке бита: необходимо целое число, в котором 27-й бит сброшен, а все остальные установлены. Чтобы сбросить только 27-й бит, это значение используется в побитовом операторе AND для переменной, содержащей данные об ответе. В ре- зультате бит 27 получит значение 0, а все остальные останутся неизменными. При- менение побитового оператора NOT к полученному целому числу установит все его биты за исключением 27-го. Таким образом, побитовый оператор AND оставит все биты переменной int_quizl неизменными, за исключением бита 27, значением ко- торого станет 0. И наконец, осталось выяснить, как ученик в позиции 27 сдал зачет. Сделать это можно следующим образом, bool status; status = bitset_quizl[27]; // как дела у ученика номер 27? status = int_quizl & (1UL<<27); // как дела у ученика номер 27? В случае применения набора битов, значение можно выбрать непосредственно и выяснить, как дела у ученика. В случае применения переменной типа unsigned long, необходимо сначала создать целое число с установленным 27-м битом. Затем это число следует использовать в побитовом операторе AND со значением int_ quizl. Если в результате получится значение, отличное от нуля, значит бит 27 в значении переменной int_quizl установлен; в противном случае будет получено значение нуль. Как правило, операции с объектами библиотечного класса bitset проще и легче для понимания. Кроме того, их применение снижает вероятность возникновения ошибок. Размер набора битов никак не ограничен, а количество битов в беззнако- вых переменных небесконечно. Таким образом, применение класса bitset, как правило, предпочтительнее прямого низкоуровневого манипулирования битами це- лочисленных значений. Упражнения раздела 5.3.1 Упражнение 5.9. Предположим, что существуют следующие два определения, unsigned long ull = 3, ul2 = 7;
Глава 5. Выражения 183 Каков результат каждого из следующих выражений? (a) ull & ul2 (с) ull I ul2 (b) ull && ul2 (d) ull I I ul2 Упражнение 5.10. Перепишите выражения, использующие набор битов для установки и сброса битов в результате опроса, таким образом, чтобы в них использовался оператор индексирования. 5.3.2. Использование операторов сдвига для организации ввода и вывода В библиотеке ввода-вывода побитовые операторы >> и << переопределены так, чтобы осуществлять действия по вводу и выводу данных. Хотя большинству про- граммистов вряд ли придется часто использовать именно побитовые операторы, в большинстве программ перегруженные версии этих операторов для ввода и вывода используются очень часто. Когда используется перегруженный оператор, он имеет тот же приоритет и порядок, что и у встроенной версии. Следовательно, программи- стам необходимо знать приоритет и порядок этих операторов, даже если они никогда не используются в их базовом виде, т.е. как операторов сдвига. Операторы ввода и вывода имеют порядок выполнения слева направо Подобно другим бинарным операторам, операторы сдвига имеют порядок вы- полнения слева направо. Их можно группировать в цепочки, организовав ввод и вы- вод в единый оператор. cout << "hi" << " there" << endl; Он выполняется следуюгцимюбразом. ( (cout << "hi") << " there" ) << endl; В этом выражении операнд "hi" группируется с первым оператором <<. Его ре- зультат группируется со вторым оператором <<, а его результат с третьим. Операторы сдвига имеют приоритет среднего уровня: ниже, чем у арифметиче- ских операторов, но выше, чем у операторов отношения, присвоения и условных операторов. Этот относительно невысокий уровень приоритета операторов ввода и вывода зачастую вынуждает использовать круглые скобки, чтобы организовать пра- вильную группировку операторов. cout <<42+10; // ок: + имеет более высокий приоритет, поэтому // отображена будет сумма cout << (10 < 42); // ок: круглые скобки имеют самый высокий // приоритет, поэтому отображается значение 1 cout << 10 < 42; // ошибка: попытка сравнивать число 42 // с объектом cout.! Последний оператор с использованием объекта cout интерпретируется следую- щим образом. (cout << 10) < 42; Это выражение можно прочитать так: записать число 10 в объект cout, а затем сравнить результат этой операции (возвращенный объект cout) с числом 42.
184 Часть I. Основы 5.4. Оператор присвоения Левым операндом оператора присвоения должно быть неконстантное 1-значение. Ниже приведено несколько примеров недопустимых попыток присвоения. int i, j, ival; const int ci = i; 1024 = ival; i + j = ival; // ok: инициализация, а не присвоение // ошибка: литерал является г-значением // ошибка: арифметическое выражение тоже // является г-значением // ошибка: ci - константа, запись в нее // невозможна Имена массивов являются немодифицируемыми 1-значениями, поэтому массиву нельзя ничего присвоить. Операторы индексирования и обращения к значению воз- вращают 1-значения. Результат обращения к значению или индексирования некон- стантного массива может быть левым операндом присвоения. int ia [10]; ia[0] = 0; // ok: результат индексирования является 1-значением ★ia - 0; // ok: результат обращения к значению также 1-значение Результат присвоения сохраняется в левом операнде, а типом результата являет- ся тип левого операнда. Как правило, правый операнд оператора присвоения получает значение левого операнда. Однако в случае, когда левый и правый операнды оператора присвоения имеют разные типы, происходит преобразование, в результате которого может полу- читься другое значение. В таких случаях значение, хранимое в левом операнде, мо- жет отличаться от значения правого операнда. ival =0; // результат: тип int, значение О ival = 3.14159; // результат: тип int, значение 3 В обоих этих случаях осуществляется присвоение значения переменной типа int. В первом случае переменная ival получит то же значение, которое указано в правом операнде. Во втором случае переменная ival получит значение, отличное от правого операнда. 5.4.1. Оператор присвоения имеет порядок выполнения справа налево Подобно операторам индексирования и обращения к значению, оператор при- своения возвращает 1-значение. Таким образом, вполне возможно осуществить не- сколько операций присвоения в одном выражении, если каждый из операндов имеет одинаковый тип. int ival, jval; ival = jval = 0; // ok: каждой переменной присвоено значение О В отличие от других парных операторов, операторы присвоения имеет порядок выполнения справа налево. При группировке нескольких операторов присвоения, выражение выполняется справа налево. В этом выражении результат правого опера- тора присвоения (т.е. jval) используется в левом (т.е. ival). Типы объектов, ис- пользуемых в группе из нескольких операторов присвоения, должны либо совпа- дать, либо допускать взаимное преобразование (раздел 5.12, стр. 204).
Глава 5. Выражения 185 int ival; int *pval; ival = pval = 0; // ошибка: переменной типа int нельзя присвоить // значение указа теля string si, s2; si = s2 = "OK”; // ok: "OK” преобразуется в строку Первый случай присвоения недопустим, поскольку объекты ival и pval имеют разные типы, причем-недопустим несмотря на то, что нулевое значение вполне может быть присвоено каждому из этих объектов. Дело в том, что результат при- своения переменной pval имеет тип указателя на тип int, который не может быть присвоен объекту типа int. Второй случай присвоения срабатывает прекрасно. Строковый литерал преобразуется в значение типа string, которое и присваивает- ся переменной s2 типа string. Затем результат присвоения переменной s2 при- сваивается переменной si. 5.4.2. Оператор присвоения имеет низкий приоритет Применение оператора присвоения внутри условия может существенно сокра- тить программу и сделать намерение разработчика более понятным. Рассмотрим, например, следующий цикл, где используется функция get_value (), возвращаю- щая значение типа int. Допустим, эти значения необходимо проверять, пока не бу- дет получено некое желаемое значение, например 42. int i = get_value(); // get_value() возвращает тип int while (i != 42) { // выполнение действий ... i = get_value(); } Выполнение программы начинается с получения первого значения и его сохра- нения в переменной i. Затем оно используется в условии цикла while при проверке на неравенство числу 4 2. Если это не так, в теле цикла выполняются некие действия, а в последнем операторе цикла функция get_value () присваивает переменной i новое значение. Этот цикл можно переписать гораздо компактнее, int i; while ((i = get_value()) != 42) { // выполнение действий ... } Теперь намерение разработчика вполне очевидно: цикл продолжается до тех пор, пока функция get_value () не возвратит значение 42. Выражение в условии при- сваивает результат обращения к функции get_value () переменной i, значение которой затем сравнивается с числом 4 2. Дополнительные круглые скобки вокруг оператора присвоения переменной i результата выполнения функции get_vaiue () необходимы, поскольку приоритет оператора при- своения ниже, чем у оператора неравенства. Без круглых скобок операндами оператора ! = будет значение, возвращаемое функцией get_value (), и число 42. Полученный результат проверки (значение true или false) будет присвоен переменной i, а это вовсе не то, что было нужно!
186 Часть I. Основы Не перепутайте операторы равенства и присвоения То, что оператор присвоения можно использовать в условии, приводит иногда к удивительным результатам, if (i = 42) Этот код вполне допустим: здесь переменной i присваивается значение 4 2, кото- рое затем используется при проверке условия. Поскольку значение 42 отлично от нуля, оно интерпретируется как значение true. Однако автор этого кода почти на- верняка предполагал проверить равенство содержимого переменной i значению 4 2: if (i == 42) Найти подобную ошибку довольно трудно. Правда, некоторые компиляторы, но не все, могут предупредить о подобном коде. Упражнения раздела 5.4.2 Упражнение 5.11. Какими будут значения переменных i и d после каждого оператора присвоения, int i; double d; d = i = 3.5; i = d = 3.5; Упражнение 5.12. Объясните, что произойдет при выполнении следующих операторов if. if (42 = i) II... if (i = 42) // . . . 5.4.3. Составные операторы присвоения Довольно нередки случаи, когда оператор применяется к объекту, а полученный результат переприсваивается тому же объекту. Рассмотрим, например, программу, вычисляющую сумму (стр. 36). int sum = 0; // сумма значений от 1 до 10 включительно for (int val = 1; val <= 10; ++val) sum += val; // эквивалент sum - sum + val Подобный вид операций характерен не только для сложения, но и для других арифметических и побитовых операторов. Для каждого из этих операторов сущест- вуют соответствующие составные версии. Общая синтаксическая форма составного оператора присвоения имеет следующий вид. a op- Ъ; Где ор= может быть одним из следующих десяти операторов. += -= *= /= %= // арифметические операторы <<= >>= 8с- | - / / побитовые операторы Каждый составной оператор, по существу, эквивалентен следующему, а = а ор Ь; Но эти формы имеют одно очень важное различие: в составном операторе при- своения левый операнд вычисляется только один раз. При использовании эквива- лентной, но более длинной версии левый операнд вычисляется дважды: один раз как
Глава 5. Выражения 187 правый операнд, а затем как левый. В подавляющем большинстве случаев это различие несущественно, возможно, кроме тех, где производительность критически важна. Упражнения раздела 5.4.3 Упражнение 5.13. Следующее присвоение недопустимо. Почему? Как исправить ситуацию? double dval; int ival; int *pi; dval = ival = pi = 0; Упражнение 5.14. Хотя ниже приведены вполне допустимые выражения, их поведение может ока- заться не таким, как предполагалось. Почему? Перепишите выражения так, чтобы они стали более понятны. (a) if (ptr = retrieve_pointer() != 0) (b) if (ival = 1024) (c) ival += ival + 1; 5.5. Операторы инкремента и декремента Операторы инкремента (++) и декремента (- - ) позволяют в краткой и удобной форме добавить или вычесть единицу из объекта. Имеются две формы этих операто- ров: префиксная и постфиксная. До сих пор использовался лишь префиксный опе- ратор инкремента, который увеличивает свой операнд и возвращает измененное зна- чение как результат. Префиксный оператор декремента работает аналогично, но уменьшает значение операнда. Постфиксные версии операторов инкремента и дек- ремента возвращают первоначальное значение операнда, а уже затем изменяют его. int i = 0, j; j = ++i; // j = 1, i = 1: префикс возвращает увеличенное значение j = i++; // j = 1, i =2: постфикс возвращает исходное значение Поскольку префиксная версия возвращает увеличенное значение, содержащий его объект является 1-значением. Постфиксная версия возвращает г-значение. Совет. Используйте постфиксные операторы только при необходимости Новички в программировании на языке С могли бы быть удивлены, почему в програм- мах, описанных ранее, использован только префиксный инкремент ? Причина очень про- ста: префиксная версия требует существенно меньше действий. Она изменяет значение переменной и возвращает значение в ту же переменную. Постфиксный оператор вынуж- ден сохранять исходное значение отдельно, чтобы его можно было вернуть как резуль- тат. Для переменных типа int и указателей, компилятор способен оптимизировать код и снизить количество дополнительных действий. Для более сложных типов итера- торов подобные дополнительные действия могут обойтись довольно дорого. При ис- пользовании префиксных версий об эффективности можно не волноваться. Постфиксные операторы возвращают первоначальное значение Постфиксная версия операторов ++ и - - используется в случае, когда в одном составном выражении необходимо использовать текущее значение переменной, а за- тем увеличить его.
188 Часть I. Основы vector<int> ivec; // пустой вектор int ent = 10; // добавить в вектор ivec элементы 10 ... 1 while (ent > 0) ivec.push_back(cnt--); // постфиксный декремент В этом коде к переменной ent применена постфиксная версия оператора Здесь необходимо сначала присвоить значение переменной ent элементу вектора, а затем, перед следующей итерацией, осуществить декремент переменной ent. Если бы в этом цикле использовалась префиксная версия, значение переменной ent сна- чала уменьшалось бы на единицу, а затем бы присваивалось элементу вектора ivec. В результате в вектор были бы добавлены элементы от 9 до 0. Объединение операторов обращения к значению и инкремента в одном выражении Следующая программа, отображающая содержимое вектора ivec, позволяет по- лучить общее представление о схеме программирования на языке C++. vector<int>::iterator iter = ivec.begin(); // отображает 10 9 8 . . . 1 while (iter != ivec.end()) cout << *iter++ << endl; // постфиксный инкремент итератора Выражение *iter++ обычно не очень понятно программистам, плохо знакомым с языками C++ и С. Поскольку приоритет постфиксного инкремента выше, чем у оператора обраще- ния к значению, выражение *iter++ эквивалентно выражению * (iter++). Часть iter++ увеличивает (перемещает на следующую позицию) итератор iter и возвра- щает копию его предыдущего значения в качестве результата. Соответственно, опера- тор * применяется к исходной, неувеличенной копии значения итератора iter. Этот подход основан на том, что постфиксный инкремент возвращает копию сво- его исходного, неувеличенного операнда. Если бы он возвратил увеличенное значе- ние, обращение к элементу вектора по такому увеличенному значению привело бы к плачевным результатам: первым оказался бы незаписанный элемент вектора ivec. Хуже того, в конце произошла бы попытка обратиться к несуществующему элементу! Совет. Краткость может быть достоинством Программисты, плохо знакомые с языком C++ и не имеющие опыта работы с такими языками, как С или ему подобными, зачастую испытывают затруднения в работе с краткими формами некоторых выражений. На самом деле, такие выражения, как *iter++, могут быть весьма полезны. Опытные программисты C++ предпочитают именно их. Они, вероятнее всего, используют следующую запись. cout << *iter++ << endl; Хотя вполне можно было бы использовать более подробный эквивалент. cout << *iter << endl; ++iter;
Глава 5. Выражения 189 Для новичков в C++ второй вариант понятней, поскольку действия приращения ите- ратора, выборки значения и отображения осуществляются по отдельности. Однако первая версия намного более естественна для большинства программистов C++. Поэтому примеры подобного кода имеет смысл внимательно изучать, чтобы они стали совершенно понятны. В большинстве программ C++, как правило, используются именно краткие формы выражений, а не их более подробные эквиваленты. Следова- тельно, желающим стать настоящим программистом на языке C++, придется привы- кать к ним. Кроме того, научившись работать с краткими формами, можно заметить, что они существенно менее подвержены ошибкам. Упражнения раздела 5.5 Упражнение 5.15. Объясните различие между префиксным и постфиксным инкрементом. Упражнение 5.16. Почему язык получил имя C++, а не ++С? Упражнение 5.17. Что произойдет, если в отображающем содержимое вектора цикле while, использовался бы префиксный оператор инкремента? 5.6. Оператор стрелка (->) Оператор стрелка (->) предназначен для замены выражений, совмещающих то- чечный оператор и оператор обращения к значению. Точечный оператор (dot operator) обеспечивает доступ к члену класса в объекте. iteml.same_isbn(item2); // вызов функции same_isbn(), // принадлежащей объекту iteml Если класс Sales_item имеет указатель (или итератор), при обращении к зна- чению указателя (или ссылке на итератор) используется точечный оператор. Sales_item *sp = &iteml; (*sp) .same_isbn(item2) ; // вызов функции same_isbn() объекта, адрес // которого содержит указатель sp Здесь для обращения к объекту класса Sales_item используется указатель sp, а для запуска функции same_isbn () полученного объекта используется точечный оператор. Оператор обращения к значению приходится поместить в круглые скобки, поскольку его приоритет ниже точечного. Без круглых скобок этот код означал бы нечто совершенно иное. // вызов функции same_isbn() объекта sp, а затем обращение по // полученному адресу! *sp.same_isbn(item2); // ошибка: объект sp не имеет члена II по имени same_isbn() В этом выражении осуществляется попытка доступа к члену same_isbn () объек- та sp. Данная запись эквивалентна следующей. *(sp.same_isbn(item2)); // эквивалент *sp.same_isbn(item2); Однако объект sp является указателем, а следовательно, никаких членов он не имеет. В результате этот код не будет даже откомпилирован. Поскольку забыть круглые скобки очень просто, а также потому, что подобный код весьма популярен, в языке C++ определен оператор стрелки, являющийся сино-
190 Часть I. Основы нимом обращения к значению, сопровождаемого точечным оператором. Допустим, что существует указатель на объект класса (или итератор). Тогда следующие выра- жения эквивалентны. (*р).foo; р -> foo; // обратиться к значению указателя р, чтобы получив II объект обратиться к его члену по имени foo II эквивалентный способ обращения к члену foo объекта, II адрес которого содержит указатель р Аналогично, прежнее обращение к функции same_isbn() можно переписать следующим образом. sp->same_isbn(item2); // эквивалент (*sp).same_isbn(item2) Упражнения раздела 5.6 Упражнение 5.18. Напишите программу, в которой определен вектор указателей на строки. Орга- низуйте перебор вектора с отображением каждой строки и ее размера. Упражнение 5.19. Исходя из того, что итератор iter имеет тип vector<string>: : iterator, укажите, какие из следующих выражений допустимы (если они есть). Объясните по- ведение допустимых выражений. (a) *iter++; (b) (*iter)++; (с) *iter.empty() (d) iter->empty(); (e) ++*iter; (f) iter++->empty(); 5.7. Условный оператор Условный оператор (conditional operator) — это единственный тройственный опе- ратор языка C++. Он позволяет внедрять простые конструкции if. . .else непо- средственно внутрь выражения. Условный оператор имеет следующий синтаксис. условие ? выражение! : выражение2; Здесь условие — это выражение, используемое в качестве условия (раздел 1.4.1, стр. 34). При выполнении оператора сначала вычисляется результат условия. Резуль- татом условия будет false, если при вычислении получится 0, а любое другое значе- ние расценивается как true. Результат выражения в условии вычисляется всегда. Если условие возвращает результат true, срабатывает выражение!, а в противном случае срабатывает выражение2. Подобно логическим операторам AND и OR (&& и | | ), условный оператор гарантирует данный порядок вычисления своих операндов. Т.е. об- работано будет только одно из выражений, выражение! или выражение2. Применение условного оператора демонстрирует следующая программа. int i = 10, j =20, k = 30; // если i > j то maxVal - i иначе maxVal = j int maxVal - i > j ? i : j; Избегайте глубокого вложения условных выражений Чтобы присвоить переменной max самое большое из трех значений перемен- ных i, j и к, можно воспользоваться набором вложенных условных выражений, int max = i; if (j > max)
Глава 5. Выражения 191 max = j; if (к > max) max = к; Эквивалентное выражение с использованием условного оператора значительно проще, int max = i > j ? i : J к ? i к ? i i : к j : k; Применение условного оператора в выражении вывода Условный оператор имеет довольно низкий приоритет. Когда условный оператор используется в большом выражении, его, как правило, следует заключать в круглые скобки. Например, условный оператор нередко используют для отображения одного из значений, в зависимости от результата проверки условия. Отсутствие круглых скобок вокруг условного оператора в выражении вывода может привести к неожи- данным результатам, cout << (i < j ? i : j); // ok: отображает большее из значений j) ? i : j ; 3 ? i : J ; cout « (i cout << i II (i или j) // выводит 1 или О // ошибка: сравнивает объект cout с // переменной j типа int Второе выражение особенно интересно: здесь результат сравнения переменных i и j используется как операнд оператора <<. В зависимости от того, окажется ли резуль- татом сравнения выражения i < j значение true или false, будет отображено зна- чение 1 или 0. Оператор << возвращает объект cout, который и будет проверен в ус- ловии условного оператора. То есть второе выражение эквивалентно следующему, cout « (i < j); // отображает 1 или 0 cout ? i : j; // проверяет объект cout, а затем вычисляет II значение переменной i или j, в зависимости от // того, соответствует ли объект cout значению // true или false Упражнения раздела 5.7 Упражнение 5.20. Напишите программу, запрашивающую у пользователя пару чисел и сообщаю- щую, какие из них меньше. Упражнение 5.21. Напишите программу, которая перебирает элементы вектора типа vector<int> и заменяет значение каждого нечетного элемента его удвоенным значением. 5.8. Функция slzeof О Функция sizeof О возвращает значение типа size_t (раздел 3.5.2, стр. 128), соответствующее размеру в байтах (раздел 2.1, стр. 56) объекта или типа, указан- ного по имени. Результат выражения, использующего функцию sizeof О , явля- ется константой времени компиляции. Оператор sizeof применяется в одной из следующих форм.
192 Часть I. Основы sizeof (имятипа); sizeof (выражение); sizeof выражение; При применении оператора sizeof к выражению, возвращается размер типа, получаемого в результате вычисления переданного выражения. Sales_item item, *р; // три способа получить размер, необходимый для хранения объекта // класса Sales_item sizeof(Sales_item); // размер, необходимый для хранения объекта II класса Sales_item sizeof item; // размер типа элемента, аналог sizeof(Sales_item) sizeof *р; // размер типа, адрес объекта которого способен // содержать указатель р, аналог sizeof(Sales_item) Функция sizeof () не вычисляет передаваемое ей выражение. Например, указа- тель р в выражении sizeof *р вполне может содержать недопустимый адрес, по- этому обращение к значению указателя р недопустимо. Результат, возвращаемый функцией sizeof (), частично зависит от переданного ей типа. Результатом передачи функции sizeof () типа char (или выражения, резуль- тат которого имеет тип char) гарантированно будет 1. Результатом передачи функции sizeof О ссылочного типа будет объем памяти, необходимой для хранения объекта, ссылка на который передана. Результатом передачи функции sizeof () указателя, будет объем памяти, необ- ходимый для хранения указателя. Чтобы получить размер объекта, адрес которо- го содержит указатель, необходимо обращение к значению. Результат передачи функции si'zeof () массива эквивалентен произведению размера типа элемента на количество элементов в массиве. Поскольку функция sizeof () возвращает размер всего массива, узнав размер одного элемента можно вычислить их количество. // возвращает количество элементов массива ia int sz = sizeof(ia)/sizeof(*ia); Упражнения раздела 5.8 Упражнение 5.22. Напишите программу, выводящую размер каждого встроенного типа. Упражнение 5.23. Предскажите результат следующей программы и объясните его. Теперь запус- тите программу. Соответствует ли результат ожидаемому? Если нет, выясните, почему. int х[10]; int *р = х; cout << sizeof(х)/sizeof(*х) << endl; cout << sizeof(p)/sizeof(*p) << endl; 5.9. Оператор запятая (,) Оператор запятая (comma expression) используется в случаях, где ряд выраже- ний отделяется запятыми. Выражения обрабатываются слева направо. Результатом оператора запятая (, ) является правый операнд. Если правый операнд является
Глава 5. Выражения 193 1-значением, результат также будет 1-значением. Оператор запятая довольно часто применяется в цикле for. int ent = ivec.size(); // присвоить значения элементам size...l вектора ivec for(vector<int>::size_type ix = 0; ix != ivec.size(); ++ix, --ent) ivec[ix] = ent; Здесь выражения в заголовке цикла for увеличивают значение итератора ix и уменьшают значение целочисленной переменной ent. Значения итератора ix и пе- ременной ent изменяются при каждой итерации цикла. Пока проверка итератора ix проходит успешно, следующему элементу присваивается текущее значение пере- менной ent. Упражнения раздела 5.9 Упражнение 5.24. Программа в этом разделе подобна программе, где в вектор добавлялись эле- менты (стр. 188). Обе программы используют при создании значений элементов декремент счетчика. В этой программе использован префиксный декремент, а ранее использовался постфиксный. Объяс- ните, почему в одном случае был использован префиксный инкремент, а в другом постфиксный. 5.10. Вычисление составных выражений Выражения с несколькими операторами называются составными (compound expression). Результат составного выражения определяет способ группировки опе- рандов в отдельных операторах. Если сгруппировать операнды иным способом, ре- зультат окажется другим. Группировку операндов определяют приоритет и порядок выполнения операто- ров. То есть приоритет и порядок операторов выражения определяют последова- тельность, в которой обрабатываются операнды. Программисты могут изменить эти правила, заключая операторы составных выражений в круглые скобки. Приоритет определяет правила группировки операндов, а не порядок, в котором они вы- полняются. 5.10.1. Приоритет Результат выражения зависит от группировки составляющих его операторов. Например, при вычислении следующего выражения слева направо получится 2 0. 6+3*4/2+2; Вполне возможны и другие результаты: 9,14 и 3 6. В языке C++ результат со- ставит 14. Умножение и деление имеют более высокий приоритет, чем сложение. Их опе- ранды связаны с операторами жестче, чем операнды операторов сложения. Умноже- ние и деление имеют одинаковый приоритет. Операторы имеют также порядок выполнения, который определяет последовательность группировки операндов у операторов с одинаковым уровнем приоритета. Арифметические операторы имеют
194 Часть I. Основы левосторонний порядок, т.е. они группируются слева направо. Теперь вполне оче- видно, что приведенное выше выражение можно переписать следующим образом. Круглые скобки позволяют переопределить приоритет Для переопределения приоритета можно использовать круглые скобки. Выраже- ния в круглых скобках обрабатываются как отдельные модули, а во всех остальных случаях применяется обычные правила приоритета. Например, используя круглые скобки в предыдущем выражении можно принудительно получить любой из четы- рех возможных вариантов. // круглые скобки позволяют изменить стандартный приоритет // и порядок выполнения операторов cout << ((б + ((3 * 4) / 2)) + 2) « endl; // результат 14 // круглые скобки позволяют создать альтернативные группировки cout « (6+3) * (4/2+2) « endl; // результат 36 cout « ((6+3) *4) /2+2 << endl; // результат 20 cout <<6+3*4/ (2+2) << endl; // результат 9 Ранее уже не раз приводились примеры, когда правила приоритета влияли на правильность выполнения программ. Давайте в качестве примера рассмотрим вы- ражение, описанное в совете на стр. 188. *iter++; Приоритет оператора ++ выше приоритета оператора *. Это значит, что сначала выполняется часть iter++, а ее результат становится операндом оператора *. Таким образом, обращение к значению итератора iter происходит после его инкремента. Но если бы необходимо было увеличить значение, на которое указывает итератор iter, пришлось бы применить круглые скобки, чтобы явно указать свои намерения. (*iter)++; // увеличить значение, на которое указывает // итератор iter Круглые скобки указывают, что операнд * применяется к итератору iter. Те- перь как операнд оператора ++ в выражении используется часть *iter. В качестве другого примера напомним условие цикла while на стр. 185. while ((i = get_value()) != 42 { Круглые скобки в записи операции присвоения были необходимы, поскольку по- зволяли реализовать необходимое действие: присвоение переменной i значения, воз- вращаемого функцией get_value (). Затем это значение сравнивалось с числом 42. Если бы присвоение произошло после сравнения, то в переменной i оказался бы ре- зультат сравнения значения, возвращенного функцией get_value (), и числа 42. Это было бы значение 1 или 0 (true или false), в зависимости от результата сравнения. 5.10.2. Порядок Порядок определяет последовательность группировки операторов при одинако- вом уровне приоритета. Вопросы порядка весьма существенны и в некоторых других
Глава 5. Выражения 195 случаях. Например, правосторонний порядок оператора присвоения позволяет соз- давать составные операторы присвоения. ival - jval = kval = Ival // правосторонний порядок (ival = (jval - (kval = Ival))) // эквивалентная версия co скобками Это выражение сначала присваивает значение переменной Ival переменной kval, полученный результат присваивается переменной jval и, наконец, перемен- ной ival. Арифметические операторы, напротив, имеют левосторонний порядок. ival * jval / kval * Ival // левосторонний порядок (((ival * jval) / kval) * Ival) // эквивалентная версия co скобками В этом выражении, например, происходит умножение значений переменных ival и jval, затем деление результата на значение переменной kval и, наконец, умножение результата деления на Ival. Полный список операторов, упорядоченный по приоритетам, приведен в табл. 5.4. Таблица разделена на сегменты двойными линиями. Операторы в каждом сегменте имеют одинаковый приоритет, причем более высокий, чем у операторов в после- дующих сегментах. Например, префиксный оператор инкремента и оператор обра- щения к значению расположены в одном сегменте. Следовательно, они имеют оди- наковый приоритет, превосходящий приоритет арифметических операторов и опе- раторов сравнения. Более подробно некоторые из этих операторов рассматриваются в последующих главах. Таблица 5.4. Приоритет операторов Поря- док Оператор Действие Применение Описание Л • • • • Глобальная область види- мости : :имя (стр. 478) л • • • • Область видимости класса класс::имя (стр. 108) л • • • • Область видимости про- странства имен пространствоимен: г имя (стр. 102) л • Обращение к члену класса объект. член (стр. 47) л — > Обращение к члену класса указатель->член (стр. 189) л [] Индексирование переменная [выражение] (стр. 137) л о Вызов функции имя(список_выражений) (стр. 47) л о Создание типа тип(список_выражений) (стр. 489) п Ч" 4" Постфиксный инкремент 1-значение++ (стр. 187) п в — Постфиксный декремент 1-значение-- (стр. 187) п typeid Идентификатор типа typeid(тип) (стр. 807) п typeid Идентификатор типа вре- typeid(выражение) (стр. 807) имя_приведения<тип>(выражение) (стр. 209) Явное при- ведение мени выполнения Преобразование типов
196 Часть I. Основы Продолжение табл. 5.4 Поря- Оператор док Действие Применение Описание п sizeof Размер объекта sizeof выражение (стр. 191) П sizeof Размер типа sizeof(тип) (стр. 191) П ++ Префиксный инкремент ++1 -значение (стр. 187) П Префиксный декремент --1-значение (стр. 187) П Побитовый NOT -выражение (стр. 179) П ! Логический NOT !выражение (стр. 176) П Унарный минус -выражение (стр. 174) П + Унарный плюс +выражение (стр. 174) П * Обращение к значению ★выражение (стр. 144) П & Обращение к адресу ^.выражение (стр. 139) П О Преобразование типов ( тип) выражение (стр. 212) П new Создание объекта new тип (стр. 199) П delete Освобождение объекта delete выражение (стр. 201) П delete [] Освобождение массива delete [] выражение (стр. 161) л ->* Указатель на член класса указатель->* указатель_на_член (стр. 815) Л •* Указатель на член класса объект. ★ указатель_на_член (стр. 815) Л * Умножение выражение ★ выражение (стр. 173) Л / Деление выражение / выражение (стр. 173) Л % Деление по модулю (остаток) выражение % выражение (стр. 173) Л + Сумма выражение + выражение (стр. 173) Л Разница выражение - выражение (стр. 173) Л « Побитовый сдвиг влево выражение « выражение (стр. 179) Л » Побитовый сдвиг вправо выражение > > выражение (стр. 179) Л < Меньше выражение < выражение (стр. 176) Л <= Меньше или равно выражение < = выражение (стр. 176) Л > Больше выражение > выражение (стр. 176) Л >= Больше или равно выражение >= выражение (стр. 176) Л Равенство выражение == выражение (стр. 176) Л ! = Неравенство выражение ! = выражение (стр. 176) Л & Побитовый AND выражение & выражение (стр. 179) л Побитовый XOR выражение * выражение (стр. 179) л I Побитовый OR выражение | выражение (стр. 179) л && Логический AND выражение && выражение (стр. 176)
Глава 5. Выражения 197 Окончание табл. 5.4 Поря- Оператор док Действие Применение Описание л 11 Логический OR выражение | | выражение (стр. 176) П ?: УСЛОВНЫЙ оператор выражение ? выражение : (стр. 179) выражение П Присвоение 1-значение = выражение (стр. 184) п *=, /=, %=, Составные 1-значение += выражение (стр. 184) п +=, -=, операторы ит.д. (стр. 184) п <<=, >>=, присвоения (стр. 184) П &=, | =, (стр. 184) п throw Передача исключения throw выражение (стр. 242) Л Запятая выражение , выражение (стр. 192) Упражнения раздела 5.10.2 Упражнение 5.25. Используя данные табл. 5.4, расставьте скобки в следующих выражениях так, чтобы указать порядок группировки их операндов. (а) ! ptr == ptr->next (b) ch = buf[ bp++ ] != '\n‘ Упражнение 5.26. Последовательность выполнения операторов в выражениях предыдущего уп- ражнения несколько странная. Расставьте круглые скобки так, чтобы эти выражения выполнялись, как ожидается. Упражнение 5.27. Приоритет операторов не позволит откомпилировать следующее выражение. Используя табл. 5.4, объясните, почему это невозможно. Как устранить проблему? string s = "word"; // добавить символ 's' в конец строки s, если ее содержимое уже II не заканчивается символом 's' string pl = s + s[s.size() - 1] == 's' ? "" : "s"; 5.10.3. Порядок вычисления операндов Порядок вычисления операндов операторов && и | | уже был продемонстрирован в разделе 5.2 (стр. 176). В обоих случаях значение правого операнда вычислялось только тогда, когда он мог повлиять на результат всего выражения. Исходя из этого правила, можно написать следующий код. // обращение к значению итератора iter, если он не указывает // на конец вектора while (iter != vec.end() && *iter !- some_val) Другими операторами, которые гарантируют порядок обработки операндов, яв- ляются условный оператор ( ? :) и оператор запятой (,). Во всех остальных случаях порядок не является определенным. fl О * f2() ;
198 Часть I. Основы Например, в этом выражении вполне понятно, что следует вызвать функции f 1 () и f 2 () и получить результат, прежде чем умножение будет возможно. В конце концов, именно их результаты перемножаются между собой. Однако нет никакого способа выяснить1, будет ли сначала вызвана функция f 1 (), а затем f 2 () или наоборот. Однако порядок вычисления операндов зачастую не имеет значения. Это может быть су- 3$^ 1 щественно только в тех случаях, когда операнды обращаются и изменяют значения тех же ^^97 объектов. Порядок вычисления операндов имеет значение только тогда, когда одна из час- тей выражения изменяет значение операнда, используемого во второй. // Упс! Язык не определяет порядок обработки if (ia[index++] < ia[index]) Поведение этого выражения непредсказуемо. Дело в том, что левый и правый операнды оператора < используют одну переменную index, причем левый операнд изменяет ее значение. Предположим, что переменная index содержит значение 0. Компилятор может создать исполняемый код, который обрабатывает это выражение одним из двух следующих способов. if (ia[0] < ia[0]) // если первым обрабатывается правый операнд if (ia[0] < ia[l]) // если первым обрабатывается левый операнд Можно предположить, что программист рассчитывал на выполнение в первую очередь левого операнда, который увеличит значение переменной index. В этом случае сравнивались бы значения элементов ia [0] и ia [1]. Однако язык не гаран- тирует порядка вычисления слева направо. Фактически подобные выражения не- предсказуемы. После компиляции вполне может оказаться, что первым обрабатыва- ется правый операнд, тогда элемент ia [0] будет сравнивается сам с собой. Совет. Манипулирование составными выражениями Новички в языках С и C++ зачастую испытывают затруднения при изучении порядка вычисления и правил приоритета. Непонимание последовательности выполнения вы- ражений и обработки операндов является причиной ошибок. Кроме того, подобные ошибки очень трудно найти, ведь при чтении кода программы они незаметны, если разработчик не понимает правил приоритета. Здесь могут пригодиться два эмпирических правила. 1. В сомнительных случаях заключайте выражения в круглые скобки, чтобы явно сгруппировать операнды в соответствии с логикой программы. 2. При изменении значения операнда, не используйте этот операнд в другом мес- те того же оператора. Если это все же необходимо, разделите выражение на от- дельные операторы так, чтобы операнд изменялся в одном операторе, а затем использовался в следующем. 1 Выяснить не сложно (достаточно вывести внутри тела функции некое сообщение на эк- ран), но сложно гарантировать одинаковый порядок выполнения во всех случаях. Дело в том, что при создании исполняемого кода компилятор оптимизирует его. В результате последователь- ность вызовов функций может быть разной при разных обстоятельствах. — Примеч. ред.
Глава 5. Выражения 199 Важнейшим исключением из второго правила является случай, когда использование результата части выражения, изменяющего операнд, вполне безопасно. Например, в выражении *++iter инкремент изменяет значение итератора iter, а измененное значение используется как операнд оператора *. В этом и подобных выражениях по- рядок обработки операндов не является проблемным. Но в больших выражениях те части, которые изменяют операнд, должны обрабатываться в первую очередь. Такой подход не создает никаких проблем и применяется достаточно часто. Не используйте операторы инкремента и декремента для одного объекта более, чем в двух операторах того же выражения. Предыдущий оператор сравнения двух элементов массива можно переписать следующим безопасным машинно-независимым способом, if (ia[index] < ia[index +1]) { // действия ++index; Теперь ни один из операндов не может повлиять на значение другого. Упражнения раздела 5.10.3 Упражнение 5.28. За исключением логических операторов AND и OR, порядок вычисления парных операторов остается неопределенным, что обеспечивает компилятору свободу оптимизации кода. Таким образом, потенциальный источник проблем при использовании языка является платой за эффективность его реализации. Не слишком ли высока эта цена? Объясните свой ответ. Упражнение 5.29. Предположим, что указатель ptr содержит адрес объекта, класс которого имеет член ival типа int. Вектор vec содержит элементы типа int, а переменные jval и kvai имеют тип int. Объясните поведение каждого наследующих выражений. Какой из них (если он есть) некорректен? Почему? Как его можно исправить? (a) ptr->ival !- 0 (b) ival != jval < kval (с) ptr != 0 && *ptr++ (d) ival++ && ival (e) vec[ival++] <= vec[ival] 5.11. Операторы new и delete В разделе 4.3.1 (стр. 159) было продемонстрировано применение операторов new и delete для создания и освобождения массивов в динамической памяти. Эти же операторы можно использовать для создания и освобождения в динамической памя- ти отдельных объектов. При определении переменной, указывают ее тип и имя. При создании объекта в динамической памяти, указывают только тип, но не имя объекта. Вместо этого опе- ратор new возвращает указатель на только что созданный объект. Этот указатель и используется для доступа к объекту. int i; // именованная, неинициализированная II переменная типа int
200 Часть I. Основы int *pi = new int; //pi указывает на созданный в динамической !/ памяти неименованный, неинициализированный // объект типа int В данном выражении оператор new создает в динамической памяти один объект типа int и возвращает адрес этого объекта. Полученный адрес используется для инициализации указателя pi. Инициализация объектов в динамической памяти Созданные в динамической памяти объекты могут быть инициализированы ана- логично обычным переменным. int i (1024); // int *pi = new int(1024); // string s(10, '9'); // string *ps = new string(10, значением i является 1024 значением объекта, на который указывает указатель pi, является 1024 значением s является "9999999999" '9'); // *ps содержит "9999999999" Объекты, созданные в динамической памяти, перед использованием следует ини- циализировать (раздел 2.3.3, стр. 70). Когда в выражении, использующем оператор new, присутствует инициализирующее значение, выделяемая под объект область памяти будет сразу заполнена этим значением. В случае, если указателем будет pi, только что созданный объект инициализируется значением 1024. Объект, адрес ко- торого получает указатель ps, инициализируется строкой из 10 девяток. Инициализация объектов в динамической памяти значениями по умолчанию Если объект в динамической памяти при создании не инициализирован значени- ем явно, его значение устанавливается аналогично переменной, определенной внут- ри функции (раздел 2.3.4, стр. 73). Объект класса инициализирует стандартный кон- структор, а объект встроенного типа остается неинициализированным. string *ps = new string(10, '9'); // *ps содержит "9999999999" int *pi = new int; // pi указывает на неинициализированную II переменную типа int Поскольку неинициализированный объект содержит хоть и вполне допустимое, но случайное значение, его всегда имеет смысл инициализировать. Подобно тому, как практически всегда имеет смысл инициализировать обычные пе- ременные, объекты в динамической памяти также почти всегда следует инициализи- ровать. Объект, созданный в динамической памяти, также может иметь инициализи- рующее значение (раздел 3.3.1, стр. 116). string *ps = new string(); // инициализируется пустой строкой int *pi = new int(); // pi указывает на переменную типа int, // инициализированную значением 0 cis *рс = new cis(); // рс указывает на объект типа cis, // инициализированный значением по умолчанию Чтобы указать на необходимость инициализировать только что созданный в ди- намической памяти объект значением ho умолчанию, достаточно расположить после его имени и типа пару пустых круглых скобок. Это обеспечит инициализацию, но не предоставит определенного исходного значения. В случае применения класса, обла-
Глава 5. Выражения 201 дающего собственными конструкторами (например string), запрос на инициали- зацию значением по умолчанию не имеет никаких последствий, поскольку объект и без него инициализируется стандартным конструктором. В случае встроенных типов или типов, у которых нет стандартных конструкторов, различие весьма существенно, int *pi - new int; // pi указывает на неинициализированную // переменную типа int int *pi = new int(); II pi указывает на переменную типа int // инициализированную по умолчанию значением 0 В первом случае переменная типа int неинициализирована, а во втором — ини- циализирована по умолчанию значением 0. Круглые скобки в выражении инициализации значением, по умолчанию должны следо- вать за именем типа, а не переменной. Подробности в разделе 7.4 (стр. 277). int х(); // нет значения, инициализирующего х Здесь объявлена функция по имени х без аргументов, которая возвращает тип int. Исчерпание памяти Хотя современные машины обладают весьма большими объемами памяти, воз- можность ее исчерпания вовсе не исключена. Если программа уже использовала всю доступную ей память, обращение к оператору new потерпит неудачу. В этом случае использующее его выражение не сможет получить запрошенный объем памяти и пе- редаст исключение по имени bad_alloc. Более подробная информация о передаче исключений приведена в разделе 6.13 (стр. 241). Удаление объектов, созданных в динамической памяти Когда использование объекта завершено, занимаемую им область в динамической памяти необходимо освободить явно. Для этого оператор delete применяется к указателю, содержащему адрес подлежащего освобождению объекта. delete pi; Здесь освобождается память, занятая объектом типа int, адрес которого содер- жит указатель pi. Нельзя применять оператор delete к указателю, который содержит адрес области па- мяти, не зарезервированной оператором new. Результат удаления указателя, содержащего адрес области памяти, не зарезерви- рованной оператором new, непредсказуем. Ниже приведены примеры корректных и некорректных операторов delete. int i; int *pi = &i; string str = "dwarves"; double *pd = new double(33); delete str; // ошибка: str не является объектом // в динамической памяти delete pi; // ошибка: pi содержит адрес локальной переменной delete pd; // ok
202 Часть I. Основы Следует заметить, что компилятор может отказаться компилировать строку, пы- тающуюся удалить переменную str. Компилятору известно, что str не является указателем и может обнаружить эту ошибку во время компиляции. Вторая ошибка коварней: как правило, компиляторы не способны выяснить, адрес какого именно объекта содержит указатель. Большинство компиляторов посчитают этот код кор- ректным, хотя на самом деле он содержит ошибку. Удаление нулевого указателя Вполне допустимо удаление указателя, значением которого является нуль, хоть это и не имеет никакого смысла. int * ip = 0; delete ip; // ok: удалить нулевой указатель вполне возможно Язык гарантирует безопасность удаления нулевого указателя. Переприсвоение значения указателю после оператора delete После следующего выражения указатель р становится неопределенным, delete р; Хоть указатель р и становится неопределенным, на многих машинах он все еще содержит адрес объекта, на который указывал. Но эта область памяти уже освобож- дена, вернулась в пул свободной памяти и, возможно, зарезервирована для другого объекта, поэтому указатель р совершенно недопустим. После удаления указатель называется потерянным (dangling pointer), диким (wild) или паразитным (stray pointer). Такой указатель содержит адрес области па- мяти, которая уже не содержит прежний объект. Потерянный указатель может стать причиной серьезной и трудно обнаруживаемой ошибки. Чтобы обезопасить потерянный указатель, достаточно присвоить ему значение о. Размещение и освобождение константных объектов в динамической в памяти Создание константных объектов в динамической памяти вполне допустимо. // создать и инициализировать константный объект const int *pci = new const int(1024); Подобно любой другой константе, константу в динамической памяти следует инициализировать в момент создания, поскольку впоследствии нельзя будет изме- нить ее значение . Полученное в результате предыдущего выражения значение будет новым указателем на константу типа int. Подобно адресу любого другого кон- стантного объекта, адрес, возвращенный оператором new, может быть присвоен только указателю на константу. Размещенный в динамический памяти константный объект типа класса, для ко- торого определен стандартный конструктор, может быть инициализирован неявно. // создать инициализируемую по умолчанию константу (пустую строку) const string *pcs = new const string;
Глава 5. Выражения 203 В этом выражении объект, на который указывает указатель pcs, неявно инициа- лизируется пустой строкой. Объекты встроенного типа, или типа класса, не обла- дающего стандартным конструктором, следует инициализировать явно. Внимание! Манипулирование динамической памятью может привести к ошибкам С динамически распределяемой памятью связаны три следующие наиболее распро- страненные ошибки. 1. Ошибочное удаление указателя на область в динамически распределяемой па- мяти. Это не позволяет освободить область и вернуть ее в пул свободной памя- ти. Невозможность освободить область динамически распределяемой памяти приводит к ее ‘ утечке ’. Обнаружить утечку памяти довольно трудно, посколь- ку зачастую она остается незаметной до тех пор, пока не возникнут условия, при которых приложение исчерпает всю память и зависнет. 2. Чтение или запись в объект после его удаления. Эта ошибка может произойти в случае, когда после удаления объекта указателю не присвоено значение 0. 3. Применение оператора delete к той же области памяти дважды. Эта ошибка может произойти в случае, когда адрес одного объекта в динамической памяти содержат два указателя. Оператор delete освободит занимаемую объектом область динамической памяти при обращении по одному из указателей. По- пытка использования второго указателя для повторного освобождения той же области динамической памяти будет серьезной ошибкой. Эти ошибки при манипулировании динамически распределяемой памятью .значи- тельно проще допустить, чем обнаружить и устранить. Освобождение константного объекта Хотя значение константного объекта не может быть изменено, сам объект вполне может быть удален. Подобно любым другим динамическим объектам, константный динамический объект вполне может быть освобожден при помощи указателя, кото- рый содержит его адрес, delete pci; // ок: удаление константного объекта Хотя операнд оператора delete является указателем на константу типа int, это выражение вполне допустимо и освобождает область памяти, адрес которой содер- жит указатель pci. Упражнения раздела 5.11 Упражнение 5.30. Какие из следующих выражений (если они есть) являются недопустимыми или ошибочными? (a) vector<string> svec(10); (b) vector<string> *pvecl = new vector<string>(10); (c) vector<string> **pvec2 = new vector<string>[10]; (d) vector<string> *pvl = &svec; (e) vector<string> *pv2 = pvecl;
204 Часть I. Основы (f) delete svec; (g) delete pvecl; (h) delete [] pvec2; (i) delete pvl; (j) delete pv2; 5.12. Преобразование типов Тип операнда (операндов) определяет допустимость выражения и тип его ре- зультирующего значения, если выражение допустимо. Однако в языке C++ некото- рые типы взаимосвязаны. Объект и значение такого типа можно использовать там, где ожидается операнд взаимосвязанного типа. Типы считаются взаимосвязанными, если между ними возможно взаимное преобразование (conversion). Давайте рассмотрим пример, где переменной ival типа int присваивается зна- чение 6. int ival = 0; ival = 3.541 +3; // обычно приводит к предупреждению // при компиляции Здесь операндами оператора суммы являются значения двух разных типов: 3.541 — литерал типа double, а 3 — литерал типа int. Вместо попытки сложить значения двух разных типов, компилятор C++ предпримет ряд преобразований и приводит сначала операнды к общему типу. Эти преобразования осуществляются компилятором автоматически, без вмешательства программиста (а иногда и без его ведома). Поэтому такое преобразование типов называют неявным (implicit type conversion). Преобразования между встроенными арифметическими типами осуществляются по возможности точно. Чаще всего, если в выражении используются целочисленные значения и значения с плавающей запятой, целое число преобразуется в число с пла- вающей запятой. В рассматриваемом примере целочисленное значение 3 преобразу- ется в тип double. После сложения получается результат 6.541 типа double. На следующем этапе полученное значение типа double следует присвоить пере- менной ival типа int. В случае присвоения, тип левого операнда доминирует, по- скольку его невозможно изменить. Когда типы левого и правого операндов операто- ра присвоения не совпадают, значение правой стороны приводится к типу левой. Здесь значение типа double преобразуется в значение типа int. В ходе этого пре- образования значение типа double усекается до значения типа int, а десятичная часть отбрасывается. Таким образом, значение 6.541 преобразуется в значение 6, которое и присваивается переменной ival. Поскольку преобразование типа double в тип int может привести к потере точности, большинство компиляторов выдает по этому поводу предупреждение. Компилятор, использованный авторами для провер- ки примеров этой книги, в частности, выдавал следующее предупреждение. warning: assignment to 'int' from 'double' (Предупреждение: присвоение переменной типа ' int' значения типа ' double') Чтобы разобраться в неявном преобразовании, следует узнать, когда они проис- ходят и какие из них возможны.
Глава 5. Выражения 205 5.12.1. Когда происходит неявное преобразование типов Компилятор применяет преобразования для объектов встроенных типов и клас- сов по мере необходимости. Неявное преобразование типов происходит в следую- щих ситуациях. В выражениях с операндами смешанных типов происходит преобразование в общий тип. int ival; double dval; ival >= dval // ival преобразуется в double При использовании в условии выражения, требующего преобразования в тип bool. int ival; if (ival) while (cin) // ival преобразуется в bool // cin преобразуется в bool В первом операнде (условии) условного оператора (? :), а также в операндах логических операторов NOT (!), AND (&&) и OR ( | | ). Условия также присутст- вуют в операторах if, while, for и do. . .while. (Более подробно оператор do. . . while рассматривается в главе 6, “Операторы”. В выражениях, используемых для инициализации переменных или присвоения им значений. int ival = 3.14; // 3.14 преобразуется в int int *ip; ip = 0; // int 0 преобразуется в нулевой указатель на тип int Кроме того, как будет продемонстрировано в главе 7, “Функции”, неявное преоб- разование происходит также при вызове функции. 5.12.2. Арифметические преобразования В языке C++ определен набор преобразований для встроенных типов. Среди них наиболее популярными являются арифметические преобразования (arithmetic conversion), которые обеспечивают преобразование двух операндов парного арифме- тического или логического оператора в общий тип перед выполнением. Этот общий тип и станет типом результата выражения. Правила преобразования определяют иерархию типов, в которые преобразуются операнды. Как правило, это самый широкий тип в выражении. Это правило преобра- зования позволяет максимально сохранить точность значений в выражении с не- сколькими типами. Например, если один операнд имеет тип long double, то и другой преобразуется в тип long double, независимо от типа, который он имел. К самым простым преобразованиям относится поддержка целочисленных дей- ствий (integral promotion). Каждый из целочисленных типов, размер которых меньше чем, у типа int (а именно char, signed char, unsigned char, short и unsigned short), преобразуется в тип int, поскольку он способен содержать все возможные значения таких типов. В противном случае значение преобразуется в тип unsigned int. Когда значения типа bool преобразуются в тип int, значение false становится нулем, а значение true — единицей.
206 Часть I. Основы Преобразования знаковых и беззнаковых типов Когда в выражении используется беззнаковое значение, правила преобразования предписывают сохранить этот тип. Преобразования, применимые для беззнаковых операндов, зависят от относительных размеров целочисленных типов данных на конкретной машине. Следовательно, такие преобразования зависимы от конкрет- ной машины. В выражениях, использующих значения типов short и int, значения типа short преобразуются в тип int. В выражениях с использованием значений типа unsigned short, преобразование в тип int происходит в случае, если размер типа int достаточно велик, чтобы вместить любые значения типа unsigned short. В про- тивном случае оба операнда преобразуются в тип unsigned int. Например, если размер типа short соответствует половине размера машинного типа word, а размер типа int — равен размеру типа word, любое беззнаковое значение будет приведено к типу int. На такой машине тип unsigned short преобразуется в тип int. Аналогичные преобразования происходят с операндами типа long и unsigned int. Операнд типа unsigned int преобразуется в тип long, если размер типа long на конкретной машине достаточно велик, чтобы содержать все возможные значения типа unsigned int. В противном случае оба операнда преобразуются в тип unsigned long. На 32-битовой машине размер типов long и int обычно совпадает и равен раз- меру машинного типа word. На таких машинах типы unsigned int и long преоб- разуются в выражениях в тип unsigned long. Преобразования для выражений, содержащих знаковый и беззнаковый тип int, могут оказаться несколько неожиданными. В таких выражениях знаковое значение преобразуется в беззнаковое. Например, если сравнивается значение обычного типа int и типа unsigned int, тип int сначала преобразуется в тип unsigned int. Если переменная типа int содержит отрицательное значение, результат преобразу- ется так, как было описано разделе 2.1.1 (стр. 58), со всеми сопутствующими про- блемами, обсуждаемыми там же. Концепция арифметических преобразований Арифметические преобразования лучше всего изучать на большом количестве примеров. В большинстве следующих примеров тип операндов выражения приво- дится к используемому в выражении типу самого большого размера, либо тип право- го операнда приводится к типу левого операнда. bool flag; short sval; int ival; long Ival; float fval; 3.14159L + 'a'; dval + ival; dval + fval; ival = dval; flag = dval; eval + fval; char eval; unsigned short usval; unsigned int uival; unsigned long uival; double dval; // приводит 'а' к int, а затем к long double // ival преобразуется в double // fval преобразуется в double I/ dval преобразуется в int (с усечением) // если dval содержит 0, flag получит false, II а в противном случае - true II eval преобразуется в int, затем int // преобразуется во float
Глава 5. Выражения 207 sval eval ival usval eval ; Ival; ulval; ival ; uival + Ival; // sval и eval преобразуется в int // eval преобразуется в long // ival преобразуется в unsigned long // преобразование зависит от соотношения // размеров типов unsigned short и int // преобразование зависит от соотношения // размеров типов unsigned int и long В первом выражении суммы, символьная константа ' а' имеет тип char, кото- рый, как известно из раздела 2.1.1 (стр. 57), является числовым значением. Числовое значение, которому соответствует символ ' а', зависит от используемого машиной набора символов. На машине авторов, где установлен набор символов ASCII, симво- лу 'а' соответствует число 97. При добавлении символа 'а' к значению типа long double, значение типа char преобразуется в тип int, а затем в тип long double. Это преобразованное значение и суммируется со значением типа long double. Интересны также два последних случая, где происходит преобразование беззнако- вых значений. 5.12.3. Другие неявные преобразования Преобразование указателя В большинстве случаев применения массива, его имя автоматически преобразу- ется в указатель на первый элемент. int ia[10]; // массив из 10 целых чисел int* ip - ia; // преобразовать ia в указатель на первый элемент Имя массива не преобразуется в указатель в случаях, когда оно является операн- дом оператора обращения к адресу (&), аргументом функции sizeof () или при ис- пользовании массива для инициализации ссылки на массив. Определение ссылки (или указателя) на массив рассматривается в разделе 7.2.4 (стр. 266). Существует еще две разновидности преобразования указателя: указатель на лю- бой тип данных может быть преобразован в тип void, а постоянное целочисленное значение 0 может быть преобразовано в любой тип указателя. Преобразование в тип bool Арифметические значения и значения указателей могут быть преобразованы в значения типа bool (логические). Если указатель или арифметическая перемен- ная содержит значение 0, в результате преобразования получится логическое значе- ние false, а в любом другом случае — true. if (ср) /* while (*ср) // true, если ср не нуль */ // обращение к значению указателя ср и // преобразование полученного значения // типа char в тип bool В условии оператора i f любое отличное от нуля значение указателя ср преобра- зуется в значение true. В условии оператора while происходит обращение к значе- нию указателя ср на тип char. Если в результате будет получен нулевой символ, он преобразуется в значение false, а если любой другой — в значение true.
208 Часть I. Основы Преобразования арифметических и логических типов Значение арифметического типа может быть преобразовано в логическое (bool), а логическое значение — в целочисленное. Когда значение арифметического типа преобразуется в значение логического типа, нуль рассматривается как false, а лю- бое другое значение — как true. И наоборот, логическое значение true преобразу- ется в арифметическое значение 1, а значение false в 0. bool b = true; int ival = b; / double pi = 3.14; bool b2 = pi; / pi = false; / ival -= 1 Ь2 получит true pi == 0 Преобразования и типы перечислений Перечисления и перечислители (раздел 2.7, стр. 84) могут быть автоматически преобразованы в целочисленный тип. В результате они применимы там, где необхо- димы целочисленные значения, например в арифметических выражениях. // point2d = 2, point2w - 3, point3d - 3, point3w - 4 enum Points { point2d = 2, point2w, point3d = 3, point3w }; const size_t array_size = 1024; // ok: pt2w преобразуется в int int chunk_size = array_size * pt2w; int array_3d = array_size * point3d; Тип, к которому приводится объект типа enum или перечислитель, определен машиной и зависит от значения самого большого перечислителя. Независимо от значения перечисление или перечислитель всегда преобразуются, по крайней мере, в тип int. Если тип int недостаточен для самого большого значения перечислите- ля, происходит преобразование в тип самого маленького размера (размер которого превосходит размер типа int), способного содержать значение перечислителя (unsigned int, long или unsigned long). Преобразование в константу Неконстантный объект может быть преобразован в константный. Это происходит при использовании неконстантного объекта для инициализации ссылки на констан- ту. Можно также привести адрес неконстантного объекта (или преобразовать некон- стантный указатель) к указателю на соответствующий константный тип. • • int i; const int ci = 0; const int = i; // ok: преобразовать неконстанту в ссылку II на константу типа int const int *р = &ci; // ok: преобразовать адрес неконстанты // в адрес константы Преобразования, определенные для библиотечных типов Для классов могут быть определены преобразования, которые компилятор спосо- бен применять автоматически. На настоящий момент было описано только одно очень важное преобразование библиотечного типа, то которое происходит при чте- нии объекта класса istream в условии оператора while.
Глава 5. Выражения 209 string s; while (cin Здесь происходит неявное преобразование, определенное в библиотеке ввода- вывода. В условии этого типа выполняется выражение cin >> s, а следовательно, происходит чтение объекта cin. Результат этого выражения зависит от успеха или неуспеха операции чтения. Исходя из условия цикла while ожидается значение типа bool, но получается значение типа istream. Здесь значение типа istream преобразуется в значение типа bool. Результат преобразования позволяет проверить состояние потока. Если последняя попытка чтения из объекта cin завершилась успехом, состояние потока преобразуется в логическое значение true и условие цикла while выполняется. Если последняя попытка потерпит неудачу (например, если был достигнут конец файла), результатом преобразования будет логическое значение false и условие цикла while выполняться перестанет. Упражнения раздела 5.12.3 Упражнение 5.31. Исходя из определений переменных на стр. 206 объясните, какие преобразо- вания происходят при вычислении следующих выражений, (a) if (fval) (b) dval = fval + ival; (c) dval + ival + eval; В случае выражений из нескольких операторов, не забывайте учитывать порядок их выполнения. 5.12.4. Явное преобразование Явное преобразование (explicit conversion) называют также приведением типов (cast). Для него предусмотрен набор следующих именованных операторов приведе- ния: static_cast, dynamic_cast, const_cast и reinterpret_cast. Иногда приведение весьма необходимо, но крайне опасно. 5.12.5. Когда может пригодиться приведение типов Иногда необходимость явного приведения обусловлена переопределением обыч- ного, стандартного преобразования. В этом составном операторе присвоения происходит преобразование содержимо- го переменной ival в тип double. Это позволит умножить его на содержимое пе- ременной dval. Затем полученный результат типа double усекается до типа int, чтобы он мог быть снова присвоен переменной ival. Ненужного преобразования
210 Часть I. Основы значения переменной ival в значение типа double вполне можно избежать, если применить явное приведение переменной dval к типу int. ival *= static cast<int> (dval); // преобразование dval в int Еще одной причиной явного приведения является выбор вполне определенного преобразования на случай, когда возможно несколько способов преобразования. Бо- лее подробная информация по этой теме приведена в главе 14, “Перегрузка операто- ров и преобразования”. 5.12.6. Именованные операторы приведения Общий синтаксис именованной формы приведения имеет следующий вид. имя - прив едениж тип> (выражение) ; Часть имя - приведения — это один из возможных операторов приведения: static_cast, const_cast, dynamic_cast или reinterpret_cast. Часть тип — это тип результата преобразования, а выражение — значение, которое следует при- вести. Тип приведения определяет конкретный вид преобразования, которое выпол- няется в выражении. Оператор dynamic_cast Оператор dynamic_cast обеспечивает идентификацию объектов во время вы- полнения по указателю или ссылке. Более подробная информация об операторе dynamic_cast приведена в разделе 18.2 (стр. 804). Оператор const_cast Оператор const_cast, как и следует из его имени, приводит выражение к кон- стантному типу. Допустим, что существует функция по имени string_copy (), ко- торая должна лишь читать, но не записывать один параметр: указатель на тип char. Если существует вероятность доступа к коду, желательно было бы передавать кон- стантный указатель на тип char, но если это невозможно, остается организовать пе- редачу константы функции string_copy () при помощи оператора const_cast. const char *pc_str; char *pc = string_copy(const_cast<char*>(pc_str)); Для приведения констант применяется лишь оператор const_cast. Примене- ние любой другой из трех форм приведения в данном случае привело бы к ошибке при компиляции. Аналогично, ошибка при компиляции произойдет в случае исполь- зования оператора const_cast в записи, которая осуществляет любые другие пре- образования типов, отличные от создания или удаления константы. Оператор static_cast Любое преобразование типов, которое компилятор выполняет неявно, может быть задано явно при помощи оператора static_cast. double d = 97.0; // явное указание приведения типа char ch = static cast<char>(d);
Глава 5. Выражения 211 Приведение такого типа осуществляется при присвоении значения большего арифметического типа переменной меньшего типа. В этом случае не исключена по- теря точности результирующего значения. По поводу присвоения значения больше- го арифметического типа переменной меньшего типа компиляторы зачастую выдают предупреждение. Когда приведение осуществляется явно, предупреждений не будет. Оператор static_cast используется также при выполнении преобразований, которые компилятор не может осуществить автоматически. Например, его можно применить для возвращения значения указателя, которое было сохранено в указате- ле типа void (раздел 4.2.2, стр. 143). void* р = &d; // ок: адрес любого объекта может быть сохранен в // указателе типа void // ok: преобразует void обратно, в исходный тип указателя double *dp = static_cast<double*>(р); После сохранения адреса в указателе типа void, можно впоследствии использо- вать оператор static_cast и привести указатель к его исходному типу, что позво- лит сохранить значение указателя. То есть результат приведения будет равен перво- начальному значению адреса. Оператор reinterpret_cast Оператор reinterpret_cast осуществляет низкоуровневую переинтерпрета- цию битовой схемы его операндов. Оператор reinterpret_cast является жестко машинно-зависимым. Чтобы безо- пасно использовать оператор reinterpret_cast, следует хорошо понимать, как именно реализованы используемые типы, а также то, как компилятор осуществляет приведение. Давайте рассмотрим следующий пример приведения. int *ip; char *рс = reinterpret_cast<char*>(ip); В подобном случае разработчик должен помнить, что фактическим типом объек- та, адрес которого содержит указатель рс, является int, а не символьный массив. Любая попытка применения указателя рс там, где необходим обычный символьный указатель, вероятнее всего, потерпит неудачу именно во время выполнения. Напри- мер, его использование для инициализации объекта типа string, как в следующем случае, приведет к весьма необычному поведению во время выполнения. string str(pc); Использование указателя рс для инициализации объекта типа string — это хо- роший пример, который служит для демонстрации того, почему явные приведения отнюдь небезопасны. Проблема заключается в том, что при изменении типа компи- лятор не выдаст никаких предупреждений или сообщений об ошибке. При инициа- лизации указателя рс адресом типа int, компилятор не выдаст ни предупреждения, ни сообщения об ошибке, поскольку имеет место явное преобразование. Однако лю- бое последующее применение указателя рс подразумевает, что он содержит адрес значения типа char. Компилятор не способен выяснить, адрес какого именно значе- ния фактически хранит указатель типа int. Таким образом, инициализация строки
212 Часть I. Основы str при помощи указателя рс вполне правомерна, хотя в данном случае абсолютно бессмысленна, если не хуже! Отследить причину такой проблемы иногда чрезвы- чайно трудно, особенно если приведение указателя ip к рс происходит в одном файле, а использование указателя рс для инициализации объекта класса string — в другом. Совет. Избегайте приведения типов Используя приведение, программист отключает или нарушает обычны! порядок кон- троля соответствия типов (раздел 2.3, стр. 66). Авторы настоятельно рекомендуют из- бегать приведения типов, поскольку в правильно спроектированной программе, по их мнению, необходимости в приведении типов не возникает. Это утверждение особенно актуально при использовании оператора reinterpret_ cast. Такие приведения всегда опасны. Аналогично, применение оператора const_cast почти всегда свидетельствует о плохой проработке проекта. В правильно разработан- ных системах приведение констант применяться не должно. Другие способы приведе- ния типов, static_cast и dynamic_cast, относительно безопасны, но необходи- мость в них возникает не часто. Каждый раз, когда в коде применяется приведение ти- пов. имеет смысл остановиться и подумать о том, а нельзя ли получить тот же результат другим способом, избежав приведения. Если приведение все же неизбежно, имеет смысл принять меры, позволяющие снизить вероятность возникновения ошиб- ки, т.е. ограничить область видимости, в которой используется приведенное значение, а также хорошо задокументировать все подобные случаи. 5.12.7. Приведение типов в старом стиле До появления именованных операторов приведения, явное приведение типов осуществлялось при заключении типа в круглые скобки. char *рс = (char*) ip; Результат этого выражения аналогичен приведению типов с использованием оператора reinterpret_cast. Однако такая форма приведения значительно ме- нее наглядна, что существенно затрудняет поиск возможных ошибок. Стандарт C++ предоставил именованные операторы приведения, которые более наглядны и позволяют программисту точнее выражать свои намерения, когда при- ведение неизбежно. Например, примененный не к указателю оператор static_ cast или const_cast может быть безопасней оператора reinterpret_cast. В ре- зультате программист (а также его коллеги, работающие с кодом программы) могут яснее идентифицировать потенциальный уровень риска в каждом случае явного приведения типа в коде. /О' Хотя стандартный компилятор C++ и поддерживает старый стиль записи приведе- шь^ _ ния, авторы настоятельно рекомендуют использовалось его при создании только та- Аколшк)уем кого кода> котоРЬ|й будет компилироваться либо компилятором языка С, либо весьма ~ устаревшим компилятором, выпущенным до появления стандарта C++. Существуют две формы записи приведения в старом стиле. type (ехрг); // форма записи приведения в стиле функции (type) ехрг; // форма записи приведения в стиле языка С
Глава 5. Выражения 213 В зависимости от используемых типов, приведение старого стиля срабатывает аналогично операторам const_cast, static_cast или reinterpret_cast. В случаях, где используются операторы static_cast или const_cast, приведение типов в старом стиле позволяет осуществить аналогичное преобразование. Но если ни один из подходов не допустим, то приведение старого стиля срабатывает анало- гично оператору reinterpret_cast. Например, используя форму записи старого стиля можно переписать выражения приведения из предыдущего раздела следую- щим образом (хоть и менее понятно). int ival; double dval; ival += int (dval); const char* pc_str; string_copy((char*)pc_str); int *ip; char *pc = (char*)ip; // static_cast преобразует double в int // const_cast - приведение к const II reinterpret_cast интерпретирует int* // как char* Форма записи приведения старого стиля поддерживается в языке C++ для совместимости с программами прежних версий, написанных для устаревших, еще нестандартных компиляторов C++, а также для обеспечения совместимости с языком С. Упражнения раздела 5.12.7 Упражнение 5.32. Предположим, что существует следующий набор определений. char eval; int ival; unsigned int ui; float fval; double dval; Укажите случаи неявного преобразования типов (если они есть). (a) eval = 'а' +3; (b) fval = ui - ival * 1.0; (с) dval - ui * fval; (d) eval = ival + fval + dval; Упражнение 5.33. Предположим, что существует следующий набор определений. int ival; double dval; const string *ps; char *pc; void *pv; Перепишите каждый из следующих случаев приведения с использованием именованной формы записи. (a) pv = (void*)ps; (b) ival = int(*pc); (c) pv = &dval; (d) pc = (char*) pv; Резюме Язык C++ предоставляет богатый набор операторов, применимых к значениям встроен- ных типов. Кроме того, он допускает перегрузку операторов, что позволяет разработчику за- давать специальное поведение операторов для объектов класса. Определение операторов для собственных типов рассматривается в главе 14, “Перегрузка операторов и преобразования”. Чтобы разобраться в составных выражениях (содержащих несколько операторов), необ- ходимо выяснить приоритет и порядок вычисления операторов. Каждый оператор имеет при- оритет определенного уровня и порядок вычисления операндов. Приоритет определяет груп- пировку операторов в составном выражении, а порядок задает последовательность выполне- ния операторов с одинаковым уровнем приоритета.
214 Часть I. Основы Для большинства операторов порядок выполнения операндов не определен, что оставляет компилятору свободу выбора в отношении того, будет ли сначала выполняться левый опе- ранд, а затем правый или наоборот. Зачастую порядок вычисления результатов операндов никак не влияет на результат выражения. Но если оба операнда обращаются к одному объек- ту, причем один из них изменяет объект, порядок выполнения становится весьма важен, а связанные с ним серьезные ошибки обнаружить крайне сложно. И наконец, можно написать такое выражение, в котором значение одного типа приме- няется там, где необходимо значение другого типа. В таких случаях компилятор автомати- чески осуществляет преобразование типа (встроенного или класса), позволяющее исполь- зовать данное значение. Преобразование можно также осуществить явно, используя приве- дение типа. Термины Арифметическое преобразование (arithmetic conversion). Преобразование одного ариф- метического типа в другой. В контексте парных арифметических операторов арифметические преобразования, как правило, сохраняют точность, преобразуя значения меньшего типа в значения большего (например, меньший целочисленный тип char или short преобразу- ется в int). Выражение (expression). Самый низкий уровень вычислений в программе на языке C++. Как правило, выражения состоят из одного или нескольких операторов. Каждое выражение возвращает результат. Выражения могут использоваться в качестве операндов, что позволяет создавать составные выражения, которым для вычисления собственного результата нужны результаты других выражений, являющихся его операндами. Потерянный указатель (dangling pointer). Указатель, содержащий адрес области памяти, в которой уже нет объекта. Потерянный указатель является весьма распространенным источ- ником ошибок в программе, причем такие ошибки крайне трудно обнаружить. Неявное преобразование (implicit conversion). Преобразование, которое осуществляется компилятором автоматически. Такое преобразование осуществляется в случае, когда опера- тор получает значение, тип которого отличается от необходимого. В этом случае компилятор автоматически преобразует операнд в необходимый тип, если соответствующее преобразова- ние определено. Операнд (operand). Значение, с которым работает выражение. Оператор (operator). Символ, который определяет действие, выполняемое выражением. В языке определен целый набор операторов, которые применяются для значений встроенных типов. В языке определен также приоритет и порядок выполнения для каждого оператора, а также задано количество операндов для каждого из них. Операторы могут быть перегружены и применены к объектам классов. Оператор &. Побитовый оператор AND. Создает новое целочисленное значение, в кото- ром каждая битовая позиция имеет значение 1, если оба операнда в этой позиции имеют значение 1. В противном случае бит получает значение 0. Оператор |. Побитовый оператор OR. Создает новое целочисленное значение, в котором каждая битовая позиция имеет значение 1, если любой из операндов содержит значение 1 в этой битовой позиции. В противном случае бит получает значение 0. Оператор ~. Побитовый оператор NOT. Инвертирует биты своего операнда. Оператор А. Побитовый оператор исключающее или. Создает новое целочисленное зна- чение, в котором каждая битовая позиция имеет значение 1, если любой (но не оба) из опе- рандов содержит значение 1 в этой битовой позиции. В противном случае бит получает значение 0.
Глава 5. Выражения 215 Оператор ++. Оператор инкремента. Оператор инкремента имеет две формы, префиксную и постфиксную. Префиксный оператор инкремента возвращает 1-значение. Он добавляет единицу к значению операнда и возвращает полученное значение. Постфиксный оператор инкремента возвращает r-значение. Он добавляет единицу к значению операнда, но возвра- щает исходное, неизмененное значение. Оператор Оператор декремента. Имеет две формы, префиксную и постфиксную. Префиксный оператор декремента возвращает 1-значение. Он вычитает единицу из значения операнда и возвращает полученное значение. Постфиксный оператор декремента возвращает r-значение. Он вычитает единицу из значения операнда, но возвращает исходное, неизменен- ное значение. Оператор ,. Оператор запятая. Отделяемые запятой выражения обрабатываются слева направо. Результатом оператора запятая является значение справа. Оператор ?:. Условный оператор. Сокращенная форма конструкции if. . .else. Син- таксис условного оператора имеет следующий вид. условие ? выражение! : выражение2; Если условие истинно (значение true) выполняется выражение!, а в противном случае выражение2. Оператор <<. Оператор сдвига влево. Сдвигает биты левого операнда влево. Количество позиций, на которое осуществляется сдвиг, задает правый операнд. Правый операнд должен быть нулем или положительным значением, ни в коем случае не превосходящим количества битов в левом операнде. Оператор >>. Оператор сдвига вправо. Аналогичен оператору сдвига влево, за исключени- ем направления перемещения битов. Правый операнд должен быть нулем или положитель- ным значением, ни в коем случае не превосходящим количества битов в левом операнде. Оператор new. Резервирует область динамически распределяемой памяти. В этой главе рассматривалась форма оператора new для создания одиночного объекта. new тип; new тип(значение); Создает в динамической памяти объект типа тип, который может быть инициализирован значением значение. Оператор возвращает указатель на объект. В языке C++ оператор new заменил библиотечную функцию malloc () языка С. Оператор delete. Освобождает память, зарезервированную оператором new. Существует две формы применения оператора delete. delete р; // освобождение объекта delete [] р; // освобождение массива В первом случае р является указателем на объект в динамической памяти, а во втором — указателем на первый элемент массива, размещенного в динамической памяти. В языке C++ оператор delete заменил библиотечную функцию free () языка С. Оператор const_cast. Применяется при преобразовании константного объекта в соот- ветствующий неконстантный тип. Оператор dynamic_cast. Используется в комбинации с наследованием и идентифика- цией типов во время выполнения. См. раздел 18.2 (стр. 804). Оператор reinterpret_cast. Интерпретирует содержимое операнда как другой тип. Очень опасен и жестко зависит от машины. Оператор static_cast. Запрос на явное преобразование типов, которое компилятор осуществил бы неявно. Зачастую используется для переопределения неявного преобразова- ния, которое компилятор выполнил бы в противном случае. Парный оператор (binary operator). Операторы, в которых используются два операнда.
216 Часть I. Основы Перегрузка оператора (operator overloading). Возможность переопределить оператор так, чтобы он стал применим к объектам класса. Более подробная информация об опреде- лении перегруженных версий операторов приведена в главе 14, “Перегрузка операторов и преобразования”. Порядок (associativity). Определяет последовательность выполнения операторов одина- кового приоритета. Операторы могут иметь правосторонний (справа налево) или левосто- ронний (слева направо) порядок выполнения. Порядок вычисления (order of evaluation). Порядок, если он есть, определяет последова- тельность вычисления операндов оператора. В большинстве случаев компилятор C++ само- стоятельно определяет порядок вычисления операндов. Преобразование (conversion). Процесс, в ходе которого значение одного типа преобразу- ется в значение другого типа. Преобразования между встроенными типами заложены в самом языке. Для классов также возможны преобразования типов. Приведение типов (cast). Явное преобразование. Приоритет (precedence). Определяет порядок, в котором выполняются операторы в со- ставном выражении. Операторы с более высоким приоритетом выполняются раньше, чем операторы с более низким приоритетом. Результат (result). Значение или объект, который получается при вычислении выражения. Составное выражение (compound expression). Выражение, состоящее из нескольких опе- раторов. Стандартное преобразование (standard conversion). Преобразования между встроен- ными типами, определенные в языке. В выражениях, использующих операнды смешанных типов, в аргументах функций и в возвращаемых значениях преобразования осуществляют- ся автоматически. Унарный оператор (unary operator). Оператор, в котором используется один операнд. Условие (condition). Выражение, результатом которого является логическое значение true (истина) или false (ложь). Нулевой результат арифметического выражения соответ- ствует значению false, а любой другой — значению true. Целочисленное преобразование (integral promotion). Подмножество стандартных преоб- разований, при которых меньший целочисленный тип приводится к ближайшему большему типу. Целочисленные типы (например short, char и т.д.) преобразуются в тип int или unsigned int. Явное преобразование (explicit conversion). Преобразование, затребованное програм- мистом.
ГЛАВА 6 Операторы В ЭТОЙ ГЛАВЕ... 6.1. Простые операторы 218 6.2. Операторы объявления 219 6.3. Составные операторы (блоки) 219 6.4. Операторная область видимости 220 6.5. Оператор if 221 6.6. Оператор switch 225 6.7. Оператор while 231 6.8. Оператор цикла for 233 6.9. Оператор цикла do. . . while 236 6.10. Оператор break 237 6.11. Оператор continue 239 6.12. Оператор goto 239 6.13. Блок try и обработка исключений 241 6.14. Использование препроцессора для отладки 246 Резюме 248 Термины 249 Операторы аналогичны предложениям в человеческом языке. Язык C++ облада- ет набором простых операторов, которые последовательно выполняют задачи и по- зволяют составлять операторы, которые в свою очередь можно объединять в блоки операторов, выполняющихся как единое целое. Подобно большинству языков, C++ предоставляет операторы для условного выполнения кода и циклов, которые позво- ляют многократно выполнять те же фрагменты кода. В данной главе поддерживае- мые языком C++ операторы рассматриваются более подробно. По умолчанию операторы выполняются последовательно. Однако последова- тельное выполнение годится лишь для самых простых программ. Поэтому в язы- ке C++, как и в любом другом языке, определен набор операторов управления по- током выполнения (flow-of-control statement), которые позволяют выполнять дру- гие операторы условно или неоднократно. Операторы if и switch обеспечивают условное выполнение. Операторы for, while и do. . .while обеспечивают по-
218 Часть I. Основы вторное выполнение. Поэтому последние операторы зачастую называют циклами или операторами цикла. 6.1. Простые операторы Большинство операторов в языке C++ заканчиваются точкой с запятой. Выраже- ние типа ival + 5 становится оператором выражения, завершающегося точкой с запятой. Операторы выражения (expression statement) составляют вычисляемую часть выражения, ival +5; // оператор выражения Это выражение бесполезно: результат вычисляется, но не присваивается, а следо- вательно, никак не используется. Как правило, выражения содержат операторы, ре- зультат вычисления которых влияет на состояние программы. К таким операторам относятся присвоение, инкремент, ввод и вывод. Пустые операторы Самая простая форма оператора — это пустой (empty), или нулевой, оператор (null statement). Он представляет собой одиночный символ точки с запятой (;). ; // пустой оператор Пустой оператор используется в случае, когда синтаксис языка требует наличия оператора, а логика программы — нет. Как правило, это происходит в случае, когда ра- бота цикла осуществляется внутри его условия. Например, можно организовать ввод, игнорируя все прочитанные данные, пока не встретится определенное значение. // читать, пока не встретится конец файла или значение, // равное содержимому переменной sought while (cin >> s && s != sought) ; // пустой оператор В условии значение считывается со стандартного устройства ввода и объект cin неявно проверяется на успешность чтения. Если чтение прошло успешно, во второй части условия проверяется, не равно ли полученное значение содержимому пере- менной sought. Если искомое значение найдено, цикл while завершается, а в про- тивном случае его условие проверяется снова, начиная с чтения следующего значе- ния из объекта cin. Случаи применения пустого оператора следует комментировать, чтобы любой, кто читает код, мог сразу понять, что оператор пропущен преднамеренно. Поскольку пустой оператор является вполне допустимым, он может распола- гаться везде, где ожидается оператор. Поэтому лишний символ точки с запятой, который может показаться явно недопустимым, на самом деле является не более, чем пустым оператором. // ок: вторая точка с запятой - это лишний пустой оператор ival = vl + v2;; Этот фрагмент состоит из двух выражений: одно содержит оператор присвоения, а второе — пустой оператор.
Глава 6. Операторы 219 Лишний пустой оператор не всегда безопасен. Случайный символ точки с запятой после условия оператора while или if мо- жет решительно изменить намерение программиста. // катастрофа: лишняя точка с запятой превратила тело цикла // в пустой оператор while (iter != svec.end()); // тело цикла while пусто! ++iter; // инкремент не является частью цикла Этот цикл выполняется бесконечно. Несмотря на отступ, выражение с операто- ром инкремента не является частью цикла. Тело цикла — это пустой оператор, обо- значенный символом точки с запятой непосредственно после условия. 6.2. Операторы объявления Определение или объявление объекта либо класса также является оператором. К опе- раторам объявления (declaration statement) обычно относят и операторы определе- ния, хотя термин оператор определения (definition statement) мог бы быть точнее. Определения и объявления переменных рассматривались в разделе 2.3 (стр. 65), а определения класса — в разделе 2.8 (стр. 85). Более подробная информация по этой теме приведена в главе 12, “Классы”. 6.3. Составные операторы (блоки) Составной оператор (compound statement), обычно называемый блоком (block), представляет собой последовательность операторов, заключенных в фигурные скоб- ки (оператор может быть и пустым). Блок операторов обладает собственной обла- стью видимости. Объявленные внутри блока имена доступны только внутри данного блока и блоков, вложенных в него. Как обычно, имя видимо только с того момента, когда оно определено, и до конца блока включительно. Составные операторы применяются в случае, когда правила языка предусматривают наличие одного оператора, а логика программы требует нескольких операторов. Напри- мер, тело цикла while или for составляет один оператор. Но в теле цикла зачастую не- обходимо выполнить несколько операторов. Заключив необходимые операторы в фи- гурные скобки можно получить блок, рассматриваемый как единый оператор. Вернемся к циклу while из примера о проблеме книжного магазина (стр. 48). // если да, прочитать транзакцию while (std::cin » trans) if (total.same_isbn(trans)) // совпадает: изменить суммарное количество total = total + trans; else { // не совпадает: отобразить и переприсвоить total std cout << total « std endl; total = trans;
220 Часть I. Основы В разделе else логика программы требует вывести значение переменной total, а затем присвоить ей значение переменной trans. Однако раздел else может со- держать только один оператор. Заключив оба необходимых оператора в фигурные скобки, их можно преобразовать в единый (составной) оператор. Этот оператор удовлетворяет и правилам языка, и потребностям программы. В отличие от большинства других операторов, блок не завершают точкой с запятой. Как и в случае с пустым оператором, вполне можно создать пустой блок. Для это- го используется пара фигурных скобок без операторов. while (cin » s && s != sought) { } // пустой блок Упражнения раздела 6.3 Упражнение 6.1. Что такое пустой оператор? Приведите пример использования пустого оператора. Упражнение 6.2. Что такое блок? Приведите пример использования блока. Упражнение 6.3. Используя оператор запятой (раздел 5.9 стр. 192), перепишите раздел else в цикле while в примере о проблеме книжного магазина так, чтобы блок стал больше не нужен. Объясните, улучшило ли это удобочитаемость кода или нет. Упражнение 6.4. Что произойдет, если в цикле while примера о проблеме книжного магазина удалить фигурные скобки? 6.4. Операторная область видимости Некоторые операторы позволяют определять переменные внутри контролируе- мой ими структуры. while (int i = get_num()) cout « i « endl; i = 0; II ошибка: переменная i недоступна вне цикла Г Определенные в условии переменные следует инициализировать. Проверяемое в условии | значение является значением, инициализирующим объект. Переменные, определенные в составе управляемой структуры оператора, видимы только до конца того оператора, в котором они определены. Область видимости та- ких переменных ограничена телом оператора. Как правило, телом оператора являет- ся блок, который в свою очередь может содержать другие блоки. Определенное в управляемой структуре имя локально для оператора, но его область видимости рас- пространяется на вложенные блоки и внутренние операторы. // переменная index видима только внутри оператора for for (vector<int>::size_type index = 0; index != vec.size(); ++index) { // новая область видимости, вложенная, внутрь области видимости // оператора for
Глава 6. Операторы 221 int square = 0 ; if (index % 2) // ok: переменная index находится // внутри области видимости square - index * index; vec[index] = square; if (index != vec.size()) // ошибка: здесь переменная index невидима Если программа нуждается в доступе к значению переменной, используемой внутри контролируемой оператором структуры, эту переменную следует определить вне структуры оператора. vector<int>::size_type index = 0; for ( /* удаление */ ; index 1= vec.size(); ++index) // как прежде if (index != vec.size()) // ok: теперь индекс находится в области // видимости // как прежде В ранних версиях языка C++ область видимости переменных, определенных внутри цикла У ] for, была иной. Определенные в заголовке цикла for переменные рассматривались как определенные непосредственно перед оператором for. В старых программах C++ могут встречаться фрагменты кода, которые полагаются на возможность доступа к таким переменным вне области видимости оператора for. Одним из преимуществ ограничения области видимости переменных, опреде- ленных внутри оператора, является возможность многократного использования их имен без необходимости контролировать соответствие их текущего значения каж- дому конкретному случаю применения. Ведь если имя выходит из области видимо- сти, использовать его уже невозможно, несмотря на его прежнее значение. 6.5. Оператор if Оператор if обеспечивает условное выполнение других операторов на основа- нии истинности выражения в его условии. Существует две формы оператора if: с разделом else и без него. Синтаксис простой формы оператора if имеет следую- щий вид. i f (условие) оператор Условие должно быть заключено в круглые скобки. Это может быть выражение следующего типа, if (а + Ь > с) {/* ... */} Или инициализированное объявление следующего типа. // переменная ival доступна только внутри оператора if if (int ival = compute_value()) {/* ... */} Как обычно, оператор может быть составным, т.е. блоком операторов, заключен- ным в фигурные скобки. Когда в условии определяется переменная, ее следует инициализировать. Значе- ние инициализированной переменной преобразуется в значение типа bool (раз- дел 5.12.3 стр. 207), а полученное в результате логическое значение используется в условии. Переменная может иметь любой тип, допускающий преобразование в тип
222 Часть I. Основы bool, т.е. арифметический или тип указателя. Как будет продемонстрировано в гла- ве 14, “Перегрузка операторов и преобразования”, условие может проверять и объект, если его класс допускает это. На настоящий момент уже было продемонстрировано применение в условии объектов классов ввода-вывода, но объекты классов vector и string в условии неприменимы. Чтобы проиллюстрировать применение оператора if, найдем самое маленькое значение вектора vector<int> и выясним, сколько раз оно встречается. Чтобы ре- шить эту проблему, понадобится два оператора if: один определяет, является ли но- вое значение минимальным, а второй осуществляет инкремент счетчика количества элементов, содержащих текущее минимальное значение. if (minVal > ivec[i]) { /* обработка нового minVal */ } if (minVal == ivec[i]) { /* инкремент счетчика */ } Блок как оператор цикла i f Сначала рассмотрим каждый оператор if по отдельности. Один из них опреде- лят, является ли новое значение минимальным, и если это так, он обнуляет счетчик, а затем модифицирует значение переменной minVal. if (minVal > ivec[i]) { // выполнить оба оператора, если // условие истинно minVal = ivec[i]; occurs - 1; Другой оператор if модифицирует счетчик. Здесь используется только один оператор, поэтому фигурные скобки не нужны. if (minVal == ivec[i]) ++occurs; Это довольно распространенная ошибка — когда забывают фигурные скобки в случае, где следует выполнить несколько операторов как один. ^t/ма^ В следующей программе, несмотря на отступ и вопреки намерению программи- ста, присвоение значения 1 переменной occurs не является частью оператора if. // ошибка: забыты фигурные скобки, создающие блок1. if (minVal > ivec[i]) minVal = ivec[i]; occurs =1; // выполняется в любом случае, поскольку // не является частью оператора if Здесь присвоение значения переменной occurs будет выполняться вне зависи- мости от истинности условия. Выявить такую ошибку может оказаться довольно трудно, поскольку внешне код выглядит вполне правильным. Ге Ж Большинство редакторов и систем разработки обладают средствами автоматическо- Ъ го выравнивания исходного кода в соответствии с его структурой. Если такие инст- жеидуем рументальные средства доступны, их имеет смысл использовать.
Глава 6. Операторы 223 6.5.1. Оператор if с разделом else Теперь эти операторы if следует собрать вместе. Порядок операторов if весьма важен. Если расположить их следующим образом, счетчик всегда будет увеличи- ваться на 1. if (minVal > ivec[i]) { minVal = ivec[i]; occurs = 1; } // потенциальная ошибка, если minVal только что был // приравнен к ivec[i] if (minVal == ivec[i]) ++occurs; Этот код дважды учитывает первое найденное минимальное значение. Выполнение обоих операторов i f для того же значения не только потенциально опасно, но и не нужно. Элемент может содержать и меньше значение, чем у пере- менной minVal, и значение, равное ему. Таким образом, если одно условие истинно, другое можно игнорировать. Оператор i f реализует логику выбора “или это, или то” при помощи директивы else. Синтаксическая форма оператора if. . .else имеет следующий вид. i f (условие) оператор! else оператор2 Если условие истинно, выполняется оператор1, а в противном случае выпол- няется оператор2. if (minVal == ivec[i]) ++occurs; else if (minVal > ivec [i]) { minVal = ivec[i]; occurs = 1; } Следует заметить, что опера тор2 может быть любым оператором или блоком операторов, заключенных в фигурные скобки. В данном примере оператор2 — это самостоятельный оператор if. Потерянный оператор else С использованием оператора if связана одна существенная сложность, которая не рассматривалась до сих пор. Обратите внимание, что ни один из операторов i f не рассматривает случай, когда значение текущего элемента больше, чем значение пе- ременной minVal. С логической точки зрения игнорирование этих элементов впол- не закономерно, если значение элемента больше минимального. Однако зачастую операторы if должны выполнять некие действия во всех трех случаях: если значе- ние больше — одно действие, если меньше — другое, если равно — третье. Рассмат- риваемый цикл можно переписать так, чтобы все три случая обрабатывались явно. // обратите внимание на выравнивание, чтобы лучше понять // соответствие разделов else операторам if if (minVal < ivec[i]) { } // пустой блок
224 Часть I. Основы else if (minVal == ivec[i]) ++occurs; else { // minVal > ivec[i] minVal = ivec[i]; occurs = 1; } Эта проверка с тремя способами обработки каждого случая вполне корректна. Однако простая перезапись сворачивает первые две проверки в одну, а вложенный оператор i f создает проблему. // Упс! Некорректная перезапись: этот код не будет работать ! if (minVal <- ivec[i]) if (minVal == ivec[i]) ++occurs; else { // этот оператор else относится к внутреннему // оператору if, а не к внешнему! minVal = ivec[i]; occurs = 1; } Эта версия иллюстрирует потенциальную неоднозначность, вполне обычную для опера- торов if во всех языках. Эта проблема, обычно называемая потерянным оператором (Л else (dangling-else), возникает в случае, когда в операторе содержится больше ди- ректив if, чем директив else. Возникает вполне резонный вопрос: к которому из ^i/ма^ операторов if какая директива else принадлежит? Отступ (выравнивание) в этом коде расставлен так, как будто оператор else относится к внешней директиве if. Однако в языке C++ подобная неоднознач- ность решается так: оператор else соответствует последнему оператору if без else. В данном случае операторы if и else фактически располагаются следую- щим образом. // Упс! Все равно неправильно, но отступ теперь // соответствует действительности if (minVal <= ivec [i]) // выровнено так, чтобы наглядно продемонстрировать // принадлежность оператора else if (minVal == ivec[i]) minVal = ivec[i]; occurs = 1, } Заключив внутренний оператор if в фигурные скобки, можно явно указать, что оператор else соответствует внешнему оператору if. if (minVal <= ivec[i]) { if (minVal == ivec[i]) ++occurs; } else { minVal = ivec[i]; occurs = 1; } Согласно некоторым стилям программирования, рекомендуется использовать фигур- ные скобки всегда, после любого оператора if. Это правило позволяет избежать возможных недоразумений и ошибок при последующих модификациях кода. Фигурные
Глава 6. Операторы 225 скобки после операторов if и while имеет смысл использовать, как минимум, в тех случаях, когда их тело содержит выражение, которое сложнее одиночного оператора или простого оператора вывода либо присвоения. Упражнения раздела 6.5.1 Упражнение 6.5. Исправьте следующие операторы, (a) if (ivall != ival2) (b) if (ival < minval) minval = ival; // запомнить новое минимальное значение occurs = 1; // сброс счетчика (с) if (int ival = get_value()) cout « "ival = " « ival « endl; (d) if (ival = 0) ival = get_value(); Упражнение 6.6. Что такое потерянный else? Как эта неоднозначность решается в языке C++? 6.6. Оператор switch Глубоко вложенные конструкции if . . .else могут часто быть вполне коррект- ны синтаксически, однако неправильно отражать логику программы. Дело в том, что заметить логическую ошибку в разветвленном наборе операторов if. . .else до- вольно сложно. Добавление новых условий и логических операторов, а также внесе- ние других изменений, тоже вполне может стать источником проблем. Оператор switch предоставляет более удобный способ записи логических действий, анало- гичный глубоко вложенной конструкции из операторов if. . .else. Предположим, что необходимо выяснить, сколько раз встречается в тексте каж- дая из пяти гласных букв. Программа будет иметь следующую логику. Читать символы по одному до конца. Сравнить каждый символ с набором искомых гласных. Если символ соответствует одной из гласных букв, добавить 1 к соответствую- щему счетчику. Отобразить результаты. Программа должна отобразить результаты в следующем виде. Number of vowel а: 3499 Number of vowel e: 7132 Number of vowel i: 3577 Number of vowel o: 3530 Number of vowel u: 1185
226 Часть I. Основы 6.6.1. Использование оператора switch Используя оператор switch, поставленную задачу можно решить следующим образом, char ch; // инициализировать счетчики для каждой гласной int aCnt = 0, eCnt = 0, iCnt = 0, oCnt = 0, uCnt = 0; while (cin » ch) { // если переменная ch содержит искомую гласную, увеличить // значение соответствующего счетчика switch (ch) { case ’а': ++aCnt; break; case 'e': ++eCnt; break; case 1i': ++iCnt; break; case 'o': ++oCnt; break; case 1u': ++uCnt; break; } // отобразить результат cout « "Number of vowel « "Number of vowel « "Number of vowel « "Number of vowel « "Number of vowel Оператор switch вычисляет результат выражения, расположенного за ключе- вым словом switch. Результат этого выражения должен быть целым числом. Полу- ченный результат сравнивается со значением, указанным в каждом операторе case. Ключевое слово case и расположенное после него значение называют также меткой case (case label). Значением каждой метки case является константное выражение (раздел 2.7, стр. 84). Существует еще одна, специальная метка default, рассматри- ваемая более подробно в разделе 6.6.3 (стр. 228). Если результат выражения соответствует значению метки case, выполнение ко- да начинается с первого оператора после этой метки. В принципе, выполнение кода продолжается до конца оператора swi t ch, но оно может быть прервано оператором break. Если среди меток case соответствие так и не найдено, срабатывает метка def auIt, а если ее нет, управление переходит к первому оператору после оператора switch. В этой программе оператор switch является единственным оператором в теле цикла while. Здесь, отсутствие совпадения с метками case оператора switch возвращает управление к условию цикла while. Подробно оператор break рассматривается разделе 6.10 (стр. 237), а сейчас лишь заметим, что он прерывает текущий поток выполнения операторов. В данном случае
Глава 6. Операторы 227 оператор break прерывает выполнение блока операторов switch и осуществляет вы- ход из него. В результате управление переходит к первому оператору после оператора switch. В данном примере управление возвращается к условию цикла while. 6.6.2. Порядок выполнения внутри оператора switch Очень важно понять, как именно осуществляется передача управления внутри оператора switch. Довольно распространено заблуждение, что выполняются только те операторы, которые принадлежат соответствующей метке case. Однако на самом деле выполнение продол- жается несмотря на границы меток case до конца оператора switch, если только не встретится оператор break. Иногда такое поведение действительно оправданно. То есть необходимо выпол- нять код начиная с определенной метки и до конца, включая код следующих меток. Однако чаще всего следует выполнять только тот участок кода, который относится к данной метке. Чтобы исключить выполнение кода последующих меток case, необ- ходимо при помощи оператора break указать явно, что на этом выполнение пре- кращается. Поэтому в большинстве случаев последним оператором метки case яв- ляется оператор break. Таким образом, приведенная ниже реализация оператора switch для подсчета количества гласных букв неправильна. // Внимание! Этот код преднамеренно сделан неправильным switch (ch) { case 1 а': ++aCnt; // Упс! Необходим оператор break case 'е': ++eCnt; // Упс! Необходим оператор break case 'i1: ++iCnt; // Упс! Необходим оператор break case 'o': ++oCnt; // Упс! Необходим оператор break case 1u': ++uCnt; // Упс! Необходим оператор break } Отследим работу этой версии кода, предположив, что значением переменной ch является ' i '. Выполнение начинается после метки case ' i ' :, т.е. сначала будет выполнен оператор ++iCnt. Однако выполнение на этом не останавливается, оно продолжается операторами ++oCnt и ++uCnt следующих меток case, увеличивая значения и других счетчиков. Если бы переменная ch содержала значение ' е', про- изошло бы приращение счетчиков eCnt, iCnt, oCnt и uCnt. Забытый оператор break — это обычный источник ошибок в операторах switch. Хоть оператор break и не строго обязателен после последней метки оператора switch, располагать его все же рекомендуется после каждой метки, даже последней. Ведь если впоследствии оператор switch будет дополнен еще одной меткой case, отсутствие оператора break после прежней последней метки не создаст проблем.
228 Часть I. Основы Операторы break нужны не всегда Существует одна стандартная ситуация, когда оператор break после метки case не нужен, а программа должна выполнять код нескольких меток. Речь идет о случае, когда та же последовательность действий должна выполняться для нескольких воз- можных значений. С меткой case может быть связано только одно значение. Чтобы указать диапазон значений, метки case располагают последовательно, одна после другой. Например, если необходимо посчитать общее количество гласных букв, а не отдельно по каждой, можно применить следующий код. int vowelCnt = 0; switch (ch) // значение счетчика vowelCnt увеличивается каждый раз, когда // встречается любой из символов а, е, i, о или и case case case case case ++vowelCnt; break; Метки case необязательно должны располагаться в отдельных строках. Поэтому диапазон значений предыдущего листинга вполне можно записать в одной строке. switch (ch) { // альтернативный, но вполне допустимый синтаксис case 'a': case 'е': case *i*: case 'o': case 'u': ++vowelCnt; break; } Значительно реже оператор break преднамеренно пропускают для того, чтобы по- следовательно выполнять код нескольких разделов case начиная с текущего случая. Случаи, когда оператор break пропускают преднамеренно, довольно редки, поэтому их следует обязательно комментировать, объясняя логику действий. 6.6.3. Метка default Метка default является эквивалентом директивы else. Если ни одна из меток case не соответствует значению выражения оператора switch, выполняются опе- раторы, расположенные после метки default. В рассматриваемом случае, напри- мер, можно организовать подсчет введенных негласных букв. Для этого создадим специальный счетчик otherCnt, оператор приращения которого расположим в раз- деле default. // если ch содержит гласную, прирастить соответствующий счетчик switch (ch) { case ’а': ++aCnt;
Глава 6. Операторы 229 break; // остальные случаи обрабатываются здесь default: ++otherCnt; break; } } В этой версии, если переменная ch не содержит гласную букву, управление пе- рейдет к метке def ault и увеличится значение счетчика otherCnt. Раздел default имеет смысл создавать всегда, даже если в нем не происходит никаких действий. Впоследствии это однозначно укажет читателю кода, что случай default не был забыт, т.е. для остальных случаев никаких действий предприни- мать не нужно. Метка не может быть автономный; она должна предшествовать оператору. Если оператор switch заканчивается разделом default, в котором не осуществляется никаких действий, за меткой default должен следовать пустой оператор. 6.6.4. Выражение switch и метки case Выражение оператора switch может иметь любую сложность. В частности, здесь можно определить и инициализировать переменную, switch(int ival = get_response()) В данном случае создается целочисленная переменная ival, которая инициали- зируется результатом обращения к функции get_response (). Именно это значе- ние и будет сравниваться со значением каждой метки case. Переменная ival суще- ствует на протяжении всего оператора switch, но не вне его. Метки case должны содержать целочисленные выражения (раздел 2.7, стр. 84). Например, примеры следующих меток приводят к ошибкам во время компиляции. // недопустимые значения меток case case 3.14: // не целое число case ival: // не константа Ошибкой будет также случай, когда две метки case содержат одинаковое значение. 6.6.5. Определение переменной внутри оператора switch Переменные могут быть определены только в последнем разделе case или default. case true: // ошибка: объявление предшествует метке case string file_name = get file named; break; case false: Этого правило помогает сохранить правильность кода, который мог пропустить определение и инициализацию переменной.
230 Часть I. Основы Напомним, что переменная применима от места ее определения до конца блока, в котором она определена. Теперь рассмотрим, что произойдет, если определить пере- менную между двумя метками case. Эта переменная должна была бы существовать до конца блока включительно. Ее может применять код раздела case, расположен- ный после того раздела case, в котором она была определена. Если выполнение ко- да в операторе switch начинается с раздела case, расположенного ниже того места, где переменная определена, произойдет ошибка, связанная с попыткой использова- ния несуществующего имени. Если необходимо определить переменную для одного раздела case, ее можно оп- ределить и инициализировать внутри блока кода, гарантируя таким образом, что она будет существовать в этой области видимости. case true: { // ок: объявление переменной внутри блока кода string file_name - get fi1e_name(); Упражнения раздела 6.6.5 Упражнение 6.7. В рассматриваемой программе есть одна проблема: она не интерпретирует за- главные буквы как гласные. Напишите программу, которая подсчитывает гласные буквы как в верхнем, так и в нижнем регистре. То есть значение счетчика acnt должно увеличиваться при встрече как символа 1 а1, так и символа • а 1 (аналогично для остальных гласных букв). Упражнение 6.8. Измените рассматриваемую программу так, чтобы она подсчитывала также ко- личество пробелов, символов табуляции и новой строки. Упражнение 6.9. Измените рассматриваемую программу так, чтобы она подсчитывала количество встреченных двухсимвольных последовательностей: f f, fl и f i. Упражнение 6.10. Каждая из программ, приведенных на стр. 230, содержит распространенную ошибку. Выявите и исправьте каждую из них. Код для упражнении раздела 6.6.5 (стр. 230) (a) switch (ival) { case 'а' : aCnt++; case 'e': eCnt++; default: iouCnt++; (b) switch (ival) { case 1: int ix - get_value(); ivec[ ix ] = ival; break; default: ix = xvec. size ()-1 ; ivec[ ix ] = ival;
Глава 6. Операторы 231 (с) switch (ival) { case 1, 3, 5, 7, 9: oddcnt++; break; case 2, 4, 6, 8, 10: evenent++; break; (d) int ival=512 jval=1024, kval=4096; int bufsize; switch(swt) { case ival: bufsize = ival * sizeof(int); break; case jval: bufsize = jval * sizeof(int); break; case kval: bufsize = kval * sizeof(int); break; 6.7. Оператор while Оператор while многократно выполняет оператор, пока его условие остается истинным. Его синтаксическая форма имеет следующий вид. wh i 1 е (условие) опера тор Пока условие истинно (значение true), оператор (который зачастую является блоком кода) выполняется. Условие не может быть пустым. Если при первой про- верке условие ложно (значение false), оператор не выполняется. Условие может быть выражением (или определением и инициализацией пере- менной). bool quit = false; while (Jquit) { // выражение как условие quit = do_something(); } while (int loc = search(name)) { // инициализация переменной // как условия // выполнение действий } Любая определенная в условии переменная видима только внутри блока, связан- ного с оператором while. При каждой итерации цикла, инициализирующее значе- ние преобразуется в логическое (тип bool) (раздел 5.12.3, стр. 207). Если значение интерпретируется как true, тело цикла while выполняется. Обычно проверяемое значение изменяется либо непосредственно в условии, либо в теле цикла. В против- ном случае цикл никогда не сможет завершиться.
232 Часть I. Основы Определенные в условии переменные создаются и удаляются при каждой итерации цикла. Использование цикла while На данный момент уже было приведено много примеров циклов while, но для полной ясности рассмотрим пример, где осуществляется копирование содержимого одного массива в другой. // arrl - массив целых чисел int *source = arrl; size_t sz - sizeof(arrl)/sizeof(*arrl); // количество элементов int *dest = new int[sz]; // неинициализированные элементы while (source != arrl + sz) *dest++ = *source++; // скопировать элемент и / / переместить указатель Сначала инициализируются указатели source и dest на первые элементы соответ- ствующих массивов. Условие цикла while проверяет, не достигнут ли конец массива, из которого осуществляется копирование. Если нет, тело цикла выполняется. Оно содержит единственный оператор, который копирует элемент и увеличивает оба указателя так, чтобы они указывали на следующий элемент соответствующих им массивов. Как упоминалось в совете на стр. 188, программисты C++ предпочитают писать краткие выражения. Оператор в теле цикла while является классическим примером. *dest++ = *source++; Это выражение можно переписать следующим образом. *dest = *source; ++dest; ++sourсе; // скопировать элемент // инкремент указателей Подобный оператор присвоения в теле цикла while весьма популярен. Поскольку такой код широко распространен, имеет смысл хорошо изучить подобную форму вы- ражений. Упражнения раздела 6.7 Упражнение 6.11. Объясните смысл каждого из следующих циклов. Исправьте все обнаруженные ошибки. (a) string bufString, word; while (cin >> bufString » word) { /* ... */ } (b) while (vector<int>::iterator iter != ivec.end()) (c) while (ptr = 0) ptr = find_ find_a_value(); (d) while (bool status find(word)) { word = get_next_word(); } if (!status) cout << " Did not find any words\n";
Глава 6. Операторы 233 Упражнение 6.12. Напишите небольшую программу, которая читает последовательность слов со стандартного устройства ввода и находит среди них повторяющиеся. Программа должна оп- ределять позицию, где повторяющиеся слова следуют непосредственно одно за другим. Вычис- лите слово, повторяющееся чаще других, а также выясните количество его повторов. Отобрази- те максимальное количество повторений, а если его нет — сообщение, свидетельствующее о том, что ни одно из слов не повторяется. Предположим, что была введена следующая последо- вательность слов. how, now now now brown cow cow Результат должен указать, что слово “now” повторялось три раза. Упражнение 6.13. Объясните подробно, как выполняется данный оператор в теле цикла while. *dest++ = *source++; 6.8. Оператор цикла for Оператор цикла for имеет следующий синтаксис. for (инициализирующий-оператор условие; выражение) оператор Инициализирующий-оператор должен быть оператором объявления, выраже- нием или пустым оператором. Каждый из этих операторов завершается точкой с за- пятой, поэтому данную синтаксическую форму можно рассматривать следующим образом. for (инициализатор; условие; выражение) оператор Правда, с технической точки зрения, точка с запятой после части инициализа - тор принадлежит первому оператору заголовка цикла for. Как правило, инициализирующий-оператор используется для инициализации или присвоения исходного значения переменной, изменяемой в цикле. Для управле- ния циклом служит условие. Пока условие истинно, оператор выполняется. Ес- ли при первой проверке условие оказывается ложным, оператор не выполняется ни разу. Для изменения значения переменной, инициализированной в инициализи- рующем операторе и проверяемой в условии, используется выражение. Оно выпол- няется после каждой итерации цикла. Как и в других случаях, оператор может быть одиночным оператором или блоком операторов. Использование цикла for Следующий цикл for отображает содержимое вектора. for (vector<string>::size_type ind = 0; ind != svec.size(); ++ind) { cout « svec[ind]; // отобразить текущий элемент // если элемент не последний, вывести пробел, чтобы отделить // от следующего значения if (ind +1 != svec.size()) cout << " "; }
234 Часть I. Основы Данный цикл имеет следующий порядок выполнения. 1. В начале цикла выполняется инициализирующий-оператор. В данном случае это определение и инициализация нулевым значением переменной ind. 2. Далее следует условие. Если значение переменной ind не равно значению, воз- вращаемому функцией svec. size (), выполняется тело цикла for. В против- ном случае цикл завершается. Если условие ложно при первой итерации, тело цикла for не выполняется ни разу. 3. Если условие истинно, тело цикла for выполняется. В данном случае оно ото- бражает значение текущего элемента, а затем проверяет, не является ли данный элемент последним. Если это не так, выводится пробел, чтобы отделить отобра- женное значение от следующего. 4. И наконец, обрабатывается выражение. В этом примере оно увеличивает значе- ние переменной ind на 1. Эти четыре этапа составляют первую итерацию цикла for. Следующая итера- ция начинается с этапа 2 и продолжается на этапах 3 и 4 до тех пор, пока условие не станет ложным, т.е. когда значение переменной ind станет равно результату функции svec. size (). Не забывайте, что область видимости любого объекта, определенного внутри заголовка J 1 цикла for, ограничивается телом цикл. Таким образом, переменная ind, в данном при- мере, станет недоступной после завершения цикла for. 6.8.1. Цикл for без частей заголовка В заголовке цикла for может отсутствовать любая из частей: инициализирую- щий-оператор, условие или выражение. Если инициализация не нужна или происходит в другом месте, инициализи- рующий-оператор не нужен. Например, если переписать программу, отображаю- щую содержимое вектора таким образом, чтобы она использовала итератор, а не ин- дексацию, его инициализацию можно вынести за пределы цикла для повышения удобочитаемости. vector<string>::iterator iter = svec.begin(); for( /* отсутствует */ ; iter != svec.endO; ++iter) { cout << *iter; // отобразить текущий элемент // если элемент не последний, вывести пробел, чтобы отделить II от следующего значения if (iter+1 != svec.endO ) cout « " "; } Обратите внимание, символ точки с запятой необходим, он означает отсутст- вующий инициализирующий-оператор. Точнее, точка с запятой представляет от- сутствующий инициализирующий - опера тор. Отсутствие части условие эквивалентно расположению в условии значения true, for (int i = 0; /* нет условия */ ; ++i)
Глава 6. Операторы 235 Это эквивалентно следующему коду, for (int i = 0; true; ++i) Тело такого цикла обязательно должно содержать оператор break или return, в противном случае цикл будет выполняться бесконечно. Аналогично, если отсут- ствует выражение, для выхода из цикла либо применяются операторы break или return, либо в теле цикла должно изменяться значение, которое проверяется в условии. for (int i = 0; i != 10; /* нет выражения */ } { // значение переменной i должно измениться в теле цикла, // иначе он не завершится Если в теле цикла не изменять значение переменной i, оно останется равным 0 и условие будет истинным всегда. 6.8.2. Несколько определений в заголовке цикла for В инициализирующем операторе может быть определено несколько объектов. Однако поскольку оператор может быть только один, все объекты окажутся одина- кового типа. const int size = 42; int val = 0, ia[size]; // объявить 3 переменные, локальные для цикла for: // ival - переменная типа int, pi - указатель на тип int, // a ri - ссылка на переменную типа int for (int ival = 0, *pi = ia, &ri = val; Упражнения раздела 6.8.2 Упражнение 6.14. Объясните каждый из следующих циклов. Исправьте все обнаруженные ошибки, (a) for (int *ptr = &ia, ix = 0; (b) for ( ix != size && ptr != ia+siz ++ix, ++ptr) { /* ... */ ; ;) { if (some_condition) return; (d) int ix; for (ix != sz; ++ix) { /* ... */ } (e) for (int ix = 0; ix != sz; ++ix, ++ sz) { /* ... */ } Упражнение 6.15. Цикл while особенно хорош в случае, когда некоторый код следует выпол- нять до тех пор, пока справедливо определенное условие. Например, читать следующие значе- ния, пока не достигнут конец файла. Цикл for иногда называют пошаговым циклом, он позво- ляет перебрать диапазон значений в коллекции, увеличивая их индекс. Напишите примеры при- менения каждого цикла, а затем перепишите каждый из них с использованием другого оператора цикла. Если бы была доступна только одна конструкция цикла, какую из них имело бы смысл выбрать? Почему?
236 Часть I. Основы Упражнение 6.16. Предположим, что существует два целочисленных вектора. Напишите про- грамму, которая поможет выяснить, не является ли один вектор подмножеством другого. Для век- торов неравной длины сравнивайте количество элементов меньшего вектора. Например, сравнив векторы со значениями (о, 1,1, 2) и (о, 1,1, 2, з, 5, 8), программа должна возвращать утвер- дительный ответ. 6.9. Оператор цикла do. . .while Иногда в программах необходимо организовывать взаимодействие с пользовате- лем. Например, может понадобиться запросить у пользователя два числа и вычис- лить их сумму. Отобразив результат, программа должна спрашивать у пользователя, не желает ли он повторить процесс еще раз. Тело подобной программы довольно простое. Необходимо лишь организовать за- прос у пользователя пары значений и отобразить полученную сумму прочитанных значений. После того как значение суммы отображено, остается сделать запрос на продолжение. Самым сложным моментом остается управляющая структура. Проблема заклю- чается в том, что цикл необходимо выполнять до тех пор, пока пользователь не по- требует его завершить. В частности, сумму необходимо вычислять даже при первой итерации. Именно это и осуществляет цикл do. . .while. Он гарантирует, что тело цикла будет выполнено в любом случае, по крайней мере один раз. Оператор цикла do. . .while имеет следующий синтаксис. do оператор while (условие); В отличие от оператора while, оператор do.. .while всегда завершается точкой с запятой. Оператор цикла do выполняется прежде, чем проверяется его условие, которое не может быть пустым. Если условие ложно (значение false), цикл завершается, а в противном случае он повторяется. Используя оператор do. . .while, требуемую программу можно написать следующим образом. // циклически запрашивать у пользователя пары суммируемых чисел string rsp; // используется в условии, поэтому не может быть // определена внутри цикла do do { cout « "please enter two values: "; int vail, val2; cin » vail >> val2; cout « "The sum of " « vail « " and " « val2 « " = " « vail + val2 « "\n\n" « "More? [yes][no] "; cin » rsp; } while (!rsp.empty() && rsp[0] != 'n'); Тело этого цикла подобно другим, уже не раз описанным, поэтому не будем его рассматривать. Значительно интересней то, что определение переменной rsp осу-
Глава 6. Операторы 237 ществляется до оператора do, а не внутри цикла. Если бы переменная rsp была оп- ределена внутри цикла do, она вышла бы из области видимости в месте расположе- ния закрывающей фигурной скобки перед оператором while. Поэтому любая пере- менная, используемая в условии цикла do.. .while, должна быть объявлена перед оператором do. Поскольку условие не проверяется до тех пор, пока оператор или блок операто- ров не будет выполнен, цикл do... while не допускает определений переменных. // ошибка: объявления внутри условия оператора do недопустимы do { mumble(foo); } while (int foo = get_foo()); // ошибка: объявление в условии II оператора do Если определить переменную в условии, она будет применена прежде, чем ока- жется определена! Упражнения раздела 6.9 Упражнение 6.17. Объясните каждый из следующих циклов. Исправьте все обнаруженные ошибки, (a) do int vl, v2; cout « "Please enter two numbers to sum:"; cin » vl » v2; if (cin) cout « "Sum is: " « vl + v2 << endl; while (cin); (b) do { } while (int ival = get_response()); (c) do { int ival = get_response(); if (ival == some_value()) Упражнение 6.18. Напишите небольшую программу, которая запрашивает у пользователя две строки и сообщает, какая из них лексикографически меньше, чем другая (т.е. посимвольно в ал- фавитном порядке). Организуйте запрос у пользователя новых значений или команды на выход. Используйте в цикле do... while тип string и оператор меньше класса string. 6.10. Оператор break Оператор break завершает выполнение ближайшего из вложенных операторов while, do. . .while, for или switch. Выполнение возобновляется с оператора, расположенного непосредственно после завершенного оператора. Например, сле- дующий цикл находит в векторе первое указанное значение. Как только искомое значение будет обнаружено, цикл прекращается.
238 Часть I. Основы vector<int>::iterator iter = vec.begin(); while (iter != vec.end()) { if (value == *iter) break; // ok: найдено! else ++iter; // не найдено, продолжать поиск } // конец цикла while if (iter != vec.end()) // остановиться здесь ... // продолжить обработку В данном примере оператор break прерывает цикл while. Выполнение продол- жается с оператора if, расположенного непосредственно после оператора while. Оператор break может располагаться только внутри цикла или оператора switch. Внутри оператора if оператор break может присутствовать только тогда, когда оператор if находится внутри оператора switch или цикла. Оператор break вне цикла или оператора switch приводит к ошибке во время компиляции. Когда оператор break встречается внутри вложенного оператора switch или цикла, он воздействует только на внутренний цикл или оператор switch, не затрагивая внешний. string inBuf; while (cin » inBuf && !inBuf.empty()) { switch(inBuf [0]) { case '-': // работать до первого пробела for (string::size_type ix = 1; ix != inBuf.size(); ++ix) { if (inBuf[ix] == ' 1 ) break; // #1, выйти из цикла for } I/ дальнейшая обработка случая ' -': break #1 передает // управление сюда break; // #2, выйти из оператора switch case ' +' : } // конец оператора switch // break #2 передает управление сюда } // конец оператора while Оператор break, помеченный меткой #1, завершает цикл for внутри раздела case для случая дефиса. Он не завершает внешний оператор switch и даже не за- вершает обработку текущего случая. Выполнение продолжается с первого оператора после цикла for, который мог бы содержать дополнительный код обработки случая дефиса или оператор break, который завершает данный раздел. Оператор break, помеченный меткой #2, завершает оператор switch уже после обработки случая дефиса, но не прерывает внешний цикл while. Выполнение кода после оператора break продолжает условие цикла while, где осуществляется чте- ние следующей строки со стандартного устройства ввода. Упражнения раздела 6.10 Упражнение 6.19. Первую программу этого раздела можно переписать короче. Фактически все ее действия можно осуществить в условии оператора while. Перепишите цикл так, чтобы его те- ло оставалось пустым, а все действия по поиску элемента происходили в условии.
Глава 6. Операторы 239 Упражнение 6.20. Напишите программу, которая читает последовательность строковых значений со стандартного устройства ввода до тех пор, пока не встретится повторяющееся слово или пока ввод слов не будет закончен. Для чтения текста по одному слову используйте цикл while. Для выхода из цикла при встрече двух совпадающих слов подряд используйте оператор break. Выве- дите повторяющееся слово, если оно есть, а в противном случае отобразите сообщение, свиде- тельствующее о том, что повторяющихся слов нет. 6.11. Оператор continue Оператор continue прерывает текущую итерацию ближайшего цикла. Выполне- ние возобновляется с условия в случае цикла while или do. . .while. В цикле for выполнение продолжается с вычисления выражения внутри заголовка оператора for. Например, следующий цикл читает по одному слову со стандартного устройства ввода. Но обрабатываться будут только те слова, которые начинаются с символа подчеркивания. При любом другом значении текущая итерация завершается и про- исходит ввод следующего слова. string inBuf; while (cin » inBuf && !inBuf.empty()) { if (inBuf [0] != } continue; // приступить к вводу следующего слова // здесь должен быть код обработки слова, // начинающегося символом } Оператор continue может располагаться только внутри циклов for, while или do. . .while, включая внутренние блоки, вложенные внутрь таких циклов. Упражнения раздела 6.11 Упражнение 6.21. Перепишите программу из последнего упражнения раздела 6.10 (стр. 238) так, чтобы она искала только те повторяющиеся слова, которые начинаются с прописной буквы. 6.12. Оператор goto Оператор goto позволяет осуществить безусловный переход к помеченному меткой оператору внутри той же функции. Использование оператора goto не было одобрено разработчиками еще в 1960-х го- дах- 0н существенно затрудняет контроль последовательности выполнения операторов Л программы, а также является источником ошибок при последующих модификациях. Любая программа, в которой был использован оператор goto, может быть переписана без него. Оператор goto имеет следующий синтаксис, goto метка; Здесь метка — это идентификатор, которым помечен оператор. Помеченный опе- ратор (labeled statement) — это любой оператор, которому предшествует идентифи- катор, сопровождаемый двоеточием.
240 Часть I. Основы end: return; // помеченный оператор, к нему может II осуществить переход оператор goto Составляющий метку идентификатор может использоваться только для операто- ра goto. Поэтому идентификатор метки может совпадать с именами переменных или других идентификаторов в программе, без всякой опасности конфликта имен с другими идентификаторами. Оператор goto и помеченный оператор, которому он передает управление, должны находиться в одной функции. Оператор goto не может перескакивать через определение переменной1. goto end; int ix = 10; // ошибка: оператор goto обошел объявление end: // ошибка: этот код мог бы использовать переменную ix, 11 но оператор goto обошел ее объявление ix = 42; Если между оператором goto и его меткой определение переменной все же необ- ходимо, его следует заключить в фигурные скобки. goto end; // ok: переход к точке, где II переменная ix не определена int ix = 10; // ... код, использующий переменную ix } end: // ix здесь больше невидима Переход назад, через уже использованное определение, вполне допустим, а пере- ход вперед, через еще не использованное определение — нет. Почему? Переход через неиспользованное определение способствует возникновению ситуации, когда будет использована применяемая переменная, определение которой оказалось пропущено. При возвращении к точке определения переменной, существующая переменная ока- жется удалена и создана снова. // переход назад к операторам объявления переменной вполне допустим Обратите внимание, при передаче оператором goto управления назад к метке begin:, переменная sz удаляется, определяется и инициализируется снова. Упражнения раздела 6.12 Упражнение 6.22. Последний пример в этом разделе, где оператор goto передает управление назад к метке begin:, может быть переписан с использованием цикла. Перепишите код так, что- бы устранить оператор goto. 1 Данное и предыдущее правило — не более, чем рекомендация. Они сродни правилам обра- щения обезьяны с гранатой. Для оператора goto существует только одно правило — никогда не используйте оператор goto. — Примеч. ред.
Глава 6. Операторы 241 6.13. Блок try и обработка исключений Обработка ошибок и разнообразных случаев аномального поведения программ является одной из наиболее сложных частей проекта любой системы. В таких долго- временных интерактивных системах, как коммуникационный коммутатор или мар- шрутизатор до 90% кода может быть посвящено именно обнаружению и обработке ошибок. Быстрый рост популярности Web-ориентированных приложений, которые выполняются постоянно, диктует необходимость обратить особое внимание на обра- ботку ошибок. Исключения (exception) — это аномалии времени выполнения, такие как исчерпа- ние памяти или ввод непредвиденных данных. Исключения существуют вне нор- мального функционирования программы и требуют непосредственной обработки2. В хорошо проработанных системах исключения используются для обработки набора программных ошибок. Исключения особенно полезны в случае, когда об- наруживший проблему код не способен с ней справиться. В таких случаях обна- руживший проблему участок программы нуждается в способе передать управле- ние другой части программы, которая с этой проблемой может справиться. Обна- руживший проблему участок кода должен быть также способен указать, какая именно проблема возникла, а также, при необходимости, передать дополнитель- ную информацию. Исключения и обеспечивают этот вид коммуникации между участком кода, об- наруживающим ошибку, и участком, ее обрабатывающим. Обработка исключений в языке C++ подразумевает следующее. Оператор throw используется участком кода, который обнаруживает ошибку, но не может ее обработать. Об операторе throw говорят, что он передает ис- ключение. Блок t гу является частью системы обработки ошибок, используемой для работы с исключением. Блок try начинается с ключевого слова try и завершается од- ним или несколькими разделами catch. Исключения, переданные из кода, рас- положенного внутри блока t гу, как правило, обрабатываются в одном из разде- лов catch. Поскольку разделы catch “обрабатывают” исключение, их называют также обработчиками (handler). Набор определенных в библиотеке классов исключений (exception class) исполь- зуется для передачи информации об ошибках между операторами throw и соот- ветствующими разделами catch. В остальной части этого раздела три компонента обработки исключений рассмат- риваются последовательно. Более подробная информация об исключениях приведе- на в разделе 17.1 (стр. 720). 2 Согласно другой трактовке, исключение — это объект системного или пользовательского класса, создаваемого операционной системой или кодом программы в ответ на обстоятельст- ва, либо не допускающие дальнейшего нормального выполнения программы, либо определенные пользователем. Обработка исключений в приложении позволяет корректно выйти из затруд- нительной ситуации. — Примеч. ред.
242 Часть I. Основы 6.13.1. Оператор throw Для передачи исключения используется оператор throw, который состоит из ключевого слова throw, сопровождаемого выражением. Оператор throw, как пра- вило, завершается точкой с запятой, что делает его выражением. Тип выражения оп- ределяет, какое именно исключение будет передано. В качестве примера вернемся к программе со стр. 46, в которой складываются два объекта класса Sales_item. Она проверяет, относятся ли обе прочитанные записи к одной книге. Если нет, программа отображает сообщение об ошибке и за- вершает работу. Sales_item iteml, item2; std::cin >> iteml >> item2; // сначала проверить, представляют ли объекты iteml и item2 II одну и ту же книгу if (iteml.same_isbn(item2)) { std:scout « iteml + item2 « std::endl; return 0; // свидетельство успеха } else { std::cerr « "Data must refer to same ISBN" « std::endl; return -1; // свидетельство отказа } В более сложной программе участок кода, складывающий объекты класса Sales_item, можно отделить от участка кода, отвечающего за взаимодействие с пользователем. В данном случае код проверки можно переписать так, чтобы вместо него использовался оператор передачи исключения throw. // сначала проверить, представляют ли объекты iteml и item2 II одну и ту же книгу if (!iteml.same_isbn(item2)) throw runtime_error("Data must refer to same ISBN"); // ok, если управление здесь, значит ISBN совпадают std::cout « iteml + item2 « std::endl; Здесь сравниваются ISBN объектов. Если они не совпадают, выполнение кода прерывается и управление передается обработчику, который и отвечает за выход из создавшейся ситуации. Оператору throw передают выражение. В данном случае выражением является объект класса runtime_error. Класс runtime_error — это один из классов ис- ключений, определенный в заголовке stdexcept стандартной библиотеки. Более подробно эти классы обсуждаются в одном из последующих разделов. При создании объекта класса runtime_error, ему можно передать строку, содержащую дополни- тельную информацию о произошедшей ошибке. 6.13.2. Блок try Блок t гу имеет следующий синтаксис, try { операторы_программы } catch (спецификатор_исключения) { операторы обработчика
Глава 6. Операторы 243 } catch (спецификатор_исключения) { операторы_обработчика Блок try начинается с ключевого слова try, за которым следует блок кода, за- ключенный в фигурные скобки. После блока try следует набор из одного или не- скольких обработчиков. Обработчик состоит из трех частей: ключевого слова catch, указания типа и объекта исключения в круглых скобках (называемого специфика- тором исключения (exception specifier)) и блока кода, как обычно, заключенного в фигурные скобки. Когда спецификатор исключения совпадает с указанным в об- работчике, выполняется именно его блок кода. По завершении выполнения кода обработчика, управление переходит к оператору, следующему непосредственно после него. Операторы программы внутри блока try являются обычными программными операторами, реализующими ее логику. Здесь могут располагаться любые операто- ры языка C++, включая объявления. Подобно любому другому блоку кода, блок try предоставляет локальную область видимости, а к объявленным внутри него пере- менным нельзя обратиться вне его, включая блоки кода обработчиков catch. Создание обработчика В приведенном ниже примере, чтобы избежать суммирования двух объектов класса Sales_item, представляющих разные книги, использовался оператор throw. Предположим, что та часть программы, которая суммирует объекты класса Sales_item, отделена от той части, которая взаимодействует с пользователем. Взаимодействующая с пользователем часть могла бы содержать код наподобие сле- дующего. Этот код обрабатывает исключения, переданные в блоке сложения. while (cin » iteml >> item2) { try { // код, который складывает два объекта класса Sales_item II если при сложении произойдет сбой, код передаст // исключение runtime_error } catch (runtime_error err) { // напомнить пользователю, что ISBN слагаемых объектов // должны совпадать cout « err.what() « "\nTry Again? Enter у or n" « endl; char c; cin » c; if (cin && c == 'n') break; // выход из цикла while } ) После ключевого слова try расположен блок кода, который реализует логику программы, выполняя сложение объектов класса Sales_item. Эта часть программы могла бы передать исключение класса runtime_error. Данный блок try обладает одним разделом catch, который обрабатывает ис- ключение типа runtime_error. Операторы в блоке после ключевого слова catch определяют действия, выполняемые в случае, если код внутри блока try передаст исключение runtime_error. В данном случае обработка подразумевает отображение сообщения об ошибке и запрос у пользователя разрешения на продолжение. Когда
244 Часть I. Основы пользователь вводит символ ' п', цикл while завершается, а в противном случае он продолжается и считывает два новых объекта класса Sales_item. В сообщении об ошибке используется текст, возвращенный функцией err. what (). Поскольку известно, что классом объекта исключения err является runtime_ error, нетрудно догадаться, что функция what () является членом (раздел 1.2, стр. 27) класса runtime_error. В каждом из библиотечных классов исключений определена функция-член по имени what (). Эта функция не получает никаких ар- гументов и возвращает символьную строку в стиле С. В случае класса runtime_ error, эта строка является копией строки, использованной при инициализации объекта класса runtime_error. Если описанный в предыдущем разделе код пере- даст исключение, отображенное разделом catch сообщение об ошибке будет иметь следующий вид. Data must refer to same ISBN Try Again? Enter у or n При поиске обработчика выполнение функций прерывается В больших системах выполнение программы может быть довольно сложным и включать разнообразные вложенные блоки try, которые способны передавать ис- ключения участкам кода, которые фактически их обрабатывают. Например, в блоке try может быть вызвана функция, в блоке try которой содержится вызов другой функции с ее собственным блоком try и т.д. Поиск обработчика осуществляется по цепочке обращений в обратном порядке. Сначала поиск обработчика исключения осуществляется в той функции, в которой оно было передано. Если соответствующего раздела catch не найдено, работа функции завершается, а поиск продолжается в той функции, которая вызвала функцию, в кото- рой было передано исключение. Если и здесь соответствующего раздела catch не найдено, эта функция также завершается, а поиск продолжается по цепочке вызовов дальше, пока обработчик исключения соответствующего типа не будет найден. Если соответствующий раздел catch так и не будет найден, управление перейдет к библиотечной функции terminate (), которая определена в заголовке exception. Поведение этой функции зависит от системы, но обычно она завершает выполнение программы. Исключения, которые были переданы в программах, не имеющих блоков t гу, об- рабатываются аналогично: в конце концов, без блоков try не может быть никаких обработчиков и ни для каких исключений, которые, однако, вполне могут быть пере- даны. В таком случае исключение приводит к вызову функции terminate (), кото- рая (как правило) и завершает работу программы. Упражнения раздела 6.13.2 Упражнение 6.23. Функция to_ulong() класса bitset передает исключение overflow_ error в случае, если размер набора битов превосходит размер типа unsigned long. Напи- шите программу, которая передает это исключение. Упражнение 6.24. Перепишите полученную программу так, чтобы в обработчик этого исключения отображал соответствующее сообщение.
Глава 6. Операторы 245 6.13.3. Стандартные исключения В библиотеке C++ определен набор классов, объекты которых могут быть исполь- зованы для передачи сообщений о проблемах, которые могут происходить в функциях, определенных в стандартной библиотеке. Эти стандартные классы исключений могут быть также использованы в программах, создаваемых разработчиком. Библиотечные классы исключений определены в четырех следующих заголовках. 1. В заголовке exception определен общий класс исключения exception. Объект этого базового для всех исключений класса обладает лишь коммуникационными возможностями, без всяких дополнительных функций. 2. В заголовке stdexcept определено несколько универсальных классов исклю- чения. Эти классы перечислены в табл. 6.1. 3. В заголовке new определен класс исключения bad_alloc, объекты которого пе- редает оператор new (раздел 5.11, стр. 199) при невозможности зарезервировать область памяти. 4. В заголовке type_inf о определен класс исключения bad_cast, более подроб- но обсуждаемый в разделе 18.2 (стр. 804). Таблица 6.1. Стандартные классы исключений, определенные в заголовке stdexcept exception Наиболее общий вид проблемы runt ime_error range_error overflow_error underflow_error logic_error Проблема, которая может быть обнаружена только во время выполнения Ошибка времени выполнения программы: полученный результат превосходит допустимый диапазон значения Ошибка времени выполнения программы: переполнение регистра при вычислении Ошибка времени выполнения программы: недополнение регистра при вычислении Проблема, которая может быть обнаружена до времени выполнения domain_error Логическая ошибка: аргумент, для которого не существует результата invalid_argument Логическая ошибка: неподходящий аргумент length_error Логическая ошибка: попытка создать объект большего размера, чем макси- мально допустимый для данного типа out of range Логическая ошибка: используемое значение вне допустимого диапазона Классы исключений стандартной библиотеки Библиотечные классы исключения поддерживают лишь несколько операций. Можно создать копию объекта любого класса исключения или осуществить при- своение. В классах exception, bad_alloc и bad_cast определен только стан- дартный конструктор (раздел 2.3.4, стр. 73), поэтому невозможно инициализировать объект этих типов. В других типах исключений определен только один конструктор, которому передают инициализирующее значение класса string. При определении любого из объектов исключений этих типов необходимо предоставить аргумент типа string. Инициализирующая строка используется для передачи дополнительной информации о произошедшей ошибке.
246 Часть I. Основы В классах исключений определена только одна функция what (). Она не получа- ет никаких аргументов и возвращает константный указатель типа char. Это указа- тель на символьную строку в стиле С (раздел 4.3, стр. 154), которая содержит текст описания переданного исключения. Содержимое символьного массива (строки в стиле С), указатель на который воз- вращает функция what (), зависит от типа объекта исключения. Для типов, которым при инициализации передают строку класса string, функция what () возвраща- ет строку как символьный массив в стиле С. Что же касается других типов, то воз- вращаемое значение зависит от компилятора. 6.14. Использование препроцессора для отладки В разделе 2.9.2 (стр. 93) было описано применение переменных препроцессора для предотвращения повторного подключения файлов заголовка. Подобную техно- логию программисты C++ иногда используют для условного выполнения отладоч- ного кода. Идея заключается в том, что добавленный в программу отладочный код будет компилироваться и выполняться только во время пробных запусков програм- мы, осуществляемых при ее разработке. Когда проект приложения будет завершен и оно окажется готово к финальной компиляции, отладочный код исключается. Для организации условной компиляции отладочного кода можно воспользоваться пере- менной препроцессора NDEBUG3. int main() { ttifndef NDEBUG cerr « "starting main" « endl; ttendif Если переменная NDEBUG не определена, программа отобразит сообщение об ошибке при помощи объекта cerr. Если переменная NDEBUG определена, программа компилируется без кода, расположенного между директивами #if ndef и # end if. По умолчанию переменная NDEBUG не определена, а следовательно, код внутри директив #ifndef и #endif войдет в состав программы. При разработке програм- мы переменная NDEBUG остается неопределенной, поэтому операторы отладочного кода при компиляции и запуске выполняются. Перед финальной компиляцией про- граммы и передачей ее клиентам эти отладочные операторы следует удалить. Для этого достаточно определить переменную препроцессора NDEBUG. Большинство компиляторов поддерживает параметр командной строки, который позволяет опре- делить переменную препроцессора (например NDEBUG). $ СС -DNDEBUG main.С Эта командная строка эквивалентна определению #def ine NDEBUG в начале файла исходного кода main. С. 3 Традиционно применяется также переменная DEBUG, с директивой проверки tfifdef DEBUG. — Примеч. ред.
Глава 6. Операторы 247 Препроцессор определяет четыре константы, которые могут пригодиться при отладке. FILE имя файла. LINE номер текущей строки. TIME время компиляции файла. DATE дата компиляции файла. Эти константы можно использовать для указания дополнительной информации в сообщении об ошибке. if (word.size() < threshold) сегг « "Error: " << FILE << " : line " << LINE << endl << " Compiled on " « DATE « " at " « TIME « endl << " Word read was " << word << " : Length too short" << endl; Если переданная этой программе строка окажется короче, чем указано в пере- менной threshold, она отобразит следующее сообщение об ошибке. Error: wdebug.cc : line 21 Compiled on Jan 12 2005 at 19:44:40 Word read was "foo": Length too short Кроме переменной препроцессора NDEBUG, для отладки используется макрос пре- процессора assert (preprocessor macro). Макрос assert определен в заголовке cassert, который необходимо подключить в любой файл, использующий мак- рос assert. Действие макроса препроцессора подобно вызову функции. Макрос assert по- лучает одно выражение, которое он использует как условие. assert{выражение) Подобно проверке определения переременной препроцессора NDEBUG, макрос assert проверяет условие, и если результат ложен (значение false), он отобража- ет сообщение и завершает программу. Если результат выражения отличается от ну- ля (значение true), макрос assert ничего не делает. В отличие от исключений, которые передаются при возникновении ошибок, ве- роятность которых предусмотрена, макрос assert используют для проверки усло- вий, которых “не может быть”. Например, программа, манипулирующая введенным текстом, может предполагать, что все полученные слова длиннее порогового значения (threshold). Такая программа могла бы содержать следующий оператор. assert(word.size() > threshold); При проверке макрос assert гарантирует, что переданные строки всегда будут иметь ожидаемый размер. Как только разработка и проверка будут завершены, про- грамма компилируется при определенной переменной препроцессора NDEBUG. В фи- нальной версии кода макрос assert ничего не делает. Безусловно, проверки во вре- мя выполнения он не осуществляет. Макрос assert следует использовать для про- верки только таких условий, которые являются недопустимыми. Этот подход может быть весьма полезен при отладке программы, но он не должен заменять логику про- верок допустимости или обработки ошибок.
248 Часть I. Основы Упражнения раздела 6.14 Упражнение 6.25. Вернитесь к программе упражнения в разделе 6.11 (стр. 239) и организуйте условное отображение информации о ее выполнении. Например, можно вывести все введенные слова, чтобы убедиться в том, что цикл правильно обнаруживает наличие повторяющихся слов, ко- торые начинаются с прописной буквы. Откомпилируйте и запустите программу сначала в отладоч- ном режиме, а затем откомпилируйте и запустите финальную версию программы. Упражнение 6.26. Что происходит в следующем цикле? string s; while (cin » s) { assert(cin); // обработка s } Объясните, что делает здесь макрос assert. Упражнение 6.27. Объясните этот цикл. string s; while (cin >> s && s != sought) { } // пустое тело assert(cin); // обработка s Резюме Язык C++ предоставляет довольно ограниченное количество операторов. Некоторые из них предназначены для управления потоком выполнения программы. Операторы while, for и do. . . while позволяют реализовать итерационные циклы. Операторы if и switch позволяют реализовать условное выполнение. Оператор cont inue останавливает текущую итерацию цикла. Оператор break осуществляет принудительный выход из цикла оператора switch. Оператор goto передает управление помеченному оператору. Операторы try и catch позволяют создать блок try, в который заключают операторы программы, потенциально способные передать исключение. Оператор catch начинает раздел обработчика исключения, код которого предназначен для реакции на исключение определенного типа. Оператор throw позволяет передать исключение, обрабатываемое в соответствующем разделе catch. Существует также оператор return, более подробно рассматриваемый в главе 7, “Функции”. Кроме того, существуют операторы выражений и операторы объявлений. Оператор выра- жения обеспечивает выполнение действий в выражении. Объявления и определения пере- менных были описаны в главе 2, “Переменные и базовые типы”.
Глава 6. Операторы 249 Термины Блок (block). Последовательность операторов, заключенная в фигурные скобки. Блок операторов может быть использован везде, где ожидается один оператор. Блок try. Блок, начинаемый ключевым словом try и содержащий один или несколько разделов catch. Если код внутри блока try передаст исключение, а один из разделов catch соответствует типу этого исключения, исключение будет обработано кодом данного обра- ботчика. В противном случае исключение будет обработано во внешнем блоке try, но если и этого не произойдет, сработает функция terminate (), которая и завершит выполнение программы. Класс исключения (exception class). В стандартной библиотеке определен целый набор классов, объекты которых можно использовать для передачи сообщений об ошибках в обра- ботчики. Универсальные классы исключений перечислены в табл. 6.1 (стр. 245). Макрокоманда assert. Макрос препроцессора, который получает одно выражение, ис- пользуемое в качестве условия. Если переменная препроцессора NDEBUG не определена, мак- рос assert проверяет условие. Если оно ложно, макрос assert выводит сообщение и за- вершает программу. Макрос препроцессора (preprocessor macro). Подобное функции средство, определенное препроцессором. Примером макроса является assert. В современных программах C++ мак- росы используют нечасто. Метка case. Целочисленное постоянное значение, которое следует за ключевым словом case в операторе switch. Метки case в одном операторе switch не могут иметь одинако- вых значений. Если значение в условии оператора switch совпадает с одной из меток case, управление передается первому оператору после соответствующей метки. Выполнение про- должается до конца оператора switch, если не встретится оператор break. Метка default. Метка оператора switch, которая соответствует любому значению ус- ловия оператора switch, не указанному в метках case явно. Обработчик исключения (exception handler). Код обработчика предназначен для реакции на исключение определенного типа, переданное из другой части программы. Синоним терми- на раздел catch. Оператор break. Завершает ближайший вложенный цикл или оператор switch. Переда- ет управление первому оператору после завершенного цикла или оператора switch. Оператор continue. Завершает текущую итерацию ближайшего вложенного цикла. Пере- дает управление условию цикла while, оператору do или выражению в заголовке цикла for. Оператор goto. Оператор, который осуществляет безусловную передачу управления по- меченному оператору в другом месте программы. Операторы goto нарушают последователь- ность выполнения операций программы, поэтому их следует избегать. Оператор if. Условное выполнение кода на основании значения в условии. Если условие истинно (значение true), тело оператора if выполняется, а в противном случае управление переходит к оператору, следующему после него. Оператор if. . . else. Условное выполнение кода в разделе if или else, в зависимости от истинности значения условия. Оператор switch. Оператор условного выполнения, который сначала вычисляет резуль- тат выражения, следующего за ключевым словом switch, а затем передает управление разде- лу case, метка которого совпадает с результатом выражения. Когда соответствующей метки нет, выполнение переходит к разделу default (если он есть) или к оператору, следующему за оператором switch, если раздела default нет. Оператор throw. Оператор, прерывает текущий поток выполнения. Каждый оператор throw передает объект, который переводит управление на ближайший раздел catch, способ- ный обработать исключение данного класса.
250 Часть I. Основы Оператор выражения (expression statement). Выражение завершается точкой с запятой. Оператор выражения обеспечивает выполнение действий в выражении. Оператор объявления (declaration statement). Оператор, который определяет или объяв- ляет переменную. Объявления рассматривались в главе 2, “Переменные и базовые типы”. Передача (raise). Выражение, которое прерывает текущий поток выполнения. Каждый оператор throw передает объект, который переводит управление на ближайший раздел catch, способный обработать исключение данного класса. Помеченный оператор (labeled statement). Оператор, которому предшествует метка. Мет- ка (label) — это идентификатор, сопровождаемый двоеточием. Потерянный оператор else (dangling else). Разговорный термин, используемый для описания проблемы, когда во вложенной конструкции операторов i f больше, чем операторов else. В языке C++ оператор else всегда принадлежит ближайшему расположенному выше оператору if. Чтобы указать явно, какому из операторов if принадлежит конкретный опера- тор else, применяются фигурные скобки. Пустой оператор (null statement). Пустой оператор представляет собой отдельный символ точки с запятой. Раздел catch. Состоит из ключевого слова catch, заключенного в круглые скобки, спе- цификатора исключения и блока операторов. Код в разделе catch предназначен для обра- ботки исключения, тип которого указан в спецификаторе. Составной оператор (compound statement). Синоним блока. Спецификатор исключения (exception specifier). Объявление объекта или класса исклю- чения, обрабатываемого в данном разделе catch. Управление потоком (flow of control). Управление последовательностью выполнения операций в программе. Функция terminate (). Библиотечная функция, обращение к которой происходит в слу- чае, если исключение так и не было обработано. Обычно завершает выполнение программы. Цикл while. Оператор цикла, который выполняет оператор тела до тех пор, пока условие остается истинным (значение true). В зависимости от истинности значения условия, опера- тор выполняется любое количество раз.
Функции В ЭТОЙ ГЛАВЕ... 7.1. Определение функций 251 7.2. Передача аргумента 256 7.3. Оператор return 271 7.4. Объявление функций 277 7.5. Локальные объекты 280 7.6. Встраиваемые функции 282 7.7. Функции-члены класса 284 7.8. Перегруженные функции 291 7.9. Указатели на функции 302 Резюме 305 Термины 306 В этой главе описано, как объявлять и определять функции. Здесь также обсуж- дается передача функции аргументов и возвращение из них полученных значений. Далее описаны три специальных вида функций: встраиваемые функции, функции- члены класса и перегруженные функции. Глава завершается более сложной темой: указателями на функции. Функцию можно рассматривать как оператор, определенный программистом. Подобно встроенным операторам, каждая функция выполняет некоторые действия и, как правило, возвращает результат. В отличие от операторов, функции имеют имена и неограниченное количество операндов. Подобно операторам, функции мо- гут быть перегружены, а следовательно, то же самое имя может быть использовано для нескольких разных функций. 7.1. Определение функций Функция (function) уникально идентифицируется по имени, набору и типу опе- рандов. Операнды функции, называемые параметрами (parameter), определены в разделяемом запятой списке, заключенном в круглые скобки. Выполняемые функ- цией действия располагаются в блоке, называемом телом функции (function body). Для каждой функции указывают возвращаемый тип (return type).
252 Часть I. Основы Для поиска самого большого общего делителя двух целых чисел, например, мож- но написать следующую функцию. // возвратить самый большой общий делитель int gcd(int vl, int v2) { while (v2) { int temp = v2; v2 = vl % v2; vl = temp; } return vl; Здесь определена функция по имени gcd, которая возвращает значение типа int и получает два параметра типа int. Для вызова функции gcd () необходимо обеспе- чить передачу ей двух значений типа int, а также получение возвращаемого значе- ния типа int. Вызов функции Для вызова функции используется оператор обращения (call operator), представ- ляющий собой пару круглых скобок. Подобно любым другим операторам, оператор обращения получает операнды и возвращает результат. Операндами оператора об- ращения являются имя функции и разделяемый запятыми список аргументов (возможно пустой). Типом результата оператора обращения является возвращаемый тип вызываемой функции, а значением результата является значение, возвращаемое самой функцией. // получить значение со стандартного устройства ввода cout << "Enter two values: \n"; int i, j; cin » i » j ; // вызвать функцию gcd(), передав ей аргументы i и j // отобразить их самый большой общий делитель cout « "gcd: " « gcd(i, j) « endl; Если ввести числа 15 и 123, эта программа отобразит результат 3. При вызове функции осуществляется два действия: параметры функции инициа- лизируются соответствующими аргументами и управление передается вызываемой функции. В результате выполнение вызывающей функции приостанавливается и начинается выполнение вызываемой функции. Выполнение функции начинается с неявного определения и инициализации ее параметров. То есть при вызове функции gcd () в первую очередь создаются переменные типа int по имени vl и v2. Эти пе- ременные инициализируются значениями, переданными при обращении к функции gcd (). В данном случае переменная vl инициализируется значением переменной i, а переменная v2 — значением переменной j. Тело функции обладает собственной областью видимости Тело функции (function body) — это блок операторов, которые осуществляют дей- ствия функции. Как обычно, блок заключается в фигурные скобки, а следовательно, создается новая область видимости. Подобно любому блоку, в теле функции можно определять переменные. Имена, определенные внутри тела функции, доступны
Глава 7. Функции 253 только внутри самой функции. Такие переменные называют локальными (local variable). Локальными они называются потому, что видимы только в области види- мости функции. Эти переменные существуют только во время выполнения функ- ции. Более подробная информация о локальных переменных приведена в разделе 7.5 (стр. 280). Выполнение функции завершает оператор return. Когда вызываемая функция завершает выполнение необходимых действий, полученное в результате значение передается оператору return. После выполнения оператора return, приостанов- ленная вызывающая функция продолжает выполнение с точки обращения. Полу- ченное в результате вызова функции значение используется при выполнении опера- тора, в котором произошло обращение к функции. Параметры и аргументы Подобно локальным переменным, параметры функции (parameter) представляют собой именованные локальные хранилища данных, используемых в функции. Раз- личие заключается в том, что параметры определяют внутри списка параметров функции и инициализируют аргументами, переданными функции при вызове. Аргумент (argument) — это выражение, которое может быть переменной, лите- ральной константой или выражением, состоящим из одного или нескольких опера- торов. При вызове, функции должно быть передано столько же аргументов, сколько параметров она имеет. Тип каждого аргумента должен соответствовать типу пара- метра точно так же, как и тип инициализирующего значения должен соответство- вать типу инициализируемого объекта. То есть аргумент должен иметь тип, совпа- дающий с типом параметра, или тип, который может быть неявно преобразован (раздел 5.12, стр. 204) в тип параметра. Более подробная информация о соответствии аргументов параметрам приведена в разделе 7.8.2 (стр. 295). 7.1.1. Тип возвращаемого значения функции Возвращаемое значение функции может иметь встроенный тип, такой как int или double, тип класса или составной тип, например int& или string*. Типом возвращаемого значения функции может быть также указан тип void. Это означает, что функция не возвращает никакого значения. Ниже приведены примеры опреде- ления функций, с описанием типов возвращаемых значений, bool is_present(int *, int); // возвращает тип bool int count(const string &, char); // возвращает тип int Date &calendar(const char*); // возвращает ссылку на тип Date void process(); // не возвращает ничего Функция не может возвращать другую функцию или массив. Однако функция может вернуть указатель на функцию или на элемент массива. // ок: указатель на первый элемент массива int *foo_bar() { /* ... */ } Эта функция возвращает указатель на тип int, который может указывать на элемент массива. Более подробная информация об указателях на функции приведена в разделе 7.9 (стр. 302).
254 Часть I. Основы Функции необходимо указать возвращаемый тип Нельзя объявить или определить функцию, не указав явно тип возвращаемого значения. // ошибка: не указан тип возвращаемого значения test(double vl, double v2) { /* ... */ } В прежних версиях языка C++ такой функции был бы неявно назначен тип int, однако, согласно стандарту C++, такое определение является ошибкой. Д° появления стандарта C++, функция без явного указания типа возвращаемого значения 1 воспринималась как возвращающая тип int. Программы C++, разработанные ранее под нестандартные компиляторы, все еще могут содержать функции, которые полагаются на неявное присвоение типа int. 7.1.2. Список параметров функции Список параметров (parameter list) функции может быть пуст, но не может быть пропущен. Функция, которой параметры не нужны, может быть объявлена либо с пустым списком параметров, либо со списком параметров, содержащим одно клю- чевое слово void. Например, два следующих объявления функции process () эк- вивалентны. void process (){/*...*/} // список параметров, неявно II указан как пустой void process (void) { /* ... */ } // эквивалентное объявление Список параметров представляет собой разделяемый запятыми список типов па- раметров и (не обязательно) имен параметров. Даже когда типы двух параметров совпадают, указывать их следует по отдельности. int manip(int vl, v2){/*...*/} // ошибка int manip(int vl, int v2) { /* ... */ } // ok Параметры не могут иметь одинаковые имена. Аналогично, локальная перемен- ная внутри функции не может иметь имя, совпадающее с именем любого из пара- метров функции. Имена в определении функций не обязательны, но все параметры обычно име- нуют. Используемый параметр должен иметь имя. Контроль соответствия типов параметров Язык C++ обладает статическим контролем типов (раздел 2.3, стр. 66), т.е. аргументы каждого обращения проверяются в момент компиляции. Когда происходит вызов функции, тип каждого ее аргумента должен либо совпа- дать с типом соответствующего параметра, либо неявно преобразовываться (раз- дел 5.12, стр. 204) в него. Список параметров функции предоставляет компилятору информацию о типах, необходимую ему для проверки аргументов. Например, опре- деленной на стр. 252 функции gcd () передают два параметра типа int.
Глава 7. Функции 255 gcd("hello", "world"); // ошибка: несоответствие типов аргументов gcd(24312); // ошибка: слишком мало аргументов gcd(42, 10, 0); // ошибка: слишком много аргументов Каждое из этих обращений приведет к ошибке во время компиляции. При первом вызове аргументы имеют тип указателя на постоянную строку (const char). По- скольку указатель на тип const char к типу int не приводится, такое обращение некорректно. Во втором и третьем случаях функции gcd () передано неправильное количество аргументов. Эту функцию следует вызывать с двумя аргументами, любое другое количество приведет к ошибке. Но что произойдет, если при обращении к функции gcd () передать два аргумен- та типа double? Будет ли это обращение корректным? gcd(3.14, 6.29); // ok: аргументы преобразуются в int Да, в языке C++ это обращение вполне корректно. В разделе 5.12.1 (стр. 205) бы- ло продемонстрировано, что значение типа double может быть преобразовано в значение типа int. Такое преобразование и происходит при этом обращении — зна- чение типа double используется для инициализации объекта типа int. Следова- тельно, помечать это обращение как ошибку не имеет смысла. Аргументы будут неявно преобразованы в тип int, правда, с усечением значений. Поскольку точ- ность значения при этом преобразовании может быть потеряна, большинство ком- пиляторов выдаст об этом предупреждение. В данном случае произойдет следую- щее обращение. gcd(3, 6) ; В результате функция возвратит значение 3. Обращение, в котором передают слишком много или мало аргументов или в ко- тором передают аргументы несоответствующего типа, почти наверняка привело бы к ошибке во время выполнения программы. Выявление этих ошибок во время ком- пиляции значительно сокращает процесс отладки больших программ. Упражнения раздела 7.1.2 Scarred fry Dfyrol Упражнение 7.1. В чем разница между параметром и аргументом? Упражнение 7.2. Укажите, какие из следующих функций некорректны и почему. Исправьте обна- руженные ошибки, (a) int f() { string s; return s; } (b) f2(int i) {/*...*/ } (c) int calc(int vl, int vl) /* ... */ } (d) double square(double x) return x * x; Упражнение 7.3. Напишите программу, функция которой обладает двумя параметрами типа int и возвращает результат, равный значению первого параметра в степени второго. Организуйте вы- зов этой функции с передачей двух целочисленных значений. Проверьте результат. Упражнение 7.4. Напишите программу, функция которой возвращает абсолютное значение ее па- раметра.
256 Часть I. Основы 7.2. Передача аргумента При каждом обращении к функции ее параметры создаются заново. Значение, используемое для инициализации параметра, предоставляет соответствующий аргу- мент, переданный при обращении. Параметры инициализируются точно так же, как и обычные переменные. Если параметр Я имеет нессылочный тип, значение аргумента просто копируется. Если параметр является '^у/ ссылкой (раздел 2.5, стр. 81), он становится лишь другим именем аргумента. 7.2.1. Нессылочные параметры Параметры простых, нессылочных, типов (nonreference type) инициализируются копией значения соответствующего аргумента. Когда параметр инициализируется копией значения, функция не имеет доступа к значению фактического аргумента. Поэтому такая функция не может изменить значение аргумента. Давайте вернемся к определению функции gcd (). // возвратить самый большой общий делитель int gcd(int vl, int v2) while (v2) { int temp - v2; v2 - vl % v2; vl = temp; } return vl; В теле цикла while значения переменных vl и v2 изменяются. Но эти измене- ния происходят лишь со значениями локальных параметров и никак не отражаются на аргументах, используемых при вызове функции gcd (). Итак, если вызов функ- ции происходит подобным образом, после выполнения функции gcd () значения переменных i и j останутся неизменными. gcd(i, j) Нессылочные параметры содержат локальные копии соответствующих аргументов. По- г, . ? I этому в функции изменяются лишь локальные копии исходный значений. Как только функция завершает работу, эти локальные копии удаляются. Указатели как параметры Параметр вполне может быть указателем (раздел 4.2, стр. 138), когда указателем является копируемый аргумент. Подобно любым нессылочным параметрам, измене- ние его значения осуществляется только в локальной копии. Если функция при- сваивает новое значение своему параметру, который является указателем, значение исходного указателя не изменяется. Как уже упоминалось в разделе 4.2.3 (стр. 144), факт копирования указателя воз- действует только на сам указатель, а не на значение, адрес которого в указателе хра-
Глава 7. Функции 257 нится. Если функция получает указатель на неконстантный тип, она вполне может использовать его для изменения значения, адрес которого содержит указатель. void reset(int *ip) { *ip = 0; // изменяет значение объекта, адрес которого II содержит указатель ip ip = 0; // изменяет значение лишь локальной копии адреса, II хранимого в указателе ip; значение аргумента и / / объекта остаются неизменными } После обращения к функции reset () аргумент останется неизменным, но объ- ект, на который он указывает, станет равен 0. int i = 42; int *р = &i; cout << "i: " << *p << ' \n‘; // выводит i: 42 reset(p); // изменяет *p, но не p cout « "i: " « *p « endl; // ok: выводит i: 0 Если необходимо предотвратить изменение значения, на которое указывает па- раметр, его следует объявить константным указателем. void use_ptr(const int *p) { // use_ptr() способна читать, но не записывать в *р } Объявление параметра указателем на константный или неконстантный тип, оп- ределяет возможные аргументы, которые можно передавать в функцию при вызове. При вызове функции use_ptr() можно передать как константный (int*), так и неконстантный (const int*) указатель на тип int, а функции resetO можно передать лишь обычный указатель на тип int, т.е. (int*). Это различие следует из правил инициализации указателей (раздел 4.2.5, стр. 151). Указатель на константу можно инициализировать адресом неконстантного объекта, но использовать указа- тель на неконстанту для хранения адреса константного объекта нельзя. Константные параметры Функцию, ожидающую передачи нессылочного, неконстантного параметра, мож- но вызвать передав ей константный или неконстантный аргумент. Например, функ- ции gcd () можнопередать два аргумента типа const int. const int i = 3, j = 6; int k = gcd(3, 6); // ok: k инициализируется значением 3 Подобное поведение обусловлено обычными правилами инициализации кон- стантными объектами (раздел 2.4, стр. 78). Поскольку при инициализации инициа- лизирующее значение копируется, неконстантный объект можно инициализировать значением константного объекта и наоборот. Если тип параметра сделать константным и нессылочным, использующая его функция не сможет изменять локальную копию значения соответствующего ар- гумента. void fen(const int i) { /* функция fcn() может читать значение параметра i, но не записывать его */ }
258 Часть I. Основы Хотя при передаче функции f сп () все еще происходит копирование значения ее аргумента, теперь это может быть константный или неконстантный объект. Удивительней всего то, что хотя параметр является константой внутри функции, компилятор воспринимает определение функции f сп () так, как будто определен обычный параметр типа int. void fen(const int i) void fen(int i) { /* { /* функция fcn() может читать значение параметра i, но не записывать его * / } .. */ } // ошибка: переопределяет fen(int) Подобный подход применяется для обеспечения совместимости с языком С, ко- торый не делает никаких различий между функциями, получающими константные или неконстантные параметры. Ограничения на копирование аргументов Копирование значений аргументов подходит не для всех ситуаций. Копирование неприменимо в следующих случаях. Когда необходимо, чтобы функция была способна изменить значение аргумента. Когда в качестве аргумента необходимо передать большой объект. В некоторых приложениях затраты времени и объемов памяти, необходимых для копирова- ния объектов, могут оказаться неоправданно высоки. Когда скопировать объект невозможно. В этих случаях параметры следует определять как ссылки или указатели. Упражнения раздела 7.2.1 Упражнение 7.5. Напишите функцию, которая получает параметр типа int и указатель на тип int, а возвращает большее из значений типа int (будь то число, переданное по значению или при помощи указателя). Какой тип указателя следует использовать? Упражнение 7.6. Напишите функцию, которая меняет местами значения типа int, указатели на которые получает функция. Проверьте функцию отобразив значения до и после вызова функции. 7.2.2. Ссылочные параметры Примером ситуации, где копирование аргументов не срабатывает, является слу- чай, когда функция должна поменять местами значения двух своих аргументов. // неправильная версия функции swap(): значения аргументов II местами не поменяются! void swap(int vl, int v2) { int tmp = v2; v2 = vl; // присвоение нового значения локальной // копии аргумента vl = tmp; } // локальные объекты vl и v2 больше не существуют В данном случае предполагалось изменить значения самих аргументов. Тем не менее, проверка показала, что функция swap () не может воздействовать на аргу- менты. В ходе выполнения функция swap () меняет местами значения локальных копий аргументов, а сами аргументы остались неизменными.
Глава 7. Функции 259 int main() int i = 10; int j = 20; cout << "Before swap():\ti: " « i « "\tj: " « j « endl; swap(i, j); cout « "After swap():\ti: " После компиляции и запуска на выполнение эта программа отобразит следую- щий результат. Before swap(): i: 10 j: 20 After swap(): i: 10 j: 20 Чтобы функция swap () сработала и поменяла местами значения своих аргумен- тов, в качестве параметров следует использовать ссылки. // ок: функция swap() получает доступ к значениям своих II аргументов при помощи ссылок void swap(int &vl, int &v2) int tmp = v2; v2 = vl; vl = tmp; } Подобно всем ссылкам, ссылочные параметры (reference parameter) относятся к самим объектам, а не к их копиям. При определении ссылки, ее необходимо инициа- лизировать объектом, к которому она будет относиться. Ссылочные параметры ра- ботают точно так же. При каждом вызове такой функции, ссылочный параметр соз- дается и привязывается к соответствующему аргументу. Теперь при вызове функции swap () параметр vl является не более, чем другим именем объекта i, a v2 — другим именем объекта j. swap(i, j); Любое изменение параметра vl фактически осуществляется для аргумента i. Аналогично, изменения параметра v2 фактически осуществляются для аргумента j. Если перекомпилировать программу с использованием этой новой версии функции swap (), результат ее выполнения окажется правильным. Before swap(): i: 10 j: 20 After swap(): i: 20 j: 10 Программисты, перешедшие на язык C++ после языка С, используют для доступа к значениям аргументов передачу указателей. В языке C++ надежней и естественней использовать ссылочные параметры. Использование ссылочных параметров для возвращения дополнительной информации Как уже было продемонстрировано на примере функции swap (), ссылочные па- раметры можно использовать для изменения значений аргументов функции. Другой областью применения ссылочных параметров является возвращение нескольких ре- зультатов вызывающей функции.
260 Часть I. Основы Функция может вернуть только одно значение, но иногда необходимо возвра- щать несколько значений. Давайте, например, определим функцию по имени f ind_ val, которая ищет в элементах вектора целых чисел определенное значение. Она возвращает итератор, который относится к найденному элементу, или значение end, если такой элемент не найден. Необходимо, чтобы функция возвращала также коли- чество найденных элементов, если их несколько. В таком случае возвращенный ите- ратор должен соответствовать первому элементу, который имеет искомое значение. Как же можно определить функцию, которая возвращает и итератор, и количест- во обнаруженных элементов? Можно, конечно, определить новый тип, который со- держит итератор и количество элементов, однако более простое решение заключает- ся в передаче дополнительного ссылочного аргумента, который функция find_val() сможет использовать для возвращения количества обнаруженных элементов. // возвратить итератор, относящийся к первому найденному значению // второе возвращаемое значение содержит ссылочный параметр vector<int>::const iterator find_val( vector<int>::const_iterator beg, // первый элемент vector<int>::const_iterator end, // элемент после последнего int value, // искомое значение vector<int>::size_type ^occurs) // количество обнаруженных { // res_iter будет содержать первый найденный итератор vector<int>::const_iterator res_iter = end; occurs - 0; // обнулить параметр счетчика обнаружений for ( ; beg != end; ++beg) if (*beg -- value) { // запомнить первое обнаружение значения if (res_iter == end) res_iter = beg; ++occurs; // инкремент счетчика обнаружений } return res_iter; // а значение счетчика возвращается неявно } При вызове, функции f ind_val () передают четыре аргумента: два итератора, кото- рые обозначают диапазон элементов (раздел 9.2.1, стр. 341) вектора, в котором осуществ- ляется поиск, искомое значение и объект типа size_type (раздел 3.2.3, стр. 108), предна- значенный для хранения количества найденных элементов. С учетом того, что ivec — это вектор vectore int>, it — итератор соответствующего типа, a ctr — переменная типа size_type, функцию f ind_val () можно вызвать следующим образом. it = find_val(ivec.begin(), ivec.end(), 42, ctr); После обращения переменная ctr будет содержать количество обнаруженных значений 42, а итератор it окажется установленным на первый из них, если, конеч- но, таковой имеется. В противном случае итератор it окажется установленным на элемент ivec. end (), а счетчик ctr будет содержать нулевое значение. Использование константных ссылок позволяет избежать копирования Ссылочные параметры полезны и при других обстоятельствах: когда функции передают большой объект. Копирование значения аргумента прекрасно срабатывает для объектов встроенных типов и небольших классов, однако копирование объектов
Глава 7. Функции 261 больших классов или больших массивов зачастую малоэффективно. Кроме того, как будет описано в главе 13, “Управление копированием”, объекты не всех классов можно копировать. Используя ссылочный параметр, функция может обратиться не- посредственно к объекту, не копируя его. Давайте рассмотрим функцию, которая сравнивает длину двух строк. Такой функции необходим лишь размер каждой строки, а содержимое строк не нужно. По- скольку строки могут быть очень длинными, желательно избежать их копирования. Избежать копирования позволит применение константных ссылок. // сравнить длину двух строк bool isShorter(const string &sl, const string &s2) { return si.size() < s2.size(); } Каждый параметр этой функции является константной ссылкой на строку. По- скольку параметры являются ссылками, содержимое аргументов не копируется, а поскольку они являются константными, функция isShorter () не может исполь- зовать их для изменения содержимого аргументов. Когда ссылка в качестве параметра используется лишь для предотвращения копирования содержимого аргумента, ее следует сделать константной. Ссылки на константы гибче Должно быть вполне очевидно, что функция, получающая обычные неконстант- ные ссылки, не может быть вызвана для константного объекта. В конце концов, функция вполне может изменить переданный ей объект, а следовательно, нарушить константность аргумента. Однако значительно менее очевиден тот факт, что такую функцию нельзя вызы- вать с г-значением (раздел 2.3.1, стр. 67) или объектом такого типа, который требует преобразования. // функция получает неконстантный ссылочный параметр int incr(int &val) { return ++val; } int main() short vl = 0; const int v2 = 42; int v3 = incr(vl); v3 = incr(v2); v3 - incr(0); v3 = incr(vl + v2); int v4 = incr(v3); // ошибка: vl имеет тип, отличный от int II ошибка: v2 константа // ошибка: литералы не являются // 1 -значениями // ошибка: в результате сложения 1-значение // не получается // ok: v3 неконстантный объект типа int Проблема заключается в том, что неконстантная ссылка (раздел 2.5, стр. 82) мо- жет быть связана только с неконстантным объектом точно того же типа.
262 Часть I. Основы Параметры, которые не изменяют значение соответствующего аргумента, долж- ны быть константными ссылками. Определение этих параметров, как неконстантные ссылки, неоправданно ограничивает возможности функции. В качестве примера рас- смотрим программу поиска указанного символа в строке. // Возвращает индекс первого найденного символа с в строке s, II или значение s.sizeO если символ с в строке s не найден. !/ Обратите внимание: строка s не изменяется, поэтому она должна // быть ссылкой на константу. string::size_type find_char(string &s, char c) { string::size_type i = 0; while (i != s.sizeO && s[i] != c) ++ i; II не найден, перейти к следующему символу return i; } Данная функция получает свой строковый аргумент как простую (неконстант- ную) ссылку несмотря на то, что модифицировать этот параметр не нужно. Это оп- ределение имеет одну проблему: такой функции нельзя передать символьный стро- ковый литерал. if (find_char("Hello World", 'о')) // ... Это обращение приведет к ошибке во время компиляции несмотря на то, что лите- рал вполне может быть преобразован в строку. Подобные проблемы весьма распро- странены и особенно коварны. Даже если программа не имеет никаких константных объектов и функции f ind_char () при вызове передают только строковые объекты (а не строковые литералы или выражения, результатом которых является строка), проблемы во время компиляции все равно могут возникнуть. Например, вполне может существовать другая функция, is_sentence О, которая пытается использовать функцию f ind_char (), чтобы выяснить, не является ли строка предложением. bool is_sentence(const string &s) { // если последний символ в строке s является точкой, // строка содержит предложение return (find_char(s, '.') == s.sizeO - 1); } Как уже упоминалось, обращение к функции f ind_char () изнутри функции is_sentence () приводит к ошибке во время компиляции. Параметр функции is_sentence () является ссылкой на константную строку и не может быть передан функции f ind_char (), которая ожидает ссылку на неконстантную строку. Ссылочные параметры, изменение которых не предполагается, должны быть ссыл- ками на константы. Простые, неконстантные, ссылочные параметры менее гибки. Такие параметры не могут быть инициализированы константными объектами или ар- гументами, которые являются литералами или выражениями, в результате вычисле- ния которых получаются г-значения. Передача ссылок на указатели Предположим, что необходимо создать функцию, которая меняет местами два указателя, подобно функции в предыдущей программе, менявшей местами два це- лых числа. Как известно, при определении указателя используется символ *, а при
Глава 7. Функции 263 определении ссылки — символ &. Вопрос заключается в том, как объединить эти операторы, чтобы получить ссылку на указатель. Рассмотрим следующий пример. // поменять местами значения двух указателей на тип int void ptrswap(int *&vl, int *&v2) { vl = tmp; Параметр int * &vl следует читать справа налево: vl — ссылка на указатель на объект типа int; т.е. vl — это только другое имя любого указателя, переданного функции pt г swap (). Функцию main () со стр. 259 можно переписать так, чтобы для перестановки указателей на значения 10 и 2 0 использовалась функция pt г swap (). int main() { int i = 10; int j = 20; int *pi = &i; // pi указывает на i int *pj = & j ; // pj указывает на j cout « "Before ptrswap():\t*pi: " << *pi << ”\t*pj: " « *pj « endl; ptrswap(pi, pj); // теперь pi указывает на j, cout << "After ptrswap():\t*pi: " « *pi « "\t*pj: " « *pj « endl; return 0; a pj на i После компиляции и запуска на выполнение эта программа выдаст следующий результат. Before ptrswap(): *pi: 10 *pj: 20 After ptrswap(): *pi: 20 *pj: 10 Что же происходит при замене значений указателей? Перед вызовом функции ptrswap () указатель pi содержит адрес значения переменной i, а указатель рj со- держит адрес значения переменной j. Внутри функции ptrswap () адреса в указате- лях pi и р j меняются местами. Таким образом, после вызова функции ptrswap () указатель pi содержит адрес значения переменной j, а указатель pj — адрес значе- ния переменной i. Упражнения раздела 7.2.2 Упражнение 7.7. Объясните различие между параметрами в следующих двух объявлениях. void f(Т); void f(Т&); Упражнение 7.8. Приведите пример случая, когда параметр должен быть ссылочным. Приведите пример случая, когда параметр не должен быть ссылочным. Упражнение 7.9. Измените объявление параметра occurs в списке параметров функции f ind_vai () (стр. 260) так, чтобы аргумент был преобразован в нессылочный тип, а затем пере- компилируйте и снова запустите программу. Как изменилось поведение программы?
264 Часть I. Основы Упражнение 7.10. Следующая функция хоть и вполне допустима, но менее полезна, чем могла бы быть. Выявите и устраните ограничения. bool test(string& s) { return s.empty(); } Упражнение 7.11. Когда ссылочные параметры должны быть константными? Какие проблемы мо- гут возникнуть, если параметр является обычной ссылкой, а какие — если он является константной ссылкой? 7.2.3. Параметры типа векторов и других контейнеров Как правило, функции не должны иметь параметров типа векторов или других биб- лиотечных контейнеров. При вызове функции, которая имеет простой, нессылочный векторный параметр, копируется каждый элемент вектора. Чтобы избежать копирования вектора, параметр можно было бы сделать ссыл- кой. Однако на практике, по причинам, подробно описанным в главе И, “Общие ал- горитмы”, программисты C++ предпочитают передавать итераторы элементов под- лежащих обработке контейнеров. // передать итераторы на первый элемент и элемент следующий II за последним, чтобы отобразить содержимое вектора void print(vector<int>::const_iterator beg, vector<int>::const_iterator end) { while (beg != end) { cout « *beg++; if (beg != end) cout << " // после последнего элемента // пробел не нужен } cout « endl; } Эта функция отображает элементы начиная с указанного итератором beg и до (но не включая) указанного итератором end. После каждого элемента, кроме по- следнего, отображается пробел. 7.2.4. Параметры в виде массива Массивы обладают двумя особенностями, влияющими на определение и ис- пользование функций, работающих с массивами: массив нельзя скопировать (раз- дел 4.1.1, стр. 136), имя массива при использовании автоматически преобразуется в указатель на его первый элемент (раздел 4.2.4, стр. 146). Поскольку копировать массив нельзя, его невозможно использовать как параметр функции. В связи с тем, что имя массива автоматически преобразуется в указатель, работающие с ними функции манипулируют значениями элементов массива косвенно. Определение параметров в виде массива Предположим, что необходима функция, отображающая содержимое массива це- лочисленных значений. В качестве параметра массив можно указать одним из трех следующих способов. // три эквивалентных определения функции printvalues() void printvalues(int*) { /* ... */ }
Глава 7. Функции 265 void printvalues(int[]) { /* ... */ } void printvalues(int[10]) { /* ... */ } Несмотря на то, что передать массив непосредственно нельзя, функции можно назначить параметр, который выглядит как массив. Несмотря на использование син- таксиса, напоминающего массив, этот параметр обрабатывается как указатель на тип элемента массива. Эти три определения эквивалентны, каждый параметр интерпре- тируется как указатель на тип int. Передавая массив в качестве параметра, обычно имеет смысл определять его как указатель, а не использовать синтаксис массива. Это наглядней укажет на то, что в функции используется указатель на элемент массива, а не массив непосредственно. Поскольку размерность массива игнорируется, ее включение в определение пара- метра особенно вводит в заблуждение. Размерность параметра может ввести в заблуждение Компилятор игнорирует любую размерность, которая может быть указана в оп- ределении параметра-массива. Неправильно функцию printvalues () можно оп- ределить следующим образом, указав размерность массива. // параметр воспринимается как константный указатель на тип int, / / а размер массива игнорируется void printvalues(const int ia[10]) { // этот код подразумевает, что используется массив из 10 / / элементов ; // катастрофа произойдет в случае, если аргумент имеет меньше // 10 элементов! for (size_t i = 0; i != 10; ++i) { cout « ia[i] « endl; } Хотя этот код подразумевает, что переданный массив имеет по крайней мере 10 элементов, реально это предположение никак не подтверждено. Поэтому все сле- дующие обращения вполне допустимы, int main() { int i = 0, j[2] = {0, 1}; printvalues (&i); // ok: &i является int*; // вероятна ошибка во время выполнения printvalues (j); // ok: j преобразуется в указатель на нулевой // элемент; аргумент имеет тип int*; // вероятна ошибка во время выполнения return 0; Несмотря на то, что по поводу этих обращений компилятор не выдает никаких предупреждений, оба они способны привести к ошибке (и, вероятнее всего, приве- дут) во время выполнения программы. В обоих случаях произойдет обращение к участкам памяти вне массива, поскольку функция print Values () подразумевает, что переданный ей массив имеет по крайней мере 10 элементов. В зависимости от расположенных в этой области памяти значений, программа либо вернет неправиль- ный результат, либо аварийно завершит работу.
266 Часть I. Основы Когда компилятор проверяет аргумент, передаваемый параметру-массиву, он лишь выяс- няет, является ли аргумент указателем и соответствует ли тип указателя типу элементов массива. Размер массива не проверяется. Массив как аргумент Подобно любым другим типам, параметр-массив можно определить как ссылоч- ный или нессылочный. Массивы чаще всего передают как обычные, нессылочные типы, которые без проблем преобразуются в указатели. Параметр нессылочного ти- па, как обычно, инициализируется копией соответствующего ему аргумента. При передаче массива, аргумент представляет собой указатель на первый элемент масси- ва. Поэтому копируется значение указателя, а не самого элемента массива. Функция работает с копией указателя, поэтому она не может изменить значение указателя ар- гумента. Однако функция может использовать этот указатель для того, чтобы изме- нить значение элемента, адрес которого он содержит. Любые изменения, внесенные в значение, адрес которого содержит параметр-указатель, на самом деле осуществ- ляются с самим элементом массива. В функции, не изменяющей значения переданных элементов массива, такие пара- метры следует объявлять указателями на константу. // f не будет изменять значения элементов массива void f(const int*) { /* ... * / } Передача массива по ссылке Подобно любому другому типу, параметр можно определить как ссылку на мас- сив. Если параметр является ссылкой на массив, компилятор не преобразует массив- аргумент в указатель. В этом случае по ссылке передается сам массив. Поскольку размер массива является частью типа аргумента и параметра, компилятор проверит соответствие размера массива-аргумента размеру массива-параметра. // ок: параметр является ссылкой на массив; // размер массива фиксирован void printvalues(int (&arr) [10]) { /* ... */ } int main() { int i = 0, j[2] = {0, 1}; int k[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; printvalues (&i); // ошибка: аргумент не является массивом /! из 10 целочисленных элементов printvalues (j); // ошибка: аргумент не является массивом II из 10 целочисленных элементов printvalues (к); // ок: аргумент является массивом // из 10 целочисленных элементов return 0; Функции printvalues () этой версии можно передавать при вызове массивы ис- ключительно из 10 элементов типа int. Однако поскольку параметр является ссыл- кой, в теле функции вполне можно положиться на правильность размера массива. // ок: параметр является ссылкой на массив; // размер массива фиксирован void printvalues(int (&arr)[10]) {
Глава 7. Функции 267 (size_t cout i = 0; i != 10; arr[i] « endl; Часть &arr необходимо заключить в круглые скобки, поскольку оператор индексирова- ния имеет более высокий приоритет. f(int &arr[10]) // ошибка: arr является массивом ссылок f(int (&arr)[10]) // ok: arr является ссылкой на массив из 10 int Как будет продемонстрировано в разделе 16.1.5 (стр. 664), эту функцию можно переписать способом, который позволит передавать в качестве параметра ссылку на массив любого размера. Передача многомерного массива Напомним, что на самом деле в языке C++ не существует никаких многомерных массивов (раздел 4.4, стр. 165). Фактически вместо многомерных массивов исполь- зуется массив массивов. Подобно любым массивам, многомерный массив передается как указатель на его первый элемент. Элементом многомерного массива является массив. Размер массива по второй размерности (и всем последующим) является частью типа элемента и подлежит указанию в определении. // первый параметр является массивом, элементы которого // представляют собой массивы из 10 целочисленных элементов void printvalues(int (matrix*)[10], int rowSize); Здесь параметр matrix объявлен как указатель на массив из 10 целочисленных элементов. Здесь часть *matrix необходимо заключить в круглые скобки. int *matrix[10]; // массив из 10 указателей int (*matrix) [10]; // указатель на массив из 10 int Многомерный массив можно также объявить используя синтаксис массива. По- добно одномерному массиву, компилятор игнорирует первую размерность, поэтому указывать ее не имеет смысла. // первый параметр является массивом, элементы которого / / представляют собой массивы из 10 целочисленных элементов void printvalues(int matrix[] [10] , int rowSize); Здесь параметр matrix объявлен как двумерный массив. Фактически параметр является указателем на элемент в массиве массивов, т.е. каждый элемент в массиве сам является массивом из 10 целочисленных элементов. 7.2.5. Манипулирование массивами, переданными в функции Как уже было продемонстрировано, контроль типов нессылочных параметров- массивов подразумевает только проверку того, что аргумент является указателем то- го же типа, что и элементы массива. Контроль типов не предусматривает проверки того, что аргумент фактически указывает на массив определенного размера.
268 Часть I. Основы Любая работающая с массивом программа должна гарантировать отсутствие обраще- ний за пределы массива. Существует три общепринятых программных подхода, позволяющие гарантиро- вать пребывание функции в пределах массива. Первый подход подразумевает нали- чие маркера, позволяющего обнаружить конец массива, непосредственно в самом массиве. Ярким примером такого подхода являются символьные строки в стиле С. Строки в стиле С являются массивами символов, завершающимися нулевым симво- лом. Программный код, работающий со строками в стиле С, использует этот маркер для остановки перебора элементов массива. Использование соглашений стандартной библиотеки Второй подход подразумевает передачу указателей на первый и последний эле- менты массива. Этот стиль программирования обусловлен методами, используемы- ми в стандартной библиотеке. Более подробно этот подход рассматривается в части II, “Контейнеры и алгоритмы”. Переписав функцию print Values () с использованием этого подхода, ее впо- следствии можно применить следующим образом. void printvalues(const int *beg, const int *end) { while (beg != end) { cout « *beg++ « endl; } } int main() { int j[2] = {0, 1); // ok: j преобразуется в указатель на первый элемент массива j / / j + 2 указывает на один элемент за пределами массива j printvalues(j, j + 2); return 0; Цикл внутри функции print Values () подобен другим, использованным ранее в программах перебора значений вектора с применением итератора. Здесь для пере- бора элементов массива использован указатель beg. Цикл останавливается в случае, когда указатель beg становится равен указателю end, который был передан функ- ции в качестве второго параметра. При вызове этой версии, функции передаются два указателя: один на первый элемент, а второй на элемент после последнего. Пока указатели вычисляются пра- вильно (т.е. соответствуют диапазону элементов), программа вполне безопасна. Явная передача размера Третий подход, наиболее распространенный в программах на языках С и C++ до появления стандарта, подразумевает передачу второго параметра, который содержит размер массива.
Глава 7. Функции 269 Используя этот подход, функцию printvalues () можно переписать еще раз. Ее новая версия и обращение к ней выглядит следующим образом. // const int ia[] эквивалентно const int* ia II размер передан явно, он используется для управления / / доступом к элементам параметра ia void printvalues(const int ia[], size_t size) { for (size_t i = 0; i != size; ++i) { cout « ia[i] « endl; } } int main() { int j[] = { 0, 1 }; // массив из двух элементов типа int printvalues(j, sizeof(j)/sizeof(*j)); return 0; } В этой версии параметр size используется для определения количества отобра- жаемых элементов. Когда происходит вызов функции printvalues (), ей необхо- димо передать дополнительный параметр. Программа работает вполне безопасно, пока переданный размер не больше фактического размера массива. Упражнения раздела 7.2.5 Упражнение 7.12. Когда следует использовать параметр, являющийся указателем? Когда сле- дует использовать параметр, являющийся ссылкой? Объясните преимущества и недостатки ка- ждого из них. Упражнение 7.13. Напишите программу, которая вычисляет сумму элементов массива. Напи- шите три версии функции, в каждой из которых используется разный способ указания пределов массива. Упражнение 7.14. Напишите программу, суммирующую элементы вектора vector<double>. 7.2.6. Функция main (): обработка параметров командной строки Функция main () является хорошим примером того, как в программах на языке С была решена проблема передачи массивов в функции. До настоящего момента функция main () в примерах определялась с пустым списком параметров. int main() { ... } Но зачастую функции main () необходимо передать аргументы. Традиционно, та- кие аргументы называют опциями (option) или параметрами командной строки. Предположим, например, что в функцию main () программы, исполняемый файл которой имеет имя prog, необходимо передавать параметры следующим образом. prog -d -о ofile dataO Фактически для организации приема всех этих значений, функции main () необ- ходимо всего два параметра. int main(int argc, char *argv[]) { ... }
270 Часть I. Основы Второй параметр, argv, является массивом символьных строк в стиле С, а пер- вый параметр, argc, передает количество строк в этом массиве. Поскольку второй параметр является массивом, функцию main (), в качестве альтернативы, можно определить следующим образом. int main(int argc, char **argv) { ... } Обратите внимание, указатель argv указывает на тип char*. При передаче аргументов функции main (), первая строка массива argv, если она есть, всегда содержит имя программы. Последующие элементы содержат допол- нительные, необязательные строки. В случае приведенной выше командной строки, параметр argc содержал бы значение 5, а параметр argv — следующие символьные строки в стиле С. argv[0] = argv[1] = argv[2] = argv[3] = argv[4] = "prog"; "-d" ; " -o" ; "ofile"; "dataO"; Упражнения раздела 7.2.6 Упражнение 7.15. Напишите функцию main (), которая в качестве аргументов получает два зна- чения и выводит их сумму. Упражнение 7.16. Напишите программу, которая способна получать параметры командной стро- ки, описанные в этом разделе. Отобразите значения аргументов, переданных функции main о. 7.2.7. Функции с варьирующимися параметрами Параметры в виде многоточия (ellipses parameter) поддерживаются в языке C++ для то- го, чтобы можно было откомпилировать программы на языке С, в которых использованы варьирующиеся параметры varargs. Более подробная информация о параметрах varargs приведена в документации по компилятору С. В программах на языке C++ параметры в виде многоточия используются для передачи функциям данных только простых типов. В частности, объекты большинства классов не будут скопированы пра- вильно при передаче параметрам-многоточиям. Параметры-многоточия используются в случае, когда невозможно перечислить тип и количество всех аргументов, которые могут быть переданы функции. Много- точия приостанавливают контроль соответствия типов. Их присутствие уведомляет компилятор о том, что при вызове функции может быть передано любое количество аргументов, типы которых неизвестны. Многоточия применяются в двух формах, void foo(parm_list, ...); void foo(...); Первая форма обеспечивает объявление лишь некоторого количества парамет- ров. В данном случае контроль соответствия типов при вызове функции осуществ- ляется только для тех аргументов, которые соответствуют параметрам, объявленным явно, а для остальных аргументов (указанных многоточием), контроль соответствия типов отсутствует. В первой форме записи запятая после объявлений параметров не обязательна.
Глава 7. Функции 271 Большинство функций с многоточием использует один или несколько парамет- ров, чтобы получить некую информацию о типе и количестве необязательных аргу- ментов, передаваемых при вызове функции. Чаще всего используется первая форма объявления функции с многоточием. 7.3. Оператор return Оператор return завершает выполнение функции и возвращает управление той функции, которая вызвала текущую. Существует две формы оператора return. return; return выражение; 7.3.1. Функции без возвращаемого значения Оператор return без значения применим только в такой функции, типом воз- вращаемого значения которой объявлен void. Функции, возвращаемым типом ко- торых объявлен void, необязательно должны содержать оператор return. В функ- ции типа void оператор return неявно размещается после последнего оператора. Как правило, функции типа void используют оператор return для преждевре- менного завершения выполнения. Это аналогично использованию оператора break (раздел 6.10, стр. 237) внутри цикла. Например, функцию swap () можно переписать так, чтобы она не делала ничего, если значения идентичны. // ок: функция swap () получает доступ к значениям своих II аргументов при помощи ссылок void swap(int &vl, int &v2) { // если значения равны, их замена не нужна; можно выйти сразу if (vl == v2) return; // ok, придется поработать int tmp = v2; v2 = vl; vl = tmp; // явно указывать оператор return не обязательно } Сначала эта функция проверяет, не равны ли значения, и если это так, то завер- шает работу. Если значения не равны, функция меняет их местами. После последне- го оператора присвоения осуществляется неявный выход из функции. Функции, для возвращаемого значения которых указан тип void, вторую форму оператора return, как правило, не используют. Однако функция типа void может вернуть результат вызова другой функции, которая тоже возвращает тип void, void do_swap(int &vl, int &v2) { int tmp = v2; v2 = vl; vl = tmp; // ok: функция типа void не нуждается в явном операторе return } void swap(int &vl, int &v2) {
272 Часть I. Основы if (vl == v2) return false; // ошибка: функция типа void не может II возвращать значение return do_swap(vl, v2); // ok: возвращение результата вызова // функции типа void Попытка возвращения результата любого другого выражения приводит к ошибке во время компиляции. 7.3.2. Функции, возвращающие значение Вторая форма оператора return предназначена для возвращения результата из функции. Каждый оператор return в функции, возвращаемое значение которой имеет тип, отличный от void, должен возвращать значение. Возвращаемое значение должно иметь тип, либо совпадающий, либо допускающий неявное преобразование в тип, указанный для возвращаемого значения функции при определении. Хотя язык C++ не может гарантировать правильность результата, он способен гарантировать, что каждое возвращаемое функцией значение будет соответствовать объявленному типу. Следующая программа, например, не будет откомпилирована. // Выяснить, не равны ли две строки. II Если они отличаются по размеру, выяснить, не содержит ли / / меньшая из них те же символы, что и большая bool str_subrange(const string &strl, const string &str2) // размеры одинаковы: возвратить обычный результат сравнения if (strl.sizeO == str2.size()) return strl == str2; // ok: == возвращает тип bool II выяснить размер меньшей строки string::size type size = (strl.sizeO < str2.size()) ? strl.sizeO : str2.size(); string::size_type i = 0; // просмотреть все элементы до размера меньшей строки while (i != size) { if (strl[i] != str2[i]) return; // ошибка: нет возвращаемого значения ) / / ошибка: выполнение может дойти до конца функции, так и // не встретив оператор return // маловероятно, что компилятор обнаружит эту ошибку ) Оператор return внутри цикла while является ошибочным потому, что он не в состоянии вернуть значение. Эту ошибку компилятор должен обнаружить. Вторая ошибка несколько сложнее. Дело в том, что функция не имеет оператора return после цикла while. Если при вызове эта функция получит строки, одна из которых является подмножеством другой, выполнение цикла while не будет пре- рвано и он завершится нормально. Однако оператор return для этого случая не предусмотрен. Эту ошибку компилятор может и не обнаружить. Программа может быть откомпилирована вполне нормально, но при указанных выше обстоятельствах результат ее выполнения окажется непредсказуемым.
Глава 7. Функции 273 Отсутствие оператора return после цикла, который этот оператор содержит, являет- ся особенно коварной ошибкой, поскольку большинство компиляторов не сможет ее об- наружить. Поведение такой программы во время выполнения непредсказуемо. Возвращение значения из функции main () Правило гласящее, что любая функция, типом возвращаемого значения которой является тип, отличный от void, обязательно должна возвращать значение, имеет одно исключение: это функция main (), она способна завершать выполнение, ничего не возвращая. Если управление достигает конца функции main () и не встречает оператора return, она неявно возвращает значение 0. Еще один способ возвращения значения из функции main () связан со специфи- кой ее обработки. Как было сказано в разделе 1.1 (стр. 24), возвращаемое функцией main () значение рассматривается как индикатор ее состояния. Возвращение нуле- вого значения свидетельствует об успешном завершении выполнения, а любых дру- гих — об отказе. Значение, отличное от нуля имеет машинно-зависимый смысл. Что- бы сделать возвращаемые значения машинно-независимыми, в заголовке cstdlib определены две переменные препроцессора (раздел 2.9.2, стр. 93), которые можно использовать для сообщения об успехе или отказе. tfinclude <cstdlib> int main() { bool some_failure = false; if (some_failure) return EXIT_FAILURE; else return EXIT_SUCCESS; } Этот код не нуждается в использовании точных машинно-зависимых значений. Эти значения определены в заголовке cstdlib, а здесь использованы их стандартные имена. Возвращение нессылочного типа Возвращенное функцией значение используется для инициализации временного объекта (temporary object), создаваемого в точке вызова функции. Компилятор соз- дает неименованный временный объект в случае, когда необходимо место для хра- нения результата вычисления выражения. Временный объект инициализируется возвращаемым функцией значением ана- логично инициализации параметров значениями их аргументов. Если возвращаемый тип не является ссылкой, возвращаемое значение копируется во временный объект по месту обращения. Возвращенное функцией значение нессылочного типа может быть локальным объектом или результатом вычисления выражения. Для примера напишем функцию, которой передают счетчик, слово и окончание. Функция возвращает слово во множественном числе, если значение счетчика боль- ше единицы. // вернуть слово во множественном числе, если значение ctr не 1 string make_plural(size_t ctr, const string &word, const string &ending)
274 Часть I. Основы return (ctr == 1) ? word : word + ending; Эту функцию можно было бы использовать при отображении сообщения, когда в зависимости от обстоятельств необходимо солово в единственном или множест- венном числе. Эта функция возвращает либо копию ее параметра по имени word, либо неиме- нованный временный строковый объект, значение которого состоит из содержимого переменных word и ending. В любом случае, оператор return скопирует результат в строковый объект по месту обращения. Возвращение ссылки Когда функция возвращает ссылочный тип, возвращаемое значение не копирует- ся. Вместо этого возвращается сам объект. Для примера рассмотрим функцию, воз- вращающую ссылку на строку, которая оказалась короче. // выяснить, какая из строк длиннее const string &shorterString(const string &sl, const string &s2) { return si.size() < s2.size() ? si : s2; } Типом параметров и возвращаемого значения является ссылка на константную строку. Эти строки не копируются ни при передаче в функцию, ни при возвращении результата. Никогда не возвращайте ссылку на локальный объект Очень важно запомнить, что ни в коем случае не следует возвращать ссылку на локаль- ную переменную. При завершении работы функции, все хранилища ее локальных объектов осво- бождаются. Поэтому после завершения работы функции ссылка на локальный объект оказывается относящейся к несуществующему объекту. Рассмотрим сле- дующую функцию. // катастрофа: функция возвращает ссылку на локальный объект const string &manip(const string& s) { string ret = s; // обработать ret некоторым образом return ret; // ошибка: возвращение ссылки на локальный объект! } Эта функция приведет к отказу во время выполнения, поскольку она возвращает ссылку на локальный объект. Когда функция завершит работу, область памяти, ко- торую занимала переменная ret, будет освобождена. Возвращаемое значение будет ссылаться на ту область памяти, которая уже недоступна. Чтобы удостовериться в безопасности возвращения значения, следует задаться во- просом: к какому существовавшему ранее объекту относится ссылка?
Глава 7. Функции 275 Возвращаемая ссылка является 1-значением Возвращаемая функцией ссылка является 1-значением. А следовательно, вызов этой функции допустим везде, где необходимо 1-значение, char &get_val(string &str, string::size_type ix) { return str[ix]; } int main() { string s("a value"); cout « s « endl; get_val(s, 0) = 'A'; cout << s « endl; return 0; } // отображает значение I/ изменяет s[0] на A // отображает значение A Как ни удивительно, но возвращаемое этой функцией значение является ссыл- кой, а следовательно, только синонимом значения возвращенного элемента. Если возвращаемая ссылка не должна быть использована для модификации зна- чения, объявление функции нужно изменить следующим образом. const char &get_val(... Никогда не возвращайте указатель на локальный объект Возвращаемое функцией значение может иметь практически любой тип. В част- ности, функция вполне может возвращать указатель. Возвращать указатель на ло- кальный объект нельзя по тем же причинам, по которым нельзя возвращать ссылку на локальный объект: при завершении работы функции локальные объекты освобо- ждаются. В результате указатель окажется потерянным (раздел 5.11, стр. 201), он бу- дет содержать адрес несуществующего объекта. Упражнения раздела 7.3.2 Упражнение 7.17. Когда следует возвращать ссылку, а когда константную ссылку? Упражнение 7.18. Какую потенциальную проблему времени выполнения содержит следующая функция? string &processText() { string text; while (cin » text) { /* ... */ } return text; } Упражнение 7.19. Является ли следующая программа допустимой? Если да, объясните, что она делает, а если нет — как ее исправить. int &get(int *arry, int index) { return arry[index]; } int main() { int ia[10]; for (int i = 0; i != 10; ++i) get(ia, i) = 0; }
276 Часть I. Основы 7.3.3. Рекурсия Функция, которая вызывает сама себя непосредственно или косвенно, называет- ся рекурсивной (recursive function). Классическим примером простой рекурсии явля- ется функция, вычисляющая факториал числа. Факториал числа п — это произведе- ние чисел от 1 до п. Факториалом числа 5, например, является 120. 1*2*3*4*5= 120 Вполне естественным способом решения этой проблемы является рекурсия. // вычислить val!, т.е. 1 *2 * 3 ... * val int factorial(int val) if (val > 1) return factorial(val-1) * val; return 1; } В рекурсивной функции всегда должно быть определено условие выхода или останова (stopping condition); в противном случае рекурсия станет бесконечной, т.е. функция про- должит вызывать себя до тех пор, пока стек программы не будет исчерпан. Иногда эта ошибка называется бесконечной рекурсией (infinite recursion). В случае функции factorial () условием выхода является равенство значения параметра val единице. Еще одним примером рекурсии является функция поиска самого большого обще- го делителя. // рекурсивная версия поиска самого большого общего делителя int rgcd(int vl, int v2) { if (v2 != 0) // рекурсия завершится, когда v2 равно О return rgcd(v2, vl%v2); // рекурсия, уменьшает значение v2 II при каждом обращении return vl; } В данном случае условием выхода является остаток, равный 0. Если при вызове функции rgcd () передать аргументы (15, 123), результатом будет 3. В табл. 7.1 приведена трассировка вызова функции rgcd (15,123). Таблица 7.1. Трассировка вызова функции rgcd (15,123) Значение vl Значение v2 Возвращает 15 123 rgcd(123, 15) 123 15 rgcd(15, 3) 15 3 rgcd(3, 0) 3 0 3 Последний вызов, rgcd (3, 0), удовлетворяет условию выхода из рекурсии. Он и возвращает самый большой общий знаменатель — 3. Это значение последователь- но становится возвращаемым значением каждого предшествующего обращения. Та- кое последовательное возвращение результата вызывающей функции, иногда назы- ваемое “подъемом вверх”, осуществляется до тех пор, пока управление не вернется к первому вызванному экземпляру функции rgcd ().
Глава 7. Функции 277 Функция main () не может вызывать сама себя. Упражнения раздела 7.3.3 Упражнение 7.20. Перепишите функцию factorial () как итерационную. Упражнение 7.21. Что произойдет, если условием выхода из рекурсии функции factorial () будет следующее, if (val != 0) 7.4. Объявление функций Подобно переменным, функции перед применением следует объявить. Анало- гично переменным (раздел 2.3.5, стр. 75), объявление функции можно отделить от ее определения. Таким образом, функция может быть определена только один раз, а объявлена несколько раз. В объявлении функции указывают тип возвращаемого значения, имя функции и список ее параметров. Список параметров должен содержать типы параметров, но их имена не обязательны. Комплект этих трех элементов называют прототипом функ- ции (function prototype). Прототип функции описывает ее интерфейс (interface). Прототипы функций обеспечивают интерфейс между программистом, определившим -i!z| : функцию, и программистом, использующим ее. При использовании функции программист обращается к прототипу функции. Имена параметров в объявлении функций игнорируются. Поэтому используемые в объявлении имена служат для дополнительного документирования. void print(int *array, int size); Объявления функций располагают в файлах заголовков Напомним, что объявления переменных располагают в файлах заголовка (раз- дел 2.9 стр. 89), а определения — в файлах исходного кода. По тем же самым причи- нам функции должны быть объявлены в файлах заголовка и определены в файлах исходного кода. Весьма соблазнительно (и вполне допустимо) размещать объявления функций непосредственно в каждом файле исходного кода, который использует функцию. Однако такой подход утомителен и приводит к ошибкам. Помещая объявления функ- ций в файлы заголовка можно гарантировать, что все объявления данной функции будут одинаковы. Если необходимо изменить интерфейс функции, достаточно мо- дифицировать его только в одном объявлении. Файл исходного кода, в котором функция определена, должен подключать заголо- вок, в котором функция объявлена. Подключение содержащего объявление функции заголовка в файл ее определения, позволяет компилятору проверить соответствие определения объявлению. В частности,
278 Часть I. Основы если определение и объявление согласуются по списку параметров, но различаются по типу возвращаемого значения, компилятор выдаст предупреждение или сообще- ние об ошибке, указывающее на это различие. Упражнения раздела 7.4 Упражнение 7.22. Напишите прототипы для каждой из следующих функций. (а) Функция по имени compare, возвращающая значение типа bool и обладающая двумя пара- метрами, представляющими собой ссылки на объекты класса matrix. (b) Функция по имени change_vai, возвращающая итератор вектора vector<int> и обла- дающая двумя параметрами: типа int и типа итератора вектора vector<int>. Подсказка: при создании этих прототипов используйте имя функции как индикатор назначения функции. Упражнение 7.23. С учетом следующих объявлений укажите, какие из обращений допустимы, а какие — нет. Объясните, почему некоторые из следующих обращений недопустимы. double calc(double); int count(const string &, char); int sum(vector<int>::iterator, vector<int>::iterator, int); vector<int> vec(10); (a) calc(23.4, 55.1); (b) count("abcda", 'a'); (c) calc(66) ; (d) sum (vec .begin () , vec.endO, 3.8); 7.4.1. Значения параметров по умолчанию Значение параметра по умолчанию (default argument) — это значение параметра, которое, не являясь универсальным, используется чаще всего. Когда происходит вы- зов функции, можно пропустить любой аргумент, для параметра которого задано значение по умолчанию. Компилятор подставит значение по умолчанию вместо лю- бого пропущенного аргумента. Чтобы задать значение аргумента по умолчанию, в списке параметров использу- ется явная инициализация. Значения по умолчанию можно задать для одного или нескольких параметров. Но если один из параметров имеет значение по умолчанию, все следующие за ним параметры также должны иметь значения по умолчанию. Предположим, например, что необходима инициализирующая строку функция, которая имитирует окно. Для высоты, ширины и фонового символа экрана должны быть заданы значения по умолчанию. string screenlnit(string::size_type height = 24, string::size_type width = 80, char background = ' '); Функция может быть вызвана и без аргумента, если для соответствующего пара- метра задано значение по умолчанию. Если аргумент предоставлен, его значение пе- реопределяет значение, заданное по умолчанию, а в противном случае используется значение, заданное по умолчанию. Каждое из следующих обращений к функции screenlnit () вполне корректно. string screen; screen = screenlnit(); // эквивалент screenlnit(24, 80, ' ')
Глава 7. Функции 279 screen = screenlnit(66); screen = screenlnit(66, 256) screen = screenlnit(66, 256, 80, ' ') 256, ' •) Аргументы при обращении различаются по позиции, а заданные по умолчанию значения используются для замены аргументов, отсутствующих в конце обращения. Если необходимо определить аргумент для параметра background, придется также предоставить аргументы для параметров height и width. screen = screenlnit(, , // ошибка: отсутствовать могут только // конечные аргументы screen = screenlnit(1?'); // эквивалент screenlnit ('?’, 80, ' ') Обратите внимание, второе обращение, в котором передано одно символьное значение, вполне допустимо. Несмотря на это, весьма маловероятно, что програм- мист именно это и имел в виду. Обращение допустимо потому, что литерал ' ? ' име- ет тип char, а значение типа char может быть преобразовано в тип крайнего левого параметра, string: :size_type, ведь это не более, чем беззнаковый целочислен- ный тип. В этом обращении аргумент типа char неявно преобразуется в значение типа string: : size_type и передается параметру height. Поскольку тип char является целочисленным (раздел 2.1.1, стр. 57), значение типа char вполне допустимо передавать параметру типа int и наоборот. Вследствие это- го возникают многие недоразумения, одно из которых состоит в передаче при вызове функциям аргументов типа char и int в неправильном порядке. Также причиной воз- никновения подобной проблемы может быть применение значений по умолчанию. Одной из задач, решаемых при проектировании функций со значениями по умолчанию, является упорядочивание параметров таким образом, чтобы наименее часто используемые значения по умолчанию располагались первыми, а те, вероят- ность использования которых выше, — после них. Инициализация значений параметров по умолчанию Для инициализации значений параметров по умолчанию может быть использо- вано любое выражение соответствующего типа. string::size_type screenHeight(); string::size_type screenwidth(string::size_type); char screenDefault(char = ' '); string screenlnit( string::size type height = screenHeight(), string::size_type width = screenwidth(screenHeight()), char background = screenDefault()); Когда для задания значения параметра по умолчанию используется выражение, во время вызова функции вычисляется его результат. В приведенном выше примере для вычисления значения параметра background функции screenlnit () приме- няется вызов функции screenDefault (). Ограничения на определение значений параметров по умолчанию Задать значение параметра по умолчанию можно либо в определении функции, либо в объявлении. Однако задать значение параметра по умолчанию в файле можно только один раз. Следующее объявление является ошибкой.
280 Часть I. Основы // ff.h int ff(int = 0); #include "ff.h" int ff(int i = 0) { /* */ } // ошибка Как правило, задание значений параметров по умолчанию осуществляется в объявлении функции и располагается в соответствующем заголовке. Если значение по умолчанию задано в списке параметров определения функции, оно будет доступно лишь в том файле исходного кода, который содержит определение. Упражнения раздела 7.4.1 Упражнение 7.24. Какое из следующих объявлений (если оно есть) содержит ошибку? Почему? (a) int ff(int a, int b = 0, int с = 0); (b) char *init(int ht = 24, int wd, char bckgrnd); Упражнение 7.25. Предположим, что функция объявлена следующим образом. Какие из обраще- ний (если они есть) являются недопустимыми? Почему? Какие из обращений (если они есть) до- пустимы, но, вероятно, не соответствуют намерениям разработчика? Почему? // объявления char *init(int ht, int wd = 80, char bckgrnd = ' '); (a) init(); (b) init(24, 10); (c) init(14, '*'); Упражнение 7.26. Напишите версию функции makejoiurai () с заданным по умолчанию зна- чением параметра. Используйте эту версию для отображения слов “success” и “failure” (“успех” и “отказ”) в единственном и множественном числе. 7.5. Локальные объекты Имена в языке C++ имеют область видимости (scope), а объекты — продолжи- тельность существования (lifetime). Чтобы понять, как работают функции, очень важно осознать обе эти концепции. Область видимости имени — это та часть текста программы, в которой оно применимо. Продолжительность существования объек- та — это время, в течение которого существует объект при выполнении программы. Имена параметров и переменных, определенных внутри функции, находятся в области видимости данной функции, т.е. имена видимы только внутри тела функ- ции. Как обычно, имя переменной становится применимо с момента ее объявления или определения и до конца области видимости включительно. 7.5.1. Автоматические объекты По умолчанию продолжительность существования локальной переменной ограни- чена продолжительностью выполнения функции. Объекты, которые существуют толь- ко во время выполнения функции, называют автоматическими (automatic object). Ав- томатические объекты создаются и удаляются при каждом обращении к функции.
Глава 7. Функции 281 Автоматический объект, соответствующий локальной переменной, создается в момент, когда выполнение функции доходит до определения переменной. Если определение содержит инициализацию, при каждом создании объект получает ис- ходное значение. Неинициализированные локальные переменные встроенного типа имеют неопределенные значения. Когда работа функции завершается, автоматиче- ские объекты удаляются. Параметры являются автоматическими объектами. Хранилище, в котором распо- лагаются параметры, создается при вызове функции и освобождается при заверше- нии ее работы. Автоматические объекты, включая параметры, удаляются в конце того блока ко- да, в котором они определены. Параметры определены в блоке кода функции, по- этому они удаляются при завершении блока функции. При выходе из функции ее локальное хранилище освобождается. После выхода из функции, значения ее авто- матических объектов и параметров оказываются недоступны. 7.5.2. Статические локальные объекты Зачастую требуется переменная, которая доступна и в области видимости функ- ции, и вне ее. Такие объекты определяют как статические (static). Статический локальный объект (static local object) должен быть гарантиро- ванно инициализирован в начале выполнения программы, не позже определения объекта. Как только такой объект будет создан, он не будет удален до тех пор, пока не завершится выполнение программы, а не функции. Локальная статическая пере- менная продолжает существовать и содержать значение на протяжении нескольких обращений к функции. В качестве стандартного примера рассмотрим функцию, ко- торая подсчитывает количество своих вызовов. size_t count_calls() { static size_t ctr =0; // значение сохранится на протяжении // всех обращений return ++ctr; for (size_t i = 0; i ’= 1 cout « count_calls() return 0; != 10; ++i) ls() « endl; Эта программа отобразит числа от 1 до 10 включительно. Прежде чем функция count_calls () будет впервые1 вызвана, переменная ctr должна быть создана и инициализирована исходным значением 0. Каждое обращение к функции увеличивает значение переменной ctr и возвращает ее текущее значение. Каждый раз, когда выполняется функция count_calls (), переменная ctr уже су- ществует и содержит значение, которое находилось в ней на момент завершения вы- полнения функции при последнем обращении. Таким образом, на момент второго вы- зова переменная ctr содержит значение 1, на момент третьего — значение 2 и т.д. 1 Вероятно, имелось в виду при первом вызове. — Примеч. ред.
282 Часть I. Основы Упражнения раздела 7.5.2 Упражнение 7.27. Объясните различия между параметром, локальной переменной и статической локальной переменной. Приведите пример программы, в которой используется каждый из них. Упражнение 7.28. Напишите функцию, которая возвращает значение о при первом вызове, а при каждом последующем вызове значение, на единицу большее, чем в предыдущий раз. 7.6. Встраиваемые функции Давайте вернемся к функции shorterString () со стр. 274, которая возвращает ссылку на более короткую строку. // выяснить, какая из строк длиннее const string &shorterString(const string &sl, const string &s2) { return si.size() < s2.size() ? si : s2; } К преимуществам определения функции для такой маленькой операции относятся. Обращение к функции shorterString () проще и понятнее, чем эквивалент- ное условное выражение. Если придется внести изменение, проще сделать это в теле функции, а не выис- кивать в коде программы все случаи применения эквивалентного выражения. Использование функции гарантирует одинаковое поведение. Она гарантирует, что каждая проверка будет выполнена тем же способом. Функция может быть многократно использована при написании других при- ложений. Однако у функции shorterString О’ есть один потенциальный недостаток: ее вызов происходит медленнее, чем вычисление эквивалентного выражения. На большинстве машин при вызове функции осуществляется довольно много действий: перед обращением сохраняются регистры, которые необходимо будет восстановить после выхода; происходит копирование значений аргументов; управление програм- мой переходит к новому участку кода. Встраиваемые функции позволяют избежать дополнительных затрат на вызов Содержимое функции, объявленной встраиваемой (inline) при компиляции, как пра- вило, встраивается по месту вызова. Предположим, что функция shorterString () объявлена встраиваемой, а ее вызов имеет следующий вид. cout << shorterString(si, s2) « endl; При компиляции тело функции окажется встроено по месту вызова и в результа- те получится нечто вроде следующего. cout « (sl.sizeO < s2.size() ? si : s2) « endl; Таким образом, во время выполнения удастся избежать дополнительных затрат, связанных с вызовом функции shorterString ().
Глава 7. Функции 283 Чтобы объявить функцию shorterString () встраиваемой, в определении, пе- ред типом возвращаемого значения, располагают ключевое слово inline. // встраиваемая версия функции сравнения двух строк inline const string & shorterString(const string &sl, const string &s2) { return sl.sizeO < s2.size() ? si : s2; } Объявление функции встраиваемой является только рекомендацией компилятору. Компи- лятор вполне может проигнорировать эту рекомендацию. На самом деле, механизм встраивания применяется в процессе оптимизации объ- ектного кода, в ходе которого код небольших функций, вызов которых происходит достаточно часто, встраивается по месту вызова. Большинство компиляторов не бу- дет встраивать рекурсивные функции. Функция на 1 200 строк также, вероятно, не будет встроена. Помещайте встраиваемые функции в файлы заголовка В отличие от других функций, определения встраиваемых функций должны находиться в файлах заголовка. Чтобы встроить код встраиваемой функции по месту вызова, компилятор дол- жен получить доступ к определению функции. Прототипа функции для этого не- достаточно. Встраиваемая функция может быть определена в программе несколько раз, одна- ко в одном файле исходного кода определение присутствует только один раз, а кро- ме того, определения в каждом файле исходного кода должны быть абсолютно оди- наковыми. Помещая встраиваемые функции в заголовки, можно гарантировать, что каждый раз будет использовано то же определение, а при вызове функции компиля- тор уже будет иметь доступ к определению функции. Всякий раз, когда встраиваемая функция изменяется или добавляется в файл заголов- ка, каждый файл исходного кода, который использует этот заголовок, придется пере- компилировать. Упражнения раздела 7.6 Упражнение 7.29. Какое из следующих объявлений и определений имеет смысл поместить в файл заголовка, а какой — в текст файла исходного кода? Объясните, почему. (a) inline bool eq(const Biglnt&, const Biglnt&) { ... } (b) void putValues(int *arr, int size); Упражнение 7.30. Перепишите функцию isshorter () (стр. 260) как встраиваемую.
284 Часть I. Основы 7.7. Функции-члены класса В разделе 2.8 (стр. 85) уже было начато определение класса Sales_item, ис- пользуемого при решении проблемы книжного магазина из главы 1, “Первые шаги”. Теперь, когда известно, как определять обычные функции, можно продолжить со- вершенствование этого класса и определить для него функции-члены. Функции-члены определяют аналогично обычным функциям. Подобно любой другой функции, определение функции-члена состоит из четырех частей: тип возвращаемого функцией значения; имя функции; разделяемый запятыми список параметров (может быть пустым); тело функции, располагающееся внутри пары фигурных скобок. Как известно, три первые части составляют прототип функции. В прототипе функции определена вся необходимая информация о типах функции: тип возвра- щаемого ей значения, имя функции, а также типы аргументов, которые могут ей быть переданы. Прототип функции должен быть определен внутри тела класса. Тело функции, однако, может быть определено как внутри класса, так и вне его. С учетом вышесказанного, давайте рассмотрим развернутое определение класса, в который будет добавлено два новых члена: функции avg_jorice () и same_isbn (). Список параметров функции avg_jorice () пуст, а возвращает она значение типа double. Функции same_isbn () передают ссылку на константу типа Sales_item, а возвращает она значение типа bool. class Sales_item { public: // операции, допустимые для объектов класса Sales_item double avg_price() const; bool same_isbn(const Sales_item &rhs) const { return isbn == rhs.isbn; } // закрытые члены как и прежде private: std::string isbn; unsigned units_sold; double revenue; }; Назначение ключевого слова const непосредственно после списка параметров будет описано вскоре, а сейчас рассмотрим, как определены функции-члены. 7.7.1. Определение тела функции-члена Все члены класса следует объявлять внутри фигурных скобок, заключающих оп- ределение класса. Впоследствии не будет никакого другого способа добавить в класс новый член. Члены класса, являющиеся функциями, следует определять так, как они были объявлены. Функцию-член можно определить либо внутри определения класса, либо вне его. В данном случае приведены примеры обоих подходов: функция same_isbn() определена внутри класса Sales_item, а функция avg_price() только объявлена внутри класса, но определена в другом месте.
Глава 7. Функции 285 Функция-член определенная внутри класса, неявно рассматривается как встраи- ваемая (раздел 7.6, стр. 282). Давайте более подробно рассмотрим определение функции same_isbn (). bool same_isbn(const Sales_item &rhs) const { return isbn == rhs.isbn; } Подобно любой другой функции, тело данной заключено в фигурные скобки (блок). В данном случае блок содержит один оператор, который возвращает результат сравнения значений переменных-членов isbn двух объектов класса Sales_item. В первую очередь обратите внимание на то, что переменная-член i sbn объявлена закрытой (private). Однако никакой ошибки здесь нет. Функция-член вполне может обращаться к закрытым членам своего класса. Весьма интересен способ, которым функция same_isbn() получает сравнивае- мые значения из объектов класса Sales_item. Функция обращается к значениям переменных-членов isbn и rhs. isbn. Вполне очевидно, что rhs. isbn — это пе- ременная-член isbn переданного функции аргумента, т.е. параметра rhs. Примене- ние переменной-члена i sbn без уточнений принадлежности не столь очевидно. Как будет продемонстрировано далее, переменная-член i sbn без уточнений относится к тому объекту, от имени которого происходит вызов функции. Функции-члены имеют дополнительный, неявный параметр Когда происходит вызов функции-члена, он осуществляется от имени объекта. Например, при вызове функции same_isbn() в программе книжного магазина (стр. 48), он осуществляется для объекта по имени total. if (total.same_isbn(trans)) В качестве аргумента в этом обращении передается объект trans. Здесь объект trans используется для инициализации параметра rhs2. Таким образом, в этом об- ращении часть rhs. isbn является ссылкой на член trans. isbn. Для связи неуточненной переменной-члена isbn с объектом total используется аналогичный процесс. Дело в том, что каждая функция-член имеет дополнительный, неявный параметр, который связывает функцию с объектом, от имени которого функция вызвана. Когда вызов функции-члена same_isbn () происходит от имени объекта total, этот объект также передается функции. При обращении функции- члена same_isbn () к переменной-члену isbn подразумевается, что имеется в виду член того объекта, от имени которого функция была вызвана. В результате этого об- ращения фактически сравниваются значения переменных-членов total. isbn и trans. isbn. ? Имя параметра rhs является общепринятым, это сокращение от right-hand side (стоящий справа). — Примеч. ред.
286 Часть I. Основы Знакомство с параметром thi s Каждая функция-член (кроме статических, рассматриваемых в разделе 12.6, стр. 496) имеет дополнительный, неявный параметр по имени this. При вызове функции-члена параметр this инициализируется адресом объекта, от имени кото- рого функция была вызвана. Предположим, что вызов функции-члена имеет сле- дующий вид. total.same_isbn(trans); Компилятор перезапишет его следующим образом. // псевдокод, иллюстрирующий откомпилированный вызов функции-члена Sales_item::same_isbn(&total, trans); В этом обращении к функции same_isbn () переменная-член isbn связана с со- держащим ее объектом total. Знакомство с константными функциями-членами Теперь рассмотрим роль ключевого слова const, следующего за списками пара- метров в объявлении функции-члена same_isbn(). Это ключевое слово изменяет тип неявного параметра this. Когда происходит вызов total. same_isbn (trans), неявный параметр this будет иметь тип const sales_Item* и содержать адрес объекта total. То есть как будто тело функции same_isbn () имеет следующий вид. // псевдокод, иллюстрирующий использование неявного указателя this II Этот код не подлежит компиляции, поскольку явно определять II указатель this нельзя. II Обратите внимание, this - это указатель на константу, II поскольку функция-член same_isbn() является константным // членом класса Sales_item. bool Sales_item::same_isbn(const Sales_item *const this, const Sales_item &rhs) const { return (this->isbn == rhs.isbn); } Функция, в которой ключевое слово const используется таким образом, называ- ется константной функцией-членом (const member function). Поскольку this явля- ется указателем на константу, константная функция-член не может изменить объект, от имени которого она была вызвана. Таким образом, функции avg_price () и same_isbn () могут читать, но не изменять значения переменных-членов объек- тов, для которых они были вызваны. Константный объект, указатель или ссылка на константный объект могут быть использо- ваны для вызова только константных функций-членов. Попытка вызова неконстантной функции-члена для константного объекта либо при помощи указателя либо ссылки на константный объект приведет к ошибке. Использование указателя thi s Внутри функции-члена необязательно явно использовать указатель this, чтобы получить доступ к члену объекта, от имени которого функция вызвана. Любое не- уточненное обращение к члену класса считается обращением к члену указателя this, т.е. члену текущего объекта.
Глава 7. Функции 287 bool same_isbn(const { return isbn == Sales_item &rhs) const rhs.isbn; } Переменная-член isbn используется в этой функции так, как если бы было на- писано this->units_sold или this->revenue. Поскольку параметр this определен неявно, его не нужно и даже ни в коем слу- чае нельзя указывать в списке параметров функции. Но в теле функции к указателю this можно обращаться явно. Это вполне допустимо, хотя и не нужно. Так функ- цию same_isbn () можно определить следующим образом. bool same_isbn(const Sales_item &rhs) const { return this->isbn == rhs.isbn; } 7.7.2. Определение функции-члена вне класса Когда определение функции-члена располагается вне определения класса, необ- ходимо указать, к какому именно классу принадлежит данная функция. double Sales_item::avg_price() const { if (units_sold) return revenue/units_sold; else return 0; } Это определение подобно другим определениям функций: здесь указан тип воз- вращаемого значения, double, и пустой список параметров, заключенный в круглые скобки после имени функции. Новым является ключевое слово const после списка параметров, а также формат имени функции. Sales_item::avg_price В имени функции использован оператор области видимости (раздел 1.2.2, стр. 30), который указывает, что определяемая функция по имени avg_price () должна на- ходиться в области видимости класса Sales_item. Ключевое слово const, следующее за списком параметров, свидетельствует о способе объявления функции-члена внутри заголовка sales item. В определении возвращаемый тип и список параметров всегда должны соответствовать объявлению (если оно есть). В случае функции-члена, объявление — это то, что находится в оп- ределении класса. Если функция-член объявлена константной, в определении после списка параметров так же должно быть указано ключевое слово const. Теперь первая строка этого кода полностью понятна: здесь определена функция avg_price (), которая является константным членом класса Sales_item. Функ- ция не получает никаких аргументов и возвращает значение типа double. Тело функции понять проще: здесь проверяется, отличается ли значение пере- менной-члена units_sold от нуля, и если это так, возвращает результат деления переменных-членов revenue и units_sold. Если значение переменной-члена units_sold равно нулю, деление недопустимо, поскольку деление на нуль — серь- езная ошибка. В этом случае данная программа возвращает значение 0 (т.е. когда ни одного экземпляра не продано, средняя выручка тоже окажется нулевой). В зависи- мости от сложности стратегии обработки ошибок, здесь может быть осуществлена передача исключения (раздел 6.13, стр. 241).
288 Часть I. Основы 7.7.3. Создание конструктора Sales_item() Существует еще один член класса, который необходимо написать, — это конст- руктор. Как упоминалось в разделе 2.8 (стр. 87), переменные-члены класса при его определении не инициализируются. Для инициализации переменных-членов ис- пользуется конструктор. Конструктор — это специальная функция-член Конструктор (constructor) — это специальная функция-член, которая отличается от других функций-членов именем, совпадающим с именем класса. В отличие от других функций-членов, конструкторы не имеют типа возвращаемого значения. Однако подобно другим функциям-членам, они имеют список параметров (который может быть пуст) и тело. Класс может иметь несколько конструкторов. Каждый кон- структор должен отличаться от других количеством или типом параметров. Параметрами конструктора являются значения, используемые для инициализа- ции переменных-членов класса при создании объекта. Конструкторы, как правило, гарантируют инициализацию всех переменных-членов. Класс Sales_item нуждается в явном определении только одного стандартного конструктора (default constructor), который отличается тем, что он не имеет пара- метров. Стандартный конструктор используется в случае, когда объект создается без явной передачи инициализирующего значения. vector<int> vi; // стандартный конструктор: пустой вектор string s; // стандартный конструктор: пустая строка Sales_item item; // стандартный конструктор: ??? Стандартные конструкторы строк и векторов уже рассматривались: каждый из них приводит объект в стандартное состояние готовности. Стандартный конструк- тор класса string создает пустую строку, т.е. " а стандартный конструктор класса vector создает вектор без элементов. Классу Sales_item стандартный конструктор необходим для того, чтобы соз- дать пустой объект. Здесь под пустым понимается такой объект, значением перемен- ной-члена isbn которого является пустая строка, а переменные-члена units_sold и revenue инициализированы нулевыми значениями. Определение конструктора Подобно любой другой функции-члену, конструктор объявляется внутри класса, а определен он может быть и вне его. Данный конструктор очень прост, поэтому он определен внутри тела класса. class Sales_item { public: // операции, допустимые для объектов класса Sales_item double avg_price() const; bool same isbn(const Sales_item &rhs) const { return isbn == rhs.isbn; } // стандартный конструктор необходим для инициализации // членов встроенного типа Sales_item():units_sold(0), revenue(O.O) { } // закрытые члены как и прежде private std::string isbn;
Глава 7. Функции 289 unsigned units_sold; double revenue; }; Прежде чем перейти к рассмотрению определения конструктора, обратите вни- мание на то, что он расположен в разделе public. Обычно (и в данном случае) кон- структор должен быть частью интерфейса класса. В конце концов, тот код, который будет использовать класс Sales_item, должен иметь возможность создать и ини- циализировать его объект. Если бы конструктор был объявлен в разделе private, он оказался бы недоступен извне. То есть создать объект класса Sales_item будет невозможно, а сам класс окажется довольно бесполезен. Само определение гласит, что это конструктор класса Sales_item, который имеет пустой список параметров и пустое тело функции. // стандартный конструктор необходим для инициализации // членов встроенного типа Sales_item() : units_sold(0), revenue (0.0) { } Наиболее интересная часть этого кода находится между двоеточием и фигурны- ми скобками, означающими пустое тело функции. Список инициализирующих значений конструктора Текст, расположенный между двоеточием и парой круглых скобок, — это список инициализирующих значений конструктора (constructor initializer list). В этом списке указаны исходные значения для одной или нескольких переменных-членов класса. Он располагается за списком параметров конструктора и начинается с двоеточия. Список инициализирующих значений конструктора содержит разделяемый запятыми пере- чень имен членов класса и соответствующих им значений в круглых скобках. Несколь- ко инициализирующих значений переменных-членов класса отделяются запятыми. Этот список инициализирующих значений свидетельствует о том, что перемен- ные-члены units_sold и revenue должны быть инициализированы значением 0. Каждый раз, когда создается объект класса Sales_i tem, эти переменные-члены бу- дут иметь значение 0. Исходное значение для переменной-члена isbn задавать не нужно. Если в списке инициализирующих значений конструктора не указано иное, те члены, которые являются объектами класса, автоматически инициализируются стандартным конструктором своего класса. Следовательно, переменную-член isbn инициализирует стандартный конструктор класса string, т.е. она будет пустой строкой. Но при необходимости для ее также вполне можно было бы указать исход- ное значение в списке инициализирующих значений. Рассмотрев список инициализирующих значений, можно завершить описание данного конструктора. Список параметров и тело рассматриваемого конструктора пусты. Список параметров пуст, поскольку это стандартный конструктор, выпол- няемый по умолчанию в случае, когда никаких инициализирующих значений нет. Тело пусто потому, что никаких действий, кроме инициализации переменных- членов units_sold и revenue, не нужно. Список инициализирующих значений явно инициализирует переменные-члены units_sold и revenue нулевыми значе- ниями, а переменную-член isbn класса string неявно инициализирует пустой строкой. Таким образом, данные переменные-члены инициализируются этими зна- чениями при создании каждого объекта класса Sales item.
290 Часть I. Основы Синтезируемый стандартный конструктор Если не определять никаких конструкторов явно, компилятор самостоятельно создаст J стандартный конструктор. Созданный конструктор, созданный компилятором, называют также синтезируе- мым стандартным конструктором (synthesized default constructor). Он инициали- зирует каждый член класса используя те же правила, которые применяются для инициализации переменных (раздел 2.3.4, стр. 73). Члены, типом которых является класс, например isbn, инициализируются стандартным конструктором этого клас- са. Исходное значение членов встроенного типа зависит от расположения определе- ния объекта. Если объект определен в глобальной области видимости (вне любой функции) или как локальный статический объект, он инициализируется нулевым значением. Если объект определен в локальной области видимости, эти члены оста- ются неинициализированными. Как обычно, использование неинициализированных членов класса для любых целей, кроме присвоения им значений, недопустимо. Синтезируемого стандартного конструктора зачастую вполне достаточно для классов, типом членов которых являются только классы. Для инициализации объектов классов, среди членов которых есть обладающие встроенным или составным типом, необходимо определять собственные стандартные конструкторы. Поскольку синтезируемый конструктор не инициализирует члены встроенных ти- пов автоматически, стандартный конструктор для класса необходимо определить явно. 7.7.4. Организация файлов кода классов Как уже упоминалось в разделе 2.9 (стр. 89), объявления классов обычно поме- щают в заголовки. Код функций-членов, определенных вне классов, помещают, как правило, в обычные файлы исходного кода. Программисты на языке C++ предпочи- тают использовать простое соглашение об именовании для заголовков и связанных с ними файлов кода определения классов. Определение класса помещают в файл с именем тип . h или тип . Н, где тип — это имя класса, определенного в файле. Опре- деления функции-члена, как правило, располагают в файле исходного кода, имя ко- торого совпадает с именем класса. Согласно этому соглашению, определение класса Sales_item следует поместить в файл по имени sales_item.h. Данный заголовок следует подключить в любую использующую этот класс программу. Определение функции same_isbn () следует поместить в файл по имени sales_item.cc. В этот файл, как и в любой другой файл, использующий класс Sales_item, следует под- ключить заголовок sales_item. h. Упражнения раздела 7.7.4 Упражнение 7.31. Напишите собственную версию класса Saies_item, добавив две новые от- крытые (public) функции-члена для чтения и записи содержимого объектов класса Saies_ item. Эти функции должны работать аналогично операторам ввода и вывода, используемым в главе 1, “Первые шаги”. Транзакции должны выглядеть аналогично используемым в той же главе. Используя этот класс, осуществите чтение и запись набора транзакций.
Глава 7. Функции 291 Упражнение 7.32. Напишите файл заголовка, содержащего новую версию класса Saies_item. Используйте для имен заголовка и всех связанных с ним файлов, необходимых для хранения не- встраиваемых функций, определенных вне тела класса, стандартные соглашения языка C++. Упражнение 7.33. Добавьте в класс функцию-член, которая складывает два объекта класса Saies_item. Используйте модернизированный класс в программе решения проблемы вычисле- ния средней стоимости проданных книг, описанной в главе 1, “Первые шаги”. 7.8. Перегруженные функции Две функции, расположенные в одной области видимости, называются пере- гружеными (overloaded), если они имеют одинаковое имя, но разные списки пара- метров. В арифметическом выражении на языке программирования используется пере- груженная функция. 1 + 3 В этом выражении используется оператор сложения целочисленных операндов, в то время как следующее выражение использует другой оператор, складывающий операнды с плавающей запятой. 1.0 + 3.0 Задача по выявлению и применению соответствующего оператора, в зависимости от типов операндов, лежит на компиляторе, а не на программисте. Аналогично, разработчик может определить набор функций, которые выполняют одинаковые действия, но для параметров разных типов. Такие функции можно вы- зывать, не заботясь о том, какая именно из них сработает. Перегрузка функций (function overloading) упрощает разработку программы и де- лает ее код более понятным, устраняя необходимость в изобретении и запоминании имен, которые существуют только для того, чтобы помочь компилятору выяснить, какую из функций следует вызвать. Приложение базы данных, например, могло бы иметь несколько функций lookup (), которые осуществляют поиск на основании имени, номера телефона, номера счета и т.д. Перегрузка функций позволяет создать набор функций, каждая из которых имеет имя lookup, но отличается используемым для поиска критерием. Таким образом, функцию lookup () можно вызывать пере- дав ей значения любого предусмотренного типа. Account&); Phone&); Name&); Record lookup(const Record lookup(const Record lookup(const Record rl, r2; rl = lookup(acct); r2 = lookup(phone); // поиск по учетной записи / / поиск по номеру телефона / / поиск по имени // вызов версии, получающей учетную запись // вызов версии, получающей номер телефона Здесь все три функции имеют одинаковое имя, но это три разные функции. Что- бы выяснить, какую из функций следует вызвать, компилятор использует тип (типы) аргумента, переданного при обращении. Чтобы понять принцип перегрузки функций, следует уяснить, как компилятор выбирает при вызове функцию из набора перегруженных функций. Этой теме по- священа остальная часть данного раздела.
292 Часть I. Основы В любой программе может существовать только один экземпляр функции main (). Функ- ция main () не может быть перегружена. Различие между перегрузкой и переобъявлением функции Если тип возвращаемого значения и список параметров двух объявлений функции совпадают, второе объявление рассматривается как переобъявление (redeclaration) пер- вой. Если списки параметров двух функций совпадают, но типы возвращаемых зна- чений разные, второе объявление будет ошибкой. Record lookup(const Account&); bool lookup(const Account&); // ошибка: другой только тип II возвращаемого значения Функции не могут быть перегружены (overloaded) на основании различия только в типе возвращаемого значения. Оба списка параметров могут быть идентичны, даже если они не выглядят оди- наково. // в каждой паре объявлена та же функция Record lookup(const Account &acct); Record lookup (const Accounts); // имена параметров игнорируются typedef Phone Telno; Record lookup(const Phone&); Record lookup(const Telno&); // Telno и Phone имеют одинаковый тип Record lookup(const Phone&, const Name&); // заданный по умолчанию аргумент не изменяет количества параметров Record lookup(const Phone&, const Name& = // ключевое слово const: безразлично для нессылочных параметров Record lookup(Phone); Record lookup (const Phone); // переобъявление Первое объявление в первой паре содержит именованный параметр. Имена парамет- ров — это лишь дополнительное документирование, они не изменяют список параметров. Кажется, что вторая пара имеет различные типы параметров, но на самом деле Telno — это не новый тип, а синоним типа Phone. Ключевое слово typedef позво- ляет присвоить альтернативное имя уже существующему типу данных, новый тип данных оно не создает. Следовательно, два параметра, которые отличаются только тем, что тот же тип одного указан настоящим именем, а второго — альтернативным, не являются разными. В третьей паре списки параметров отличаются только наличием заданного по умолчанию аргумента. Это не изменяет количества параметров. Функция все равно получает два аргумента, независимо от того, будут ли они предоставлены пользова- телем или компилятором. Последняя пара отличается только тем, что один из параметров объявлен кон- стантным. Это различие не имеет никакого значения для объектов, которые могут быть переданы функции; поэтому второе объявление рассматривается как переобъ- явление первого. Все зависит от того, как аргументы переданы. Когда параметр ко- пируется, совершенно неважно, является ли он постоянным или нет, ведь функция работает с копией и никак не может изменить аргумент. В результате константный объект можно передать как константному, так и неконстантному параметру. Таким образом, оба параметра одинаковы.
Глава 7. Функции 293 Следует заметить, что константный и неконстантный параметры эквивалентны только тогда, когда имеют нессылочный тип. Функция, получающая константную ссылку, отличается то получающей неконстантную ссылку. Аналогично, функция, получающая указатель на константный объект, отличается от функции, получающей указатель на неконстантный объект того же типа. Совет. Когда не следует перегружать функции Хотя перегрузка функций позволяет избежать необходимости создавать и запоминать имена общепринятых операций, она не всегда целесообразна. В некоторых случаях разные имена функций предоставляют дополнительную информацию, которая упроща- ет понимание программы. Давайте рассмотрим набор функций-членов класса Screen, отвечающих за перемещение курсора. Screen& moveHome(); Screens moveAbs(int, int); Screens moveRelfint, int, char *direction); На первый взгляд может показаться, что этот набор функций имеет смысл перегру- зить под именем move следующим образом. Screens move(); Screens move(int, int); Screens move(int, int, *direction); Однако в результате перегрузки имена этих функций потеряли дополнительную ин- формацию об их назначении, что сделало код менее наглядным. Хотя перемещение курсора — это общая операция, совместно используемая всеми этими функциями, определенный характер перемещения уникален для каждой из этих функций. Рассмотрим, например, функцию moveHome (), осуществляющую вполне определенное перемещение курсора. Которое из двух приведенных ниже об- ращений понятнее при чтении кода программы, использующей класс Screen? // которая из записей понятней? myScreen.home(); // вероятно эта! myScreen.move(); 7.8.1. Перегрузка и область видимости Как уже было продемонстрировано в программе на стр. 77, области видимости в языке C++ могут быть вложенными. Имя, объявленное в локальной области види- мости, например внутри функции, скрывает то же имя, объявленное в глобальной области видимости (раздел 2.3.6, стр. 76). Это справедливо и для имен функций, и для имен переменных. /* Программа предназначена лишь для иллюстрации, * поскольку не следует определять в функции локальную переменную * с тем же именем, что и у глобальной * / string init(); // имя init имеет глобальную область видимости void fcn() { int init =0; // локальное имя init скрывает глобальное string s = init(); // ошибка: глобальное имя init скрыто )
294 Часть I. Основы К именам перегруженных функций применимы обычные правила областей видимо- сти. Если объявить функцию локально, она скроет, а не перегрузит одноименную функ- цию, объявленную во внешней области видимости. Как следствие, объявления всех вер- сий перегруженной функции должны располагаться в одной области видимости. Объявлять функцию локально нежелательно. Объявления функций следует располагать в файле заголовка. Чтобы объяснять, как область видимости взаимодействует с перегрузкой, нарушим эту рекомендацию и используем локальное объявление функции. В качестве примера рассмотрим следующую программу. void print(const string &); void print(double); // перегружает функцию print() void fooBar(int ival) { void print(int); // новая область видимости: скрывает // предыдущие экземпляры функции print() print("Value: "); // ошибка: print(const string &) скрыта print(ival); // ok: print(int) видима print(3.14); // ok: callsprint(int); print(double) скрыты Объявление функции print (int) в функции fooBar () скрывает другие вер- сии функции print (). Теперь существует только один доступный вариант функции print (): тот, который имеет один параметр типа int, и любое обращение к имени print в этой области видимости (или области видимости, вложенной в данную) подразумевает именно данный экземпляр. При обращении к функции print О , компилятор ищет сначала объявление это- го имени. Он находит локальное объявление функции print (), т.е. версию с одним параметром типа int. Как только имя найдено, компилятор прекращает дальнейшие поиски и уже не проверяет внешнюю область видимости на предмет искомой функ- ции. Компилятор полагает данное объявление единственно доступным для исполь- зования. Да и зачем искать дальше, ведь объявление имени вполне допустимо. При первом вызове, функции print () передают строковый литерал, но для нее указан параметр типа int. Строковый литерал не может быть неявно преобра- зован в значение типа int, поэтому это обращение является ошибкой. Функция print (const string&), которая соответствовала бы этому обращению, скрыта и не рассматривается при поиске данного обращения. При вызове функции print () с передачей значения типа double процесс по- вторяется. Компилятор находит локальное определение функции print (int). Но аргумент типа double может быть преобразован в значение типа int, поэтому такое обращение допустимо. Поиск имен в языке C++ осуществляется до проверки соответствия типов. Если бы объявление print (int) находилось в той же области видимости, что и объявления других версий функции print (), это была бы еще одна перегруженная
Глава 7. Функции 295 версия функции print (). В этом случае данные обращения рассматривались бы по-другому. void print(const string &); void print(double); // перегружает функцию print() void print(int); // еще один перегруженный экземпляр void fooBar2(int ival) { print("Value: "); // ok: вызов print(const string &) print(ival); // ok: вызов print(int) print(3.14); // ok: вызов print(double) } Теперь, когда компилятор ищет имя print, он находит три функции. При каж- дом обращении он выбирает ту версию функции print (), которая соответствует переданному аргументу. Упражнения раздела 7.8.1 Упражнение 7.34. Определите набор перегруженных функций по имени error, который соот- ветствует следующим обращениям. int index, upperBound; char selectVal; error("Subscript out of bounds: ", index, error("Division by zero"); error("Invalid selection", selectVal); upperBound); Упражнение 7.35. Объясните результат второго объявления в каждом из следующих наборов. Укажите, какое из них (если есть) недопустимо. (a) int calc(int, int); int calc(const int, const int); (c) int *reset(int *); double *reset(double *); 7.8.2. Подбор функций и преобразование аргументов Поиск перегруженной функции (function overload resolution), или подбор функции (function matching), — это процесс, в ходе которого вызов функции ассоциируется с определенной версией из набора перегруженных функций. Компилятор подбирает обращение к функции автоматически, сравнивая фактические аргументы, исполь- зуемые в обращении, с параметрами, указанными для каждой функции в наборе. Здесь возможны три варианта. 1. Компилятор находит одну функцию, которая является наилучшим соответстви- ем (best match) для фактических аргументов, и создает код ее вызова. 2. Компилятор не может найти ни одной функции, параметры которой соответст- вуют аргументам в обращении. В этом случае компилятор сообщает об ошибке.
296 Часть I. Основы 3. Компилятор находит несколько функций, которые в принципе подходят, но ни одна из них не соответствует полностью. В этом случае компилятор также сооб- щает об ошибке, об ошибке неоднозначности (ambiguous). Как правило, вовсе несложно выяснить, является ли определенное обращение допустимым, и если является — какая из версий функции будет вызвана компилято- ром. Функции в наборе перегруженных версий отличаются количеством или типом аргументов. Подбор функции усложняется в случае, когда параметры нескольких функций допускают преобразование (раздел 5.12, стр. 204) переданных аргументов. В подобных случаях программисту необходимо знать, как именно проходит процесс подбора функций. 7.8.3. Три этапа подбора перегруженной версии Давайте рассмотрим следующий набор перегруженных функций и их вызов. void f(); void f(int); void f(int, int); void f(double, double = 3.14); f(5.6); // вызов версии void f(double, double) Функции-кандидаты На первом этапе подбора перегруженной функции выявляют набор версий, под- ходящих для рассматриваемого обращения. Такие функции называются функциями- кандидатами (candidate function). Функция-кандидат имеет имя, указанное при вы- зове, и видима в точке обращения. В данном примере кандидатами являются все че- тыре функции по имени f. Выявление подходящих функций Второй этап выбора перегруженной функции из набора функций-кандидатов подразумевает выявление тех из них, которые могут быть вызваны с аргументами, указанными при обращении. Выбранные функции называют подходящими (viable function). Чтобы считаться подходящей, функция должна удовлетворять двум усло- виям. Во-первых, функция должна иметь столько же параметров, сколько аргумен- тов передано при обращении, а во-вторых, тип каждого аргумента должен совпадать или допускать преобразование в тип соответствующего параметра. Когда функция имеет аргументы, заданные по умолчанию (раздел 7.4.1, стр. 278), в об- ращении может быть меньшее аргументов, чем введено фактически. Аргументы, задан- ные по умолчанию, являются вполне легальным способом передачи значений и при подборе учитываются наравне с любыми другими аргументами. При обращении f (5.6), две функции-кандидата можно исключить сразу, из-за несоответствия количеству аргументов. Речь идет о версии без параметров и версии с двумя параметрами типа int. В данном случае обращение имеет только один ар- гумент, а эти функции не имеют вообще или имеют два параметра соответственно. С другой стороны, та версия функции, которая обладает двумя параметрами типа double, вполне может оказаться подходящей. В обращении к функции, для которой
Глава 7. Функции 297 задан аргумент по умолчанию (раздел 7.4.1, стр. 278), соответствующий параметр может отсутствовать. Компилятор автоматически применит заданное по умолчанию значение для пропущенного аргумента. Следовательно, данное обращение вполне может иметь больше аргументов, чем указано явно. После проверки количества аргументов, позволяющей выявить функции подхо- дящие потенциально, проверяется соответствие типов параметров функций типам аргументов, переданных при вызове. Как и при любом обращении, тип аргумента может либо совпадать, либо допускать преобразование в тип параметра. В данном случае подходят обе оставшиеся функции. Функция f (int) является подходящей потому, что аргумент типа double мо- жет быть неявно преобразован в параметр типа int. Функция f (double, double) также является подходящей потому, что для второго параметра задано значение по умолчанию, а первый параметр имеет тип double, который точно соответствует типу аргумента. Если никаких подходящих функций не обнаружено, компилятор выдает сообщение об ошибке. Поиск наилучшего соответствия, если он есть На третьем этапе подбора перегруженной функции выясняется, какая из допусти- мых функций наилучшим образом соответствует фактическим аргументам в обраще- нии. Этот процесс анализирует каждый аргумент в обращении и выбирает подходя- щую функцию (или функции), для которой соответствие параметра аргументу являет- ся наилучшим. Подробно критерии “наилучшего соответствия” рассматриваются в следующем разделе, а пока достаточно знать, что чем ближе типы аргумента и пара- метра друг к другу, тем лучше соответствие. Так, например, точное соответствие типов аргумента и параметра подразумевает, что преобразование не требуется вообще. В данном случае существует только один явный аргумент, подлежащий рассмот- рению. Этот аргумент имеет тип double. При вызове версии f (int), значение ар- гумента придется преобразовать из типа double в тип int. Вторая подходящая функция, f (double, double), точно соответствует типу этого аргумента. По- скольку точное соответствие лучше соответствия требующего преобразования, ком- пилятор решает, что обращение f (5.6) осуществляется к функции версии с двумя параметрами типа double. Подбор перегруженной версии с несколькими параметрами Подбор перегруженной версии функции несколько усложняется, если явно пере- дано несколько аргументов. Предположим, что функции имеют то же имя f, но ана- лизируется следующее обращение. f(42, 2.56); Набор допустимых функций выявляется как прежде. Компилятор выбирает те версии функции, которые имеют необходимое количество параметров, типы кото- рых соответствуют типам аргументов. В данном случае в набор допустимых вошли
298 Часть I. Основы функции f (int, int) и f (double, double). Затем компилятор перебирает ар- гументы один за одним и определяет, какая из версий функций имеет наилучшее со- ответствие. Соответствующей является одна, и только одна функция, для которой выполняются следующие условия. 1. Соответствие по каждому аргументу не хуже, чем у остальных допустимых функций. 2. По крайней мере у одного аргумента соответствие лучше, чем у остальных до- пустимых функций. Если после просмотра всех аргументов не было найдено ни одной функции, кото- рая считалась бы наиболее приемлемой, компилятор выдаст сообщение об ошибке, свидетельствующее о том, что обращение неоднозначно. В рассматриваемом примере обращения, при анализе лишь первого аргумента, для версии f (int, int) функции f () обнаруживается точное соответствие. При анализе второй версии функции f () оказывается, что аргумент 42 типа int следует преобразовать в значение типа double. Соответствие в результате встроенного пре- образования хуже, чем точное. Таким образом, рассматривая только этот параметр, лучше соответствует та версия функции f (), которая обладает двумя параметрами типа int, а не двумя параметрами типа double. Но при переходе ко второму аргументу оказывается, что версия функции f () с двумя параметрами типа double точно соответствует аргументу 2.56. Вызов версии функции f () с двумя параметрами типа int потребует преобразования аргумента 2.56 из типа double в тип int. Таким образом, при рассмотрении только второго па- раметра версия f (double, double) функции f () имеет лучшее соответствие. Следовательно, данное обращение неоднозначно: каждая допустимая функция лучшее соответствует обращению по одному из аргументов. В результате компиля- тор выдаст сообщение об ошибке. Чтобы выбрать версию принудительно, следует явно привести тип одного из аргументов. f(static_cast<double>(42), 2.56); // вызов f(double, double) f(42, static_cast<int>(2.56)); // вызов f(int, int) При вызове перегруженных функций, приведения аргументов практически не нужны: потребность в приведении означает, что наборы параметров перегруженных функ- ций проработаны плохо. Упражнения раздела 7.8.3 Упражнение 7.36. Что такое функция-кандидат? Что такое допустимая функция? Упражнение 7.37. С учетом приведенных выше объявлений функции f () укажите, какие из сле- дующих обращений допустимы. Перечислите для каждого обращения допустимые функции, если они есть. Если обращение недопустимо, укажите, произошло ли это из-за отсутствия соответствия или из за неоднозначности. Если обращение допустимо, укажите, какая из версий функции f () обладает наилучшим соответствием. (a) f(2.56, 42); (b) f(42); (с) f(42, 0); (d) f(2.56, 3.14);
Глава 7. Функции 299 7.8.4. Преобразование типов аргументов Чтобы выявить наилучшее совпадение, компилятор ранжирует преобразования, применяемые для приведения типа аргумента к типу соответствующего ему пара- метра. Преобразования ранжируются в порядке убывания следующим образом. 1. Точное соответствие. Типы аргумента и параметра совпадают. 2. Соответствие в результате приведения (раздел 5.12.2, стр. 205). 3. Соответствие в результате стандартного преобразования (раздел 5.12.3, стр. 207). 4. Соответствие в результате преобразования класса. (Более подробно это преобра- зование рассматривается в разделе 14.9 (стр. 566). В контексте соответствия функций, приведение и преобразование значений встроенных типов может привести к удивительным результатам. К счастью, в хорошо разработанных / системах редко используют функции с параметрами, столь похожими, как в следующих примерах. Следующие примеры призваны закрепить знания об анализе при подборе функ- ций, а также продемонстрировать особенности и отношения между встроенными ти- пами вообще. Соответствие, требующее приведения и преобразования Приведение или преобразование применяется в случае, когда тип аргумента мо- жет быть трансформирован в тип соответствующего параметра при помощи одного из стандартных преобразований. Рассмотрим более подробно, как небольшие целочисленные типы приводятся к типу int. Предположим, что существует две версии функции, одна из которых имеет параметр типа int, а вторая — типа short. Версия с параметром типа int будет лучшее соответствовать значениям любого отличного от short целочис- ленного типа даже при том, что на первый взгляд кажется, что тип short должен бы подходить лучше, void ff(int); void ff(short); ff('a'); // тип char приводится к int, поэтому применяется f(int) Символьный литерал имеет тип char, а преобразуется он в тип int. Этот приве- денный тип соответствует параметру функции f f (int). Символ может быть также преобразован в тип short, но преобразование менее предпочтительно при поиске соответствия, чем приведение. Итак, это обращение будет распознано как обращение к функции f f (int). Трансформация, осуществляемая в ходе приведения, предпочтительнее стан- дартного преобразования. Так, например, значение типа char лучшее соответствует версии функции для типа int, а не для версии функции, получающей аргумент типа double. Все остальные стандартные преобразования расцениваются как эквива- лентные. Преобразование из типа char в тип unsigned char, например, не имеет приоритета над преобразованием из типа char в тип double. В качестве конкретно- го примера рассмотрим следующий.
300 Часть I. Основы extern void manip(long); extern void manip(float); manip(3.14); // ошибка: неоднозначное обращение Литеральная константа 3.14 имеет тип double. Значение этого типа могло бы быть преобразовано в значение либо типа long, либо типа float. Поскольку суще- ствует два возможных стандартных преобразования, обращение неоднозначно. Дело в том, что ни одно из стандартных преобразований в данном примере не имеет при- оритета над другим. Соответствие параметров и перечисления Напомним, что объект типа enum может быть инициализирован только другим объектом типа enum или одним из его перечислителей (раздел 2.7, стр. 85). Цело- численный объект, который имеет то же значение, что и перечислитель, не может быть использован при вызове функции, ожидающей аргумент типа enum. enum Tokens {INLINE = 128, VIRTUAL = 129}; void ff(Tokens); void ff(int); int main() { Tokens curTok - INLINE; ff(128); // точное соответствие ff(int) ff(INLINE); // точное соответствие ff(Tokens) ff(curTok); // точное соответствие ff(Tokens) return 0; } Обращение, в котором передан литерал 128, соответствует версии функции f f () с параметром типа int. Хотя целочисленное значение нельзя передавать как параметр типа enum, значе- ние типа enum можно передать как параметр целочисленного типа. В этом случае значение типа enum приводится к типу int или большему целочисленному типу. Фактический тип приведения зависит от значений перечислителей. Если функция перегружена, тип, к которому приводится значение типа enum, определяет, какая именно из версий окажется вызвана. void newf(unsigned char); void newf(int); unsigned char uc = 129; newf(VIRTUAL); // вызов newf(int) newf(uc); // вызов newf(unsigned char) Перечисление Tokens содержит лишь два перечислителя, самый большой из которых имеет значение 12 9. Это значение может быть представлено типом unsigned char (и многие компиляторы именно так и поступают). Но типом зна- чения VIRTUAL является не unsigned char. Перечислители и значения типа enum не приводятся к типу unsigned char, даже если значения перечислителей соот- ветствуют им. Используя перегруженные функции с параметрами типа enum, помните, что в зависи- мости от значений констант в процессе подбора перегруженной версии функции, два типа перечисления могут вести себя совершенно по-разному. Перечислители опреде- ляют тип, к которому они приводятся, и этот тип является машинно-зависимым.
Глава 7. Функции 301 Перегрузка и константные параметры Является ли параметр константным или нет, имеет значение только тогда, когда он пред- ставляет собой ссылку или указатель. Перегрузить функцию можно на основании использования ссылочного парамет- ра константного или неконстантного вида. Перегрузка для константного ссылочного параметра вполне допустима, поскольку компилятор при вызове функции способен различать, является ли аргумент константным или нет. Record lookup(Account&); Record lookup (const Account&); // новая функция const Account a(0); Account b; lookup(a); // вызов lookup(const Account&) lookup(b); // вызов lookup(Accounts&) Если параметр представляет собой обычную ссылку, ему нельзя передать констант- ный объект. Таким образом, при передаче константного объекта, единственно допусти- мой окажется та версия функции, в которой параметр является константной ссылкой. При передаче неконстантного объекта, любая из версий функции будет подходя- щей. Неконстантный объект можно использовать для инициализации как констант- ной, так и неконстантной ссылки. Однако инициализация константной ссылки не- константным объектом потребует преобразования, а инициализация неконстантного параметра — точного соответствия. Параметры в виде указателей работают аналогично. Адрес константного объекта можно передать только той функции, которая ожидает передачи указателя на кон- станту. Указатель на неконстантный объект можно передать функции, которая ожи- дает передачи указателя на константу или неконстанту. Если две функции отлича- ются только тем, является ли параметр указателем на константу или неконстанту, для указателя на неконстантный объект лучшее соответствие будет у версии с указа- телем на неконстантный тип. Здесь компилятор также способен различать, является ли аргумент константой (в этом случае произойдет вызов функции, получающей указатель на константу) или неконстантой (в этом случае произойдет вызов функ- ции, получающей обычный указатель). Следует заметить, что невозможно организовать перегрузку на основании того, является ли сам указатель константой. f(int *); f(int *const); // переобъявление Здесь ключевое слово const применяется к указателю, а не типу, на который ука- зывает указатель. В обоих случаях указатель копируется, ведь компилятор не делает никаких различий того, является ли сам указатель константой или нет. Как упомина- лось на стр. 292, когда значение аргумента передается как копия, невозможно перегру- зить функцию на основании того, является ли этот параметр константой или нет. Упражнения раздела 7.8.4 Упражнение 7.38. Предположим, что существуют следующие объявления. void manip(int, int); double dobj;
302 Часть I. Основы Каков порядок (раздел 7.8.4 стр. 299) преобразований в каждом из следующих обращений? (a) manip('а', 'z'); (b) manip(55.4, dobj); Упражнение 7.39. Объясните назначение второго объявления в каждом из следующих наборов. Укажите, какие из них (если они есть) недопустимы. (a) int calc(int, int); int calc(const int&, const int&); (b) int calc(char*, char*); int calc(const char*, const char*); Упражнение 7.40. Действительно ли допустим следующий вызов функции? Если нет, почему? enum Stat { Fail, Pass }; void test(Stat); test(0); 7.9. Указатели на функции Указатель на функцию (function pointer) содержит адрес функции, а не объекта. Подобно любому другому указателю, указатель на функцию имеет вполне опреде- ленный тип. Тип функции определен типом ее возвращаемого значения и списком параметров. Имя функции не является частью ее типа. // pf указывает на функцию, получающую две константные ссылки // на строки и возвращающую значение типа bool bool (*pf)(const string &, const string &) ; Этот оператор объявляет, что pf является указателем на функцию, получающую два параметра типа const string& и возвращающую значение типа bool. Часть *pf необходимо заключить в круглые скобки. // объявление функции pf(), возвращающей указатель на тип bool bool *pf(const string &, const string &); Применение определения типа для упрощения создания указателя на функцию Типы указателей на функции очень быстро могут стать неконтролируемыми. Однако определив синонимы для типов указателей при помощи ключевого слова typedef (раздел 2.6, стр. 83), их применение для указателей на функции можно су- щественно облегчить. typedef bool (*cmpFcn)(const string &, const string &) ; Данное определение гласит, что cmpFcn — это имя типа, который является указа- телем на функцию. Точнее, тип указателя на функцию, которая возвращает значение типа bool и получает две ссылки на тип const string. Теперь, когда необходимо применить данный тип указателя на функцию, можно просто воспользоваться типом cmpFcn, а не писать каждый раз полное определение типа.
Глава 7. Функции 303 Инициализация и присвоение указателей на функции При использовании имени функции без обращения к ней, имя автоматически преобразуется в указатель на функцию. Рассмотрим следующий пример. // сравнить длину двух строк bool lengthCompare(const string &, const string &) ; В любом случае, применения функции lengthCompare О , за исключением ис- пользования в качестве левого операнда при вызове функции, ее имя рассматривает- ся как указатель следующего типа, bool (*)(const string &, const string &); Имя функции можно использовать для инициализации или присвоения значения указателю на функцию. cmpFcn pfl =0; // ok: несвязанный указатель на функцию cmpFcn pf2 = lengthCompare; // ok: тип указателя соответствует // типу функции pfl - lengthCompare; // ok: тип указателя соответствует II типу функции pf2 = pfl; // ok: типы указателей совпадают Применение имени функции эквивалентно использованию оператора обращения к адресу имени функции. cmpFcn pfl = lengthCompare; cmpFcn pf2 = SclengthCompare; Указатель на функцию может быть присвоен или инициализирован только именем функ- Я ции, указателем на функцию, имеющим тот же тип, либо константным выражением, ре- 4 зультатом которого является нуль. Инициализация указателя на функцию нулевым значением означает, что указа- тель не указывает ни на одну функцию. Преобразования между типами указателей на функции не происходят. string::size_type sumLength(const string&, const string&); bool cstringCompare(char*, char*); // указатель на функцию, получающую два параметра // типа const_string& и возвращающую значение типа bool cmpFcn pf; pf = sumLength; // ошибка: разные типы возвращаемого значения pf = cstringCompare; // ошибка: разные типы параметров pf = lengthCompare; // ok: типы функции и указателя // совпадают точно Вызов функции при помощи указателя Для вызова функции вполне можно использовать указатель на нее. Указатель можно применять непосредственно, поскольку для вызова функции не нужно ис- пользовать оператор обращения к значению. cmpFcn pf = lengthCompare; lengthCompare ("hi", "bye"); // прямое обращение pf ("hi", "bye"); // эквивалентно неявному обращению к значению (*pf) ("hi", "bye"); // эквивалентно неявному обращению к значению
304 Часть I. Основы Если указатель на функцию неинициализирован или имеет нулевое значение, он не мо- жет быть использован при обращении. Только инициализированные указатели могут быть безопасно использованы для вызова функции. Указатель на функцию как параметр Параметры функции вполне могут быть указателями на функции. Такой пара- метр можно указать одним из двух следующих способов. /* Третий параметр функции useBigger() является указателем на * функцию, которая возвращает значение типа bool и получает две * константные ссылки на строки. Этот параметр можно определить */ двумя способами: / / третий параметр имеет тип функции и автоматически // рассматривается как указатель на функцию void useBigger(const string &, const string &, bool(const string &, const string &)); // эквивалентное объявление: здесь параметр явно определен // как указатель на функцию void useBigger(const string &, const string &, bool (*)(const string &, const string &)); Возвращение указателя на функцию Функция может возвращать указатель на функцию, хотя правильная запись типа возвращаемого значения может быть проблематична. // функция ff(), получает значение типа int и возвращает II указатель на функцию I/ функция указана как возвращающая значение типа int и получающая // параметры типа int* и int int (*ff(int))(int*, int); Лучше всего читать объявление указателя на функцию изнутри, начиная с объявления имени. Исходя из этого можно предположить, что часть ff (int) объявляет функцию f f () получающей один параметр типа int, а часть int (*) (int*, int) ; свиде- тельствует о возвращении указателя на функцию, которая возвращает тип int и по- лучает два параметра типа int* и int. Применение определения типа позволит существенно упростить такие объявления. // PF - указатель на функцию, возвращающую тип int и получающую // параметры типа int* и int typedef int (*PF)(int*, int); PF ff(int); // функция ff() возвращает указатель на функцию Вполне можно определить параметр типа функции. Тип возвращаемого значения функции должен быть указателем на функцию, но не функцией. Аргумент для параметра, который имеет тип функции, автоматически преобразу- ется в соответствующий тип указателя на функцию. При выходе из функции анало- гичное преобразование не осуществляется.
Глава 7. Функции 305 // func имеет тип функции, а не указателя на функцию! typedef int func(int*, int); void fl(func); // ok: fl() имеет параметр типа функции func f2(int); // ошибка: f2() имеет тип возвращаемого значения // типа функции func *f3(int); // ok: f3() возвращает указатель на тип функции Указатели на перегруженные функции Для обращения к перегруженной функции вполне можно использовать указатель на функцию. extern void ff(vector<double>); extern void ff(unsigned int); // к которой из функций относится указатель pfl? void (*pfl)(unsigned int) = &ff; // ff(unsigned) Тип указателя должен точно соответствовать одной из перегруженных функций. Но если точного соответствия не найдено, попытка инициализации или присвоения приведет к ошибке во время компиляции. ошибка: void (*pf2) не соответствует (int) = &ff; список параметров // ошибка: не соответствует тип возвращаемого значения double (*pf3)(vector<double>); pf3 = &ff; Резюме Функции представляют собой именованные блоки действий, применяемые для структу- рирования даже небольших программ. При их определении указывают тип возвращаемого значения, имя, список параметров (возможно, пустой) и тело функции. Тело функции — блок операторов, выполняемых при вызове функции. Переданные функции при вызове аргументы должны быть совместимы с типами соответствующих параметров. При передаче функции аргументов выполняются те же правила, что и при инициализации переменной. Каждый нессылочный параметр инициализируется копией значения соответст- вующего аргумента. Любые изменения, внесенные в значение параметра нессылочного типа, происходят с локальной копией, а не со значением самого аргумента. Копирование больших, сложных значений может обойтись чересчур дорого. Чтобы избежать дополнительных затрат на передачу копии, параметры могут быть определены как ссылки. Изме- нения, внесенные в ссылочные параметры, отражаются на самом аргументе. Ссылочный параметр, который не должен изменять значение его аргумента, должен быть константной ссылкой. В языке C++ функции могут быть перегружены. То есть одинаковое имя может быть ис- пользовано при определении разных функций, отличающихся количеством или типами па- раметров. На основании переданных в обращении аргументов, компилятор автоматически выбирает наиболее подходящую версию функции. Процесс выбора правильной версии из на- бора перегруженных функций называют подбором функции с наилучшим соответствием. В языке C++ существует два вида специальных функций: встраиваемые функции и функ- ции-члены. Ключевое слово inline рекомендует компилятору встроить содержимое функ- ции в код непосредственно по месту ее вызова. Встраиваемые функции позволяют избежать дополнительных затрат, связанных с вызовом функции. Функции-члены — это члены класса, которые являются функциями. В этой главе рассматривались лишь простые функции-члены, а более подробная информация по этой теме приведена в главе 12, “Классы”.
306 Часть I. Основы Термины Автоматический объект (automatic object). Объект, являющийся для функции локаль- ным. Автоматические объекты создаются и инициализируются при каждом обращении и уда- ляются по завершении блока, в котором они были определены. Как только функция заверша- ет работу, автоматические объекты прекращают существование. Аргумент (argument). Значение, предоставляемое при вызове функции. Эти значения ис- пользуются для инициализации соответствующих параметров, аналогично инициализации переменных того же типа. Временный объект (temporary object или temporary). Неименованный объект, автомати- чески создаваемый компилятором в ходе вычисления результата выражения. Временный объект сохраняется до конца вычисления выражения, для которого он был создан. Встраиваемая функция (inline function). Функция, тело которой встраивается по месту обращения, если это возможно. Встраиваемые функции позволяют избежать обычных допол- нительных затрат, поскольку ее вызов заменяет код тела функции. Константная функция-член (const member function). Функция, которая является членом класса и может быть вызвана для константных объектов этого класса. Константные функции- члены не могут изменять переменные-члены объекта, с которым они работают. Конструктор (constructor). Функция-член, имя которой совпадает с именем класса. Кон- структор инициализирует объект своего класса. Он не имеет никакого типа возвращаемого значения. Конструкторы могут быть перегружены. Локальная переменная (local variable). Переменная, определенная внутри функции. Ло- кальные переменные доступны только внутри тела функции. Локальный статический объект (local static object). Локальный объект, который создается и инициализируется только один раз перед первым вызовом функции, в которой использует- ся ее значение. Значение локального статического объекта сохраняющееся на протяжении всех вызовов функции. Наилучшее соответствие (best match). Одна из версий в наборе перегруженных функций может иметь наилучшее соответствие с аргументами конкретного обращения. Неоднозначное обращение (ambiguous call). Ошибка времени компиляции, причиной которой является невозможность найти версию перегруженной функции с наилучшим со- ответствием. Оператор обращения (call operator). Оператор, запускающий функцию на выполнение. Оператор обращения представляет собой пару круглых скобок и получает два операнда: имя вызываемой функции и разделяемый запятыми список аргументов (возможно, пустой), под- лежащий передаче функции. Параметр (parameter). Локальная переменная функции, исходное значение которой пре- доставляется при вызове функции. Перегруженная функция (overloaded function). Функция, которая имеет то же имя, что и по крайней мере одна другая функция. Перегруженные функции должны отличаться по ко- личеству или типу их параметров. Подбор функции (function matching). Процесс, в ходе которого компилятор ассоциирует вызов функции с определенной версией из набора перегруженных функций. При подборе функции, используемые в обращении аргументы сравниваются со списком параметров каж- дой версии перегруженной функции. Подходящая функция (viable function). Подмножество перегруженных функций, которые могли бы соответствовать данному обращению. У подходящие функций количество парамет- ров совпадает с количеством переданных при обращении аргументов, а тип каждого аргумен- та может быть преобразован в тип соответствующего параметра. Поиск перегруженной функции (overload resolution). Синоним подбора функции.
Глава 7. Функции 307 Продолжительность существования объекта (object lifetime). Каждый объект характери- зуется своей продолжительностью существования. Объекты, определенные внутри блока, существуют от места их определения и до конца блока. Локальные статические объекты и глобальные объекты, определенные вне функций, создаются при запуске программы и уда- ляются при завершении функции main (). Динамические объекты создаются при обращении к оператору new и существуют до тех пор, пока занимаемая ими область память не будет ос- вобождена соответствующим оператором delete. Прототип функции (function prototype). Синоним для объявления функции. В прототипе указано имя, тип возвращаемого значения и типы параметров функции. Чтобы функцию можно было вызвать, ее прототип должен быть объявлен перед точкой обращения. Рекурсивная функция (recursive function). Функция, которая способна вызвать себя не- посредственно или косвенно. Синтезируемый стандартный конструктор (synthesized default constructor). Если ни один из конструкторов класса не объявлен явно, компилятор самостоятельно создаст (синтезирует) стандартный конструктор. Этот конструктор инициализирует значением по умолчанию каждую переменную-член класса. Список инициализирующих значений конструктора (constructor initializer list). Список, используемый в конструкторе для указания исходных значений переменных-членов. Список инициализирующих значений располагается в определении конструктора между списком па- раметров и телом конструктора. Список состоит из двоеточия, сопровождаемого разделяе- мым запятой списком имен членов класса, каждое из которых сопровождается соответст- вующим исходным значением в круглых скобках. Стандартный конструктор (default constructor). Конструктор, используемый при отсутст- вии явной инициализации. Компилятор создает стандартный конструктор сам, если в классе не определен другой конструктор. Тело функции (function body). Блок операторов, в котором определены действия функции. Тип возвращаемого значения (return type). Тип значения, возвращаемого функцией. Указатель this. Неявный параметр функции-члена. Указатель this содержит адрес объ- екта, функция-член которого вызвана. Это указатель на тип класса. В константной функции- члене указатель this является указателем на константу. Функция (function). Именованный блок действий. Функция-кандидат (candidate function). Одна из функций набора, рассматриваемая при поиске соответствия вызову функции. Кандидатами считаются все функции, объявленные в области видимости обращения, имя которых совпадает с используемым в обращении.
308 Часть I. Основы
Библиотека ввода-вывода В ЭТОЙ ГЛАВЕ... 8.1. Объектно-ориентированная библиотека 310 8.2. Значения состояния потока 314 8.3. Управление буфером вывода 317 8.4. Ввод и вывод в файл 319 8.5. Строковые потоки 326 Резюме 329 Термины 329 В языке C++ операции ввода и вывода поддерживает библиотека. В библиотеке определено целое семейство классов, которые обеспечивают ввод и вывод данных на такие устройства, как файлы и консольные окна. Дополнительные классы позволя- ют строкам выступать в качестве файлов, что обеспечивает преобразование данных в символьные формы и наоборот без операции ввода и вывода. Каждый из этих клас- сов ввода-вывода (Input Output — ввод и вывод) определяет, как осуществляется чтение и запись значений встроенных типов данных. Кроме того, разработчики клас- сов, как правило, используют средства библиотеки ввода-вывода для чтения и запи- си данных в объекты создаваемых классов. Для чтения и записи данных в объекты классов, как правило, используя те же операторы и соглашения, которые библиотека ввода-вывода предоставляет для встроенных типов. В этой главе представлены основные принципы библиотеки ввода-вывода. В по- следующих главах рассматриваются дополнительные возможности: глава 14, “Пе- регрузка операторов и преобразования”, посвящена возможности создания собст- венных операторов ввода и вывода, а в приложении А, “Библиотека”(стр. 840), опи- саны способы управления форматированием и произвольный доступ к файлам. В предыдущих программах уже не раз использовались библиотечные средства ввода-вывода. Класс istream (input stream — поток ввода) обеспечивает операции ввода. Класс ostream (output stream — поток вывода) обеспечивает операции вывода. Объект cin (произносится “си-ин”) класса istream читает данные со стандарт- ного устройства ввода.
310 Часть I. Основы Объект cout (произносится “си-аут”) класса ostream стандартное устройство вывода. записывает данные на Объект cerr (произносится “си-ерр”) класса ostream записывает данные на стандартное устройство сообщений об ошибке. Объект cerr, как правило, ис- пользуется для сообщений об ошибках в программе. Оператор >> используется для чтения данных, передаваемых в объект класса istream. Оператор << используется для записи данных, передаваемых в объект класса ostream. Функция getline () получает ссылку на объект класса istream и ссылку на объект класса string, а затем читает слово из потока ввода в строку. В этой главе кратко описаны несколько дополнительных операций ввода-вывода, а также рассматривается чтение и запись данных в файлы и строки. В приложении А, “Библиотека” описаны форматированные операции ввода-вывода, поддержка произ- вольного доступа к файлам и поддержка неформатированных операций ввода- вывода. Этот вводный курс не содержит описания всей библиотеки iostream — в частности, здесь не рассматриваются системно-специфические детали реализа- ции и механизмы, которые библиотека использует для управления буфером ввода и вывода, а также способы записи собственных классов для работы с буфером. Эти темы в данной книге не рассматриваются. Основное внимание здесь уделено тем частям библиотеки ввода-вывода, которые являются наиболее полезными в обыч- ных программах. 8.1. Объектно-ориентированная библиотека До сих пор классы и объекты ввода-вывода использовались для чтения и записи данных в потоки, взаимодействующие с пользователем через окно консоли. Безус- ловно, реальные программы не могут быть ограничены вводом-выводом исключи- тельно на консоль (console)1. Программам зачастую приходится читать и записывать данные в именованные файлы. Кроме того, операции ввода-вывода можно использо- вать для оформления данных в памяти, избегая таким образом проблем и временных задержек при чтении или записи данных на диск или другое устройство во время выполнения. Приложениям зачастую приходится также читать и записывать данные на языках, требующих поддержки набора символов Unicode. Концептуально, ни вид устройства, ни используемый набор символов не влияет на выполнение операций ввода-вывода. Например, оператор > > можно использовать для чтения данных независимо от того, осуществляется ли чтение с консоли, из фай- ла на диске или строки в оперативной памяти. Аналогично, этот оператор можно ис- пользовать независимо от того, осуществляется ли чтение обычных символов, 1 Напомню, что стандартными устройствами ввода и вывода, как правило, являются, соот- ветственно, клавиатура и экран (в системе Windows окно Сеанс MS-DOS). Их комплект на- зывается консолью. Однако существуют и другие устройства, способные получать и/или вы- давать данные, например файлы на жестком или гибком диске, принтер, сканер, CD, сеть и многие др. — Примеч. ред.
Глава 8. Библиотека ввода-вывода 311 сохраняемых в переменной типа char, или символов Unicode, сохраняемых в пере- менной типа wchar_t (раздел 2.1.1, стр. 57). На первый взгляд, сложности, связанные с поддержкой или использованием всех этих разнообразных устройств и различных по размеру символов потоков, могут по- казаться непреодолимой проблемой. Чтобы упростить проблему, при определении набора объектно-ориентированных (object-oriented) классов в библиотеке использу- ется наследование (inheritance). Более подробная информация о наследовании и объ- ектно-ориентированном программировании приведена в части IV, “Объектно- ориентированное и общее программирование”, а пока достаточно знать, что связан- ные наследованием классы совместно используют общий интерфейс. Когда один класс происходит от другого (наследуется), для обоих классов (как правило) можно использовать те же операции. Другими словами, когда два класса объединены насле- дованием, говорят, что один класс наследует (inherit) поведение (интерфейс) своего предка. В языке C++ предок называется базовым классом (base class), а наследник — производным классом (derived class). Классы ввода-вывода определены в трех отдельных заголовках: в заголовке iostream определены классы, используемые для чтения и записи данных на кон- соль, в заголовке f stream определены классы, используемые для чтения и записи данных в именованный файл, а в заголовке sstream определены классы, исполь- зуемые для чтения и записи данных в строку, расположенную в оперативной памяти. Каждый из классов, определенных в заголовках f stream и sstream, является про- изводным от соответствующего класса, определенного в заголовке iostream. Спи- сок классов ввода-вывода приведен в табл. 8.1, а наследственные отношения между ним проиллюстрированы на рис. 8.1. Наследование обычно иллюстрируют анало- гично генеалогическому дереву. Верхний овал представляет базовый класс (предок). Линии соединяют базовый класс с классами, производными он него (наследниками). Так, например на этом рисунке указано, что класс istream является базовым по от- ношению к классам if stream и istringstream. Он также является базовым для класса iostream, который в свою очередь является базовым для классов istringstreamи fstream. Таблица 8.1. Типы и заголовки библиотеки ввода-вывода Заголовок Класс iostream istream — читает данные из потока ©stream — записывает данные в поток iostream — читает и записывает данные в поток. Происходит от классов istream и ©stream f stream if stream — читает данные ИЗ файла. Происходит ОТ класса istream ofstream —записывает данные в файл. Происходит от класса ©stream f stream — читает и записывает данные в файл. Происходит от класса iostream sstream istringstream — читает данные из строки. Происходит от класса istream ©stringstream —записывает данные в строку. Происходит от класса ©stream stringstream — читает и записывает данные в строку. Происходит от класса iostream
312 Часть I. Основы Рис. 8.1. Упрощенная иерархия наследования биб- лиотеки iostream (ostringstream) ^istringstream) Поскольку классы if stream и istringstream происходят от знакомого класса istream, большинство их возможностей уже известно. Каждая написанная ранее программа, в которой объекты класса istream применялись для чтения, могут быть переписаны для чтения данных из файла (при помощи класса if stream) или стро- ки (при помощи класса istringstream). Точно так же, в программах, выводящих данные, можно использовать классы of stream или ostringstream вместо класса ostream. Кроме классов istream и ostream, в заголовке iostream определен также класс iostream. Хотя в приведенных ранее программах сам класс iostream фактически не использовался, информации о его применении было приведено дос- таточно. Класс iostream является производным от классов istream и ostream. Из этого следует, что объект класса iostream совместно использует интерфейс обоих своих родительских классов. То есть объект класса iostream можно исполь- зовать как для ввода, так и для вывода данных в тот же поток. В библиотеке опреде- лены также еще два класса, производных от класса iostream. Эти классы приме- няются для чтения или записи данных в файл или строку. Применение наследования для классов ввода-вывода имеет и другое немаловаж- ное последствие: как будет продемонстрировано в главе 15, “Объектно-ориентиро- ванное программирование”, при наличии функции, получающей ссылку на объект базового класса, ей можно передать объект производного класса. Это означает, что функция, написанная для работы с параметром типа istream&, может быть вызвана для объектов класса if stream или istringstream. Аналогично, функция с пара- метром типа ostream& может быть вызвана для объектов класса of st ream или ostringstream. Поскольку классы ввода-вывода связаны наследованием, можно создать одну функцию и применять ее ко всем трем видам потоков: консоли, файлам на диске или строковым потокам. Поддержка национальных наборов символов Описанные до сих пор потоковые классы применялись для чтения и записи в по- ток данных типа char. В библиотеке определен также набор соответствующих клас- сов для типа wchar_t. Каждый такой класс отличается от его аналога для типа char префиксом w. Таким образом, классы wostream, wistream и wiostream приме-
Глава 8. Библиотека ввода-вывода 313 няются для чтения и записи данных на консоль. Классами для ввода и вывода дан- ных в файл являются wif stream, wof stream и wf stream, а версиями ввода и вывода для потока строк Unicode (тип wchar_t) — классы wistringstream, wostringstream и wstringstream. В библиотеке определены также классы объек- тов для чтения и записи символов Unicode со стандартных устройств ввода и выво- да. Имена этих объектов также отличаются от аналога для типа char префиксом w: объект wcin применяется для ввода со стандартного устройства данных типа wchar_t, объект wcout — для вывода, а объект wcerr — для сообщений об ошибках. В каждом из заголовков ввода-вывода определены классы и объекты ввода- вывода на стандартные устройства как для типа char, так и для типа wchar_t. По- токовые классы для типа wchar_t определены в заголовке iostream, потоковые классы для файлов Unicode — в заголовке fstream, а класс stringstream для символов Unicode — в заголовке sstream. Объекты ввода-вывода не допускают копирования и присвоения По причинам, которые станут более очевидными впоследствии, при рассмот- рении классов и наследования в частях III, “Абстракция, классы и данные”, и IV, “Объектно-ориентированное и общее программирование”, библиотечные типы не допускают копирования и присвоения. ofstream outl, out2; outl = out2; // ошибка: потоковые объекты нельзя присваивать // функция print(): копирование параметра ofstream print(ofstream); out2 = print(out2); // ошибка: нельзя копировать потоковые объекты Это требование имеет два очень важных последствия. Как будет продемонстри- ровано в главе 9, “Последовательные контейнеры”, в векторах и контейнерах других типов могут быть сохранены элементы только таких типов (классов), которые до- пускают копирование. Поскольку потоковые объекты копировать нельзя, невозмож- но создать вектор (или любой другой контейнер), который способен содержать объ- екты потоковых классов. Вторым последствием является то, что невозможно создать параметр или полу- чить возвращаемое значение, типом которых будет один из потоковых классов. Если необходимо передать или возвратить объект ввода-вывода, его следует передавать или возвращать как указатель или ссылку. ofstream &print(ofstream&); // ok: передать ссылку, а не // копию объекта while (print(out2)) { /* ... */ } // ok: передать ссылку на out2 Как правило, поток передают в качестве неконстантной ссылки, поскольку пред- полагается, что объект ввода-вывода будет использован для чтения или записи. Чте- ние или запись в объект ввода-вывода изменяет его состояние, поэтому ссылка должна быть неконстантной. Упражнения раздела 8.1 Упражнение 8.1. С учетом того, что os является объектом класса ofstream, что делает сле- дующая программа? os << "Goodbye!" << endl;
314 Часть I. Основы Что она делает, если os является объектом класса os t ringstream или класса if stream? Упражнение 8.2. Следующее объявление ошибочно. Выявите и устраните проблему, ©stream print(©stream os); 8.2. Значения состояния потока Прежде чем перейти к описанию классов, определенных в заголовках f stream и sstream, необходимо рассмотреть механизм, при помощи которого библиотека вво- да-вывода управляет буфером и состоянием потока. Имейте в виду, что материал, рассматриваемый в этом и следующем разделах, одинаково применим к обычным, файловым и строковым потокам. В связи с наследованием классов ввода-вывода, возможно возникновение оши- бок. Некоторые из ошибок исправимы, другие происходят глубоко внутри системы и не могут быть исправлены в области видимости программы. Библиотека ввода- вывода манипулирует набором флагов состояния (condition state), которые указы- вают, находится ли данный объект ввода-вывода в пригодном для использования со- стоянии или произошла ошибка определенного вида. В библиотеке определен также набор перечисленных в табл. 8.2 функций и флагов, которые предоставляют доступ и позволяют манипулировать состоянием каждого потока. В качестве примера ошибки ввода-вывода рассмотрим следующий код. cin » ival; Если со стандартного устройства ввода ввести, например, слово Borges, после неудачной попытки прочитать строку символов как значение типа int, объект cin перейдет в состояние ошибки. Точно так же, объект cin окажется в состоянии ошибки, если ввести символ конца файла. Если бы было введено значение 1024, чтение оказалось бы успешным, а объект cin остался бы в допустимом состоянии. Чтобы поток мог быть использован для ввода или вывода, он не должен нахо- диться в состоянии ошибки. Самый простой способ проверить, находится ли поток в работоспособном состоянии, подразумевает проверку истинности его значения, if (cin) // ок: чтобы использовать объект cin, он должен находиться в // допустимом состоянии while (cin » word) // ok: операция чтения успешна ... Оператор if проверяет состояние потока непосредственно, а оператор while косвенно, т.е. он проверяет поток, возвращаемый из выражения в условии. Если опе- рация ввода успешна, ее результат расценивается как значение true. Таблица 8.2. Флаги состояния библиотеки ввода-вывода st rm-.: iostate Имя машинно-зависимого целочисленного типа, определенного каждым клас- сом iostream, использующимся при определении состояния потока strm: :badbit Значение флага strm: iostate указывает, что поток недопустим strm:: failbit Значение флага strm:: iostate указывает, что операция ввода-вывода за- кончилась неудачей
Глава 8. Библиотека ввода-вывода 315 Окончание табл. 8.2 strm::iostate strm: :eofbit s.eof () s.fail () s. bad () s.good() s.clear() s.clear(флаг) Имя машинно-зависимого целочисленного типа, определенного каждым клас- сом iostream, использующимся при определении состояния потока Значение флага strm:: iostate указывает, что поток достиг конца файла Возвращает значение true, если для потока s установлен флаг eofbit Возвращает значение true, если для потока s установлен флаг failbit Возвращает значение true, если для потока s установлен флаг badbit Возвращает значение true, если поток s находится в допустимом состоянии Возвращает все флаги потока s в допустимое состояние Устанавливает определенный флаг (флаги) потока s в допустимое состояние. Флаг имеет ТИП strm: : iostate s. setstate (флаг) Добавляет в поток s определенный флаг. Флаг имеет тип strm:: iostate s.rdstate () Возвращает текущее состояние потока s как значение типа strm:: iostate Значения флагов состояния В большинстве случаев достаточно знать только то, является ли поток допусти- мым. Но иногда требуется более подробное управление состоянием потока. То есть кроме информации о том, находится ли поток в состоянии ошибки, может понадо- биться выяснить, какая именно ошибка произошла. Например, может понадобиться различать случаи, когда достигнут конец файла и когда произошла ошибка устрой- ства ввода-вывода. Каждый потоковый объект обладает переменной-членом, содержащим состояние потока. Манипулировать этим значением можно при помощи функций set state () и clear (). Эта переменная-член имеет тип iostate, который представляет собой машинно-зависимый целочисленным тип, определенный каждым классом библио- теки iostream. Он используется как коллекция битов, аналогично переменной int_quizl, использованной для хранения результатов зачета в примере разде- ла 5.3.1 (стр. 181). В каждом классе ввода-вывода определено также три константных значения типа iostate, которые представляют специальные битовые схемы. Эти константные значения используются для указания специальных видов флагов ввода-вывода. Для проверки или установки нескольких флагов в одной операции, они применяются с побитовыми операторами (раздел 5.3, стр. 179). Значение флага badbit означает отказ на системном уровне, такой, например, как неустранимая ошибка чтения или записи. После такой ошибки использовать по- ток, как правило, уже невозможно. Значение флага failbit устанавливается после устранимой ошибки, такой, например, как чтение символа, когда ожидалось число- вое значение. Зачастую исправить проблему, в результате которой было установлено значение флага failbit, вполне возможно. Значение флага eofbit устанавливает- ся в случае, когда достигнут конец файла. Достижение конца файла приводит также к установке флага failbit. Состояние потока можно выяснить при помощи функций bad (),fail(),eof() и good (). Если хотя бы одна из функций bad (), fail () или eof () возвращает
316 Часть I. Основы для потока значение true, это означает, что поток находится в состоянии ошибки. Функция good () возвращает значение true только в том случае, когда ни одна из других функций не возвращает значение true. Функции clear () и setstateO изменяют значение состояния. Функция clear () возвращает все значения флагов потока в допустимое состояние. Вызов этой функции осуществляют после того, как произошедшая ошибка была исправле- на и поток необходимо возвратить в допустимое состояние. Функция setstate () позволяет установить флаг, указывающий на возникшую проблему. Эта функ- ция изменяет значение лишь указанного флага, оставляя другие флаги состояния неизменными. Опрос и контроль состояния потока Манипулировать операциями ввода можно следующим образом. int ival; // читать из объекта cin и проверять только на достижение конца // файла; цикл продолжается, даже если произойдет любой из II 10 других отказов while (cin >> ival, Icin.eofO) { if (cin.bad()) // поток ввода нарушен; выход throw runtime_error("10 stream corrupted"); if (cin.fail()) { // недопустимый ввод cerr << "bad data, try again"; // предупредить пользователя cin.clear (istream::failbit); // восстановить поток continue; // принять следующий ввод } // ok: обработать ival } Этот цикл читает данные из объекта cin до тех пор, пока не будет достигнут ко- нец файла или не произойдет неисправимая ошибка чтения. В условии использован оператор запятой (раздел 5.9, стр. 192). Напомним, что оператор запятой обрабаты- вает каждый операнд и возвращает в качестве результата свой правый операнд. Сле- довательно, в условии происходит чтение из объекта cin, а результат игнорируется. Проверяется в условии результат выражения ! cin. eof (). Когда объект cin дости- гает конца файла, условие становится ложным и цикл while завершается. Если объект cin не достиг конца файла, цикл продолжается независимо от ошибки, которая мо- жет произойти при чтении. Сначала внутри цикла проверяется, не нарушен ли поток. Если это так, осущест- вляется выход из цикла с передачей исключения (раздел 6.13, стр. 241). Если введе- ны недопустимые данные, на экран выводится предупреждение, а флаг состояния failbit восстанавливается. В данном случае для возврата к началу цикла while и чтения следующего значения в переменной ival, используется оператор continue (раздел 6.11, стр. 239). Если никаких ошибок не произошло, в остальной части тела цикла переменную ival можно использовать безопасно. Доступ к состоянию условия Функция-член rdstate () возвращает значение типа iostate, которое соответ- ствует состоянию текущего потока. // запомнить текущее состояние объекта cin istream::iostate old_state = cin.rdstate();
Глава 8. Библиотека ввода-вывода 317 cin.clear() ; process_input(); cin.clear(old_state) // использовать объект cin // вернуть объект: cin в прежнее состояние Работа с несколькими флагами состояния Зачастую необходимо устанавливать или сбрасывать значения нескольких битов состояния. Это можно сделать при помощи нескольких обращений к функции setstate () или clear (). В качестве альтернативы, для создания значения, изме- няющего два или несколько битов состояния в одном обращении, можно использо- вать побитовый оператор OR (раздел 5.3, стр. 179). Побитовый OR позволяет соз- дать целочисленное значение, используя битовые схемы его операндов. Бит резуль- тата будет иметь значение 1, если соответствующий бит в любом из операндов побитового оператора OR имеет значение 1. Рассмотрим пример. // устанавливает флаги badbit и failbit is.setstate(ifstream::badbit I ifstream::failbit); Здесь у объекта is устанавливаются флаги failbit и badbit. Аргумент is. badbit | is. failbit создает значение, в котором биты, соответствующие флагам badbit и failbit, установлены (т.е. оба они имеют значение 1). Все ос- тальные биты имеют значение нуль. В обращении к функции setstate () это зна- чение используется для установки битов, соответствующих флагам badbit и failbit в переменной-члене состояния потока. Упражнения раздела 8.2 Упражнение 8.3. Напишите функцию, получающую и возвращающую ссылку на объект класса istream. Функция должна читать данные из потока до тех пор, пока не будет достигнут конец файла. Функция должна выводить прочитанные данные на стандартное устройство вывода. Перед возвращением потока, верните все значения его флагов в допустимое состояние. Упражнение 8.4. Проверьте созданную функцию, передав ей при вызове объект cin в качестве аргумента. Упражнение 8.5. В каких случаях завершится следующий цикл while? while (cin >> i) /* ... */ 8.3. Управление буфером вывода Каждый объект ввода-вывода управляет буфером, используемым для хранения данных, которые программа читает или записывает. В следующем операторе лите- ральная строка сохраняется в буфер, связанный с потоком os. os << "please enter a value: Сброс буфера, т.е. фактическая запись на соответствующее устройство вывода или в файл, происходит в следующих случаях. 1. Программа завершается нормально. Все буферы вывода освобождаются при вы- ходе из функции main (). 2. В некий случайный момент времени буфер может оказаться заполнен. В этом случае перед записью следующего значения происходит сброс буфера.
318 Часть I. Основы 3. Сброс буфера можно осуществить явно, использовав такой манипулятор как endl (раздел 1.2.2, стр. 30). 4. Используя манипулятор unitbuf можно установить такое внутреннее состоя- ние потока, чтобы буфер освобождался после каждой операции вывода. 5. Поток вывода можно связать с потоком ввода, тогда буфер вывода будет сбрасы- ваться всякий раз, когда связанный с ним поток ввода читает данные. Сброс буфера вывода В приведенных ранее программах уже не раз использовался манипулятор endl, который записывает символ новой строки и сбрасывает буфер. Существует еще два подобных манипулятора. Первый, flush, используется довольно часто. Он сбрасы- вает буфер потока, но никаких символов в вывод не добавляет. Второй, ends, ис- пользуется существенно реже. Он добавляет в буфер нулевой символ, а затем сбра- сывает его. cout cout cout " hi! " " hi! " " hi! " flush; ends; endl ; // сброс буфера без добавления данных II добавить нулевой символ и сбросить буфер II добавить новую строку и сбросить буфер Манипулятор unitbuf Если сброс необходим при каждом выводе, лучше использовать манипулятор unitbuf. Этот манипулятор сбрасывает буфер потока после каждой записи. cout << unitbuf << "first" << " second" << nounitbuf; Это эквивалентно следующей записи. cout << "first" << flush << " second" << flush; Манипулятор nounitbuf восстанавливает нормальное системное управление сбросом буфера потока. Внимание! При сбое программы буфер не сбрасывается Буфер вывода не сбрасывается, если программа завершается аварийно. При попытке отладить аварийно завершающуюся программу, зачастую используют последний вы- вод, чтобы выявить область программы, в которой произошла ошибка. То есть если аварийный отказ произошел после определенного оператора вывода, можно предпола- гать, что ошибка находится после этой точки в программе. При отладке программы необходимо гарантировать, что любой подозрительный вывод будет сброшен сразу. Поскольку при сбое программы система не сбрасывает буфер ав- томатически, существует вероятность того, что последний сделанный вывод на стан- дартном устройстве не будет отображен, а просто останется в буфере вывода Если последний вывод используется при поиске ошибки, необходимо обеспечить его гарантированное отображение. Для этого все операции вывода должны явно завер- шаться манипулятором flush или endl. Протраммист может впустило потратить множество часов на отслеживание вовсе не того кода только потому, что фактически последний буфер вывода просто не сбрасывается. Поэтому при записи вывода имеет смысл использовать манипулятор endl, а не управ- ляющую последовательность \п. Используя .манипулятор endl, можно не задаваться вопросом, будет ли сброшен буфер при аварийном завершении программы или нет.
Глава 8. Библиотека ввода-вывода 319 Связывание потоков ввода и вывода Когда поток ввода связан с потоком вывода, любая попытка чтения данных из по- тока ввода приведет к предварительному сбросу буфера, связанного с потоком вывода. Библиотечные объекты cout и cin уже связаны, поэтому оператор cin >> ival; заставит сбросить буфер, связанный с объектом cout. В интерактивных системах потоки ввода и вывода обычно связаны. При выполнении про- Lg I граммы это гарантирует, что приглашения к вводу будут отображены до того, как система перейдет к ожиданию ввода пользователем данных. Функцией tie () обладают объекты как класса istream, так и ostream. Ей пе- редают указатель на объект класса ostream, поток которого следует связать с объ- ектом, от имени которого вызвана функция tie (). Когда поток привязан к объекту класса ostream, любая операция ввода-вывода в поток, для которого вызвана функция tie (), приводит к сбросу буфера объекта, переданного функции t i е () в качестве аргумента. cin.tie(&cout); // только для демонстрации: библиотека // автоматически связывает объекты cin и cout ostream *old_tie - cin.tie(); cin.tie(O); // разрыв связи с объектом cout // буфер cout больше не сбрасывается при // чтении из объекта cin cin.tie(&cerr); // связывание объектов cin и cerr не обязательно, II но это хорошая идея! сin.tie(0); // разрыв связи между объектами cin и cerr cin.tie(old_tie); // восстановление обычной связи между // объектами cin и cout Объект класса ostream может быть привязан только к одному объекту класса istream. Чтобы разорвать существующую связь, достаточно передать в качестве аргумента значение 0. 8.4. Ввод и вывод в файл В заголовке f stream определены три класса, поддерживающие операции ввода и вывода в файл. 1. Класс if stream, производный от класса istream, читает данные из файла. 2. Класс ofstream, производный от класса ostream, записывает данные в файл. 3. Класс f stream, производный от класса iostream, читает и записывает данные в тот же файл. Поскольку эти классы являются производными от соответствующих классов за- головка iostream, большая часть их возможностей уже известна. В частности, для ввода-вывода данных в файл можно использовать операторы << и >>, а изложенный в предыдущих разделах материал о флагах состояния вполне применим и к объектам заголовка f stream. Кроме поведения, унаследованного от классов заголовка f stream, здесь опреде- лены также две новые функции open () и close (), а также конструктор, получаю-
320 Часть I. Основы щий имя файла, который следует открыть. Эти функции применимы только к объек- там классов f stream, if stream и of stream, но не к другим классам ввода-вывода. 8. 4.1. Использование объектов файловых потоков До сих пор в программах использовались объекты cin, cout и сегг, определен- ные в библиотеке. Когда данные необходимо читать или записывать в файл, для это- го следует определить собственные объекты чтения-записи и связать их с необходи- мыми файлами. Предположим, что строки ifile и ofile содержат имена файлов, используемые при чтении и записи данных. Чтобы определить и открыть два объек- та класса f stream, можно применить следующий код. // создать объект класса ifstream и связать, его с файлом, // имя которого хранится в переменной ifile ifstream infile(ifile.c_str()); // создать объект класса ofstream для выходного файла, / / имя которого содержит переменная ofile ofstream outfile(ofile.c_str()); Объект infile — это поток, который можно использовать для чтения, а объект outf ile — это поток, в который можно осуществлять запись. При инициализации, объектам классов ifstream или ofstream передают имена файлов. В результате соответствующие файлы будут открыты. ifstream infile; // несвязанный поток файла для ввода ofstream outfile; // несвязанный поток файла для вывода Здесь определен потоковый объект infile, который позволит читать данные из файла, и объект outf ile, который можно использовать для записи данных в файл. Но ни один из этих объектов с файлом пока не связан. Прежде чем использовать объект класса f stream, его следует связать с файлом. infile.open("in") ; outfile.open("out"); // открыть файл по // paсположенный в // открыть файл по // расположенный в имени "in", текущем каталоге имени "out", текущем каталоге Чтобы связать уже существующий объект класса f stream с определенным фай- лом, применяется его функция-член open (). Функция open () осуществляет все системно-зависимые операции, необходимые для поиска указанного файла и его от- крытия для чтения или записи. Внимание! Имена фа» лов в языке С++ По историческим причинам, для указания имен файлов в библиотеке ввода-вывода используются символьные строки в стиле С (раздел 4.3, стр. 154), а не строки С ++. Когда при вызове функции ореп() или при создании и инициализации объекта f stream используется имя файла, передаваемый аргумент должен быть строкой в стиле С, а не объектом библиотечного класса string. Зачастую программы получают имена файлов, считывая их со стандартного устройства ввода. Как правило, читать имеет смысл в объект класса string, а не в массив символов стиля С. Поскольку имя файла находится в объекте класса string, перед применением его необходимо преобразовать в строку стиля С при помощи функции-члена c_str () (раздел 4.3.2, стр. 164).
Глава 8. Библиотека ввода-вывода 321 Проверка успешности открытия файла Открыв файл, имеет смысл удостовериться, был ли он открыт успешно. // проверить успешность открытия файла if (!infile) { сегг << "error: unable to open input << ifile « endl; return -1; } f ile: II Здесь условие подобно использованному при проверке объекта cin на достиже- ние конца файла или возникновение некой другой ошибки. При проверке потоково- го объекта ввода или вывода результат должен быть положительным. В случае сбоя функции open () объект класса f stream окажется не готов для выполнения опера- ций ввода-вывода. Значение true, полученное при проверке объекта, свидетельст- вует, что файл можно использовать. if (outfile) // объект outfile готов к применению? Поскольку сообщение об ошибке следует выдать в случае, когда файл не го- тов к применению, возвращаемое при проверке потока значение необходимо ин- вертировать. if (loutfile) // объект outfile не готов к применению? Переназначение файлового потока новому файлу Как только поток объекта класса f stream будет открыт, он остается связанным с указанным файлом. Чтобы связать объекта класса f stream с другим файлом, необ- ходимо сначала закрыть существующий файл, а затем снова открыть другой файл. ifstream infile("in"); infile.close() ; infile.open("next") ; // открыть файл // закрыть файл // открыть файл "in" для чтения "in " "next" для чтения Необходимо закрывать файловый поток перед попыткой открыть новый файл. Функция open () проверяет, не открыт ли уже данный поток. Если поток уже от- крыт, она переводит его в состояние отказа. Поэтому последующие попытки исполь- зовать данный файловый поток закончатся неудачей. Восстановление состояния файлового потока Давайте рассмотрим программу, в которой вектор содержит имена файлов. Эти файлы следует открыть, прочитать содержимое и некоторым образом обработать слова, хранимые в каждом из файлов. С учетом того, что вектор имеет имя files, такая программа могли бы иметь следующий цикл while. // для каждого указанного в векторе файла while (it != files.end()) { ifstream input(it->c_str()); // открыть файл // если файл открыт удачно, "обработать" содержимое if (!input) break; // ошибка: выйти! while(input >> s) // проделать работу с файлом process(s); + + it; // инкремент итератора для доступа к следующему файлу
322 Часть I. Основы Каждая итерация цикла создает объект input класса if stream, открывающий для чтения файл, имя которого указано в текущем элементе вектора. Для получения инициализирующего значения, в конструкторе использован оператор стрелки (раздел 5.6, стр. 189), который позволяет обратиться к объекту it класса string и вызвать его функцию-член c_str (). Если конструктор открыл файл успешно, дан- ные из него считываются до тех пор, пока не будет достигнут конец файла или не произойдет ошибка. После этого объект input оказывается в состоянии ошибки. Любая дальнейшая попытка чтения с использованием объекта input потерпит не- удачу. Поскольку объект input локален для цикл while, он создается при каждой итерации. Это значит, что он начинает каждую итерацию в допустимом состоянии, т.е. функция input. good () возвращает значение true. Если необходимо избежать создания нового потокового объекта при каждой ите- рации цикла while, определение объекта input можно вынести из цикла. Это про- стое изменение означает, что теперь управлять состоянием потока придется более внимательно. Когда встречается конец файла или любая другая ошибка, внутреннее состояние потока становится таким, которое не допускает дальнейшего чтения или записи. Закрытие потока не изменит внутреннего состояния потокового объекта. Если последняя операция чтения или записи завершается неудачей, объект оказыва- ется в состоянии отказа до тех пор, пока вызов функции clear () не восстановит работоспособное состояние потока. После вызова функции clear () объект как буд- то создается заново. Если существующий потоковый объект необходимо использовать многократно, цикл while должен содержать функции close () и clear (), возвращающие поток в работоспособное состояние после каждой итерации. ifstream input; vector<string>::const_iterator it = files.begin() ; // для каждого указанного в векторе файла while (it != files.end()) { input.open(it->c_str()); // открыть файл // если файл открыт удачно, "обработать" содержимое if (!input) break; // ошибка: выйти! while (input >> s) // проделать работу с файлом process(s); input.close(); // закрывает файл, когда работа закончена input.clear(); // восстановление состояния ++it; // инкремент итератора для доступа к следующему файлу ) Если бы обращение к функции clear () отсутствовало, этот цикл прочитал бы лишь первый файл. Чтобы выяснить причину, давайте рассмотрим, что происходит в данном цикле. Сначала открывается указанный файл. Если файл открыт успешно, чтение данных осуществляется до тех пор, пока не встретится конец файла или не произойдет ошибка. После этого объект input оказывается в состоянии ошибки. Если вызвать для потока функцию close О но не вызвать функцию clear (), лю- бая последующая операция ввода с использованием объекта input потерпит неуда- чу. Закрытый файл можно открыть снова. Однако попытка чтения из объекта input внутри цикла while потерпит неудачу, ведь при последней операции чтения из это- го потока встретился конец файла. Тот факт, что это был конец другого файла, со- вершенно несущественен!
Глава 8. Библиотека ввода-вывода 323 Если файловый поток многократно используется для чтения или записи нескольких файлов, перед следующим применением для потока необходимо вызвать функцию clear(). Упражнения раздела 8.4.1 Упражнение 8.6. Поскольку класс ifstream происходит от класса istream, объект класса ifstream можно передать как ссылку функции, ожидающей объект класса istream. Исполь- зуйте функцию, написанную для первого упражнения в разделе 8.2 (стр. 317), для чтения из име- нованного файла. Упражнение 8.7. Две написанные в этом разделе программы использовали оператор break для выхода из цикла while, если указанный в векторе файл был открыт неудачно. Перепишите эти два цикла так, чтобы при невозможности открыть файл отображалось соответствующее сообще- ние, а обработка продолжалась с получения из вектора следующего имени файла. Упражнение 8.8. Программы в предыдущем упражнении могут быть написаны и без использова- ния оператора continue. Напишите программу с оператором continue и без него. Упражнение 8.9. Напишите функцию, которая открывает файл и читает его содержимое в вектор строк, сохраняя каждую строку как отдельный элемент вектора. Упражнение 8.10. Перепишите предыдущую программу так, чтобы каждое слово сохраня- лось в отдельном элементе. 8. 4.2. Режимы файла Каждый раз, когда открывается файл, либо при обращении к функции open (), либо при инициализации потока именем файла, необходимо указать режим файла (file mode). Для каждого класса заголовка f stream определен набор значений, соот- ветствующих различным режимам, в которых поток может быть открыт. Подобно флагам состояния, режимы файла представляют собой целочисленные константы, которые можно использовать с побитовыми операторами (раздел 5.3, стр. 179) для установки одного или нескольких режимов при открытии конкретного файла. Кон- структоры файлового потока и функция open () имеют заданный по умолчанию аргумент (раздел 7.4.1, стр. 278), устанавливающий режим файла. Это значение по умолчанию варьируется в зависимости от типа потока. В качестве альтернативы, режим файла можно указать явно. Список режимов файла и их значений приведен в табл. 8.3. Таблица 8.3. Режимы файла in out арр ate trunc binary Открывает файл для ввода Открывает файл для вывода Переходит в конец файла перед каждой записью Переходит в конец файла непосредственно после открытия Усекает существующий поток при открытии Осуществляет операции ввода-вывода в бинарном режиме
324 Часть I. Основы Режимы out, trunc и арр могут быть определены только для файлов, связанных с объектами класса ofstream или f stream, а режим in может быть определен только для файлов, связанных с объектами класса if stream или f stream. В режиме ate или binary может быть открыт любой файл. Режим ate осуществляет переход в конец фай- ла только при открытии. Поток, открытый в режиме binary, рассматривает файл как последовательность байтов, символы в потоке при этом никак не интерпретируются. По умолчанию файлы, связанные с объектом класса if stream, открываются в режиме in, который позволяет читать данные из файла. Файлы, связанные с объ- ектом класса ofstream, открываются в режиме out, который разрешает записывать данные в файл. Файл, открытый в режиме out, усекается, т.е. все хранимые в файле данные уничтожаются. Фактически указание режима out для объекта класса ofstream эквивалентно указа- нию режимов out и trunc. 4^ Единственный способ сохранить существующие данные в файле, открытом для объекта класса ofstream, заключается в явном указании режима арр. // режим вывода по умолчанию; усечение файла по имени "filel" ofstream outfile("filel"); // эквивалентное действие: файл "filel" усечен явно ofstream outfile2("filel", ofstream::out I ofstream::trunc); // режим арр; новые данные добавляются в конец существующего // файла по имени "file2" ofstream appfile("file2", ofstream::app); В определении объекта outfile2 используется побитовый оператор OR (раз- дел 5.3, стр. 179), чтобы открыть файл и в режиме out, и в режиме trunc. Использование того же файла для ввода и вывода Объект класса f stream может одновременно читать и записывать данные в свя- занный файл. То, как именно объект класса f stream использует свой файл, зависит от режима, указанного при открытии файла. По умолчанию объект класса f stream открывается в режимах in и out. Такой файл не усекается. Если файл, связанный с объектом класса fstream, открыть только в режиме out, без режима in, файл будет усечен. Файл усекается в случае, если указан режим trunc, независимо от того, указан ли режим in. В приведенном ниже определении открывается файл copyOut в обоих режимах, ввода и вывода. / / открыть для ввода и вывода fstream inOut("copyOut", fstream::infstream::out); Использование файла, открытого для ввода и вывода, описано в приложении А, “Библиотека”, (раздел 870, стр. А.3.8). Режим — это атрибут файла, а не потока Режим устанавливается каждый раз, когда открывается файл, ofstream outfile; // установлен режим вывода out, файл "scratchpad" усекается
Глава 8. Библиотека ввода-вывода 325 outfile.open("scratchpad", ofstream::out); outfile.close(); // файл закрыт, его можно связать снова // открыть файл "precious" в режиме арр out file.open("precious", ofstream::app); outfile.close(); // режим вывода по умолчанию, файл "out" усекается out file.open("out"); При первом вызове функции open () задан режим ofstream: : out. В результа- те файл по имени scratchpad в текущем каталоге будет открыт в режиме вывода и усечен. Файл precious открывается в режиме добавления. Все находящиеся в нем данные остаются, а новые записи добавляются в конец. При открытии файла out режим вывода не был указан явно. В результате файл будет открыт в режиме out, а находившиеся в нем ранее данные будут уничтожены. При каждом вызове функции open (), режим для файла устанавливается, либо явно, ли- бо неявно. Если режим не указан явно, используется значение по умолчанию. Допустимые комбинации режимов открытого файла Не все режимы могут быть указаны одновременно. Некоторые из их комбина- ций бессмысленны, например in и trunc. В результате файл будет открыт для чтения, но находившиеся в нем данные предварительно усечены, т.е. уничтожены. В результате читать будет нечего. Списков допустимых комбинаций режимов при- веден в табл. 8.4. Таблица 8.4. Комбинации режимов открытого файла out Файл открыт для вывода; все прежние данные удалены out | арр файл открыт для вывода; все новые записи добавляются в конец out | trunc Аналогично режиму out in файл открыт для ввода in | out файл открыт для ввода и вывода; позиционирование для чтения на начало файла in | out | trunc файл открыт для ввода и вывода; все прежние данные удалены Любая из этих комбинаций может также содержать режим ate. Добавление режима ate в любую из этих комбинаций осуществляет переход к концу файла перед первой операцией ввода или вывода. Режим ate изменяет только начальную позицию файла. 8. 4.3. Программа, открывающая и проверяющая файл Поскольку в программах этой книги еще не раз понадобится открывать файл для ввода, давайте напишем функцию по имени open_file, которая и реализует это действие. Функция будет получать ссылки на объекты класса if stream и string. Строка будет содержать имя файла, который предстоит связать с передаваемым объ- ектом класса if stream.
326 Часть I. Основы // открыть и связать с указанным файлом ifstream& open_file(ifstream &in, const string &file) { in.close(); // закрыть, на случай если ранее уже был открыт in.clear(); // восстановить состояние после любых ошибок // при сбое открытия поток окажется в недопустимом состоянии in.open(file.c_str()); // открыть указанный файл return in; // состояние допустимо, если файл открыт успешно Поскольку неизвестно, в каком состоянии находится поток, сначала следует вы- звать функции close () и clear (), которые приведут поток в допустимое состоя- ние. Затем предпринимается попытка открыть указанный файл. Если при попытке открыть файл произойдет сбой, состояние потока укажет, что он непригоден для ис- пользования. И наконец, функция возвращает поток, который либо связан с указан- ным файлом и готов к применению, либо находится в состоянии ошибки. Упражнения раздела 8.4.3 Упражнение 8.11. Объясните, почему в функции open_file О происходит вызов функции clear () перед вызовом функции open (). Что произошло бы без этого обращения? Что про- изошло бы, если бы вызов функции clear () располагался после вызова функции open () ? Упражнение 8.12. Объясните, каков был бы результат, если бы функция open_f ile () в про- грамме не сумела выполнить функцию close (). Упражнение8.13. Напишите функцию, подобную функции open_file(), которая открывает файл для вывода. Упражнение 8.14. Используйте функцию open_f ile () в программе, написанной для первого упражнения в разделе 8.2 (стр. 317), чтобы открыть указанный файл и прочитать его содержимое. 8.5. Строковые потоки Библиотека iostream поддерживает операции ввода-вывода в область опера- тивной памяти, т.е. поток может быть связан с объектом класса string, располо- женным в оперативной памяти. В эту строку можно записывать данные и читать их, используя операторы ввода и вывода библиотеки iostream. В библиотеке опреде- лены три следующих вида строковых потоков. Класс istringstream, производный от класса istream, читает данные из строки. Класс ostringstream, производный от класса ostream, записывает дан- ные в строку. Класс stringstream, производный от класса iostream, читает и записывает данные в строку. Чтобы воспользоваться любым из этих классов, необходимо подключить заголо- вок sstream. Поскольку, подобно классам заголовка f stream, эти классы так же происходят от классов заголовка iostream, к ним применимы все те же операции, что и к клас- сам заголовка iostream. Но кроме унаследованных возможностей, классы заголовка
Глава 8. Библиотека ввода-вывода 327 sstream имеют конструктор, которому передают объект класса string. Конструк- тор копирует аргумент класса string в объект класса stringstream. Операции чтения и записи в объект класса stringstream приводят к чтению и записи в объ- ект класса string. В этих классах определена также переменная-член по имени str, позволяющая получить или установить значение строки, которой манипулиру- ет объект класса string stream. Обратите внимание, хотя классы заголовков f stream и sstream имеют общий базовый класс, никакой другой взаимосвязи между ними нет. В частности, к объекту класса stringstream нельзя применить функции open () и close (), а объекты класса f stream не имеют переменной-члена str. Таблица 8.5. Операции, специфические для класса stringstream stringstream strm; stringstream strm(s); strm.str() strm.str(s) Создает несвязанный объект класса stringstream Создает объект класса stringstream, содержащий копию строки s Возвращает копию строки, которую хранит объект strm Копирует строку s в объект strm. Возвращает тип void Использование класса stringstream Ранее в программах уже не раз приходилось осуществлять ввод слов или целых строк. В первом случае использовался оператор ввода класса string, а во втором — функция getline (). Однако в программах зачастую необходимы обе возможности: обработка по строкам и обработка по отдельному слову внутри каждой строки. При- менение класса stringstream позволяет реализовать это следующим образом, string line, word; // объекты для хранения введенных строк и слов while (getline(cin, line)) { // читать введенные строки в // переменную line // осуществить построчную обработку istringstream stream(1ine); // связать поток с читаемой строкой while (stream >> word) { // читать слова из строки // осуществить обработку по одному слову } ) Здесь для ввода всей строки используется функция getline (). Чтобы получать доступ к каждому ее слову, объект класса istringstream связывается с прочитан- ной строкой. Теперь для чтения слов из каждой строки можно использовать обыч- ный оператор ввода. Класс string stream поддерживает преобразование и форматирование Одним из наиболее популярных случаев применения класса stringstream яв- ляется автоматическое преобразование некоторых типов данных. Может, например, возникнуть необходимость представить коллекцию числовых значений в виде строк или наоборот. Операции ввода и вывода классов заголовка sstream автоматически преобразуют арифметический тип в соответствующее ему строковое представление и наоборот.
328 Часть I. Основы int vail = 512, val2 = 1024; ostringstream formatjiessage; // ok: преобразует значения в строковое представление format_message « "vail: " << vail << "\n" << "val2: " << val2 << "\n"; Здесь создается пустой объект класса ostringstream по имени format_ message и в него помещается необходимый текст. Но важнее всего то, что значения типа int автоматически преобразуются в их строковые эквиваленты. Таким обра- зом, объект f ormat_message содержит следующие символы. vail: 512\nval2: 1024 Используя объект класса istringstream можно получать числовое значение из прочитанной строки. Символьное представление числового значения, прочитанное при помощи объекта класса istringstream, автоматически преобразуется в соот- ветствующее арифметическое значение. // переменная-член str получает строку, связанную с /I объектом класса stringstream istringstream input_istring(format_message.str()); string dump; // поместить в строку dump отформатированное // сообщение /I извлечь хранимые значения ascii и преобразовать их // обратно, в значения арифметических типов input_istring » dump >> vail >> dump >> val2; cout « vail « " " << val2 << endl; // отобразит 512 1024 Здесь переменная-член str используется для доступа к копии строки, связанной с ранее созданным объектом класса ostringstream. С этой строкой связан объект input_istring. При чтении значений объекта input_istring, они преобразу- ются обратно в их первоначальные числовые представления. Чтобы прочитать содержимое объекта input_string, необходимо проанализировать компоненты его строки. Чтобы получить числовые значения, необходимо прочитать (и проигнорировать) метки, которые используются для разделения данных. Поскольку оператор ввода читает типизированные значения, очень важно, чтобы типы объектов, в которые осуществляется чтение, были совместимы с типами значе- ний, читаемых из объекта класса stringstream. В данном случае объект input_ istring имел четыре компонента: строковое значение vail:, далее 512, затем строка val2: и наконец, 1024. Как обычно, при чтении строки с использованием оператора ввода, пробелы игнорируются. Таким образом, при чтении строки, свя- занной с объектом format_message, можно игнорировать символы перехода на новую строку, которые являются частью этого значения. Упражнения раздела 8.5 Упражнение 8.15. Используйте функцию, написанную для первого упражнения в разделе 8.2 (стр. 317), чтобы отобразить содержимое объекта класса istringstream. Упражнение 8.16. Напишите программу, сохраняющую каждую строку из файла в векторе vector<string>. Затем используйте объект класса istringstream для чтения каждого слова из всех строк, сохраненных в векторе.
Глава 8. Библиотека ввода-вывода 329 Резюме Для ввода и вывода данных в языке C++ используются библиотечные классы. Классы заголовка iostream осуществляют ввод и вывод в поток. Классы заголовка f stream осуществляют ввод и вывод в именованный файл. Классы заголовка stringstream осуществляют ввод и вывод в строки, расположенные в оперативной памяти. Все эти классы связаны наследованием. Классы ввода происходят от класса istream, а классы вывода — от класса ostream. Таким образом, возможности присущие объекту класса istream, доступны и в объектах классов ifstream или istringstream. То же справедли- во и для классов вывода, производных от класса ostream. Каждый объект ввода-вывода содержит набор флагов, свидетельствующих о том, может ли он осуществлять операции ввода-вывода. Если произошла ошибка, например достигнут конец файла в потоке ввода, состояние объекта станет таким, что дальнейший ввод окажется невозможен, пока ошибка не будет устранена. Библиотека предоставляет набор функций, по- зволяющих проверять и устанавливать состояние потока. Термины Базовый класс (base class). Класс, то которого происходят другие классы. Базовый класс определяет интерфейс, который наследуют производные классы. Класс f stream. Потоковый объект этого класса позволяет читать и записывать данные в именованный файл. Кроме возможностей, присущих классу iostream, класс f stream обла- дает также функциями-членами ореп() и close (). Функция-член ореп() получает сим- вольную строку в стиле С, которая содержит имя открываемого файла и необязательный ар- гумент, задающий режим. По умолчанию объект класса if streams открывает файл в режиме in, объект класса of streams — в режиме out, а объект класса f streams — одновременно в режимах in и out. Функция-член close () закрывает файл, с которым связан поток. Ее сле- дует вызвать прежде, чем может быть открыт другой файл. Класс stringstream. Потоковый объект, который читает и записывает данные в строку. Кроме возможностей, присущих классу iostream, класс stringstream обладает также пе- регруженной функцией доступа str (). Вызов функции str () без аргументов возвращает строку, с которой связан объект класса stringstream, а вызов со строковым значением при- своит копию этого значения строке, с которой связан объект класса stringstream. Наследование (inheritance). Классы, которые связаны наследованием, совместно исполь- зуют общий интерфейс. Производный класс наследует свойства своего базового класса. Более подробно наследование рассматривается в главе 15, “Объектно-ориентированное программи- рование”. Объектно-ориентированная библиотека (object-oriented library). Набор классов, связан- ных наследованием. Вообще, базовый класс объектно-ориентированной библиотеки опреде- ляет интерфейс, который совместно используется классами, производными от него. В биб- лиотеке ввода-вывода, классы istream и ostream являются базовыми для классов, опреде- ленных в заголовках f stream и sstream. Объект производного класса можно использовать так, как будто это объект базового класса. Например, для объекта класса ifstream примени- мы те операции, которые определены для класса istream. Производный класс (derived class). Класс, использующий интерфейс своего базового класса.
330 Часть I. Основы Режим файла (file mode). Флаги классов заголовка f stream, устанавливаемые при от- крытии файла и задающие способ применения файла. Они перечислены в табл. 8.3 (стр. 323). Флаг состояния (condition state). Флаги и связанные с ними функции потоковых клас- сов позволяют выяснить, пригоден ли данный поток для использования. Флаги состояния и функции, позволяющие устанавливать и выяснять их значения, перечислены в табл. 8.2 (стр. 314).
II Контейнеры и алгоритмы В ЭТОЙ ЧАСТИ... Глава 9. Последовательные контейнеры Глава 10. Ассоциативные контейнеры Глава 11. Общие алгоритмы Как уже упоминалось, язык C++ относительно эффективен для программирова- ния с использованием абстракций. Хороший пример — стандартная библиотека: здесь определены ряд контейнерных классов и семейство обобщенных алгоритмов, которые позволяют создавать весьма компактные, абстрактные и эффективные про- граммы. Детали реализации берет на себя библиотека (в частности, управление па- мятью), а разработчик занимается решением фактических задач проекта. В главе 3, “Библиотечные типы данных”, был представлен контейнерный класс vector. Более подробная информация о векторах и других типах последовательных контейнеров, предоставляемых библиотекой, приведена в главе 9, “Последователь- ные контейнеры”. Кроме того, там будут описаны другие возможности, предостав- ляемые классом string. Строку можно рассматривать как специальный вид кон- тейнера, который способен содержать только символы. Класс string обладает мно- гими, но не всеми возможностями контейнера. В библиотеке определено также несколько ассоциативных контейнеров. Элемен- ты в ассоциативном контейнере упорядочены не последовательно, а по ключу. Ассо- циативные контейнеры имеют много общего с последовательными контейнерами, а также обладают специфическими возможностями. Ассоциативные контейнеры рас- сматриваются в главе 10, “Ассоциативные контейнеры”. В главе 11, “Общие алгоритмы”, описаны общие алгоритмы. Как правило, эти ал- горитмы работают с элементами контейнеров или другими наборами данных. Биб- лиотека алгоритмов предоставляет эффективную реализацию таких классических алгоритмов, как поиск, сортировка и т.д. Например, алгоритм сору позволяет ско- пировать элементы одной последовательности в другую, алгоритм find позволяет найти указанный элемент и т.д. Общими алгоритмы называют по двум причинам: они либо могут быть применены к различным видам контейнеров, либо эти контей- неры могут содержать элементы разных типов. Библиотека разработана так, чтобы контейнерные классы поддерживали общий интерфейс: если несколько контейнеров обладают подобной функцией, эта функция
332 Часть II. Контейнеры и алгоритмы определена тождественно для всех классов контейнеров. Например, все контейнеры могут возвращать количество содержащихся в них элементов. Во всех классах контейнеров эта функция имеет называние size, а возвращает она значение типа size_type. Точно так же и алгоритмы имеют унифицированный интерфейс. На- пример, большинство алгоритмов работают с диапазоном элементов, определенным двумя итераторами. Поскольку функции и алгоритмы контейнеров определены во многом аналогич- но, освоить их библиотеку довольно просто: изучив функциональные возможности одного класса контейнера, их можно применять и для других. Однако важнее всего унифицированность интерфейса, что позволяет создавать более гибкие программы. То есть можно взять программу, использующую один тип контейнера, и переписать ее так, чтобы она использовала другой тип контейнера, не изменяя при этом сущест- венно код. Как будет продемонстрировано, контейнеры обладают разной эффектив- ностью, поэтому возможность замены типа контейнера особенно ценна в случае, ко- гда эффективность системы существенна.
Последовательные контейнеры В ЭТОЙ ГЛАВЕ... 9.1. Определение последовательного контейнера 334 9.2. Итераторы и диапазоны итераторов 339 9.3. Операции с последовательными контейнерами 343 9.4. Как увеличивается размер вектора 358 9.5. Как выбрать тип контейнера 361 9.6. Еще раз о строках 363 9.7. Адаптеры контейнеров 376 Резюме 380 Термины 380 В этой главе обсуждение стандартной библиотеки завершается описанием клас- сов последовательных контейнеров. Здесь более подробно рассматривается упоми- навшийся в главе 3, “Библиотечные типы данных”, класс последовательного контей- нера vector, который используется чаще всего. Элементы в последовательном кон- тейнере сохраняются и возвращаются по позиции. В библиотеке определено также несколько ассоциативных контейнеров, для доступа к элементам которых применя- ется ключ. Ассоциативные контейнеры рассматриваются в следующей главе. Контейнерные классы имеют общий интерфейс. Этот факт упрощает изучение библиотеки: то, что известно об одном контейнере, применимо и к остальным. Каж- дый класс контейнера обладает своим набором функциональных возможностей и характеризуется определенной эффективностью. Зачастую программа, использую- щая один тип контейнера, может перейти на другой без существенной переделки ко- да, для чего, как правило, достаточно изменить объявления типа. Контейнер (container) содержит коллекцию объектов определенного типа. Один из контейнеров, уже не раз использовавшихся ранее, это библиотечный класс vector. Вектор (vector) — это последовательный контейнер (sequential container). Поскольку вектор содержит коллекцию элементов одинакового типа, он является контейнером, а поскольку значения его элементов сохраняются и возвращаются по позиции, он является последовательным контейнером. Порядок расположения эле- ментов в последовательном контейнере не зависит от их значений. Он устанавлива- ется по мере заполнения контейнера новыми элементами.
334 Часть II. Контейнеры и алгоритмы В библиотеке определены три вида последовательных контейнеров: вектор (vector), список (list) и двухсторонняя очередь (deque — сокращение от double-ended queue). Эти виды контейнеров отличаются способом доступа к элементам, а также временем на до- бавление и удаление элементов. В библиотеке также определены три контейнерных адаптера (adaptor). На самом деле адаптер приспосабливает (адаптирует) контейнер базового типа к общему интерфейсу. Адаптерами последовательных контейнеров яв- ляются стек (stack), очередь (queue) и приоритетная очередь (priority queue). В самих контейнерах определено лишь небольшое количество операций. Боль- шинство дополнительных операций предоставляет библиотека алгоритмов, рас- сматриваемая в главе 11, “Общие алгоритмы”. Для тех функциональных возможно- стей, которые определены в самих контейнерах, библиотека требует соответствия общему интерфейсу. Контейнеры различаются по функциональным возможностям, но если два контейнера обладают одинаковой функцией, интерфейс (имя и количество аргументов) у контейнеров обоих типов будет совпадать. Набор функций в классах контейнеров составляет своего рода иерархию. Функциональные возможности, поддерживаемые контейнерами всех типов. Функциональные возможности, поддерживаемые только последовательными или только ассоциативными контейнерами. Функциональные возможности, присущие только некоторым из последователь- ных или ассоциативных контейнеров. В остальной части этой главы подробно рассматриваются возможности контей- неров лишь последовательного типа. Таблица 9.1. Классы последовательных контейнеров Класс Последовательные контейнеры vector Обеспечивает быстрый произвольный доступ list Обеспечивает быструю вставку и удаление deque Двухсторонняя очередь Адаптеры последовательного контейнера stack Стек. Последним пришел — первым вышел queue Очередь. Первым пришел — последним вышел priority queue Приоритетная очередь 9.1. Определение последовательного контейнера Некоторые сведения об использовании последовательных контейнеров уже были приведены в разделе 3.3 (стр. 114). Чтобы определить объект контейнера, в код необходимо подключить файл его заголовка. Это может быть любая их сле- дующих директив. #include <vector> ttinclude <list> #include <deque>
Глава 9. Последовательные контейнеры 335 Каждый из контейнеров представляет собой шаблон класса (раздел 3.3, стр. 114). Чтобы создать контейнер определенного вида, необходимо указать его имя и в угло- вых скобках тип элементов. vector<string> list<int> deque<Sales_item> svec; ilist; i t ems; // пустой вектор для хранения строк // пустой список для хранения целых чисел II пустая двухсторонняя очередь для II хранения объектов класса Sales_item Класс каждого контейнера обладает стандартным конструктором, который по- зволяет создать пустой контейнер определенного типа. Напомним, что стандартный конструктор не получает никаких аргументов. По причинам, которые будут описаны ниже, для создания контейнера зачастую имеет смысл использовать именно стандартный конструктор. В большинстве программ исполь- зование стандартного конструктора обеспечивает во время выполнения более высокую производительность. 9.1.1. Инициализация элементов контейнера Кроме стандартного конструктора, в классе каждого контейнера определены также такие конструкторы, которые позволяют задать начальные значения элементов. Таблица 9.2. Конструкторы контейнеров с<т> с; Создает пустой контейнер по имени с. с — это имя контейнера, например vector, а т — тип элемента, например int или string. Применим для любых контейнеров с с (с2); Создает контейнер с как копию контейнера с2. У контейнеров с и с2 должны совпадать типы самих контейнеров и их элементов. Применим для любых контейнеров с с (Ь, е)Создает контейнер с как копию элементов диапазона, указанного итераторами ь и е. Применим для любых контейнеров с с (n, t)Создает контейнер с из п элементов, каждый из которых имеет значение t. Тип значе- ния t должен совпадать или допускать преобразование в тип элемента контейнера. Применим только для последовательных контейнеров с с (п)Создает контейнер с из п элементов, инициализированных значением по умолчанию (раздел 3.3.1, стр. 116). Применим только для последовательных контейнеров Инициализация контейнера как копии другого контейнера При инициализации последовательного контейнера с использованием любого кон- структора, отличного от стандартного, необходимо указать количество создаваемых элементов. Для этих элементов необходимо также предоставить исходные значения. Один из способов задания значений и количества элементов является инициализация создаваемого контейнера копией уже существующего контейнера того же типа. vector<int> ivec; vector<int> ivec2(ivec); list<int> ilist(ivec); vector<double> dvec(ivec); // ok: ivec - вектор vector<int> // ошибка: ivec не list<int> // ошибка: ivec содержит int а не double При копировании содержимого одного контейнера в другой, типы контейнеров и их эле- ментов должны точно совпадать.
336 Часть II. Контейнеры и алгоритмы Инициализация копией диапазона элементов Хотя непосредственно скопировать элементы из контейнера одного вида в другой нельзя, это можно сделать косвенно, передав пару итераторов (раздел 3.4, стр. 120). Когда используются итераторы, типы контейнеров необязательно должны быть идентичными. Типы элементов контейнеров могут отличаться, но они должны быть совместимыми. То есть при копировании элементов контейнеров возможно преобразование их типов. Итераторы обозначают диапазон подлежащих копированию элементов. Эти эле- менты используются для инициализации элементов нового контейнера. Итераторы отмечают первый и следующий элемент после последнего копируемого. Эту форму инициализации можно использовать для копирования содержимого контейнера, ко- торый не может быть скопирован непосредственно. Но важнее всего то, что этот подход позволяет скопировать в другой контейнер только часть содержимого. // инициализировать список slist копией всех элементов вектора svec list<string> slist(svec.begin(), svec.endO); // найти середину вектора vector<string>::iterator mid = svec.begin() + svec.size()/2; // инициализировать двухстороннюю очередь front первой половиной // вектора svec (исключая элемент, указанный итератором mid) deque<string> front(svec.begin(), mid); // инициализировать двухстороннюю очередь back второй половиной II вектора svec (от элемента mid и до конца) deque<string> back(mid, svec.end()); Напомним, что указатели аналогичны итераторам, поэтому нет ничего удиви- тельного в том, что контейнер можно инициализировать используя пару указателей на элементы встроенного массива. char *words[] = {"stately", "plump", "buck", "mulligan"}; // вычислить количество элементов в массиве words size_t words_size = sizeof(words)/sizeof(char *); // использовать весь массив для инициализации списка words2 list<string> words2(words, words + words_size); Здесь для вычисления размера массива используется функция sizeof () (раз- дел 5.8, стр. 192). Полученный размер добавляется к значению указателя на первый элемент, чтобы вычислить указатель на элемент за пределами массива. Инициали- зирующие значения для списка words2 задает указатель на первый элемент массива words и указатель на элемент за пределами того же массива. Таким образом задает- ся диапазон копируемых элементов. Создание и инициализация определенного количества элементов При создании последовательного контейнера можно явно указать количество и (необязательно) инициализирующее значение создаваемых элементов. Количество может быть задано либо константным, либо неконстантным выражением. Инициа- лизирующее значение должно быть вполне допустимым для элемента данного типа, const list<int>::size_type list_size = 64; list<string> slist(list_size, "eh?"); // 64 строки, содержащие слово eh? Этот код инициализирует список slist, который будет иметь 64 элемента, со- держащие значение eh?.
Глава 9. Последовательные контейнеры 337 В качестве альтернативы можно указать только количество элементов. list<int> ilist(list_size); // 64 элемента, инициализированные // значением О // вектор svec будет иметь столько элементов, сколько укажет // результат вызова функции get_word_count extern unsigned get_word_count(const string &file_name); vector<string> svec(get_word_count("Chimera")); Когда инициализирующее значение элемента отсутствует, библиотека создает его сама (раздел 3.3.1, стр. 116). Чтобы использовать эту форму инициализации, элемент должен иметь либо встроенный, либо составной тип, либо тип класса, об- ладающего стандартным конструктором. В противном случае необходима явная инициализация элементов. Конструкторы, в которых можно указать размер, допустимы только для последовательных контейнеров, но не для ассоциативных. Упражнения раздела 9.1.1 Упражнение 9.1. Объясните следующие примеры инициализации. Укажите, есть ли среди них ошибочные, и если есть, то в чем ошибка. int ia[7] = { 0, 1, 1, 2, 3, 5, 8 }; string sa[6] = { "Fort Sumter", "Manassas", "Perryville", "Vicksburg", "Meridian", "Chancellorsville" (a) vector<string> svecfsa, sa+6); (b) list<int> ilist( ia+4, ia+6); (c) vector<int> ivec(ia, ia+8); (d) list<string> slist(sa+6, sa); Упражнение 9.2. Приведите пример для каждого из четырех способов создания и инициализации вектора. Объясните, какие значения содержит каждый вектор. Упражнение 9.3. Объясните различия между конструктором, который получает контейнер для ко- пирования, и конструктором, получающим два итератора. 9.1.2. Ограничения типов элементов, которые может содержать контейнер Контейнеры способны хранить элементы практически любого типа, однако суще- ствует два ограничения. Тип элемента должен поддерживать присвоение. Объекты данного типа должны допускать копирование. На типы элементов ассоциативных контейнеров налагаются дополнительные ог- раничения, рассматриваемые в главе 10, “Ассоциативные контейнеры”. Большинство типов удовлетворяют этим минимальным требованиям, включая все встроенные и составные типы. Исключение составляют ссылки, которые не поддерживают присвоение в его обычном значении, так что контейнеры ссылок невозможны.
338 Часть II. Контейнеры и алгоритмы Все библиотечные типы, за исключением типов библиотеки ввода-вывода и рас- сматриваемого в разделе 17.1.9 (стр. 734) типа auto_ptr, являются вполне допус- тимыми для элементов контейнеров. В частности, этим требованиям удовлетворяют сами контейнеры. Можно создать контейнер, элементами которого будут другие контейнеры. Рассматриваемый в этой книге класс Sales_item также удовлетворя- ет этим требованиям. Классы библиотеки ввода-вывода не поддерживают ни копирования, ни при- своения. Следовательно, невозможно создать контейнер, который содержит объекты классов ввода-вывода. Функции контейнеров могут налагать дополнительные требования Поддержка копирования и присвоения — это лишь минимальные требования к типам элементов. Некоторые функции контейнеров предъявляют к типам элементов дополнительные требования. Если тип элемента этим требованиям не удовлетворя- ет, такая операция оказывается невыполнимой: т.е. контейнер такого типа создать можно, но использовать специфическую функцию нельзя. Примером функции, налагающей ограничения на типы элементов, являются кон- структоры, получающие одно значение, задающее размер контейнера. Если элемен- тами контейнера являются объекты класса, этот конструктор можно использовать только тогда, когда класс элемента обладает стандартным конструктором. Большин- ство классов имеют стандартный конструктор. Предположим, например, что в клас- се Foo стандартный конструктор не определен, но он имеет конструктор, получаю- щий аргумент типа int. Теперь рассмотрим следующие объявления. vector<Foo> empty; vector<Foo> bad(10); vector<Foo> ok(10, 1); // ok: стандартный конструктор элементу II не нужен II ошибка: класс Foo не имеет стандартного // конструктора II ок: каждый элемент инициализируется 1 Пустой контейнер, содержащий объекты класса Foo, можно создать без проблем, но если необходим контейнер определенного размера, первоначальное количество элементов придется указать. По мере описания функциональных возможностей контейнеров, будут упомяну- ты и другие ограничения (если они есть) налагаемые каждой из них на тип элемента. Контейнеры контейнеров Поскольку контейнеры налагают ограничения на типы элементов, давайте рас- смотрим контейнер, типом элементов которого является другой контейнер. Например, lines можно определить как вектор, типом элементов которого будет вектор строк. // обратите внимание, при определении элемента типа контейнера II используется синтаксис "> >", а не ">>" vector< vector<string> > lines; // вектор векторов Обратите внимание на пробел между символами " > > " при указании типа эле- мента контейнера как контейнера. vector< vector<string> > lines; vector< vector<string>> lines; // ok: пробел между закрывающими II угловыми скобками обязателен II ошибка: » воспринимается как // опера тор сдвига
Глава 9. Последовательные контейнеры 339 -Хо&ст-^ Закрывающие символы > необходимо разделить пробелом, ведь речь идет о двух разных символах, а не об одном двойном. Символ >> без пробела воспринимается как единый оператор сдвига вправо, что приведет к ошибке во время компиляции. Упражнения раздела 9.1.2 Упражнение 9.4. Определите список, который содержит элементы, представляющие собой двух- сторонние очереди, хранящие элементы типа int. Упражнение 9.5. Почему невозможно создать контейнер, хранящий объекты класса iostream? Упражнение 9.6. Предположим, что класс Foo не имеет стандартного конструктора, но конструк- тор, получающий значение типа int, в нем определен. Создайте список, который содержит 10 элементов класса Foo. 9.2. Итераторы и диапазоны итераторов Наиболее популярным примером использования возможностей библиотеки яв- ляются конструкторы, которым передают два итератора. Поэтому прежде чем перей- ти к дальнейшему изучению функциональных возможностей контейнеров, давайте немного подробнее обсудим итераторы и диапазоны итераторов. В разделе 3.4 (стр. 120) уже упоминались итераторы векторов. С классом каждого контейнера связано несколько типов итераторов. Подобно контейнерам, итераторы имеют общий интерфейс: если итератор поддерживает некую функцию, она выполняется одинаково для всех итераторов, которые ее поддерживают. Например, итераторы всех контейнеров позволяют прочитать значение элемента и все они используют для этого оператор обращения к значению. Аналогично, все итераторы поддерживают операторы инкремента и декремента, что позволяет перемещаться с одного элемента на следующий. Список операций, поддерживаемых итераторами всех библиотечных контейне- ров, приведен в табл. 9.3. Таблица 9.3. Операции, поддерживаемые всеми итераторами Возвращает ссылку на элемент, обозначенный итератором iter iter->mem Обеспечивает доступ к члену mem элемента, на который указывает итератор iter. Эквивалентно выражению (*iter) .mem ++iter Инкремент итератора iter позволяет перейти на следующий элемент контейнера - -iter iter-- Декремент итератора iter позволяет перейти на предыдущий элемент кон- тейнера iteri == iter2 Оператор равенства (неравенства) двух итераторов. Два итератора равны, если iteri != iter2 они относятся к одному элементу того же контейнера или если они указывают на первый элемент за пределами того же контейнера (раздел 3.4, стр. 121) Итераторы векторов и двухсторонних очередей поддерживают дополнительные операции Важнейшими наборами операций, которые поддерживают итераторы только век- торов и двухсторонних очередей, являются арифметические операторы (раздел 3.4.1,
340 Часть II. Контейнеры и алгоритмы стр. 124) и операторы отношения (в дополнение к операторам == и ! =) для сравне- ния двух итераторов. Эти операции перечислены в табл. 9.4. Таблица 9.4. Операции, поддерживаемые итераторами векторов и двухсторонних очередей iter + п iter - п iterl += iter2 iterl -= iter2 iterl - iter2 Добавление (вычитание) целочисленного значения п к (из) итератору переводит его позицию на соответствующее количество элементов вперед (назад). Полученный в результате итератор должен указывать на существующий в контейнере элемент или на одну позицию за пределами контейнера Составная версия операторов присвоения со сложением и вычитанием. Результат сложения (вычитания) итераторов iterl и iter2 присваивается итератору iterl В результате вычитания двух итераторов получается число, которое следует приба- вить к правому итератору, чтобы получить второй. Итераторы должны относиться либо к существующим элементам того же контейнера, либо указывать на один эле- мент за пределами контейнера. Операция допустима только для вектора и двухсто- ронней очереди Операторы сравнения итераторов. Считается, что один итератор меньше другого тогда, когда он относится к элементу, позиция которого ближе к началу контейнера. Итераторы должны относиться либо к существующим элементам того же контейне- ра, либо указывать на один элемент за пределами контейнера. Операция допустима только для вектора и двухсторонней очереди Причина, по которой операторы сравнения поддерживают только вектор и двух- стороннюю очередь, заключается в том, что только они обеспечивают быстрый произ- вольный доступ к своим элементам. Эти контейнеры позволят быстро и эффективно переходить непосредственно к указанному элементу по его позиции в контейнере. По- скольку эти контейнеры обеспечивают произвольный доступ по позиции, для их ите- раторов можно эффективно реализовать арифметические и реляционные операторы. Середину вектора, например, можно вычислить следующим образом. vector<int>::iterator iter = vec.begin() + vec.size()/2 ; Следующий код, напротив, содержит ошибку. // скопировать элементы вектора vec в список Hist list<int> ilist (vec .begin () , vec.endO); ilist.begin() + ilist.size()/2; // ошибка: итераторы списка не // поддерживают сложения Итератор списка не поддерживает ни арифметических операций (сложение или вычитание), ни реляционных (<=, <, >=, >), но только префиксный и постфиксный инкремент/декремент, а также операторы равенства и неравенства. В главе 11, “Общие алгоритмы”, будет продемонстрировано, что поддерживаемые итератором операции обусловлены фундаментальными алгоритмами библиотеки. Упражнения раздела 9.2 Упражнение 9.7. Что неверно в следующей программе? Как исправить ошибку?
Глава 9. Последовательные контейнеры 341 Упражнение 9.8. Предположим, что итератор vec_itex* связан с элементом вектора, который содержит строки. Что выполняет этот оператор? if (vec_iter->empty()) /* ... */ Упражнение 9.9. Напишите цикл, который заполняет элементы списка в обратном порядке. Упражнение 9.10. Какие из следующих вариантов применения итератора содержат ошибки, если они есть? const vector< int > ivec(10); vector< string > svec(10); list< int > ilist(10); (a) (b) (c) (d) iterator ivec.begin() ; ilist.begin()+2; &svec[0]; 9.2.1. Диапазоны итераторов Диапазон итераторов является одной из фундаментальных концепций стандартной биб- лиотеки. Диапазон итераторов (iterator range) обозначают парой итераторов, которые отно- сятся к двум элементам того же контейнера или к первому элементу за пределами кон- тейнера. Эти два итератора, отмечающие диапазон элементов контейнера, зачастую на- зывают first (первый) и last (последний) или beg (начало) и end (конец). Хоть имена last и end являются общепринятыми, они способны ввести в за- блуждение. Второй итератор указывает не на последний элемент диапазона, а на по- зицию после него. Элементы диапазона включают элемент, упомянутый как first, и все элементы начиная с него и до элемента, расположенного непосредственно пе- ред элементом last. Если итераторы равны, диапазон пуст. Подобный диапазон элементов называют интервалом, включающим левый эле- мент (left-inclusive interval). В стандартной форме записи для такого диапазона ука- зывают его начало (first) и конец (last), который в диапазон не входит. // включить элемент first и все до элемента last, // исключая последний [ first, last ) ребования к итераторам, формирующим диапазон Два итератора, first и last, позволяют задать диапазон при следующих условиях. ♦ Итераторы относятся к существующим элементам или к элементу за предела- ми того же контейнера. • Если итераторы не равны, элемент last должен быть достижимым благодаря последовательному приращению итератора first. Другими словами, итера- тор last не должен предшествовать итератору first.
342 Часть II. Контейнеры и алгоритмы Итератор last может указывать на тот же элемент, что и итератор first, или на следующий после него. Итератор last не должен указывать на элемент перед ука- занным итератором first. Компилятор не может самостоятельно налагать эти требования. Ему неизвестно, ни с каким именно контейнером связан итератор, ни сколько элементов в нем находится. Невозможность проверки соответствия этим требованиям приводит к непредсказуемо- му результату во время выполнения. Значения, используемые в диапазонах Библиотека использует диапазоны, включающие левый элемент, по той причине, что они обладают двумя очень полезными качествами (напомним, что допустимый диапазон обозначают итераторы first и last). 1. Когда итератор first равен итератору last, диапазон пуст. 2. Когда итератор first не равен итератору last, в диапазоне окажется по край- ней мере один элемент, а итератор first будет указывать на первый из них. Кроме того, приращение итератора first будет перемещать его до тех пор, пока итератор first не станет равен итератору last (т.е. first == last). Благодаря этим качествам можно создавать вполне безопасные циклы, такие, например, что представлены ниже, перебирающие диапазон элементов, проверяя итераторы. while (first != last) { // указатель first можно использовать вполне безопасно, / / поскольку по крайней мере один элемент существует ++first; } Поскольку итераторы first и last задают диапазон, цикл закончится сразу, если выполняется условие first == last, в противном случае, если диапазон не пуст, итератор first позволит обратиться к элементу контейнера. Поскольку усло- вие цикла while отрабатывает случай, когда диапазон пуст, не нужно организовы- вать специальную защиту от подобной ошибки. Когда диапазон не пуст, цикл вы- полнится по крайней мере один раз. Поскольку в теле цикла происходит прираще- ние итератора first, цикл в конечном счете завершится. Кроме того, в теле цикла указатель first вполне безопасен: здесь он обязательно относится к существующе- му элементу, поскольку диапазон между итераторами first и last не пуст. Упражнения раздела 9.2.1 Упражнение 9.11. Каким условиям должны удовлетворять итераторы, формирующие диапазон? Упражнение 9.12. Напишите функцию, которая получает два итератора и значение типа int. Ор- ганизуйте поиск этого значения в диапазоне и возвратите логическое значение (тип bool), ука- зывающее, что значение найдено.
Глава 9. Последовательные контейнеры 343 Упражнение 9.13. Перепишите программу поиска значения так, чтобы она возвращала итера- тор на найденный элемент. Убедитесь, что функция работает правильно, даже если элемент не существует. Упражнение 9.14. Напишите программу, использующую итераторы при чтении последовательно- сти строк со стандартного устройства ввода в вектор. Отобразите элементы вектора. Упражнение 9.15. Перепишите программу из предыдущего упражнения так, чтобы вместо вектора использовался список. Перечислите изменения, которые повлекла смена типа контейнера. 9.2.2. Некоторые операции с контейнерами делают итераторы некорректными В следующих разделах будет продемонстрировано несколько операций с контей- нерами, которые изменяют их внутреннее состояние или перемещают элементы. В результате таких операций недопустимыми (invalidate) станут либо все итераторы, либо те из них, которые относятся к перемещенным элементам. Применение недо- пустимого итератора ведет к таким же непредсказуемым последствиям, что и ис- пользование потерянного указателя. В классе каждого контейнера, например, определена одна или несколько функ- ций erase (). Эти функции удаляют из контейнера элементы. Любой итератор, ко- торый относится к удаленному элементу, имеет недопустимое значение. В конце концов, такой итератор указывает на элемент, которого в контейнере больше нет. При создании программ, которые используют итераторы, не следует забывать об опе- рациях, способных сделать итераторы недопустимыми. Использование недопустимого итератора во время выполнения программы приводит к серьезным ошибкам. Нет никакого способа исследовать итератор и выяснить, допустим ли он. Ника- кая проверка не позволит обнаружить, что итератор уже недопустим. Любая попыт- ка использования недопустимого итератора, вероятнее всего, приведет к сообщению об ошибке во время выполнения, однако программа может либо просто зависнуть, либо в ней проявится некая совершенно иная проблема, обнаружить реальную при- чину которой окажется крайне сложно. При использовании итераторов программу, как правило, можно написать так, чтобы участок кода, в котором итератор должен остаться допустимым, был относительно коротким. В этом фрагменте кода следует внимательно просмотреть все операторы и выявить случаи добавления и удаления элементов, а затем откорректировать значе- ния итераторов соответствующим образом. 9.3. Операции с последовательными контейнерами В классе каждого последовательного контейнера определен набор вспомогатель- ных типов и поддерживаемых операций, которые перечислены ниже: добавление элемента в контейнер;
344 Часть II. Контейнеры и алгоритмы удаление элемента из контейнера; возвращение размера контейнера; возвращение первого и последнего элементов контейнера (если они есть). 9.3.1. Вспомогательные типы, определенные в классе контейнера До сих пор использовались только три типа данных, определенные в классах кон- тейнеров: size_type, iterator и const_iterator. Эти типы определены в каж- дом контейнере, наряду с некоторыми другими (см. табл. 9.5). Таблица 9.5. Типы данных, определенные в классе контейнера size_type iterator const_iterator reverse_iterator const_reverse_iterator difference_type value_type reference const reference Целочисленный беззнаковый тип, размер которого достаточно велик, чтобы содержать значение размера самого большого возможного кон- тейнера этого класса Тип итератора контейнера этого класса Тип итератора, который позволяет читать, но не записывать данные в элементы Итератор, обеспечивающий доступ к элементам в обратном порядке Реверсивный итератор, позволяющий читать, но не записывать данные в элементы Целочисленный знаковый тип, размер которого достаточно велик, что- бы содержать значение разницы между двумя итераторами (значение может быть отрицательным) Тип элемента Тип 1-значения элемента; то же, что и vaiue_type& Тип константного 1-значения элемента; аналог const value type& Более подробно реверсивные итераторы рассматриваются в разделе 11.3.3 (стр. 439), а пока заметим, что реверсивный итератор (reverse iterator) — это итера- тор, который позволяет перебирать содержимое контейнера в обратном порядке, а обычные операции с ним приводят к инверсному результату: например, применение оператора ++ к реверсивному итератору приводит к перемещению на предыдущий элемент контейнера. Три последних типа в табл. 9.5 позволяют использовать тип элементов контейне- ра, даже не зная о том, каков именно этот тип. Если необходим тип элемента, можно указать тип value_type данного контейнера. Если необходима ссылка на этот тип, можно воспользоваться типом reference или const_reference. Преимущества этих связанных с элементом типов станут более очевидными после обсуждения, представленного в главе 16, “Шаблоны и общее программирование”. Выражения, использующие определенный контейнером тип, могут иметь очень сложный вид. // iter имеет тип iterator, определенный в списке list<string> 1ist<string>::iterator iter;
Глава 9. Последовательные контейнеры 345 // ent имеет тип difference_type, определенный в // векторе vector<int> vector<int>::difference_type ent; В объявлении итератора iter используется оператор области видимости, позво- ляющий указать, что имя с правой стороны от символа : : принадлежит области видимости, заданной с левой стороны. В результате итератор iter будет иметь тот тип, который определен для члена iterator класса list, содержащего элементы типа string. Упражнения раздела 9.3.1 Упражнение 9.16. Какой тип следует использовать в качестве индекса для вектора целых чисел? Упражнение 9.17. Какой тип следует использовать для чтения элементов в списке строк? 9.3.2. Функции-члены begin () и end () Функции-члены begin () и end () возвращают итераторы, которые относятся к первому и следующему за последним элементам контейнера соответственно. Как правило, эти функции используют при создании диапазона итераторов, охватываю- щих все элементы контейнера. Таблица 9.6. Функции-члены контейнера begin () и end () с. begin () Возвращает итератор, указывающий на первый элемент контейнера с с. end () Возвращает итератор, указывающий на следующий элемент после последнего в контей- нере с с. rbegin () Возвращает реверсивный итератор, указывающий на последний элемент в контейнере с с. rend () Возвращает реверсивный итератор, указывающий на элемент, который предшествует первому элементу в контейнере с Существует две версии каждой из этих функций: одна для константных элемен- тов (раздел 7.7.1, стр. 286), а вторая — для неконстантных. Тип возвращаемого зна- чения этих функций зависит от того, является ли контейнер константным. Если кон- тейнер не константен, в результате возвращается итератор типа iterator или reverse_iterator, а в противном случае тип будет иметь префикс const_, т.е. const_iterator или const_reverse_iterator. Более подробная информация о реверсивных итераторах приведена в разделе 11.3.3 (стр. 439). 9.3.3. Добавление элементов в последовательный контейнер В разделе 3.3.2 (стр. 117) уже был продемонстрирован один из способов добавле- ния элементов в контейнер (при помощи функции push_back () ). Каждый после- довательный контейнер реализует функцию push_back (), которая добавляет эле- мент в конец контейнера. Приведенный ниже цикл читает строки в переменную text_word, а затем добавляет их в контейнер container.
346 Часть II. Контейнеры и алгоритмы // читать слова со стандартного устройства ввода и помещать // их в конец контейнера string text_word; while (cin » text_word) container.push_back(text_word); Обращение к функции push_back () создает новый элемент в конце контейнера container и увеличивает его размер на единицу. Значением этого элемента будет копия содержимого переменной text_word. Контейнер может быть любого типа: list, vector или deque. Кроме функции push_back (), контейнеры list и deque обладают аналогич- ной функцией push_front (). Эта функция добавляет новый элемент в начало контейнера. Например, здесь функция push_back () используется для добавления последовательности чисел 0,1, 2 и 3 в конец списка ilist. list<int> ilist; // добавить элементы в конец списка ilist for (size_t ix = 0; ix != 4; ++ix) ilist.push_back(ix); В качестве альтернативы можно использовать функцию push_f ront (), которая добавляет элементы 0,1, 2 и 3 в начало списка ilist. // добавить элементы в начало списка ilist for (size_t ix = 0; ix != 4; ++ix) ilist.push_front(ix); Поскольку каждый элемент добавляется в начало списка list, они располагают- ся в обратном порядке. После завершения обоих циклов, список ilist содержит следующую последовательность 3,2,1,0,0,1,2,3. Фундаментальная концепция. Элементы контейнера содержат копии значений При добавлении элемента в контейнер, его значение копируется. Аналогично, при ини- циализации контейнера предоставленным диапазоном элементов другого контейнера, новый будет содержать копии значений исходного. Никакой взаимосвязи между элемен- тами нового и исходного контейнеров не остается. Последующие изменения значений эле- ментов контейнера никак не влияют на значения исходного конгенера и наоборот. Таблица 9.7. Функции, добавляющие элементы в последовательный контейнер с.push_back(t) с.push_front(t) c.insert(p, t) c.insert(p, n, t) c. insert (p, b, e) Добавляет элемент co значением t в конец контейнера с. Возвращает тип void Добавляет элемент со значением t в начало контейнера с. Возвращает тип void. Допустима только для контейнеров list и deque Добавляет элемент со значением t перед элементом, указанным итератором р. Возвращает итератор добавленного элемента Добавляет п элементов со значением t перед элементом, указанным итерато- ром р. Возвращает тип void Вставляет элементы в диапазон, обозначенный итераторами ь и е перед эле- ментом, указанным итератором р. Возвращает тип void
Глава 9. Последовательные контейнеры 347 Добавление элементов в указанную точку контейнера Функции push_back () и push_f ront () предоставляют весьма удобный спо- соб добавления одиночных элементов в конец или в начало последовательного кон- тейнера. Функция insert () обладает более общим характером, она позволяет вставлять элементы в любую указанную позицию контейнера. Существует три вер- сии функции insert (). Первая передает итератор и значение элемента. Итератор указывает позицию вставки значения. Эту версию функции insert () можно ис- пользовать для вставки элемента в начало контейнера. vector<string> svec; list<string> slist; string spouse("Beth"); // эквивалент вызова slist.push_front(spouse) ; slist.insert(slist.begin(), spouse); // вектор не обладает функцией push_front(), но вместо нее можно // воспользоваться функцией insert () , вставив элемент перед begin() // внимание: функция insert() применима везде, но в конце // вектора она неэффективна svec.insert(svec.begin(), spouse); Значение вставляется перед позицией, указанной итератором. Итератор может указывать на любую позицию в контейнере, включая один элемент после его по- следнего элемента. Поскольку итератор может указывать на несуществующий эле- мент после последнего элемента контейнера, функция insert () разработана так, чтобы добавлять элемент перед указанной позицией, а не после нее. Приведенный ниже код добавляет копию значения переменной spouse непосредственно перед элементом, указанным итератором iter. slist.insert(iter, spouse); // вставить значение переменной spouse / / непосредственно перед элементом, // указанным итератором iter Эта версия функции insert () возвращает итератор, указывающий на только что добавленный элемент. Это возвращаемое значение можно использовать для мно- гократного добавления элементов в указанную позицию контейнера. list<string> 1st; list<string>::iterator iter - lst.begin(); while (cin >> word) iter = 1st.insert(iter, word); // аналог вызова push_front() Очень важно хорошо понимать, как именно работает этот цикл, а также почему он экви- валентен вызову функции push_f ront (). Перед началом цикла итератор iter инициализируется значением, возвращае- мым функцией lst.begin(). Поскольку список пуст, возвращаемые функциями 1st.begin() и 1st.end() значения равны и итератор iter указывает на сле- дующий элемент после конца списка. Первое обращение к функции insert () по- мещает прочитанное значение в элемент, расположенный перед тем, на который ука- зывает итератор iter. Возвращенное функцией insert () значение представляет собой итератор, указывающий на новый элемент, который теперь является первым и единственным элементом списка 1st. Это значение присваивается итератору iter и при повторении цикла while считывается следующее слово. Пока слова не закон-
348 Часть II. Контейнеры и алгоритмы чатся, каждое повторение цикла while вставляет новый элемент перед тем, на кото- рый указывает итератор iter, а затем переводит итератор iter на только что соз- данный элемент. Этот элемент всегда остается первым, поэтому каждая итерация вставляет элемент перед первым элементом списка 1st. Вставка диапазона элементов Вторая версия функции insert () добавляет указанное количество одинаковых элементов в определенную позицию. svec.insert(svec.end(), 10, "Anna"); Этот код вставляет 10 элементов в конец вектора svec и инициализирует каж- дый из них строкой "Anna". Последняя форма функции insert () добавляет в контейнер диапазон элементов, обозначенных двумя итераторами. Рассмотрим, например, следующий массив строк, string sarray[4] - {"quasi", "simba", "frolic", "scar"}; Из приведенного выше массива строк в список slist можно вставить либо все строки, либо их подмножество. // вставить все элементы массива sarray в конец списка slist slist.insert(slist.end(), sarray, sarray+4); list<string>::iterator slist_iter = slist.begin(); // вставить два последних элемента массива sarray перед позицией, / / указанной итератором slist_iter slist.insert(slist_iter, sarray+2, sarray+4); Вставка элементов может стать причиной некорректности итератора Как будет продемонстрировано в разделе 9.4 (стр. 358), добавление элемента в кон- тейнер может привести к сдвигу всех его элементов. При сдвиге элементов контейнера, все его итераторы оказываются некорректными. Даже если сдвига элементов при вставке фактически не произошло, итераторы элементов все равно будут недопустимы. Итераторы могут стать недопустимыми после любого случая выполнения функций Д—. insert () или push () для вектора или двухсторонней очереди. При создании цик- |F Л лов, добавляющих элементы в вектора или двухсторонние очереди, следует программ- но восстанавливать допустимость итератора при каждом выполнении цикла. Не сохраняйте итератор, возвращенный функцией end () При добавлении элементов в вектор или двухстороннюю очередь, некоторые или все итераторы могут стать некорректны. Надежней полагать, что некорректными станут все итераторы. Это особенно справедливо для итератора, возвращаемого функцией end (). Этот итератор всегда будет некорректным после любой операции добавления элемента в контейнер. Рассмотрим, например, цикл, который читает все элементы, обрабатывает их не- ким образом и добавляет новый элемент после исходного. Необходимо, чтобы цикл обработал каждый исходный элемент. Здесь используется версия функции insert (), получающая одно значение и возвращающая итератор на элемент, который был только что вставлен. После каждой операции вставки значение возвращенного ите- ратора увеличивается так, чтобы в цикле обрабатывался следующий, т.е. исходный
Глава 9. Последовательные контейнеры 349 элемент. Если попытаться “оптимизировать” цикл, сохранив итератор, возвращен- ный функцией end (), произойдет катастрофа. vector<int>::iterator first = v.begin(), last = v.end(); // сохранить последний // итератор // катастрофа: поведение этого цикла непредсказуемо while (first != last) { // выполнить некую обработку // вставить новое значение и переназначить итератор first, II который в противном случае окажется некорректен first = v.insert(first, 42); ++first; // продвинуть итератор first вперед, на один // элемент после добавленного } Поведение этого кода непредсказуемо, однако в большинстве реализаций полу- чится бесконечный цикл. Проблема заключается в том, что возвращенное функцией end () значение итератора сохраняется в локальной переменной last, а в теле цик- ла происходит добавление элемента, которое сделает некорректным итератор, хра- нимый в переменной last. Теперь он указывает на элемент внутри контейнера v, а не на элемент после его последнего элемента. Не сохраняйте итератор, возвращенный функцией end (). Вставка или удаление элементов в вектор или двухстороннюю очередь сделает его значение некорректным. Итератор, возвращенный функцией end (), следует не сохранять, а повторно вы- числять после каждой вставки. // безопасней повторно вычислять конец контейнера при каждой // итерации, когда в цикле происходит добавление или удаление // элементов while (first != v.end()) { // выполнить некую обработку first = v.insert (first, 42); // вставить новое значение ++first; // продвинуть итератор first вперед, на один // элемент после добавленного Упражнения раздела 9.3.3 Упражнение 9.18. Напишите программу, копирующую элементы из списка целых чисел в две двухсторонние очереди. Четные элементы списка должны войти в одну двухстороннюю очередь, а нечетные — во вторую. Упражнение 9.19. Допустим, что вектор iv содержит целые числа. В чем состоит проблема сле- дующей программы? Как можно ее устранить? vector<int>::iterator mid = iv.begin() + while (vector<int>::iterator iter != mid) if (iter == some_val) iv.insert(iter, 2 * some_val); iv.size()/2; 9.3.4. Операторы сравнения Каждый контейнер поддерживает операторы сравнения (relational operator) (раз- дел 5.2, стр. 176), которые позволяют сравнить два контейнера. Контейнеры должны
350 Часть II. Контейнеры и алгоритмы быть одинакового вида и содержать элементы одинакового типа. Вектор vector<int> можно сравнивать только с другим вектором vectore int >. Нельзя сравнивать вектор vector<int > со списком listcint > или вектором vector<double>. Сравнение двух контейнеров осуществляется на основании сравнения пар их элемен- тов. Для сравнения используется тот же реляционный оператор, который определен для типа элементов: при сравнении двух контейнеров на неравенство (1 =) используется опе- ратор ! = типа их элементов. Если тип элемента не поддерживает определенный опера- тор, то для сравнения контейнеров такого типа данный оператор использовать нельзя. Эти операторы работают аналогично операторам сравнения строк (раздел 3.2.3, стр. 109). Если оба контейнера имеют одинаковый размер и все их элементы совпадают, контейнеры равны, а в противном случае — не равны. Если контейнеры имеют различный размер, но каждый элемент короткого сов- падает с соответствующим элементом длинного, считается, что короткий кон- тейнер меньше длинного. Если значения элементов контейнеров не совпадают, результат их сравнения за- висит от значений первых неравных элементов. Проще всего понять работу операторов рассмотрев их на примерах. ivecl: 1 3 5 7 9 12 ivec2 ivec3 ivec 4 0 2 4 6 8 10 12 13 9 13 5 7 ivec5: 1 3 5 7 9 12 II ivecl и ivec2 отличаются элементом [0]: ivecl больше ivec2 ivecl < ivec2 // false ivec2 < ivecl // true II ivecl и ivec3 отличаются элементом [2]: ivecl меньше ivec3 ivecl < ivec3 // true // все элементы равны, но в ivec4 их меньше: ivecl больше ivec4 ivecl < ivec4 // false ivecl -= ivec5 // true; все элементы равны и их количество равно ivecl == ivec4 // false; ivec4 имеет меньше элементов, чем ivecl ivecl != ivec4 // true; ivec4 имеет меньше элементов, чем ivecl При сравнении контейнеров используются операторы сравнения их элементов Сравнить два контейнера можно только тогда, когда используемый оператор сравнения определен для типа элемента контейнера. Каждый контейнерный оператор сравнения работает осуществляя сравнение па- ры элементов из двух контейнеров, ivecl < ivec2 Предположим, что ivecl и ivec2 являются векторами vector<int>. Для их сравнения используется оператор меньше встроенного типа int. Если бы векторы содержали строки, использовался бы тот строковый оператор, который меньше.
Глава 9. Последовательные контейнеры 351 Если бы векторы содержали объекты класса Sales_item, использованного в разделе 1.5 (стр. 42), сравнение оказалось бы невозможным. Дело в том, что опера- торы сравнения для класса Sales_item не определены, т.е. два контейнера для элементов класса Sales_itern сравнить нельзя. vector<Sales vector<Sales if (storeA < item> storeA; item> storeB; storeB) // ошибка: класс Sales_item не имеет // оператора меньше Упражнения раздела 9.3.4 Упражнение 9.20. Напишите программу, которая определяет, содержит ли вектор vector<int> те же элементы, что и список iist<int>. Упражнение 9.21. Допустим, cl и с2 являются контейнерами. Какие условия налагают типы их элементов в следующем выражении? if (cl < с2) 9.3.5. Операции с размером контейнера Контейнер любого типа обладает четырьмя функциями, связанными с размером. Функции size () и empty О использовались в разделе 3.2.3 (стр. 107). Функция size () возвращает количество элементов в контейнере, а функция empty О воз- вращает логическое значение true, если размер равен нулю, и значение false — в противном случае. Функция resize () изменяет количество элементов в контейнере. Если текущий размер больше, чем новый, элементы удаляются с конца контейнера. Если текущий размер меньше, чем новый, элементы добавляются в конец контейнера. // в конец списка Hist ilist.resize(25, -1); // добавляет 10 элементов со значением -1 // в конец списка ilist ilist.resize(5); // удаляет 20 элементов от конца списка Hist Функция resize () получает необязательный аргумент — значение элемента. Если этот аргумент присутствует, каждый добавленный элемент получают данное значение. Если необязательный аргумент отсутствует, каждый добавленный элемент инициализируется значением по умолчанию (раздел 3.3.1, стр. 116). Функция resize О способна сделать итераторы некорректными. Операция измене- ния размера вектора или двухсторонней очереди потенциально объявляет все итерато- ры некорректными. Таблица 9.8. Функции размера последовательного контейнера с.size () c.max_size() Возвращает количество элементов контейнера с. Тип возвращаемого значения — с::size_type Возвращает максимально возможное количество элементов контейнера с. Тип воз- вращаемого значения — С : : size type
352 Часть II. Контейнеры и алгоритмы Окончание табл. 9.8 с.size() с.empty() с.resize(п) с.resize(n,t) Возвращает количество элементов контейнера с. Тип возвращаемого значения — с::size_type Возвращает логическое значение, которое указывает, является ли контейнер пустым Изменяет размер контейнера с так, чтобы он содержал п элементов. Если число п меньше размера контейнера, излишек элементов отбрасывается. Если следует до- бавить новые элементы, они инициализируются значением по умолчанию Изменяет размер контейнера с так, чтобы он содержал п элементов. Все добавляе- мые элементы получат значение t Упражнения раздела 9.3.5 Упражнение 9.22. Допустим, что контейнер vec содержит 25 элементов. Что выполняет выраже- ние vec. resize (loo) ? Что произойдет, если затем выполняется вызов vec. resize (ю) ? Упражнение 9.23. Какие ограничения (если они есть), налагает использование функции resize () с одиночным аргументом типа элемента? 9.3.6. Доступ к элементам Если контейнер не пуст, функции-члены front () и back () возвращают ссылки на первый и последний элементы контейнера. // перед обращением к значению итератора удостовериться, что // элемент существует, либо вызвать функции front() и back() if (!ilist.empty()) { // val и val2 относятся к тому же элементу 1ist<int>::reference val = *ilist.begin(); list<int>::reference val2 = ilist.front(); // last и last 2 относятся к тому же элементу list<int>::reference last = *--ilist.end(); list<int>::reference last2 - ilist.back(); } В этой программе использованы два различных способа получения ссылки на пер- вый и последний элементы списка ilist. Прямой подход — обращение к функциям front () и back (). Косвенный подход получения ссылки на тот же элемент подразу- мевает обращение к значению итератора, возвращенного функцией begin (), или к значению элемента, предшествующего тому, итератор которого возвращает функция end (). В этой программе примечательны два момента: поскольку возвращенный функцией end () итератор указывает на элемент, следующий после последнего, для доступа к последнему элементу контейнера применяется декремент полученного ите- ратора. Вторым очень важным моментом является необходимость удостовериться в том, что список ilist не пуст, перед вызовом функций front () и back () или обра- щением к значению итераторов, возвращенных функциями begin () и end (). Если список окажется пустым, все выражения в блоке операторов if будут некорректны. При обсуждении индексации в разделе 3.3.2 (стр. 118) упоминалось, что про- граммист должен организовать проверку наличия элемента, индекс которого ис- пользуется. Сам оператор индексирования этой проверки не осуществляет. То же справедливо и для функций front () и back (). Если контейнер пуст, результат вы-
Глава 9. Последовательные контейнеры 353 зова этих функций непредсказуем. Если контейнер содержит только один элемент, функции f ront () и back () возвратят ссылку на этот элемент. Применение индекса, выходящего за диапазон существующих элементов, а также вызов функции front () и back () для пустого контейнера является серьезной ошибкой. Таблица 9.9. Функции доступа к элементам последовательного контейнера с.back() с.front() с [п] с.at (п) Возвращает ссылку на последний элемент контейнера с, если он не пуст Возвращает ссылку на первый элемент контейнера с, если он не пуст Возвращает ссылку на элемент номер п. Если п < оилип >= с. s i ze (), результат непредсказуем. Допустимо только для вектора и двухсторонней очереди Возвращает ссылку на элемент номер п. Если индекс находится вне диапазона, передает исключение out of range. Допустимо только для вектора и двухсторонней очереди Альтернативой индексированию является применение функции-члена at (). Она работает подобно индексированию, но если индекс недопустим, функция at () пе- редает исключение out_of_range (раздел 6.13, стр. 241). vector<string> svec; // пустой вектор cout « svec[0]; // ошибка во время выполнения: вектор svec // еще не имеет элементов! cout << svec.at(O); // передает исключение out_of_range Упражнения раздела 9.3.6 Упражнение 9.24. Напишите программу, которая обращается к первому элементу вектора. Реали- зуйте ее с использованием функции at (), оператора индексирования, а также функций front () и begin (). Проверьте программу на пустом векторе. 9.3.7. Удаление элементов Напомним, что функция insert () позволяет вставить элемент в любую пози- цию контейнера, а функции push_front О и push_back() — только в начало и конец. Им соответствуют универсальная функция erase () и специализированные функции pop_f ront () и pop_back (), удаляющие элементы. Удаление первого и последнего элемента Функции pop_f ront () и pop_back () удаляют, соответственно, первый и по- следний элементы контейнера. Векторы функцией pop_f ront () не обладают. Уда- лив соответствующий элемент, эти функции возвращают тип void. Как правило, функция pop_f ront () используется вместе с функцией f ront (), это позволяет обработать контейнер как стек. while (!ilist.empty()) { process(ilist.front()); // выполнение действий с текущей // вершиной списка ilist
354 Часть II. Контейнеры и алгоритмы ilist.pop_front(); } // обработка закончена, удалить // первый элемент Этот цикл довольно прост: функция f ront () используется для получения обра- батываемого значения, а затем функция pop_front () удаляет ненужный элемент из списка. Функции pop_front () и pop_back() возвращают тип void; они не возвращают У* | значение удаленного элемента. Чтобы получить удаляемое значение, сначала необходимо вызвать функцию front () ИЛИ back (). Таблица 9.10. Функции, удаляющие элементы из последовательного контейнера с.erase(р) с.erase(Ь, е) с. clear () с.pop_back() с.pop_front() Удаляет элемент, указанный итератором р. Возвращает итератор элемента после удаленного или после последнего элемента контейнера, если итератор р указывал на него. Результат непредсказуем, если итератор р указывает на следующий эле- мент после последнего элемента Удаляет диапазон элементов, обозначенных итераторами ь и е. Возвращает итера- тор элемента после удаленного или после последнего элемента контейнера, если итератор е указывал на последний элемент Удаляет все элементы контейнера с. Возвращает void Удаляет последний элемент контейнера с. Возвращает void. Результат непредска- зуем, если контейнер с пуст Удаляет первый элемент контейнера с. Возвращает void. Результат непредсказу- ем, если контейнер с пуст. Допустимо только для списка или двухсторонней очереди Удаление элемента в середине контейнера Поскольку функция erase () позволяет удалять как отдельные элементы, так и их диапазоны, она существует в двух версиях: для удаления одного элемента, ука- занного итератором, и удаления диапазона элементов, указанного двумя итератора- ми. Обе версии функции erase () возвращают итератор на элемент, расположен- ный после удаленного элемента или диапазона. То есть если элемент j расположен непосредственно после элемента i и из контейнера был удален элемент i, функция erase () возвратит итератор на элемент j. Как обычно, функции удаления не проверяют свои аргументы. Разработчик должен сам организовать проверку допустимости итераторов и их диапазонов. Функция erase () зачастую используется после поиска элемента, подлежащего удалению из контейнера. Проще всего организовать поиск элемента воспользовав- шись библиотечным алгоритмом find. Более подробная информация об алгоритме find приведена в разделе 11.1 (стр. 418). Чтобы использовать алгоритм find или любой другой общий алгоритм, необходимо подключить заголовок algorithm. Функция find () получает два итератора, которые обозначают диапазон поиска и
Глава 9. Последовательные контейнеры 355 искомое значение. Функция find() возвращает итератор на первый найденный элемент или элемент после последнего элемента контейнера. string searchvalue("Quasimodo"); list<string>::iterator iter = f ind (slist .begin () , slist.endO, if (iter != slist.endO) slist.erase(iter); searchvalue); Обратите внимание, здесь перед удалением элемента выполняется проверка, не указывает ли итератор на элемент после последнего. При вызове функции erase () для одиночного элемента, должен существовать удаляемый элемент, причем это не может быть элемент после последнего, поскольку в этом случае результат окажется непредсказуемым. Удаление всех элементов контейнера Чтобы удалить все элементы контейнера, можно вызвать функцию clear () или функцию erase () передав ей итераторы, возвращаемые функциями begin () и end(). slist.clear () ; // удалить все элементы контейнера slist.erase(slist.begin(), slist.end()); // эквивалент Версия функции erase () для двух итераторов позволяет удалять диапазон эле- ментов. // удалить диапазон элементов между двумя значениями list<string>::iterator elemi, elem2; // elemi ссылается на vail elemi = find(slist.begin(), slist.end(), vail); // elem2 ссылается на первый элемент со значением val2 после vail elem2 = find(eleml, slist.endO, val2); // удалить диапазон от vail до val2 (исключая последний) slist.erase(elemi, elem2); Этот код начинается с двух вызовов функции f ind (), позволяющих получить итераторы на два элемента. Итератор е 1 eml указывает на позицию первого найден- ного значения vail или на элемент после последнего элемента, если значение vail в списке отсутствует. Итератор elem2 указывает на позицию первого значения val2, найденного после значения vail, или на элемент после последнего элемента, если значение elem2 в списке отсутствует. Обращение к функции erase () удаляет элементы начиная от указанного итератором elemi до (но не включая) указанного итератором е 1 ет2. Функции erase(), pop_front() и pop_back() делают некорректными любые итераторы, которые относятся к удаленным элементам. У векторов некорректными стано- Ч вятся все итераторы на элементы, расположенные после удаленного. В двухсторонней очереди, если удаление не затрагивает первый или последний элементы, некорректными ^i/ма^ становятся все итераторы. Упражнения раздела 9.3.7 Упражнение 2.25. Что произойдет, если в программе удален диапазон элементов, указанный равными итераторами vail и vai2. Что произойдет, если элемент, указанный итератором vail или val2 (или обоими), не существует.
356 Часть II. Контейнеры и алгоритмы Упражнение 2.26. Используя приведенное ниже определение массива ia, скопируйте его со- держимое в вектор и в список. Используя версию функции erase () для одного итератора, уда- лите из списка элементы с нечетными значениями, а из вектора — с четными. int ia[] = { 0, 1, 1, 2, 3, 5, 8, 13, 21, 55, 89 }; Упражнение 2.27. Напишите программу для обработки списка строк. Организуйте поиск указан- ного значения и его удаление (если оно есть). Повторите программу для двухсторонней очереди. 9.3.8. Присвоение и функция swap () Операторы, связанные с присвоением, воздействуют на весь контейнер. Если бы не функция swap (), любой из них можно было бы реализовать при помощи функ- ций erase () и insert (). Оператор присвоения удаляет весь диапазон элементов в контейнере, указанном слева, а затем вставляет в него элементы из контейнера, указанного справа, cl = с2; // заменяет содержимое контейнера cl копией // элементов контейнера с2 // эквивалентная операция, но с использованием // функций erase() и insert() cl.erase(cl.begin(), cl.end()); // удалить все элементы cl cl.insert(cl.begin(), c2.begin(), c2.end()); // вставить c2 После присвоения левый и правый контейнеры равны. Даже если контейнеры име- ли неодинаковый размер, после присвоения оба они имеют размер правого операнда. Присвоение и функция assign () делает некорректными все итераторы левого контей- нера. Функция swap () не делает итераторы некорректными. После выполнения функции swap () итераторы продолжают указывать на те же элементы, хотя теперь они принадле- жат другому контейнеру. Таблица 9.11. Операции присвоения последовательного контейнера cl = с2 cl.swap(с2) c.assign(b, е) с.assign(n, t) Удаляет элементы контейнера ci и копирует элементы контейнера с2 в контейнер ci. Контейнеры ci и с2 должны иметь одинаковый тип Меняет местами содержимое контейнеров. После вызова контейнер ci содержит элементы, которые ранее содержал контейнер с2, а контейнер с2 — элементы, ко- торые ранее содержал контейнер ci. Контейнеры ci и с2 должны иметь одинако- вый тип. Срабатывает гораздо быстрее, чем просто копирование элементов из кон- тейнера с2 в контейнер ci Заменяет элементы контейнера с элементами диапазона, обозначенного итерато- рами ь и е. Итераторы ь и е не должны относиться к элементам контейнера с Заменяет элементы контейнера с набором из п элементов со значением t Применение функции assign () Функция assign () удаляет все элементы контейнера, а затем вставляет в него новые элементы, как определено аргументами. Подобно конструктору, который ко- пирует элементы из контейнера, оператор присвоения (=) позволяет присвоить один контейнер другому только тогда, когда типы контейнеров и их элементов совпадают.
Глава 9. Последовательные контейнеры 357 Если необходимо присвоить контейнеры разных типов и (или) контейнеров с элемен- тами разных, но совместимых типов, необходимо использовать функцию assign (). Например, функцию assign () можно применить для присвоения диапазона значе- ний типа char* из вектора в список строк. Поскольку исходные элементы из контейнера удаляются, передаваемые функции assign () итераторы не должны относиться к элементам того контейнера, для кото- рого вызвана функция assign (). Аргументы функции assign () задают количество вставляемых элементов, а также значения, которые будут иметь новые элементы. Приведенный ниже оператор использует версию функции assign () с двумя итераторами. // эквивалент slist1 = slist2 slist1.assign(slist2.begin(), slist2.end()); После удаления элементов списка slistl, функция копирует элементы в диапа- зон, обозначенный итераторами списка slist2. Таким образом, этот код эквивален- тен присвоению списка slist2 списку slistl. Функция assign (), получающая два итератора, позволяет присвоить элементы контей- нера одного типа другому. Вторая версия функции assign () получает целочисленное значение и значение элемента. Она заменяет элементы контейнера, определенные количеством элемен- тов, указанным значением элемента. // эквивалент slistl. clear () ; // следует за slistl.insert(slistl.begin(), 10, "Hiya!"); slistl.assign(10, "Hiya!"); // 10 элементов co значением Hiya! После выполнения этого оператора, список slistl содержит 10 элементов, каж- дый из которых имеет значение Hiya!. Применение функции swap () для ускорения удаления элементов Функция swap () меняет местами значения двух операндов. Типы контейнеров и их элементов должны совпадать, ведь им предстоит содержать значения одинаково- го типа. После обращения к функции swap (), элементы правого операнда оказыва- ются в левом и наоборот. vector<string> svecl(lO); // вектор из 10 элементов vector<string> svec2(24); // вектор из 24 элементов svec1.swap(svec2); После выполнения функции swap (), вектор svecl содержит 24 строки, а вектор svec2 — 10. Важнейшей особенностью функции swap () является то, что она не удаляет и не встав- ляет никаких элементов, а следовательно, ее выполнение занимает немного времени и не зависит от размера контейнера. Поскольку элементы не перемещаются, итераторы оста- ются корректными.
358 Часть II. Контейнеры и алгоритмы Поскольку элементы не перемещаются, итераторы продолжают относиться к тем же элементам, что и перед перестановкой, а следовательно, остаются корректными. Однако после выполнения функции swap () эти элементы находятся в другом кон- тейнере. Предположим, например, что итератор iter указывает на строку в позиции svecl [3]. После выполнения функции swap () итератор iter будет указывать на элемент в позиции svec2 [3 ]. Упражнения раздела 9.3.8 Упражнение 9.28. Напишите программу, присваивающую значения элементов списка указателей на символьные строки в стиле С (тип char*) элементам вектора строк. 9.4. Как увеличивается размер вектора При вставке или добавлении элемента в объект контейнера, сразу увеличивается размер такого объекта. Аналогично, при увеличении размера контейнера (функция resize О ), в него добавляются дополнительные элементы. Область памяти, необ- ходимая для хранения этих новых элементов, выделяется библиотекой. Как правило, разработчику не нужно выяснять подробностей работы библио- течного класса, ему достаточно знать лишь то, как его использовать. Однако в слу- чае векторов некоторая информация о его реализации будет весьма полезна. Что- бы обеспечить быстрый произвольный доступ к значениям, элементы векторов со- храняются подряд, т.е. каждый элемент располагается в области памяти рядом с предыдущим. Давайте рассмотрим, что происходит при добавлении элемента в вектор. По- скольку для ускорения индексации элементы вектора расположены в памяти после- довательно, среди них нет места для нового элемента. Поэтому приходится выделять для вектора новую область памяти, размер которой достаточен для размещения уже существующих элементов плюс один новый, а затем копировать элементы из преж- ней области в новую, затем копировать добавляемый элемент и освобождать преж- нюю область памяти. Если бы вектор осуществлял распределение памяти, копиро- вание значений и освобождение прежней области при каждом добавлении нового элемента, эффективность его работы была бы неприемлемо низка. Для контейнеров, которые не хранят элементы подряд, проблем резервирова- ния памяти не существует. Например, чтобы добавить элемент в список, библио- теке достаточно создать новый элемент и связать его с уже существующим спи- ском. Выделять новую область памяти и копировать все существующие элементы здесь не нужно. Вполне логично было бы всегда использовать списки, а не векторы. Но ничто не бывает бесплатным: для большинства приложений наилучшим контейнером являет- ся вектор. Дело в том, что разработчики библиотеки реализовали такую стратегию использования распределяемой памяти, которая минимизируют издержки сохране- ния элементов подряд. Таким образом, преимущество быстрого доступа к элемен- там, предоставляемое непрерывной последовательностью элементов, как правило, компенсируется неудобствами при их добавлении.
Глава 9. Последовательные контейнеры 359 Высокая эффективность работы с памятью, присущая векторам, достигается за счет резервирования несколько большего объема памяти, чем необходимо в настоя- щий момент. Вектор использует этот резерв для размещения новых элементов по мере их добавления. Таким образом удается избежать повторного создания контей- нера для каждого нового элемента. Точный объем резервируемой емкости зависит от конкретной реализации библиотеки. Стратегия резервирования существенно эф- фективней повторного создания контейнера для каждого нового элемента. Фактиче- ски его эффективность столь высока, что на практике вектор увеличивает свой раз- мер быстрее, чем список или двухсторонняя очередь. 9.4.1. Функции-члены capacity () и reserve () Детали работы вектора с распределяемой памятью относятся к его реализации, однако частично эта реализация доступна через интерфейс вектора. Класс вектора содержит две функции-члена, capacity () и reserve (), которые позволяют час- тично взаимодействовать с его реализацией, отвечающей за распределение памяти. Функция capacity () сообщает количество элементов, которое контейнер может создать прежде, чем ему понадобится занять больший объем памяти. Функция reserve () позволяет задать количество резервируемых элементов. Очень важно понять различие между емкостью (capacity) и размером (size). Размер — это количество элементов в векторе, а емкость — это количество элементов, которое вектор может содержать, не прибегая к следующей операции резервирования памяти. Чтобы проиллюстрировать взаимосвязь между размером и емкостью, рассмотрим следующую программу. vector<int> ivec; // размер нулевой; емкость зависит от реализации cout << "ivec: size: " << ivec.size() « " capacity: " « ivec.capacity() « endl; // присвоить вектору ivec 24 элемента for (vector<int>::size_type ix - 0; ix != 24; ++ix) ivec.push_back(ix); // размер 24; емкость на 24 элемента меньше, чем определено // в реализации cout « "ivec: size: " << ivec.size() « " capacity: " « ivec.capacity() « endl; При запуске на компьютере автора, эта программа отобразила следующий ре- зультат. ivec: size: 0 capacity: О ivec: size: 24 capacity: 32 Как известно, пустой вектор имеет нулевой размер, вполне очевидно, что библио- тека для пустого вектора также устанавливает нулевую емкость. При добавлении элементов в вектор, его размер составляет количество добавленных элементов. Ем- кость будет, по крайней мере, совпадать с размером, но может быть и больше. В дан- ной конкретной реализации, добавление 24 элементов по одному приводит к созда- нию емкости 32. Визуально текущее состояние вектора ivec можно представить так, как показано на рис. 9.1.
360 Часть II. Контейнеры и алгоритмы 0 12 ... 23 резервная емкость t t ivec.size() ivec.capacity() Рис. 9.1. Визуальное представление состоя- ния вектора ivec Теперь при помощи функции reserve () можно зарезервировать дополнитель- ное пространство. ivec.reserve (50); // задать емкость 50 элементов (можно и больше) // размер будет 24, а емкость - 50 или больше, если так // определено в реализации cout << "ivec: size: " << ivec.size() < < " capacity: " << ivec.capacity() << endl; Как видим, это изменило емкость (capacity), но не размер (size). ivec: size: 24 capacity: 50 Резервную емкость можно исчерпать следующим образом. // добавить элементы, чтобы исчерпать резервную емкость while(ivec.size() != ivec.capacity()) ivec.push_back(0) ; // размер будет 50, а емкость останется неизменной cout << "ivec: size: " << ivec.size() < < " capacity: " << ivec.capacity() << endl; Поскольку исчерпана только резервная емкость, вектору не нужно выделять но- вую область памяти. Фактически, пока емкость достаточна, вектору не нужно пере- распределять свои элементы. Результат выполнения свидетельствует о том, что резервная емкость в настоя- щий момент исчерпана, т.е. размер и емкость равны. ivec: size: 50 capacity: 50 Если теперь добавить новый элемент, вектор будет перераспределен, ivec.push_back(42); // добавить еще один элемент // размер будет 51, а емкость 51 или больше, если так // определено в реализации cout << "ivec: size: " << ivec.size() < < ” capacity: " << ivec.capacity() << endl; Результат выполнения этой части программы имеет следующий вид. ivec: size: 51 capacity: 100 Он свидетельствует о том, что в данной реализации класса vector использо- вана стратегия удвоения текущей емкости при каждом резервировании новой об- ласти памяти. Каждая реализация класса vector может иметь собственную стратегию размещения в памяти, однако она должна предоставлять функции reserve () и capacity (), при- / чем эт0 не должно приводить к резервированию новой области памяти, пока в этом не возникнет необходимость. То, какой именно объем памяти будет зарезервирован, зави- сит от конкретной реализации. Разные библиотеки используют разные стратегии. Кроме того, каждая реализация обязательно должна гарантировать эффективность функции push_back (), используемой для заполнения вектора. С технической точки
Глава 9. Последовательные контейнеры 361 зрения время создания п элементов вектора составляет время выполнения функции push_back () для первоначально пустого вектора, умноженное на п. Упражнения раздела 9.4.1 Упражнение 9.29. Объясните различие между емкостью вектора и его размером. Почему понятие емкости столь важно для контейнера, хранящего элементы подряд, а не, например, для списка? Упражнение 9.30. Напишите программу, позволяющую исследовать реализованную библиотекой стратегию распределения памяти. Используйте эту программу для объектов векторов. Упражнение 9.31. Может ли контейнер иметь емкость, меньшую, чем его размер? Равна ли ем- кость используемому размеру? А первоначально? А после добавления элемента? Почему? Упражнение 9.32. Объясните, что выполняет следующая программа. vector<string> svec; svec.reserve(1024); string text_word; while(cin >> text_word) svec,push_back(text_word); svec.resize(svec.size()+svec.size()/2); Если программа прочитает 256 слов, какова наиболее вероятная емкость контейнера? Что про- изойдет, если программа прочитает 512,1 000 и 1 048 слов? 9.5. Как выбрать тип контейнера Как было продемонстрировано в предыдущем разделе, стратегия выделения памя- ти для хранении новых элементов существенно влияет на производительность кон- тейнера. Используя интеллектуальные методы реализации, авторы библиотеки мини- мизировали затраты времени на выделение областей памяти. Последовательный ха- рактер хранения элементов обуславливает следующие немаловажные факторы: издержки на добавление и удаление элементов из середины контейнера; издержки на непоследовательный доступ к элементам контейнера. Частота применения этих операций в программе определяет тип используемого контейнера. Вектор и двухсторонняя очередь обеспечивают быстрый непоследова- тельный доступ к элементам за счет удорожания операций добавления и удаления элементов в середине контейнера. Список обеспечивает быструю вставку и удаление в любом месте, но за счет непоследовательного доступа к элементам. Как на выбор контейнера влияет вставка Память списка состоит из нескольких несмежных участков, но он позволяет пе- ребирать элементы как вперед, так и назад, а также быстро вставлять и удалять эле- менты в любой позиции. Вставка и удаление элементов списка не требует перемеще- ния остальных элементов. С другой стороны, список не обеспечивает произвольного доступа к элементам. Доступ к элементу требует перебора предыдущих элементов. Вставка (или удаление) элемента в любой позиции, за исключением последнего элемента вектора, требует сдвига всех элементов вправо от вставленного (или влево от удаленного). Например, если удалить элемент номер 23 в векторе из 50 элементов, каждый из элементов после 23-го придется переместить на одну позицию вперед.
362 Часть II. Контейнеры и алгоритмы Иначе в последовательности элементов вектора появится просвет и его данные пере- станут быть непрерывными. Двухсторонняя очередь (deque) — это более сложная структура данных. Она га- рантируют, что добавление и удаление элементов с любого конца двухсторонней очереди осуществляется довольно быстро. Добавление и удаление элементов из се- редины осуществляется значительно дольше. Двухсторонняя очередь обладает не- которыми из возможностей списка и вектора. Подобно вектору, вставка и удаление элементов в середину двухсторонней оче- реди осуществляется медленно. В отличие от вектора, двухсторонняя очередь позволяет быстро вставлять и уда- лять элементы в начало контейнера. Добавление элементов в начало и конец двухсторонней очереди не делает итера- торы некорректными. Удаление первого или последнего элемента не делает не- корректными никакие итераторы, за исключением относящихся к удаленным элементам. Вставка или удаление элемента в других местах двухсторонней оче- реди сделает все ее итераторы недопустимыми. Выбор контейнера и доступ к элементам И вектор и двухсторонняя очередь обеспечивают эффективный произвольный доступ к своим элементам. То есть можно быстро и эффективно обращаться к эле- менту номер 5, а затем к элементу 15, 7 и т.д. Произвольный доступ к вектору столь эффективен потому, что положение каждого элемента фиксировано и может быть вычислено по смещению от начала вектора. Намного медленнее происходит переход между элементами в списке, поскольку он осуществляется последовательно, от од- ного элемента к другому. Перемещение от 5-го элемента к 15-му подразумевает по- сещение каждого промежуточного элемента. Как правило, предпочтительнее использовать вектор, а не другой контейнер. Советы по выбору контейнера Существует несколько эмпирических правил, которые можно использовать при выборе используемого контейнера. 1. Если в программе необходим произвольный доступ к элементам, используйте вектор или двухстороннюю очередь. 2. Если в программе необходимо часто вставлять или удалять элементы в середине контейнера, используйте список. 3. Если в программе необходимо вставлять или удалять элементы в начале или в конце, но не в середине контейнера, используйте двухстороннюю очередь. 4. Если элементы необходимо вставлять в середину контейнера только при чтении с устройства ввода, а затем требуется произвольный доступ к элементам, имеет смысл рассмотреть возможность чтения данных в список, их переупорядочива- ние для последующего доступа и копирование содержимого списка в вектор.
Глава 9. Последовательные контейнеры 363 Но что если программа должна беспорядочно обращаться к элементам в середине контейнера, а также вставлять и удалять их произвольно? Решение будет зависеть от того, что в данном случае хуже: отсутствие произ- вольного доступа к элементам списка или необходимость копирования элементов при вставке и удалении в случае вектора или двухсторонней очереди. В общем слу- чае выбор типа контейнера определяет преобладающая операция приложения, т.е. каких операций больше: доступа или вставки и удаления. В конце концов, если это принципиально, в поиске правильного решения можно опробовать каждый контейнер в соответствующих условиях и выявить наиболее эффективный. Выбрав тип контейнера, постарайтесь написать код приложения так, чтобы в нем использовались операции, совпадающие у вектора и списка, т.е. используйте итера- fe ЛкомеиЭуем ТОРЫ> а не индексирование и избегайте произвольного доступа к элементам. В та- ком коде, при необходимости, будет проще заменить вектор на список. Упражнения раздела 9.5 Упражнение 9.33. Какой из контейнеров (вектор, двухсторонняя очередь или список) более всего подходит для приведенных ниже задач? Объясните, почему. Если нельзя отдать предпочтение тому или иному контейнеру, объясните, почему? (а) Чтение неизвестного заранее количества слов из файла, для генерации фраз на английском языке. (Ь) Чтение фиксированного количества слов и добавление их в контейнер в алфавитном порядке по мере ввода. В следующей главе будет продемонстрировано, что для решения этой пробле- мы лучше подходят ассоциативные контейнеры. (с) Чтение неизвестного заранее количества слов. Все слова добавляются в конец контейнера. Удалять элементы предполагается из начала контейнера. (d) Чтение неизвестного заранее количества целых чисел из файла. Числа необходимо отсортиро- вать и вывести на стандартное устройство вывода. 9.6. Еще раз о строках Класс string был представлен в разделе 3.2 (стр. 104). Табл. 9.12 (стр. 364) обобщает операции со строками, рассматриваемые в этом разделе. Кроме операций, уже использованных ранее, строки обладают большинством функциональных возможностей последовательного контейнера. Строку можно даже рассматривать как подобие контейнера для символов. За некоторым исключением, класс string поддерживают те же операции, что и вектор. Исключением являются операции, позволяющие использовать контейнер подобно стеку: класс string не обладает функциями front (), back () и pop_back (). Класс string поддерживает следующие контейнерные операции. Определения типов, включая типы итераторов, перечисленные в табл. 9.5 (стр. 344). Конструкторы, перечисленные в табл. 9.2 (стр. 335), за исключением того, что получает размер как единственный параметр.
364 Часть II. Контейнеры и алгоритмы Функции добавления элементов, перечисленные в табл. 9.7 (стр. 346), которые поддерживает вектор. Обратите внимание: ни класс vector, ни класс string не поддерживают функцию push_f ront (). Функции размера, перечисленные в табл. 9.8 (стр. 351). Функция at () и операции индексирования, перечисленные в табл. 9.9 (стр. 353). Класс string не поддерживает функции back () и front (), указанные в этой таблице. Функции begin () и end (), перечисленные в табл. 9.6 (стр. 345). Функции erase () и clear О, перечисленные в табл. 9.10 (стр. 354). Класс string не поддерживает функции pop_back () и pop_f ront (). Операции присвоения, перечисленные в табл. 9.11 (стр. 356). Подобно элементам вектора, символы в строке сохраняются подряд. Следова- тельно, класс string поддерживает функции capacity () и reserve (), опи- санные в разделе 9.4 (стр. 358). Таблица 9.12. Операции со строками, описанные в разделе 3.2 string s; String s(ср); string s(s2); is >> s; os << s; getline(is, s) si + s2 si += s2 Реляционные опера- торы Определяет новую пустую строку по имени s Определяет новый объект класса string, инициализированный содержимым строки ср в стиле С (с нулевым символом в конце) Определяет новый объект класса string, инициализированный копией содер- жимого строки S2 Читает разделяемые пробелами данные из потока ввода is в строку s Записывает строку s в поток вывода os Читает символы из потока ввода i s в строку s до первого символа новой строки Конкатенирует строки si и s2 в новую строку Добавляет к строке si содержимое строки s2 Для сравнения строк можно использовать операторы равенства (== и > =) и срав- нения (<, <=, > и >=). При сравнении строк регистр символов учитывается Как уже упоминалось, строка обладает функциональными возможностями кон- тейнера. Иными словами, программу, использующую вектор, можно переписать так, чтобы вместо него применялась строка. Например, для построчного вывода симво- лов строки на стандартное устройство вывода можно использовать итераторы. string s("Hiya!"); string: : iterator iter = s.beginO; while (iter != s.endO) cout « *iter++ << endl; // постфиксный инкремент: вывести II прежнее значение Не удивительно, что этот код выглядит практически идентичным коду со стр. 188, vector<int>. Кроме операций, присущих и строкам, и контейнерам, класс string поддер- живает ряд других функций, специфических для строк. В остальной части этого раздела рассматриваются операции, специфические для строк. Сюда относятся
Глава 9. Последовательные контейнеры 365 дополнительные версии контейнерных операторов, а также совершенно новые функции. Дополнительные функции класса string рассматриваются начиная с раздела 9.6.3 (стр. 369). Дополнительные версии контейнерных операторов, предназначенных для строк, манипулируют атрибутами, которые специфичны именно для строк и не используются в контейнерах. Некоторые функции, например, позволяют указывать аргументы, которые являются указателями на символьные массивы. Эти функции обеспечивают взаимосвязь между объектами библиотечного класса string и сим- вольными массивами, а также символьными строками с нулевым символом в кон- це или без него. Другие версии позволяют использовать индексы, а не итераторы. Эти версии работают в зависимости от позиции: т.е. можно задать исходную пози- цию, а в некоторых случаях и количество элементов, определяющих диапазон ис- пользуемых элементов. Упражнения раздела 9.6 Упражнение 9.34. Используйте итераторы для перевода символов строки в верхний регистр. Упражнение 9.35. Используйте итераторы для поиска и удаления всех заглавных букв из строки. Упражнение 9.36. Напишите программу, которая инициализирует строку содержимым вектора vector<char>. Упражнение 9.37. При условии, что данные необходимо читать в строку по одному символу и этих символов будет по крайней мере 100, как можно улучшить эффективность такой программы? В библиотечном классе string определено множество функций, которые исполь- зуют похожие схемы. Поскольку этих функций очень много, данный раздел можно считать лишь ознакомительным. 9.6.1. Дополнительные способы создания строк Класс string поддерживает все конструкторы, перечисленные табл. 9.2 (стр. 335), кроме одного, получающего размер как единственный параметр. Строку можно соз- давать следующим образом: как пустую строку (конструктор без аргументов), как копию другой строки, как копию части другой строки (передав два итератора) и как последовательность, состоящую из заданного количества указанного символа. string si; string s2(5, 'a'); string s3(s2); string s4(s3.begin() s3.begin() // si пустая строка II s2 == "aaaaa" // s3 копия s2 + s3.size() / 2); // s4 -= "aa" Кроме этих конструкторов, класс string предоставляет три других способа соз- дания строки. Существует также использовавшийся ранее конструктор, который по- лучает указатель на первый символ массива символов, завершающегося нулевым символом. Существует конструктор, получающий указатель на элемент в символь- ном массиве и количество подлежащих копированию символов. Поскольку этот конструктор получает количество подлежащих копированию символов, массив не- обязательно должен заканчиваться нулевым символом.
366 Часть II. Контейнеры и алгоритмы char *ср = "Hiya"; char c_array[] = "World!!!!" char no_null[] = {'H', 'i'}; string sl(cp); string s2(c_array, 5); string s3(c_array + 5, 4); string s4(no_null); // string s5(no_null, 2); // массив с нулевым символом в конце // нулевой символ в конце // без нулевого символа в конце // si == "Hiya" // s2 == "World" // s3 == "! I!! " ошибка во время выполнения: no_null не имеет нулевого символа в конце // ok: s5 == "Hi" Строка s 1 создана при помощи конструктора, получающего указатель на первый символ массива с нулевым символом в конце. В создаваемую строку копируются все символы массива, за исключением завершающего нулевого символа. Для создания и инициализации строки s 2 использует второй конструктор, полу- чающий указатель и количество. В данном случае копирование начинается с симво- ла, обозначенного указателем, и продолжается до символа, указанного вторым аргу- ментом. Следовательно, строка s2 является копией первых пяти символов из массива с_аггау. Не забывайте, что переданное имя массива автоматически преобразуется в указатель на его первый элемент. Безусловно, передать можно указатель не только на начало массива. Строка s3 инициализируется четырьмя восклицательными зна- ками, ведь конструктору передан указатель на первый восклицательный знак в мас- сиве с_аггау. Инициализирующие значения строк s4 и s5 не являются строками в стиле С. Определение строки s4 является ошибкой. Эта форма инициализации может быть применена только с массивом, содержащим в конце нулевой символ. Передача мас- сива, не содержащего нулевой символ, — это серьезная ошибка (раздел 4.3, стр. 154), причем компилятор не сможет ее обнаружить. Поведение такого кода во время вы- полнения непредсказуемо. Инициализация строки s 5 вполне корректна, поскольку здесь конструктор полу- чает количество копируемых символов. Пока последний копируемый символ нахо- дится внутри массива, не имеет никакого значения, завершается ли массив нулевым символом или нет. Таблица 9.13. Дополнительные способы создания строк string s(cp, п) Создает строку s как копию п символов массива ср string s (s2, pos2) Создает строку s как копию символов строки s2 начиная с указанного ин- дексом pos2. Если pos2 > s2. size (), результат непредсказуем string s (s2, pos2, 1еп2) Создает строку s как копию 1еп2 символов строки s2 начиная с указанного индексом pos2. Если pos2 > s2. size (), резуль- тат непредсказуем. Независимо от значения 1еп2, копирует по крайней мере s2. size () - pos2 символов. Обратите внимание: значения п, 1еп2 и pos2 имеют беззнаковый тип Использование подстрок при инициализации Следующая пара конструкторов позволяет создавать строку как копию подстро- ки символов другой строки.
Глава 9. Последовательные контейнеры 367 Первые два аргумента — это строка, из которой копируются символы и исходная позиция. В версии с двумя аргументами, создаваемая строка инициализируется сим- волами начиная с указанной позиции до конца строки-аргумента. Третий аргумент позволяет указать количество копируемых символов. В данном случае, начиная с за- данной позиции, копируется столько символов, сколько указано (конец строки не достигнут). То есть при создании строки s7 будет скопировано два символа из стро- ки s 1 начиная с нулевой позиции. При создании строки s 8 будет скопировано толь- ко четыре символа из запрошенных девяти. Независимо от того, сколько символов указано для копирования, библиотека не позволит выйти за размер строки. 9.6.2. Дополнительные способы изменения строк Большинство контейнерных операций, которые поддерживает строка, осуществ- ляются с использованием итераторов. Например, функция erase () получает один или два итератора, определяющих элемент или диапазон элементов, подлежащих удалению из контейнера. Аналогично, первый аргумент каждой версии функции insert () является итератором, указывающим позицию, перед которой вставляется значение (значения), представляемое другими аргументами. Хотя класс string поддерживает эти ориентированные на итераторы операции, он предоставляет также такие функции, которые используют индекс. Индекс позволяет указать начальный элемент для функции erase () или позицию, перед которой функция insert () вставит значение. Списков функций, одинаковых для строк и контейнеров, приведен в табл. 9.14, а список функций, специфических только для строк, — в табл. 9.15. Таблица 9.14. Функции, одинаковые для строк и контейнеров s.insert (р, t) s. insert(р, n, t) s.insert(р, Ь, е) s.assign(Ь, е) s.assign(n, t) s.erase(p) s.erase(b, e) Вставляет копию значения t перед элементом, указанным итератором р. Воз- вращает итератор на вставленный элемент Вставляет п копий значения t перед элементом р. Возвращает тип void Вставляет элементы в диапазон, указанный итераторами ь и е перед элемен- том р. Возвращает тип void Заменяет значение строки s элементами диапазона, указанного итераторами ь и е. Для строк возвращает значение s, а для контейнеров — тип void Заменяет значение строки s количеством п копий значения t. Для строк воз- вращает значение s, а для контейнеров — тип void Удаляет элемент, указанный итератором р. Возвращает итератор на элемент после удаленного Удаляет элементы в диапазоне, указанном итераторами ь и е. Возвращает итератор на первый элемент непосредственно после удаленного диапазона Аргументы, связанные с позицией Специфические для строк версии функций получают аргументы, подобные аргу- ментам дополнительных конструкторов, описанных в предыдущем разделе. Эти функции позволяют работать с позициями элементов, когда используемые аргумен- ты являются указателями на массивы символов, а не на строки.
368 Часть II. Контейнеры и алгоритмы Все контейнеры, например, позволяют указать диапазон удаляемых элементов при помощи двух итераторов. Для строк также можно указать диапазон, передав ис- ходную позицию и количество удаляемых элементов. Предположим, что строка s содержит по крайней мере пять символов. Таким образом, пять последних символов можно удалить следующим образом. s.erase(s.size() - 5, 5); // удалить пять последних символов из // строки s Аналогично, в контейнер можно вставить указанное количество значений перед элементом, обозначенным итератором. В случае строк, точку вставки можно задать при помощи индекса, а не итератора. s.insert(s.size(), 5, // добавить пять восклицательных знаков // в конец строки s Определение нового содержимого Вставляемые в строку или присваиваемые символы могут быть взяты из символь- ного массива или другой строки. Например, чтобы присвоить или добавить в строку значения, можно использовать символьный массив с нулевым символом в конце. char *ср = "Stately plump Buck"; string s; s.assign(cp, 7); // s == "Stately" cout << s << endl; s.insert(s.size(), cp + 7); // s == "Stately plump Buck" Аналогично, копию одной строки в другую можно вставить следующим образом. s = "some string"; s2 = "some other string"; // 3 эквивалентных способа вставки всех символов из строки s2 в // на чало строки s II вставить диапазон от итератора s.begin() s.insert(s.begin(), s2.begin(), s2.end()); // вставить копию строки s2 перед позицией 0 в строку s s.insert(0, s2); // вставить s2.size() символов из строки s2 начиная с s2[0] // перед s[0] s.insert(0, s2, 0, s2.size()); Таблица 9.15. Версии функций, специфических только для строк s. insert (pos, n, с) s.insert(pos, s2) s. insert (pos, s2, pos2, s.insert(pos, cp, len) s.insert(pos, ср) s.assign(s2) Вставляет n копий символа с перед элементом, указанным ин- дексом pos Вставляет копию строки s2 перед элементом, указанным ин- дексом pos len) Вставляет len символов строки s2 перед элементом, указан- ным индексом pos, начиная с позиции pos2 Вставляет len символов из массива ср перед элементом, ука- занным индексом pos Вставляет копию строки ср с завершающим нулевым символом перед элементом, указанным индексом pos Заменяет значение строки s копией содержимого строки s2
Глава 9. Последовательные контейнеры 369 s.insert (pos, n, с) s.assign(s2, pos2, len) s.assign(cp, len) s.assign(cp) s.erase(pos, len) Окончание табл. 9.15 Вставляет n копий символа с перед элементом, указанным ин- дексом pos Заменяет значение строки s копией len символов из строки s2 начиная с индекса pos2 в строке s2 Заменяет значение строки s количеством len символов мас- сива ср Заменяет значение строки s массивом ср с нулевым символом в конце Удаляет len символов начиная с позиции pos Если не указано иное, все эти функции возвращают ссылку на строку S 9.6.3. Операции, специфические только для строк Класс string предоставляет несколько других функций, которыми контейнеры не обладают. Функция substr (), которая возвращает подстроку из текущей строки. Функции append () и replace (), которые модифицируют строку. Семейство функций f ind (), обеспечивающих поиск в строке. Функция substr () Функция substr () позволяет получить подстроку из переданной строки. Для этого ей достаточно передать исходную позицию и количество символов. Она созда- ет новую строку, которая содержит указанное количество символов исходной строки (или до конца исходной строки) начиная с заданной позиции. string s("hello world"); / / возвращает подстроку из 5 символов начиная с позиции 6 string s2 = s.substr(б, 5); // s2 = "world" В качестве альтернативы, тот же самый результат можно получить следующим образом. / / возвращает подстроку начиная с позиции 6 и до конца строки s string s3 = s.substr(6); // s3 = "world" Таблица 9.16. Операции с подстроками s. substr (pos, n) Возвращает строку, содержащую n символов из строки s начиная с позиции pos s. substr (pos) Возвращает строку, содержащую символы, начиная с позиции pos и до конца строки S s. substr () Возвращает копию строки s
370 Часть II. Контейнеры и алгоритмы Функции append () и replace () Существует шесть перегруженных версий функции append () и десять версий функции replace (). Функции append () и replace () перегружены так, чтобы в них использовался набор аргументов, перечисленных в табл. 9.18. Эти аргументы позволяют задать символы, добавляемые в результирующую строку. Функция append () добавляет символы в конец строки, а функция replace () заменяет ими указанный диапазон существующих символов исходной строки. Функция append () — это упрощенная версия для вставки символов в конец. string s("C++ Primer"); // инициализировать строку s II значением "C++ Primer" s.append(" 3rd Ed."); // s == "C++ Primer 3rd Ed." I/ эквивалент s. append (" 3rd Ed.") s.insert(s.size(), " 3rd Ed."); Функция replace () удаляет указанный диапазон символов и вставляет вместо них новый набор. Это аналогично вызову функций erase () и insert (). Таблица 9.17. Функции изменения содержимого строки (аргументы args приведены в табл. 9.18) s. append (args) Добавляет аргументы args в строку s. Возвращает ссылку на строку S s. replace (pos, len, args) Удаляет len символов из строки s начиная с позиции pos и за- меняет их символами, сформированными аргументами args. Воз- вращает ссылку на строку s. Для этой версии аргументы Ь2, е2 неприменимы s. replace (b, е, args) Удаляет диапазон символов, обозначенный итераторами ь и е, а затем заменяет их аргументами args. Возвращает ссылку на стро- ку s. Для этой версии аргументы s2, pos2,1еп2 неприменимы Десять разных версий функции replace () отличаются друг от друга способом ука- зания удаляемых и заменяющих их символов. Первые два аргумента определяют диапа- зон удаляемых элементов. Его можно указать либо парой итераторов, либо индексом и количеством. Остальные аргументы определяют, как вставить новые символы. Функцию replace () можно рассматривать как сокращенный вариант для уда- ления некоторых символов и вставки вместо них других символов. // начиная с позиции 11, удалить 3 символа и вставить // значение "4th" s.replace(11, 3, "4th"); // s == "C++ Primer 4th Ed." s.erase(ll, 3); // s == "C++ Primer Ed." s.insert(11, "4th"); // s == "C++ Primer 4th Ed." Размеры удаляемого и вставляемого текста вовсе не обязаны совпадать. В приведенном выше обращении к функции replace О, размер удаляемого и добавляемого текста совпал случайно. Вполне можно добавить строку больше или меньше удаленной.
Глава 9. Последовательные контейнеры 371 s.replace(11, 3, "Fourth"); // s == "C++ Primer Fourth Ed." Здесь удаляются три символа и вместо них добавляется шесть символов. Таблица 9.18. Аргументы функций append () и replace () s2 Строка s2 S2 , pos2 , 1еп2 1еп2 символов из строки S2 начиная С ПОЗИЦИИ pos2 ср Массив ср с нулевым символом в конце ср, 1еп2 1еп2 символов из символьного массива ср п, с п копий символа с Ь2, е2 Символы в диапазоне, указанном итераторами Ь2 и е2 9.6.4. Операции поиска строк Класс string предоставляет шесть вариантов функций поиска. Все они возвра- щают либо значение типа string: :size_type, которое является индексом най- денного элемента, либо специальное значение, string: :npos, если ничего не най- дено. Класс string определяет тип проз как гарантированно большой, чтобы со- держать любой допустимый индекс. Каждый из вариантов функции поиска существует в четырех версиях, которые отличаются наборами аргументов. Аргументы функций поиска перечислены в табл. 9.20. Эти функции отличаются в основном тем, что они ищут: одиночный сим- вол, другую строку в стиле С, строку с завершающим нулевым символом или ука- занный набор символов в символьном массиве. Таблица 9.19. Строковые функции поиска (аргументы args приведены в табл. 9.20) s.find(args) s.гfind(args) s.find_first_of(args) s.find_last_of(args) s.find_first_not_of(args) s.find_last_not_of(args) Ищет первое местоположение аргумента args в строке s Ищет последнее местоположение аргумента args в строке s Ищет первое местоположение любого символа аргумента args в строке s Ищет последнее местоположение любого символа аргумента args в строке s Ищет первое местоположение символа в строке s, который отсут- ствует в аргументе args Ищет последнее местоположение символа в строке s, который от- сутствует в аргументе args Поиск точного соответствия Самой простой функцией поиска является функция f ind (). Она находит первое местоположение переданного аргумента и возвращает его индекс или значение проз, если соответствующее значение не найдено. string name("AnnaBelle"); string::size_type posl = name.find("Anna"); // posl
372 Часть II. Контейнеры и алгоритмы в строке Возвращает значение 0, т.е. индекс, по которому подстрока "Anna" расположена AnnaBelle". По умолчанию функции поиска (и другие строковые операции с символами) используют встроенные операторы для сравнения символов в строке. В результате эти функции (и другие строковые операции) становятся чувствительны к регистру. При поиске значения в строке, регистр символов имеет значение. string lowercase("annabelle"); posl = lowercase.find("Anna"); // posl == npos Этот код присвоит переменной posl значение npos, поскольку строка "Anna" не соответствует строке " anna ". Таблица 9.20. Аргументы строковых функций поиска с, pos Ищет символ с начиная с позиции pos в строке s. Значением аргумента pos по умол- чанию является о s2, pos Ищет строку s2 начиная с позиции pos в строке s. Значением аргумента pos по умол- чанию является о ср, pos Ищет строку с завершающим нулевым символом в стиле С ср начиная с позиции pos в строке s. Значением аргумента pos по умолчанию является о ср, pos, п Ищет первые п символов в массиве ср начиная с позиции pos в строке s. Аргументы pos и п не имеют значений по умолчанию Функции поиска возвращают значение типа string: :size_type. Для сохране- ния значения, возвращаемого функцией find О, используется объект именно этого типа. Поиск любого символа Немного сложнее найти соответствие любому символу, содержащемуся в строке. Например, следующий код находит первую цифру внутри строки name. string numerics("0123456789" ) ; string name("r2d2"); string::size_type pos = name.find_first_of(numerics); cout « "found number at index: " « pos « " element is " << name[pos] « endl; В этом примере переменной pos присвоено значение 1 (помните, индексирова- ние элементов строки начинается с 0). Откуда начинать поиск Функции поиска можно передать необязательный аргумент исходной позиции. Этот необязательный аргумент указывает позицию, с которой начинается поиск. По умолчанию значением этого аргумента является нуль. Общепринятой практикой программирования является использование этого аргумента в цикле перебора стро- ки при поиске всех местоположений искомого значения. Таким образом, для подсче- та всех подстрок " г2 d2" в строке name можно применить следующий код.
Глава 9. Последовательные контейнеры 373 string::size_type pos = 0; I/ каждый цикл переводит pos на следующий символ в строке name while ((pos - name.find_first_of(numerics, pos)) != string::npos) { cout « "found number at index: " « pos << " element is " « name[pos] << endl; ++pos; // перевести на следующий символ } В данном случае переменная pos инициализируется нулевым значением, что- бы при первой итерации цикла while поиск в строке name начинался с позиции 0. Условие цикла while присваивает переменной pos индекс первой найденной подстроки начиная с текущего значения pos. Пока функция f ind_f irst_of () возвращает допустимый индекс, результат отображается, а значение pos увели- чивается. Если не увеличивать значение переменной pos в конце этого цикла, он никогда не завершится, поскольку при последующих итерациях поиск начнется сначала и найден будет тот же элемент. Поскольку значение npos так и не будет возвращено, цикл никогда не завершится. Увеличение значения переменной pos очень важно, поскольку это гарантирует, что поиск будет возобновляться со следующей позиции после найденного элемента. Поиск несоответствия Функция f ind_f irst_not_of () позволяет найти первую позицию, где содер- жимое аргумента не соответствует строке. Например, для поиска первого нечислово- го символа в строке можно применить следующий код. string numbers("0123456789"); string dept("03714p3"); // возвращает значение 5, которое является индексом символа 'р' string::size_type pos = dept.find_first_not_of(numbers); Поиск в обратном направлении Каждая из описанных до сих пор функций поиска выполняется слева направо1. Библиотека предоставляет аналогичный набор функций, которые просматривают строку справа налево (т.е. от конца к началу). Функция-член г find () ищет послед- нюю, т.е. расположенную справа, позицию искомой подстроки. string river("Mississippi"); string::size_type first—pos = river.find("is"); string::size_type last—pos = river.rfind("is"); // возвращает 1 // возвращает 4 Функция f ind () возвращает индекс 1 указывая, что подстрока " is" первый раз встречается начиная с позиции 1, а функция г find () возвращает индекс 4, указы- вая начало последнего местонахождения подстроки "is". 1 То есть осуществляет поиск от начала строки до конца. — Примеч. ред.
374 Часть II. Контейнеры и алгоритмы Функции find_last О Функция find_last() работают подобно функциям f ind_f irst (), но воз- вращают последнее местоположение, а не первое. Функция f ind_last_of () ищет последний символ, который соответствует любому элементу искомой строки. Функция f ind_last_not_of () ищет последний символ, который не соответ- ствует ни одному элементу искомой строки. Каждая из этих функций имеет второй необязательный аргумент, который ука- зывает позицию начала поиска. Упражнения раздела 9.6.4 Упражнение 9.38. Напишите программу, которая находит в строке "ab2c3d7R4E6" каждую цифру, а затем каждую букву. Напишите две версии программы: с использованием функции f ind_f irst_of () И функции f ind_f irst_not_of (). Упражнение 9.39. Напишите программу, которая использует приведенные ниже строки при под- счете количества слов в строке sentence и выявляет самые большие и самые маленькие из них. Если самую большую или самую маленькую длину имеют несколько слов, отобразите их все. string linel = "We were her pride of 10 she named us:"; string line2 = "Benjamin, Phoenix, the Prodigal" string line3 = "and perspicacious pacific Suzanne"; string sentence = linel + ' ' + line2 + 1 1 + line3; 9.6.5. Сравнение строк Как уже было продемонстрировано в разделе 3.2.3 (стр. 109), в классе string определены все реляционные операторы, которые позволяют выяснять равенство (==) и неравенство (! =) двух строк, а также сравнивать их (<, <=, >, >=). Сравнение строк осуществляется лексикографически (lexicographical), т.е. оно происходит с уче- том регистра символов в порядке возрастания, т.е. как в словаре. string cobol_program_crash("abend"); string cplus_program crash("abort"); Здесь строка cobol_program_crash меньше, чем строка cplus_jDrogram_crash. Операторы отношения сравнивают две строки символ за символом, пока не встре- тится позиция, где строки отличаются. Результат сравнения строк зависит от ре- зультата сравнения этих несовпадающих символов. В данном случае первые несов- падающие символы — это ' е' и ' о'. В английском алфавите символ ' е' располо- жен раньше символа ' о', и поэтому он меньше, а следовательно, значение " abend" меньше, чем значение " abort". Если строки имеют разную длину, а их содержимое совпадает, короткая строка будет меньше, чем длинная. Функция compare () Кроме операторов сравнения, класс string предоставляет набор перегруженных версий функции compare (), которые осуществляют лексикографическое сравне-
Глава 9. Последовательные контейнеры 375 ние. Работа этих функций подобна работе функции st гетр () из библиотеки С (раздел 4.3, стр. 156). si.compare(args) ; В данном случае функция compare () может вернуть одно из трех значений. 1. Положительное значение, если строка si больше строки, представленной аргу- ментом args. 2. Отрицательное значение, если строка si меньше строки, представленной аргу- ментом args. 3. Значение 0, строка si равна строке, представленной аргументом args. Рассмотрим пример. // возвращает отрицательное значение cobol_program_crash.compare(cplus_program_crash); // возвращает положительное значение cplus_program_crash.compare(cobol_program_crash); Таблица 9.21. Функция сравнения строк s.compare(s2) s.compare(posl, nl, s2) s.compare(posl, nl, s2, pos2, n2) s.compare(cp) s.compare(posl, nl, cp) s.compare(posl, nl, cp, n2) Сравнивает строку s co строкой s2 Сравнивает nl символов начиная с позиции posl из строки s со строкой S2 Сравнивает nl символов начиная с позиции posl из стро- ки s со строкой s2 начиная с позиции pos2 в строке s2 Сравнивает строку s со строкой ср, завершающейся ну- левым символом Сравнивает nl символов начиная с позиции posl из строки s со строкой ср Сравнивает nl символов начиная с позиции posl из строки s со строкой ср начиная с символа п2 Набор из шести перегруженных функций compare () позволяет сравнивать строки или подстроки. Они позволяют также сравнить строку с символьным масси- вом или его частью. char second_ed[] = "C++ Primer, 2nd Edition"; string third_ed("C++ Primer, 3rd Edition"); string fourth_ed("C++ Primer, 4th Edition"); // сравнить строку C++ co строкой в стиле С fourth_ed.compare(second_ed); // ok: second_ed - строка с нулевым II символом в конце // сравнить подстроки fourth_ed и third_ed fourth_ed.compare(fourth_ed.find("4th"), 3, third_ed, third_ed.find("3rd"), 3); Интересен второй вызов функции compare (). Здесь использована та версия пе- регруженной функции compare (), которой передаются пять аргументов. Для поис- ка позиции начала подстроки "4th" используется функция find (). Начиная с этой позиции происходит сравнение трехсимвольной подстроки из строки third_ed. Эта подстрока начинается с позиции, возвращенной функцией f ind () при поиске
376 Часть II. Контейнеры и алгоритмы подстроки "3rd", и снова сравниваются три символа. По существу, здесь происхо- дит сравнение подстрок "4th" и "3rd". Упражнения раздела 9.6.5 Упражнение 9.40. Напишите программу, которая имеет две следующие строки. string ql("When lilacs last in the dooryard bloom'd"); string q2("The child is father of the man"); Используя функции assign () и append (), создайте следующую строку. string sentence("The child is in the dooryard"); Упражнение 9.41. Напишите программу, которая имеет две следующие строки. string genericl("Dear Ms Daisy:"); string generic2("MrsMsMissPeople"); Реализуйте функцию, приведенную ниже. string greet(string form, string lastname, string title, string::size_type pos, int length); Где для замены значения "Daisy" в параметре lastname используется функция replace (), а параметры pos и length являются индексом и количеством символов в строке generic2, используемым для замены значения "Ms". string lastName("AnnaP"); string salute = greet(genericl, lastName, generic2, 5, 4); Например, приведенный выше вызов возвращает следующую строку. Dear Miss AnnaP: 9.7. Адаптеры контейнеров Кроме последовательных контейнеров, библиотека предоставляет три адаптера по- следовательного контейнера: queue (очередь), priority_queue (приоритетная оче- редь) и stack (стек). Адаптер (adaptor2) — это фундаментальная концепция библио- теки. Существуют адаптеры контейнера, итератора и функции. По существу, адап- тер — это механизм для создания чего-либо одного, подобного другому. Адаптер контейнера получает контейнер существующего типа и заставляет его действовать по- добно другому абстрактному типу. Например, адаптер stack получает любой из по- следовательных контейнеров и заставляет его работать подобно стеку. Функции и ти- пы данных, общие для всех адаптеров контейнеров, перечислены в табл. 9.22 (стр. 377). Чтобы использовать адаптер, необходимо подключить соответствующий заголовок. #include<stack> #include<queue> // адаптер stack II оба адаптера, queue и priority_queue Инициализация адаптера Для каждого адаптера определено два конструктора: стандартный конструктор (создающий пустой объект) и конструктор, который после получения контейнера создает его копию как базовое значение. Предположим, например, что deq представ- 2 Здесь и везде в оригинале именно adaptor, а не adapter. — Примеч. ред.
Глава 9. Последовательные контейнеры 377 ляет собой двухстороннюю очередь deque< int >. Следующий код позволяет ини- циализировать новый стек. stack<int> stk(deq); // копирует элементы deq в stk Переопределение базового типа контейнера По умолчанию стек и очередь реализованы при помощи двухсторонней очереди, а приоритетная очередь реализована на базе вектора. Заданный по умолчанию тип контейнера можно переопределить, указав имя типа последовательного контейнера в качестве второго аргумента при создании адаптера. // пустой стек, реализованный поверх вектора stack< string, vector<string> > str_stk; // str_stk2 реализован поверх вектора и содержит копию svec stack<string, vector<string> > str_stk2(svec); Существуют некоторые ограничения на применение контейнеров с определен- ными адаптерами. Любой из последовательных контейнеров может быть использо- ван как базовый контейнер для стека. Таким образом, стек может быть создан на базе вектора, списка или двухсторонней очереди. Адаптер queue (очередь) требует нали- чия в базовом контейнере функции push_ front (), поэтому он может быть создан на базе списка, но не на базе вектора. Адаптер priority_queue (приоритетная очередь) требует произвольного доступа к элементам, поэтому он может быть создан на базе вектора или двухсторонней очереди, но не на базе списка. Операторы сравнения адаптеров Два адаптера одинакового типа можно сравнить на равенство и неравенство, а также применить операторы меньше, больше, меньше или равно и больше или равно, если их поддерживает тип базового элемента. При выполнении этих операций срав- ниваются значения самих элементов. Результат сравнения, больше или меньше, оп- ределяет первая пара несовпадающих элементов. Таблица 9.22. Функции и типы, общие для всех адаптеров size_type value_type container_type А а; А а (с) ; Реляционные опера- торы Тип данных, достаточно большой, чтобы содержать размер самого большого объекта этого типа Тип элемента Тип контейнера, на базе которого реализован адаптер Создает новый пустой адаптер по имени а Создает новый адаптер по имени а, содержащий копию контейнера с Каждый адаптер поддерживает все операторы сравнения: ==, ! =, <, <=, > и >= 9.7.1. Адаптер stack Функции адаптера stack (стек), перечислены в табл. 9.23, а в следующей про- грамме приведены примеры их применения. // количество помещаемых в стек элементов const stack<int>::size_type stk_size = 10;
378 Часть II. Контейнеры и алгоритмы stack<int> intStack; // пустой стек // заполнить стек int ix = 0; while (intStack.size() != stk_size) // используя постфиксный инкремент поместить исходные // значения в стек intStack intStack.push(ix++); // intStack содержит значения 0...9 int error_cnt = 0; // просмотреть каждое значение и извлечь их из стека while (intStack.empty() == false) { int value = intStack.top(); // прочитать верхний элемент стека if (value != --ix) { cerr << "oops! expected " « ix « " received " « value « endl; ++error_cnt; } intStack.pop () ; // удалить верхний элемент и повторить } cout « "Our program ran with " « error_cnt « " errors!" « endl; Ниже приведено объявление пустого стека intStack, предназначенного для хранения целочисленных элементов. stack<int> intStack; // пустой стек Цикл for добавляет в стек stk_size элементы, инициализируя каждый из них следующим целым числом начиная с нуля. Цикл while перебирает стек, исследуя значения, возвращаемые функцией top (), и удаляя элементы из стека, пока он не опустеет. Каждый адаптер контейнера имеет собственный набор функций, предоставлен- ных контейнером базового типа. По умолчанию стек реализован на базе двухсторон- ней очереди и, соответственно, использует ее функции. // используя постфиксный инкремент поместить исходные II значения в стек intStack intStack.push(ix++); // intStack содержит значения 0...9 В этом фрагменте кода, например, фактически осуществляется вызов функции push_back () объекта двухсторонней очереди, на базе которой создан объект стека intStack. Хоть стек и реализован на базе двухсторонней очереди, прямого доступа к функциям двухсторонней очереди здесь нет. В стеке нельзя вызывать функцию push_back (), вместо нее следует использовать стековую функцию push (). Таблица 9.23. Функции, поддерживаемые адаптером контейнера stack s.empty() s.size() s.pop() s.top() s.push(элемент) Возвращает значение true, если стек пуст, и значение false в противном случае Возвращает количество элементов стека Удаляет, но не возвращает верхний элемент из стека Возвращает, но не удаляет верхний элемент из стека Помещает новый элемент в вершину стека
Глава 9. Последовательные контейнеры 379 9.7.2. Очередь и приоритетная очередь Библиотечный адаптер queue (очередь) использует для поиска и хранения принцип “первым пришел — первым вышел” (FIFO — First-In, First-Out). Объекты помещаются в конец очереди, а возвращаются и удаляются в ее начале. Существует два вида очередей: обычная (FIFO), которая далее упоминается как просто очередь, и приоритетная очередь. Адаптер priority_queue (приоритетная очередь) позволяет устанавливать приоритет элементов, расположенных в очереди. Здесь добавляемый элемент поме- щается не в конец очереди, а перед теми элементами, которые обладают более низ- ким приоритетом. По умолчанию для определения относительных приоритетов в библиотеке используется оператор <. Пример приоритетной очереди из реальной жизни — это служба проверки багажа в аэропорту. Тех, кто вылетает через 30 минут, пропускают вперед, чтобы они успели оформить багаж и успеть на регистрацию. Пример приоритетной очереди в компью- тере — это планировщик процессов операционной системы, который выбирает и пе- редает на выполнение процесс из набора ожидающих процессов. Чтобы использовать адаптеры queue или priority_queue, необходимо под- ключить заголовок queue. Списков функций, поддерживаемых адаптерами queue и priority_queue, приведен в табл. 9.24. Таблица 9.24. Функции, поддерживаемые адаптерами queue И priority_queue q. empty () Возвращает значение true, если очередь пуста, и значение false — в против- ном случае q. s i z е () Возвращает количество элементов очереди q • pop () Удаляет, но не возвращает первый элемент очереди q. front () Возвращает, но не удаляет первый элемент очереди. Эта функция применима только для адаптера queue q. back () Возвращает, но не удаляет последний элемент очереди. Эта функция применима только для адаптера queue q • top () Возвращает, но не удаляет элемент с самым высоким приоритетом. Эта функция применима только для адаптера prior ity_queue q. push (элемент) Помещает новый элемент в конец очереди или в соответствующую приоритету позицию приоритетной очереди Упражнения раздела 9.7.2 Упражнение 9.42. Напишите программу, которая читает набор слов в стек. Упражнение 9.43. Используйте стек для обработки выражений со скобками. Встретив открываю- щую скобку, запомните ее положение. Встретив закрывающую скобку, после открывающей скобки, удалите эти элементы, включая открывающую скобку, и поместите полученное значение в стек, переместив таким образом заключенное в скобки выражение.
380 Часть II. Контейнеры и алгоритмы Резюме В библиотеке C++ определено несколько типов последовательных контейнеров. Контей- нер — это шаблон класса, который содержит объекты указанного типа. Доступ к элементам в последовательном контейнере осуществляется по позиции. Последовательные контейнеры имеют одинаковый, стандартизированный интерфейс: если два последовательных контейнера поддерживают некую операцию, она будет имеет тот же интерфейс и смысл для обоих кон- тейнеров. Все контейнеры обеспечивают эффективное управление динамической памятью. Элементы в контейнер можно добавлять не заботясь о том, где именно их сохранить. Контей- нер сам управляет процессом хранения. Наиболее популярным контейнером является вектор, который обеспечивает быстрый, произвольный доступ к элементам. Элементы могут быть быстро удалены и добавлены толь- ко в конец вектора, в других местах это происходит существенно дольше. Контейнер deque подобен контейнеру vector, но он обеспечивает быстрое удаление/вставку и в начало. Кон- тейнер list обеспечивает только последовательный доступ к элементам, но позволяет быст- ро вставлять и удалять элементы в любой позиции. Контейнеры обладают большим количеством функций, включая конструкторы, функции добавления и удаления элементов, выяснения размера контейнера и возвращения итераторов на те или иные элементы. Другие весьма полезные функции, такие как сортировка и поиск, определены не типами контейнеров, а стандартными алгоритмами, которые будут описаны в главе 11, “Общие алгоритмы”. Функции контейнеров, которые добавляют или удаляют элементы, способны сделать су- ществующие итераторы некорректными. При совмещении действий с итераторами и опера- ций с контейнерами, очень важно не забывать, что данные операции могут сделать итераторы недопустимыми. Большинство функций, которые способны сделать итераторы недопусти- мыми, например insert () или erase (), возвращают новый итератор, который позволяет не потерять позицию внутри контейнера. Особую осторожность следует соблюдать в циклах, которые используют итераторы и операции с контейнерами, способные изменить их размер. Термины Адаптер (adaptor). Библиотечный тип, функция или итератор, который заставляет один объект действовать подобно другому. Для последовательных контейнеров существует три адаптера: stack, queue и priority_queue. Каждый из этих адаптеров определяет новый интерфейс базового последовательного контейнера. Адаптер priority_queue (приоритетная очередь). Адаптер последовательных контей- неров, позволяющий создать очередь, в которой элементы добавляются не в конец, а согласно определенному уровню приоритета. По умолчанию при определении приоритета использует- ся оператор “меньше” для типа элемента. Адаптер queue (очередь). Адаптер последовательных контейнеров, позволяющий создать очередь, в которой элементы добавляются в конец, а предоставляются и удаляются в начале. Адаптер stack (стек). Адаптер последовательных контейнеров, позволяющий создать стек, в который элементы добавляют и удаляют только с одного конца. Диапазон итераторов (iterator range). Диапазон элементов, обозначенный двумя итерато- рами. Первый итератор относится к первому элементу в последовательности, а второй — к следующему элементу после последнего. Если диапазон пуст, итераторы равны (и наобо- рот — если итераторы равны, они обозначают пустой диапазон). Если диапазон не пуст, вто- рой итератор можно достичь последовательным увеличением первого итератора. Последова- тельное приращение итератора позволяет обработать каждый элемент диапазона.
Глава 9. Последовательные контейнеры 381 Интервал, включающий левый элемент (left-inclusive interval). Диапазон значений, включающий первый элемент, но исключающий последний. Обычно обозначается как [ i, j ), т.е. начальное значение последовательности i включено, а последнее, j, исключено. Итератор (iterator). Тип, обеспечивающий функции перемещения между элементами контейнера и исследования содержащихся в контейнере значений. В каждом из библиотеч- ных контейнеров определены четыре типа итераторов, перечисленных в табл. 9.5. Все библио- течные итераторы поддерживают операторы обращения к значению (*) и стрелки (->), по- зволяющие исследовать значения, на которые указывает итератор. Они поддерживают также префиксный и постфиксный инкремент (++) и декремент (- -), а также операторы равенства (==) и неравенства (! =). Контейнер (container). Тип (класс), который содержит коллекцию объектов определенно- го типа. Каждый библиотечный контейнер является шаблоном класса. Чтобы создать контей- нер, необходимо указать тип хранимых в нем элементов. Библиотечные контейнеры имеют переменный размер. Контейнер deque (двухсторонняя очередь). Последовательный контейнер, к элементам которого можно обратиться по индексу (позиции). Двухсторонняя очередь подобна вектору во всех отношениях, за исключением того, что он обеспечивает быструю вставку как в начало, так и в конец контейнера, без перемещения элементов внутри. Контейнер list (список). Последовательный контейнер, к элементам которого можно обратиться только последовательно, т.е. начиная с одного элемента можно перейти к другому, увеличивая или уменьшая итератор. Обеспечивает быструю вставку (и удаление) в любой по- зиции. Добавление новых элементов никак не влияет ни на другие элементы, ни на сущест- вующие итераторы. Когда элемент удаляется, некорректным становится лишь итератор уда- ленного элемента. Контейнер vector (вектор). Последовательный контейнер, к элементам которого можно обратиться по индексу (позиции). Для добавления элементов в вектор применяются функции push_back () и insert (). Добавление элементов в вектор может привести к его перерас- пределению в памяти, что сделает некорректными все созданные ранее итераторы. При до- бавлении (или удалении) элемента в середину вектора, итераторы всех расположенных далее элементов становятся некорректными. Некорректный итератор (invalidated iterator). Итератор на элемент, который больше не существует. Использование некорректного итератора может привести к серьезным пробле- мам во время выполнения. Последовательный контейнер (sequential container). Контейнер, позволяющий содержать упорядоченную коллекцию объектов одинакового типа. К элементам последовательного кон- тейнера обращаются по позиции. Функция begin (). Функция контейнера, возвращающая итератор на первый элемент в кон- тейнере (если он есть), или итератор f f (off-the-end — после конца), если контейнер пуст. Функция end (). Функция контейнера, возвращающая итератор на элемент после по- следнего элемента контейнера.

ГЛАВА 10 Ассоциативные контейнеры В ЭТОЙ ГЛАВЕ... 10.1. Предварительные сведения: тип pair 384 10.2. Ассоциативные контейнеры 386 10.3. Тип тар 387 10.4. Тип set 399 10.5. Типы multimap и multiset 403 10.6. Применение контейнеров: программа TextQuery 407 Резюме 415 Термины 416 В этой главе обзор контейнеров стандартной библиотеки завершают ассоциатив- ные контейнеры. Ассоциативные контейнеры (associative container) от последова- тельных отличаются принципиально: доступ к их элементам осуществляется не по позиции внутри контейнера, а по ключу (key). Хотя ассоциативные контейнеры во многом подобны последовательным, способ доступа к элементам у них разный. После описания ассоциативных контейнеров, эта глава завершается примером, в котором используются и ассоциативные, и последо- вательные контейнеры. Ассоциативные контейнеры обеспечивают быстрый поиск элементов по ключу. Двумя первичными типами ассоциативных контейнеров являются тар (карта) и set (набор). Элементами контейнера тар являются пары ключ-значение (key-value pair): ключ выступает в роли индекса, а значение представляет собой хранимые в контейнере данные. Контейнер set содержит только ключи и предоставляет эффек- тивные способы запроса на проверку наличия определенного ключа. Как правило, контейнер set используют в случае, когда следует эффективно хранить коллекцию несовпадающих значений, а контейнер тар наиболее полезен при необходимости хранить (а возможно, и модифицировать) значения, связанные с каждым ключом. Контейнер set, например, можно использовать для хранения на- бора слов, которые следует игнорировать при обработке текста, контейнер тар по- зволит создать словарь: слово будет ключом, а его перевод — значением. Контейнер типа тар или set может содержать только один элемент с определен- ным ключом. В такой контейнер невозможно добавить второй элемент с тем же клю- чом. Если необходимо иметь несколько элементов с одинаковым ключом, следует
384 Часть II. Контейнеры и алгоритмы использовать контейнеры multimap или multiset, которые допускают наличие нескольких элементов с совпадающим ключом. Ассоциативные контейнеры поддерживают большинство тех же операций, что и последовательные. Но они поддерживают также и специализированные операции, которые позволяют манипулировать ключами. В следующих разделах подробно рас- сматриваются ассоциативные контейнеры разных типов и операции с ними. Завер- шается глава реализацией небольшой программы, в которой контейнеры использу- ются для обращения к тексту. Таблица 10.1. Типы ассоциативных контейнеров тар Ассоциативный массив, в котором доступ к хранимым элементам осуществляется по ключу set Коллекция переменного размера с быстрым доступом к ключу multimap Карта, в которой может быть несколько одинаковых ключей multiset Набор, в котором может быть несколько одинаковых ключей 10.1. Предварительные сведения: тип pair Прежде чем рассматривать ассоциативные контейнеры, имеет смысл ознакомить- ся с простым вспомогательным библиотечным типом pair (пара), который опреде- лен в заголовке utility. Создание и инициализация пар Объект типа pair хранит два значения. Подобно контейнерам, тип pair явля- ется шаблоном. Однако в отличие от контейнеров, описанных до сих пор, при соз- дании пары следует указать имена двух типов, ведь объект типа pair содержит две переменные-члена, каждая из которых имеет соответствующий тип. Совпадать эти типы вовсе не обязаны. pair<string, string> anon; // содержит две строки pair<string, int> word_count; // содержит строку и целое число pair<string, vector<int> > line; // содержит строку и vector<int> При создании объекта пары без указания инициализирующих значений, исполь- зуются стандартные конструкторы типов его переменных-членов. Таким образом, пара anon содержит две пустых строки, а пара 1 ine — пустую строку и пустой вектор целых чисел. Значением переменной-члена типа int в паре word_count будет 0, а его переменная-член типа string окажется инициализирована пустой строкой. Однако для каждого элемента пары можно также предоставить инициализирую- щее значение. pair<string, string> author("James", "Joyce"); Этот код создает пару по имени author, в которой каждый элемент имеет тип string. Объект author инициализирован двумя строками: "James" и "Joyce". Чтобы избежать недоразумений при определении ряда объектов пар одинакового типа, можно воспользоваться определением типов (раздел 2.6, стр. 83). Author proust("Marcel", "Proust"); Author joyce("James", "Joyce");
Глава 10. Ассоциативные контейнеры 385 Таблица 10.2. Операции с парами paircTl, Т2>р1; paircTl, T2>pl(vl, v2) ; make_pair(vl, v2) pl < p2 pl == p2 p.first p.second Создает пустую пару с двумя элементами типа ti и Т2. Элементы инициализируются значениями, как описано в разделе 3.3.1 (стр. 116) Создает пару с элементами типов Т1 и Т2, первый из которых ини- циализирован значением элемента vi, а второй — v2 Создает новую пару из значений vi и v2. Типы элементов пары за- дают типы значений vi и v2 Сравнение двух объектов пар. Сравнение осуществляется подобно упорядочиванию в словаре, т.е. оператор < возвращает значение true в случае, если pi. first < р2. first или если ! (р2 . first < pl. first) && pl. second < p2 . second Две пары равны, если их первый и второй члены соответственно рав- ны. При сравнении используется оператор == хранимых элементов Возвращает открытую переменную-член first пары р Возвращает открытую переменную-член second пары р Операции с парами В отличие от других библиотечных типов, класс pair предоставляет непосредст- венный доступ к своим переменным-членам. Его переменные-члены, first (пер- вая) и second (вторая), соответственно, объявлены открытыми (public). К ним можно обращаться используя обычный точечный оператор (раздел 1.5.2, стр. 46). string firstBook; // обратиться и проверить переменные-члены пары if (author.first == "James" && author.second == "Joyce") firstBook = "Stephen Hero"; В библиотеке определено весьма ограниченное количество операций с парами. Они перечислены в табл. 10.2. Создание новой пары Кроме конструкторов, в библиотеке определена функция make_pair (), которая создает новую пару из двух переданных ей аргументов. Эту функцию можно исполь- зовать для создания новой пары и присвоения ей значений уже существующей. pair<string, string> next_auth; string first, last; while (cin » first >> last) { // создать пару из first и last next_auth = make_pair(first, last); // обработать next_auth ... } Этот цикл обрабатывает последовательность объектов, содержащих информа- цию об авторах. Функция make_pair () создает новую пару из имен, прочитан- ных в условии цикла while. Это эквивалентно следующему, несколько более сложному коду. // использовать конструктор pair(), для создания пары из // значений переменных first и last next_auth = pair<string, string>(first, last);
386 Часть II. Контейнеры и алгоритмы Поскольку переменные-члены класса pair являются открытыми (public), дан- ные в них можно прочитать даже непосредственно. pair<string, string> next auth; // читать непосредственно в переменные-члены объекта next_auth while (cin » next_auth.first >> next_auth.second) { // обработать next_auth... } Упражнения раздела 10.1 Упражнение 10.1. Напишите программу, читающую последовательность строк и целых чисел, со- храняя каждую прочитанную пару в объекте класса pair. Сохраните пары в векторе. Упражнение 10.2. Существует по крайней мере три способа создания пар в программе предыду- щего упражнения. Напишите три версии программы, создающей пары каждым из этих способов. Укажите, какая из форм проще и почему. 10.2. Ассоциативные контейнеры Ассоциативные контейнеры поддерживают многие, но не все функции последо- вательных контейнеров. Ассоциативные контейнеры не имеют функций front (), push_f ront (), pop_f ront (), back (), push_back () и pop_back (). Общими для последовательных и ассоциативных контейнеров являются сле- дующие функции. Три первых конструктора, описанные в табл. 9.2 (стр. 335). С<Т> с; // создать пустой контейнер // с2 должен иметь тот же тип, что и cl С<Т> с1(с2); // копировать элементы контейнера с2 в cl // b и е - итераторы, обозначающие последовательность С<Т> с(Ь, е); // копировать последовательность элементов в // контейнер с Нельзя создать ассоциативный контейнер, указав только его размер, поскольку неизвестно, какие значения следует присвоить ключам. Операторы сравнения, описанные в разделе 9.3.4 (стр. 349). Функции begin (), end (), rbegin () и rend (), описанные в табл. 9.6 (стр. 345). Определения типов, перечисленные в табл. 9.5. Обратите внимание, для контей- нера тар тип value_type не совпадает с типом элемента. Тип value_type со- ответствует текущему типу pair (с учетом типа ключа и типа значения). Более подробно определение типа карт рассматривается в разделе 10.3.2 (стр. 389). Функция swap () и оператор присвоения, описанные в табл. 9.11 (стр. 356). Ас- социативные контейнеры не поддерживают функцию assign (). Функции clear () и erase (), описанные в табл. 9.10 (стр. 354), за исключени- ем версии функции erase (), возвращающей void. Функции размера, описанные в табл. 9.8 (стр. 351), за исключением функции resize (), которая к ассоциативным контейнерам не применима.
Глава 10. Ассоциативные контейнеры 387 Элементы упорядочиваются по ключу В контейнерах ассоциативных типов определены и дополнительные функции, кроме перечисленных. Они позволяют переопределить значение или тип возвра- щаемого значения функций, аналогичных применяемым для последовательных кон- тейнеров. Различия в этих общих функциях обусловлены применением в ассоциа- тивных контейнерах ключей. Тот факт, что элементы упорядочены по ключу, имеет одно важное следствие: при пере- 1 боре ассоциативного контейнера это гарантирует доступ к элементам по порядку ключей, независимо от последовательности, в которой элементы были помещены в контейнер. Упражнения раздела 10.2 Упражнение 10.3. Опишите различия между ассоциативными и последовательными контейнерами. Упражнение 10.4. Приведите примеры, когда имеет смысл применить такие контейнеры, как list, vector, deque, map и set. 10.3. Тип map Контейнер map (карта) — это коллекция пар ключ-значение. Карту зачастую на- зывают ассоциативным массивом (associative array): она похожа на массив встроен- ного типа, где ключ применяется в качестве индекса для доступа к значению. Ассо- циативность здесь проявляется в том, что доступ к значениям осуществляется по связанным (ассоциированным) с ними ключам, а не по позиции в массиве. 10.3.1. Определение карты Чтобы воспользоваться контейнером тар, необходимо подключить заголовок тар. При определении объекта карты, следует указать тип ключа и тип значения. // подсчитать, сколько раз встречается каждое введенное слово map<string, int> word_count; // пустая карта из строк и чисел Здесь определен объект карты по имени word_count, который индексирован строками и содержит ассоциированные значения типа int. Таблица 10.3. Конструкторы контейнера шар map<k, v> m; map<k, v> m(m2) ; map<k, v> m(b, e) ; Создает пустую карту по имени m, типами ключей и значений которой явля- ются к и v соответственно Создает карту m как копию карты m2, причем карты m и m2 должны иметь одинаковые типы ключей и значений Создает карту m как копию элементов диапазона, обозначенного итераторами ь и е. Элементы должны иметь тип, который может быть преобразован в тип paircconst k, v>
388 Часть II. Контейнеры и алгоритмы Ограничения типов ключей Тип ключа ассоциативного контейнера должен обладать функцией сравнения. По умолчанию для сравнения ключей библиотека использует оператор <. В разде- ле 15.8.3 (стр. 637) описано, как можно переопределить стандартный оператор и предоставить вместо него собственную функцию. Каждый используемый для ключей тип должен обладать функцией строгого сравнения (strict weak ordering). Под строгим сравнением можно понимать оператор “меньше”, хотя вполне допустимо определить и более сложную функцию сравнения. Но эта функция всегда должна возвращать значение false, когда ключ сравнивает- ся сам с собой. Кроме того, при сравнении двух ключей оба они не могут быть мень- ше друг друга, а если ключ kl меньше, чем ключ к2, который в свою очередь меньше, чем ключ кЗ, то ключ kl должен быть меньше, чем ключ кз. Если существует два ключа, ни один из которых не меньше другого, контейнер рассматривает их как рав- ные. Любое значение, используемое в качестве ключа контейнера тар, применимо для доступа к соответствующему элементу. Весьма важно то, что для типа ключа должен быть определен оператор <, который удов- летворяет изложенным выше требованиям. Например, в программу, решающую проблему книжного магазина, можно было бы добавить класс по имени ISBN, инкапсулирующий правила для ISBN. В данном случае ISBN — это строки, которые можно сравнивать и определять, какой из ISBN меньше другого. Следовательно, класс ISBN должен поддерживать операцию <. При условии, что такой класс имеется, можно создать карту bookstore (книжный мага- зин), которая позволит быстро найти определенную книгу. map<ISBN, Sales_item> bookstore; Здесь определен объект карты по имени bookstore, индексом которой явля- ется класс ISBN. С каждым элементом этой карты ассоциирован экземпляр класса Sales_item. Тип ключа должен поддерживать только оператор <. Другие операторы сравнения или равенства он может не поддерживать. Упражнения раздела 10.3.1 Упражнение 10.5. Определите контейнер тар, который ассоциирует слова со списком (контей- нер list) номеров строк, в которых может встречаться данное слово. Упражнение 10.6. Можно ли создать карту для итератора vector<int>:: iterator вектора целых чисел? А как насчет итератора iist<int>:: iterator списка целых чисел или пар pair<int, string>? В каждом случае, где это невозможно, объясните, почему.
Глава 10. Ассоциативные контейнеры 389 10.3.2. Типы, определенные в шаблоне тар Элементами контейнера тар являются пары ключ-значение, т.е. каждый элемент имеет две части: ключ (key) и связанное (ассоциированное) с ним значение (value). Этот факт подтверждает обращение к типу value_type для контейнера тар. Этот тип существенно сложнее, чем у других контейнеров, рассмотренных ранее: тип pair содержит ключ и значение данного элемента. Кроме того, ключ является кон- стантой (const). Например, типом value_type контейнера word_count будет paircconst string, int>. Таблица 10.4. Типы, определенные в шаблоне тар тар<к, v>:: key_type Тип ключей, используемых при индексировании карты тар<к, v>: :mapped_type Тип значений, связанных с ключами карты тар<к, v>:: value_type Пара, элемент first которой имеет тип const map<K, V>::key_type, а элемент second — тип mapcK, V>: :mapped_type Изучая интерфейс шаблона map, не следует забывать, что типом vaiue_type для не- 1 го является тип pair, а также то, что изменять в этой паре можно только значение, но не ключ. Обращение к значению итератора карты возвращает пару При обращении к значению итератора возвращается ссылка на значение типа value_type контейнера. В случае карты, типом value_type является тип pair. // получить итератор на элемент контейнера word_count map<string, int>::iterator map_it = word_count.begin(); // map_it - ссылка на объект типа paircconst string, int> cout << map_it->first; // отобразить ключ элемента cout « " " « map_it->second;// отобразить значение элемента map_it->first = "new key"; // ошибка: ключ является константой ++map_it->second; // ok: значение можно изменить // используя итератор Обращение к значению, на которое указывает итератор, возвращает объект типа pair, элемент first которого содержит константный ключ, а элемент second — соответствующее ему значение. Дополнительные типы шаблона тар В шаблоне тар определено два дополнительных типа, key type и mapped_ type, которые позволяют обратиться к типу ключа и значения. Для контейнера word_count, типу key_type соответствует тип string, а типу mapped_type — тип int. Подобно последовательным контейнерам (раздел 9.3.1, стр. 344), для дос- тупа к члену класса здесь используется оператор области видимости (: :), например, map<string, int>::key_type.
390 Часть II. Контейнеры и алгоритмы Упражнения раздела 10.3.2 Упражнение 10.7. Каковы типы mapped_type, key_type и value_type карты из пар типа int И vector<int>? Упражнение 10.8. Напишите выражение, где для присвоения значения элементу используется итератор карты. 10.3.3. Добавление элементов карты Как только карта определена, ее следует заполнить элементами, представляющи- ми собой пары ключ-значение. Для этого можно воспользоваться либо функцией- членом insert (), либо получив доступ к элементу при помощи оператора индек- сирования присвоить ему значение. В обоих случаях на выполнение этих действий повлияет тот факт, что для каждого ключа может существовать только один элемент. 10.3.4. Индексация карты Рассмотрим пример. map <string, int> word_count; // пустая карта // вставить инициализированный по умолчанию элемент с ключом Anna, // а затем присвоить ему значение 1 word_count["Anna"] = 1 ; Выполнение этого кода подразумевает следующие действия. 1. В контейнере word_count происходит поиск элемента с ключом Anna. Но эле- мент не найден. 2. В контейнер word_count добавляется новая пара ключ-значение. Ключ — это константная строка, содержащая слово Anna. Значение инициализируется по умолчанию, в данном случае значением 0. 3. В контейнер word_count добавляется новая пара ключ-значение1. 4. Происходит обращение к только что добавленному элементу и ему присваивает- ся значение 1. Индексация у карт происходит совсем не так, как у массив или векторов: применение ин- декса не возвращает, а добавляет элемент с указанным индексом. Подобно другим операторам индексирования, при индексации карт передают ин- декс (т.е. ключ), а получают значение, связанное с этим ключом. Обращение по клю- чу, который уже содержится в карте, происходит так же, как и при индексировании вектора: возвращается значение, связанное с ключом. Только у карт, если ключ еще не существует, создается и добавляется новый элемент с указанным ключом. Ассо- циированное значение инициализирует либо стандартный конструктор, если это объект класса, либо значение 0, если объект имеет встроенный тип. 1 Эта строка, вероятно, лишняя. — Примеч. ред.
Глава 10. Ассоциативные контейнеры 391 Использование значений, возвращенных в результате индексирования Как обычно, оператор индексирования возвращает 1-значение. Это то 1-значение, которое связано с ключом. Элемент можно записать или прочитать. cout << word_count["Anna"]; ++word_count["Anna"]; cout << word_count["Anna"]; // доступ к элементу с индексом Anna; // отображает значение 1 // доступ к элементу и добавление // единицы II доступ к элементу и отображение // значения; отображает значение 2 В отличие от вектора или строки, тип данных, возвращаемых оператором индексирования карты, отличается из типа, полученного при обращении к значению итератора карты. Как уже упоминалось, типом value_type итератора карты является тип pair, который содержит константный элемент типа key_type и элемент типа mapped_ type, а оператор индексирования возвращает значение типа mapped_type. Применение индексирования Тот факт, что индексирование добавляет элемент, если карта его еще не содер- жит, позволяет создавать удивительно сжатый код. // подсчитать, сколько раз встречается каждое введенное слово map<string, int> word_count; // пустая карта из строк и чисел string word; while (cin » word) ++word_count[word]; Этот код создает карту, которая используется при подсчете количества слов. Цикл while читает слова по одному со стандартного устройства ввода. Каждый раз, когда встречается новое слово, оно используется для индексации карты word_count. Если карта уже содержит это слово, соответствующее ему, значение увеличивается. Самое интересное происходит тогда, когда слово встречается впервые: в карте word_count создается новый элемент, индексированный данным словом, и с нуле- вым значением. Поскольку при добавлении нового слова в карту значение соответ- ствующее элемента немедленно увеличивается, счет начинается с единицы. Упражнения раздела 10.3.4 Упражнение 10.9. Напишите программу, подсчитывающую и отображающую каждое введенное слово. Упражнение 10.10. Что выполняет следующая программа? mapcint, int> m; m[0] = 1; Сравните ее поведение со следующей программой. vector<int> v; v[0] = 1;
392 Часть II. Контейнеры и алгоритмы Упражнение 10.11. Какой тип применяется при индексировании карты? Какой тип возвращает оператор индексирования? Приведите конкретный пример — т.е. создайте карту, используйте ти- пы, которые применимы для ее индексирования, а затем выявите типы, которые будет возвращать оператор индексирования. 10.3.5. Применение функции map:: insert О Таблица 10.5. Версии функции insert () для карты m.insert(е) т.insert(beg, end) т.insert(iter, e) e — значение типа value_type, добавляемое в карту m. Если ключа (е. first) в карте m еще нет, добавляется новый элемент со значением е. second. Если такой ключ в карте m уже есть, карта остается неизменной. Возвращает пару, содержащую итератор на элемент с ключом е. first, и логическое значение, указывающее, был ли элемент вставлен или нет beg и end — итераторы, обозначающие диапазон элементов, являющихся парами ключ-значение, тип которых совпадает с типом value_type карты ш. Для каждого элемента в диапазоне, если карта m еще не содержит такого ключа, добавляется новый элемент. Возвращает void е — значение типа value_type, добавляемое в карту т. Если ключа (е. first) в карте m еще нет, добавляется новый элемент, причем итератор iter указывает, где начинать поиск места для сохранения нового элемента. Возвращает итератор на элемент с данным ключом в контейнере m Здесь функция-член insert () работает так же, как и в последовательных кон- тейнерах (раздел 9.3.3, стр. 345), но с одним важным отличием, обусловленным на- личием ключей. Это влияет на тип аргумента: версии, которые добавляют одиноч- ный элемент, получают аргумент, являющийся парой ключ-значение. Аналогично, в версии, получающей два итератора, итераторы должна указывать на элементы, ко- торые представляют собой пары ключ-значение. Еще одним отличием является тип возвращаемого значения той версии функции insert (), которая получает одно значение. Этому посвящена остальная часть данного раздела. Применение функции inser t () вместо индексации Когда при добавлении элемента карты используется индексирование, та часть элемента, которая составляет значение, инициализируется. Зачастую значение ей присваивается немедленно, при инициализации или присвоении. Но к качестве аль- тернативы можно вставить готовый элемент, используя синтаксически более слож- ную версию функции-члена insert (). // если word__count еще не содержит элемент Anna, добавить его // со значением 1 word_count.insert(mapcstring, int>::value type("Anna", 1)); Эта версия функции insert () имеет следующий аргумент. map<string, int>::value_type(anna, 1) Это вновь созданная пара, которая сразу добавляется в карту. Помните, что value type — это синоним типа paircconst К, V>, где К — тип ключа, а V — тип
Глава 10. Ассоциативные контейнеры 393 связанного с ним значения. Аргумент функции insert () создает новый объект пары соответствующего типа, готовый для вставки в карту. Используя функцию insert () можно избежать дополнительных операций инициализации значе- ния, неизбежных при добавлении в карту нового элемента при помощи индекси- рования. Аргумент функции insert () довольно сложен. Его можно упростить двумя способами. Можно использовать функцию make_pair (). word_count.insert(make_pair("Anna", 1)); Также можно использовать определение типа. typedef map<string,int>::value_type valType; word_count.insert(valType("Anna", 1)); Любой из этих подходов повышает удобочитаемость кода и упрощает его. Проверка значения, возвращаемого функцией insert () В карте может существовать только один элемент с данным ключом, т.е. значения ключей уникальны. На попытку вставить элемент с ключом, который совпадает с имеющимся в карте, функция insert () никак не отреагирует. Версии функции insert (), которым передают итератор или пару итераторов, вовсе не гарантируют, что данный элемент или все элементы диапазона будут добавлены. Версия функции insert (), получающая одну пару ключ-значение, возвраща- ет значение. Это значение представляет собой пару, в состав которой входит ите- ратор на элемент карты с соответствующим ключом и логическое значение (тип bool), указывающее, был ли элемент вставлен. Если карта уже содержит такой ключ, соответствующее ему значение не будет изменено, а логическая часть воз- вращаемой пары будет содержать значение false. Если ключ отсутствовал, элемент окажется добавлен, а логическая часть будет содержать значение true. В любом случае, итератор будет относиться к элементу с указанным ключом. Та- ким образом, программу подсчета слов можно переписать с использованием функ- ции insert () таким образом. // подсчитать, сколько раз встречается каждое введенное слово map<string, int> word_count; // пустая карта из строк и чисел string word; while (cin >> word) { // вставить элемент с ключом равным слову и значением 1 // если слово уже в word_count, ничего не вставлять pair<map<string, int>::iterator, bool> ret = word_count.insert(make_pair(word, 1)); if (!ret.second) // слово уже в word_count ++ret.first->second; // инкремент счетчика } Для каждого вставляемого слова применяется значение 1. Если в результате про- верки возвращенного функцией insert () значения окажется, что логическая часть пары содержит значение false, то это свидетельствует о том, что вставка не удалась и элемент с таким словом в индексе уже был в карте word_count. В таком случае следует увеличить значение, связанное с данным элементом.
394 Часть II. Контейнеры и алгоритмы Еще раз о синтаксисе Определение и инкремент пары ret не вполне очевидны, поэтому рассмотрим их подробнее. pair<map<string, int>::iterator, bool> ret = word_count.insert(make_pair(word, 1)); Как можно заметить, здесь определена пара, типом второго элемента которой яв- ляется bool. Тип первого элемента пары немного сложнее. Это тип итератора для элемента карты типа map<string, int>. Чтобы понять, как происходит инкремент, сначала имеет смысл расставить скоб- ки, чтобы приоритет операторов (раздел 5.10.1, стр. 193) стал более очевиден. ++((ret.first)->second); // эквивалентное выражение Рассмотрим это выражение поэтапно. Пара ret содержит значение, возвращаемое функцией insert (). Первым эле- ментом этой пары является итератор на вставленный элемент карты с указан- ным ключом. Выражение ret. first предоставляет итератор карты, содержащийся в воз- вращенной функцией insert () паре ret. Выражение ret. first ->second осуществляет обращение к значению итера- тора. Этот объект также является парой, второй элемент которой содержит зна- чение добавленного элемента. Часть ++ret .first - >second осуществляет инкремент этого значения. Исходя из вышеизложенного, можно сделать вывод, что оператор инкремента получив итератор элемента, ключ которого соответствует введенному слову, увели- чивает с его помощью значение данного элемента. Упражнения раздела 10.3.5 Упражнение 10.12. Перепишите созданную в упражнениях раздела 10.3.4 (стр. 391) программу подсчета слов так, чтобы вместо индексации использовалась функция insert (). Укажите, код какой из версий программы проще написать и прочитать. Объясните, почему. Упражнение 10.13. Предположим, что существует карта map<string, vector<int> >. Укажите типы, используемые как аргумент и как возвращаемое значение той версии функции insert (), которая добавляет один элемент. 10.3.6. Поиск и возвращение элементов карты Самый простой способ возвращения значения предоставляет оператор индекси- рования. map<string, int> word count; int occurs = word count["foobar"]; Но как уже упоминалось, применение индексирования имеет серьезный побочный эффект: если карта еще не содержит элемент с таким ключом, он будет добавлен. Является ли такое поведение преимуществом, зависит от обстоятельств. Если в примере, приведенном выше, карта еще не содержит элемента с ключом foobar, он
Глава 10. Ассоциативные контейнеры 395 будет добавлен, а его значение окажется инициализировано нулем. В данном случае переменная occurs получит значение 0. Рассматриваемая программа подсчета слов полагается на тот факт, что индекси- рование несуществующего элемента добавляет его и инициализирует значение ну- лем. Тем не менее, иногда необходимо выяснить, существует ли определенный эле- мент, не добавляя его. В таких случаях оператор индексирования неприменим. Существует две функции, count () и f ind (), при помощи которых можно вы- яснить, присутствует ли элемент с указанным ключом, не добавляя его. Таблица 10.6. Исследование карты без внесения в нее изменений m. count (к) Возвращает количество к внутри карты m m. find (к) Возвращает итератор на элемент с индексом к, если он есть, или итератор на элемент после последнего элемента (раздел 3.4, стр. 121) карты т, если такого элемента нет Применение функции count () для выяснения наличия в карте ключа Функция-член count () контейнера тар всегда возвращает значение 0 или 1. Карта может иметь только один экземпляр элемента с определенным ключом, по- этому на самом деле функция count () указывает, присутствует ли данный ключ в карте или нет. Применение функции count () существенно полезней для контейне- ра multimap, рассматриваемого в разделе 10.5 (стр. 403). Если функция count () возвращает значение, отличное от нуля, для доступа к значению, связанному с дан- ным ключом, можно воспользоваться оператором индексирования, не опасаясь, что в карту будет добавлен соответствующий элемент, int occurs = 0; if (word__count. count (" foobar") ) occurs = word_count["foobar"]; Безусловно, если после функции count () применить индексирование, поиск элемента произойдет дважды. Если необходимо использовать найденный элемент, имеет смысл применить функцию find (). Доступ к элементу без риска добавления Функция f ind () возвращает итератор на найденный элемент или итератор end, если элемент отсутствует, int occurs = 0; map<string,int>::iterator it - word_count.find("foobar; if (it !- word_count.end()) occurs = it->second; Функцию find() имеет смысл использовать в случае, когда необходимо полу- чить ссылку на элемент с определенным ключом, если он существует, и не создавать элемент, если он не существует. Упражнения раздела 10.3.6 Упражнение 10.14. В чем разница между функциями count () и find () ?
396 Часть II. Контейнеры и алгоритмы Упражнение 10.15. Какие проблемы позволяет решить применение функции count О? Когда вместо нее имеет смысл использовать функцию find () ? Упражнение 10.16. Определите и инициализируйте переменную, способную содержать результат обращения к функции find () карты, элементы которой имеют типы string и vectorcint >. 10.3.7. Удаление элементов карты Существует три версии функции erase (), позволяющей удалять элементы из карты. Подобно последовательным контейнерам, передав функции erase () один или два итератора, здесь можно удалить как одиночный элемент, так и диапазон элементов. Данные версии функции erase () аналогичны соответствующим функ- циям последовательных контейнеров за одним исключением: они возвращают тип void (т.е. не возвращают ничего), в то время как у последовательных контейнеров они возвращают итератор на элемент после удаленного. Тип тар обладает дополнительной версией функции erase (), которой передают значение типа key_type. Она удаляет элемент с указанным ключом, если он суще- ствует. Эту версию можно использовать для удаления определенного слова из кон- тейнера word_count перед отображением результатов. // версия функции erase(), получающая ключ, возвращает количество // удаленных элементов if (word_count.erase(removal_word)) cout << "ok: " << removal_word « " removed\n"; else cout « "oops: " << removal_word << " not found!\n"; Эта версия функции erase () возвращает количество удаленных элементов. В слу- чае карты возвращаемым числом может быть нуль или единица. Возвращение нуле- вого значения свидетельствует о том, что элемент, который предполагалось удалить, в карте отсутствует. Таблица 10.7. Удаление элементов из карты m. erase (к) Удаляет из карты m элемент с ключом к. Возвращает значение типа size_type, указывающее количество удаленных элементов m. erase (р) Удаляет из карты m элемент, указанный итератором р. Возвращает тип void. Ите- ратор р должен относиться к фактически существующему элементу карты т, он не может быть равен итератору, возвращаемому функцией m. end () m. erase (b, e) Удаляет из карты m диапазон элементов, указанный итераторами ь и е. Возвраща- ет тип void. Итераторы ь и е должны обозначать допустимый диапазон элемен- тов карты т, т.е. они должны относиться либо к существующему элементу, либо к первому элементу за пределами карты т. Итераторы ь и е могут быть равны (когда диапазон пуст). Элемент, на который указывает итератор ь, должен располагаться перед элементом, на который указывает итератор е 10.3.8. Перебор элементов карты Подобно любому другому контейнеру, карта обладает функциями begin () и end (), возвращающими итераторы, которые можно использовать для перебора элементов. Например, отобразить содержимое карты word_count, созданной на стр. 391, мож- но следующим образом.
Глава 10. Ассоциативные контейнеры 397 // получить итератор на первый элемент map<string, int>::iterator map_it = word_count.begin(); // для каждого элемента карты while (map_it != word_count.end()) { // отобразить ключ и значение элемента cout « map_it->first << " occurs " << map_it->second « " times" « endl; ++map_it; // приращение итератора позволяет перейти к // следующему элементу } В этом цикле while, как и в предыдущих программах, отображающих содержи- мое векторов или строк, осуществляется инкремент и проверка итератора. Исполь- зуемый итератор map_it инициализируется так, чтобы он указывал на первый эле- мент карты word_count. Пока итератор не равен значению, возвращаемому функ- цией end (), отображается содержимое текущего элемента, а затем происходит приращение итератора. Тело данного цикла несколько сложнее, чем в предыдущих программах, поскольку отобразить необходимо и ключ, и значение элемента. Программа отображает слова в алфавитном порядке. Когда для перебора карты исполь- зуется итератор, доступ к элементам происходит в порядке возрастания ключа. 10.3.9. Карта преобразования слов Этот раздел завершается программой, которая демонстрирует создание, поиск и перебор элементов карты. Задача программы заключается в преобразовании од- них слов в другие. Исходные данные содержатся в двух файлах. Первый файл со- держит несколько пар слов. Первое слово пары — это то слово, которое может встретиться во вводимой строке. Второе — это слово, которое будет отображено. По существу, этот файл предоставляет набор преобразований слов — когда найде- но первое слово, его следует заменить вторым. Второй файл содержит текст, кото- рый предстоит преобразовать. Предположим, что файл преобразования слов имеет следующее содержимое. ' ет cuz gratz nah pos sez tanx wuz them because grateful I no supposed said thanks was Второй файл содержит следующий подлежащий преобразованию текст, nah i sez tanx cuz i wuz pos to not cuz i wuz gratz После выполнения программы должен получиться следующий результат, no I said thanks because I was supposed to not because I was grateful
398 Часть II. Контейнеры и алгоритмы Программа преобразования слов Представленное на следующей странице решение загружает содержимое файла преобразования слов в карту, причем заменяемое слово используется как ключ, а за- меняющее слово — как соответствующее ему значение. Затем программа читает ис- ходный текст и ищет слова, которые следует преобразовать. Вместо найденных слов отображаются преобразованные, а остальные отображаются в исходном состоянии. Функция main () программы получает два аргумента (раздел 7.2.6, стр. 269): имя файла преобразования слов и имя файла, содержащего исходный текст. Сначала проверяется количество аргументов. Первый аргумент, argv [ 0 ], всегда содержит имя команды. Имена файлов содержатся в аргументах argv [ 1 ] и argv [2 ]. Если передано необходимое количество аргументов, аргумент argv[l] ис- пользуется при вызове функции open_file() (раздел 8.4.3, стр. 325), откры- вающей файл преобразования слов. Если файл открыт успешно, начинается чте- ние пар преобразования. При вызове функции insert () первое слово использу- ется как ключ, а второе как значение. Когда цикл while заканчивается, карта trans_map уже содержит данные, необходимые для преобразования ввода. Но если с аргументами есть проблемы, передается исключение (раздел 6.13, стр. 241) и программа завершает работу. Затем происходит вызов функции open_f ile (), открывающий файл с преобра- зуемым текстом. Второй цикл while использует функцию getline () для построч- ного чтения текста. Чтение осуществляется построчно для того, чтобы концы строк в выводе совпадали с концами строк в исходном файле. Для выборки слов из каждой строки, применяется вложенный цикл while, в котором используется объект класса istringstream. Эта часть программы подобна примеру на стр. 327. * Программа преобразования слов. * Получает два аргумента: первый - имя файла преобразования слов, * второй - имя файла с исходным текстом * / int main(int argc, char * **argv) { // карта, содержащая пары преобразования слов: // ключ - это слово, искомое во вводе, // а значение - слово, заменяющее его в выводе map<string, string> trans_map; string key, value; if (argc != 3) throw runtime_error("wrong number of arguments"); // открыть файл преобразования и проверить успешность // этой операции ifstream map_file; if (!open_file(map_file, argv[l])) throw runtime_error("no transformation file"); // читать пары преобразований и создать карту while (map_file >> key >> value) trans_map.insert(make_pair(key, value)); // ok, теперь все готово для преобразования // открыть файл исходного текста и удостовериться в / / успехе операции ifstream input; if (!open_file(input, argv[2])) throw runtime_error("no input file");
Глава 10. Ассоциативные контейнеры 399 string line; // содержит каждую исходную строку // читать преобразуемый текст построчно while (getline(input, line)) { istringstream stream(line); // читать по одной строке string word; bool firstword = true; // контролирует пробелы while (stream >> word) { // ok: фактическая работа карты, это // основная часть программы map<string, string>::const_iterator map_it = trans_map.find(word); // если это слово присутствует в карте преобразования if (map_.it != trans_map. end () ) // заменить его значением из карты word = map_it->second; if (firstword) firstword = false; else cout << " "; // отобразить пробел между словами cout « word; } cout « endl; // строка закончилась Упражнения раздела 10.3.9 Упражнение 10.17. Для поиска слов рассматриваемая программа преобразования использует функцию f ind (). map<string, string>::const_iterator map_it = trans_map.find(word); Почему здесь используется именно функция findO? Что произойдет, если вместо нее исполь- зовать оператор индексирования? Упражнение 10.18. Определите карту, в которой ключ является фамилией семьи, а значение — вектором имен детей. Заполните карту по крайней мере шестью элементами. Проверьте ее, пре- доставив пользователю возможность делать запросы на основании фамилии и получать список имен детей в этой семье. Упражнение 10.19. Усовершенствуйте карту из предыдущего упражнения так, чтобы вектор хра- нил пары, содержащие имя ребенка и день его рождения. Измените проверку так, чтобы она по- зволяла проверить модернизированную карту. Упражнение 10.20. Приведите пример по крайней мере трех возможных приложений, в которых имеет смысл использовать контейнер типа тар. Напишите определение каждой карты и укажите, как лучше всего обеспечить вставку и доступ к элементам. 10.4. Тип set Контейнер тар (карта) — это коллекция пар ключ-значение, например адресов или номеров телефона, проиндексированных по имени их владельца. Контейнер set (набор) — напротив, является коллекцией только ключей. В бизнесе, например, мог бы найти применение набор bad__checks, содержащий имена лиц, которые имеют плохую привычку выписывать необеспеченные чеки. Набор полезнее всего в случае,
400 Часть II. Контейнеры и алгоритмы когда необходимо быстро выяснить, присутствует ли в нем некое значение. Напри- мер, прежде чем принимать от клиента чек, неплохо было бы обратиться к набору bad_checks и выяснить, нет ли там его имени. За двумя исключениями, набор поддерживает те же функции, что и карта. Функции, общие для всех контейнеров, описанные в разделе 10.2 (стр. 386). Конструкторы, перечисленные в табл. 10.3 (стр. 387). Версии функции insert (), описанные в табл. 10.5 (стр. 392). Функции count () и find (), описанные в табл. 10.6 (стр. 395). Версии функции erase (), описанные в табл. 10.7 (стр. 396). От контейнера тар контейнер set отличается тем, что он не поддерживает ин- дексирования и не имеет типа mapped_type. В наборе тип value_type не соответ- ствует паре, он совпадает с типом key_type. Оба они соответствуют типу элемен- тов, хранимых в наборе. Эти различия отражают тот факт, что в наборе хранятся только ключи, с которыми не связаны никакие значения. Подобно карте, ключи на- бора являются уникальными и не могут быть изменены. Упражнения раздела 10.4 Упражнение 10.21. Объясните различие между картой (тар) и набором (set). Когда имеет смысл использовать контейнер первого и второго типа? Упражнение 10.22. Объясните различие между набором (set) и списком (list). Когда имеет смысл использовать контейнер первого и второго типа? 10.4.1. Определение и применение наборов Чтобы воспользоваться контейнером set, необходимо подключить заголовок set. Функции набора, по существу, идентичны функциям карты. Подобно карте, в наборе может существовать только один элемент с определен- ным ключом. При инициализации или вставке диапазона элементов, фактически бу- дет добавлен только один элемент с каждым ключом. // создать вектор из 20 элементов, содержащий по две копии каждого // числа от 0 до 9 vector<int> ivec; for (vector<int>::size_type i = 0; i != 10; ++i) { ivec.push_back(i); ivec.push_back(i) ; // копия каждого числа } // iset содержит лишь уникальные элементы ivec set<int> iset (ivec.begin(), ivec.end()); cout << ivec.size() << endl; // отображает 20 cout << iset.size() << endl; // отображает 10 Сначала создается вектор целых чисел по имени ivec, который содержит 20 эле- ментов: по две копии каждого из целых чисел от 0 до 9 включительно. Затем все эле- менты вектора ivec используются для инициализации набора целых чисел. Как свидетельствует результат, набор имеет только десять элементов: по одному уни- кальному элементу из вектора ivec.
Глава 10. Ассоциативные контейнеры 401 Добавление элементов в набор Для добавления элементов в набор можно воспользоваться функцией insert (). set<string> setl; // пустой набор setl.insert("the"); // теперь setl имеет только один элемент set1.insert("and"); // теперь setl имеет два элемента В качестве альтернативы можно добавить диапазон элементов, обозначенный двумя итераторами. Данная версия функции insert () работает аналогично конст- руктору, получающему два итератора, — добавлены будут только те элементы, кото- рые не повторяются. set<int> iset2; // пустой набор iset2.insert(ivec.begin(), ivec.end()); // iset2 содержит // 10 элементов Подобно функциям контейнера map, версия функции insert (), получающая ключ, возвращает пару, содержащую итератор на элемент с этим ключом и логиче- ское значение, указывающее, был ли элемент добавлен фактически. Версия функции insert (), получающая два итератора, возвращает тип void. Доступ к элементам набора В наборах нет оператора индексирования. Для поиска элемента в наборе по клю- чу используется функция f ind (). Если необходимо только узнать, присутствует ли такой элемент а наборе, можно воспользоваться функцией count (), которая воз- вращает количество элементов в наборе с данным ключом. Безусловно, для набора это может быть только значение 1 (если элемент присутствует) или 0 (если нет), iset.find(1) // возвращает итератор на элемент с ключом == 1 iset.find(11) // возвращает итератор == iset. end() iset.count(1) // возвращает 1 iset.count(11) // возвращает О Подобно тому, как в элементе карты нельзя изменить ключ, ключи набора также константны. Если существует итератор на элемент набора, то все, что с ним можно сделать, — это прочитать ключ, а запись при помощи итератора невозможна. // set_it указывает на элемент с ключом == 1 set<int>::iterator set_it = iset.find(1); *set_it =11; // ошибка: ключи в наборе только для чтения cout << *set_it << endl; // ok: читать ключи можно 10.4.2. Создание набора, исключающего слова На стр. 396 из карты word_count было удалено указанное слово. Этот подход можно усовершенствовать и расположить все подлежащие удалению слова в опре- деленном файле. То есть программа подсчета слов должна учитывать только те сло- ва, которых нет в наборе исключенных слов. Такая программа, использующая кон- тейнеры set и тар, довольно проста. void restricted_wc(ifstream &remove_file, map<string, int &word_count) set set<string> excluded; // набор игнорируемых слов string remove_word; while (remove_file >> remove_word) remove_word)
402 Часть II. Контейнеры и алгоритмы excluded.insert(remove_word); // читать введенные слова и подсчитывать те из них, которых нет // в наборе исключенных string word; while (cin » word) // инкремент счетчика только тогда, когда слова нет // среди исключенных if (!excluded.count(word)) ++word_count[word]; } Эта программа похожа на программу подсчета слов со стр. 391. Различие заклю- чается только в том, что здесь не учитываются некоторые слова. Функция начинается с чтения переданного файла. Этот файл содержит список исключенных слов, которыми заполняется набор по имени excluded. Когда завер- шается первый цикл while, элементы набора содержат все слова из файла. Следующая часть похожа на первоначальную программу подсчета слов. Важное отличие заключается в том, что перед подсчетом каждое слово проверяется на при- надлежность набору исключенных. Проверку осуществляет следующий оператор i f во втором цикле while. // инкремент счетчика только тогда, когда слова нет // среди исключенных if (!excluded.count(word)) Обращение к функции count () возвращает значение 1, если слово word при- сутствует в наборе исключаемых слов excluded, и значение 0 — в противном слу- чае. В результате инверсии (!) логического значения, возвращаемого функцией count (), условие оператора if будет истинным, если слово word в наборе excluded отсутствует. В этом случае происходит приращение значения, соответст- вующего слову в карте word_count. Как и в предыдущей версии программы подсчета слов, здесь все основано на том факте, что обращение по индексу к элементу карты приводит к его добавлению, если такого ключа еще не существовало. ++word_count[word]; В результате слово word будет добавлено в контейнер word_count, если оно там отсутствовало. Первоначально добавленный элемент имеет значение 0. Независимо от того, существовал ли элемент ранее или был добавлен, его значение увеличивает- ся на единицу. Упражнения раздела 10.4.2 Упражнение 10.23. Напишите программу, которая хранит исключаемые слова в векторе, а не в наборе. Каковы преимущества применения набора? Упражнение 10.24. Напишите программу, которая преобразует слова в единственное число, уда- ляя из них последний символ 's'. Создайте набор слов, из которых не следует удалять послед- ний символ 1 s'. Для примера поместите в этот набор слова success и class. Используйте этот набор в составе программы, которая удаляет из введенных слов окончания множественного числа, за исключением указанных слов, которые остаются неизменными. Упражнение 10.25. Создайте вектор для названий книг, которые предполагается прочесть на протяжении следующих шести месяцев, и набор для названий уже прочитанных книг. Напишите программу, которая самостоятельно выбирает из вектора следующую книгу, если она еще не про-
Глава 10. Ассоциативные контейнеры 403 читана. Выбранное название должно быть занесено в набор прочитанных книг. Если книга оказа- лась неинтересной и фактически не была прочитана, организуйте удаление ее названия из набора прочитанных книг. По завершении шести виртуальных месяцев, отобразите содержимое набора прочитанных книг и тех книг, которые не были прочитаны. 10.5. Типы multimap и multiset Контейнеры типа тар и set способны содержать только один экземпляр каждого ключа, а контейнеры типа multiset и multimap — несколько. В телефонной книге, например, для некоего лица может быть указан целый список номеров телефонов. Список доступных книг автора может содержать отдельный перечень по каждой те- ме. Типы multimap и multiset определены в тех же заголовках, тар и set, что и соответствующие версии для уникальных элементов. Контейнеры multimap и multiset обладают теми же функциональными воз- можностями, что и контейнеры тар и set соответственно, но с одним исключением: multimap не поддерживает индексации, поскольку с данным ключом может быть связано несколько значений. Функции, которые являются общими для контейнеров тар и multimap или set и multiset, варьируются с учетом возможности сущест- вования нескольких одинаковых ключей. Используя контейнер multimap или multiset, следует быть готовым получить несколько значений, а не одно. 10.5.1. Добавление и удаление элементов Для добавления и удаления элементов контейнеров multimap и multiset ис- пользуются функции insert (), описанные в табл. 10.5 (стр. 392), и функции, erase (), описанные в табл. 10.7 (стр. 396). Поскольку ключи вовсе необязательно должны быть уникальными, функция insert () добавляет элемент всегда. Например, можно создать контейнер multimap, связывающий имена авторов с названиями написанных ими книг. Такая карта могла бы содержать по несколько элементов для каждого автора. // добавить первый элемент с ключом Barth authors.insert(make_pair( string("Barth, John"), string("Sot-Weed Factor"))); // ok: добавить второй элемент с ключом Barth authors.insert(make_pair( string("Barth, John"), string("Lost in the Funhouse"))); Версия функции erase (), которой передают ключ, удаляет все элементы с ука- занным ключом. Она возвращает количество удаленных элементов. Версии функции erase (), которым передают один или два итератора, удаляют только указанный элемент (элементы). Эти версии возвращают void. multimap<string, string> authors; string search_item("Kazuo Ishiguro"); // удалить все элементы с этим ключом; // возвращает количество удаленных элементов multimap<string, string>::size_type ent = authors.erase(search_item);
404 Часть II. Контейнеры и алгоритмы 10.5.2. Поиск элементов в контейнерах multimap Hmultiset Как уже упоминалось, карты и наборы хранят свои элементы упорядоченно. Кон- тейнеры multimap и multiset поступают точно так же. В результате, когда кон- тейнер multimap или multiset содержит несколько экземпляров одинакового ключа, хранящие их элементы располагаются внутри контейнера рядом. При переборе элементов контейнера multimap или multiset эта упорядоченность 1 гарантирует быстрое возвращение всех элементов с указанным ключом. С / Результат поиска элемента в карте или наборе довольно прост — элемент в кон- тейнере или есть, или нет. У контейнеров multimap и multiset все несколько сложнее: элемент может присутствовать в нескольких экземплярах. Контейнер с именами авторов и названиями их книг, например, вполне позволил бы найти и ото- бразить список всех книг определенного автора. Кроме того, для поиска и отображения всех книг указанного автора можно при- менить три способа. Каждый из способов опирается на тот факт, что все элементы для указанного автора располагаются внутри контейнера multimap рядом. Сначала рассмотрим способ, подразумевающий использование только описанных ранее функций. Эта версия потребует большого количества кода, поэтому далее бу- дут описаны более компактные варианты. Применение функций find () и count () Проблему можно решить при помощи функций find О и count (). Функция count () сообщит количество элементов с искомым ключом, а функция find() возвратит итератор на первый из них. // искомый автор string search_item("Alain de Botton"); // количество элементов данного автора typedef multimap<string, string>::size_type sz_type; sz_type entries = authors.count(search_item); // получить итератор первой записи данного автора multimap<string,string>::iterator iter = authors.find(search_item); // цикл перебора полученного количества элементов данного автора for (sz_type ent = 0; ent != entries; ++cnt, ++iter) cout << iter->second << endl; // вывод названия каждой книги Сначала при помощи функции count () необходимо выяснить количество элемен- тов, относящихся к указанному автору, а затем, при помощи функции f ind (), следует получить итератор на первый элемент с этим ключом. Количество итераций цикла for зависит от количества элементов, возвращенного функцией count (). В частности, ес- ли функция count () возвратит значение 0, цикл не будет выполнен ни разу. Решение с применением итератора Другой, более изящный способ подразумевает применение двух новых функций ассоциативных контейнеров: lower_bound () и upper_bound (). Эти функции,
Глава 10. Ассоциативные контейнеры 405 описанные в табл. 10.8 (стр. 406), применимы ко всем ассоциативным контейнерам, включая тар и set, но чаще всего они используются с контейнерами multimap и multiset. Каждая из них получает ключ и возвращает итератор. Вызов функций lower_bound () и upper_bound () с одинаковым ключом по- зволяет получить итераторы, которые обозначают диапазон (раздел 9.2.1, стр. 341) всех элементов с таким ключом. Если ключ в контейнере присутствует, итераторы отличаются: возвращенный функцией lower_bound () указывает на первый эле- мент с данным ключом, а возвращенный функцией upper_bound О — на следую- щий элемент после последнего. Если в контейнере нет элементов с указанным клю- чом, функции lower_bound () и upper_bound () возвратят равные итераторы, причем оба будут указывать на место, куда, согласно порядку расположения элемен- тов, могли бы быть добавлены элементы с данным ключом. Безусловно, возвращенный этими функциями итератор может указывать на эле- мент непосредственно после конца контейнера. Если искомый элемент имеет самый большой ключ в контейнере, вызов функции upper_bound () возвратит итератор на элемент после последнего элемента контейнера. Если элемент отсутствует и ключ является самым большим в контейнере, вызов функции lower_bound () также воз- вратит итератор на элемент после последнего элемента контейнера. Итератор, возвращенный функцией iower_bound (), может указывать, а может и не Зэщ? ) указывать на элемент с указанным ключом. Если элемента с указанным ключом в контей- У неРе нет> функция iower_bound () возвращает итератор на первую позицию, в кото- рую, согласно порядку расположения элементов, мог бы быть вставлен элемент с данным ключом. Используя эти функции, программу поиска и отображения всех книг указанного автора можно переписать следующим образом. // автор и определение search_item как прежде // итераторы beg и end обозначают диапазон элементов данного автора typedef multimap<string, string>::iterator authors_it; authors_it beg = authors.lower_bound(search_item), end = authors.upper_bound(search_item); // цикл перебора полученного количества элементов данного автора while (beg != end) { cout << beg->second << endl; // вывод названия каждой книги ++beg; При помощи функций count () и find () эта программа решает ту же задачу, что и предыдущая, но несколько проще. Обращение к функции lower_bound () по- зиционирует итератор beg на первый элемент с хранимым в переменной search_ item ключом, если он есть. Если такого элемента нет, итератор beg укажет на пер- вый элемент с ключом, который больше, чем в переменной search_item. Обраще- ние к функции upper_bound () позиционирует итератор end на элемент, располо- женный непосредственно после последнего элемента с указанным ключом. Эти функции не сообщают о наличии элемента с указанным ключом. Важнее всего то, что они возвращают диапазон итераторов.
406 Часть II. Контейнеры и алгоритмы Если элемента для этого ключа нет, функции lower_bound () и upper_bound () вернут одинаковые итераторы, которые указывают на тот же элемент или на элемент после последнего элемента контейнера. В обоих случаях это будет позиция, в кото- рую мог бы быть добавлен элемент с данным ключом. Если элементы с указанным ключом существуют, итератор beg будет указывать на первый из них, а итератор end — на следующий после последнего. Увеличивая значение итератора beg, можно перебрать все элементы с этим ключом. Когда ите- ратор beg станет равен итератору end, все существующие элементы с данным клю- чом будут перебраны. При условии, что эти итераторы формируют диапазон, для его перебора можно воспользоваться тем же циклом while, что и при переборе других диапазонов. Цикл выполняется соответствующее количество раз и отображает все имеющиеся в нали- чии элементы данного автора. Если соответствующих элементов нет, сразу же ока- жутся равны итераторы beg и end и тело цикла не будет выполнено ни разу. В про- тивном случае, как известно, инкремент итератора beg в конечном счете сделает его равным итератору end, что позволит отобразить каждую запись, связанную с дан- ным автором. Таблица 10.8. Функции ассоциативных контейнеров, возвращающие итераторы m.lower_bound(к) m.upper_bound(к) m.equal_range(к) Возвращает итератор на первый элемент, значение ключа которого не мень- ше, чем к Возвращает итератор на первый элемент, значение ключа которого большее, чем к Возвращает пару итераторов. Первый элемент пары эквивалентен итератору, возвращаемому функцией m. lower_bound (k), а второй эквивалентен итератору, возвращаемому функцией m. upper bound (k) Функция equal range () Существует даже более простой способ решения этой проблемы: вместо функций upper_bound () и lower_bound () можно применить функцию equal_range (). Эта функция возвращает пару итераторов. Если значение присутствует, первый ите- ратор указывает на первый элемент с указанным ключом, а второй — на следующий после последнего. Если соответствующий элемент не найден, оба итератора указы- вают на позицию, где элемент с данным ключом мог бы находиться. Используя функцию equal_range (), рассматриваемую программу можно пе- реписать таким образом. // автор и определение search_item как прежде // pos содержит итераторы, обозначающие диапазон элементов pair<authors_it, authors_it> pos = authors.equal_range(search_item); // цикл перебора полученного количества элементов данного автора while (pos.first != pos.second) { cout « pos.first->second << endl; // вывод названий книг ++pos.first; } По существу, эта программа идентична предыдущей, в которой использовались функции upper_bound () и lower_bound (). Но вместо локальных переменных
Глава 10. Ассоциативные контейнеры 407 beg и end, содержащих итераторы диапазона, здесь используется пара (объект типа pair), возвращаемая функцией equal_range (). Переменная-член first типа pair содержит тот же итератор, который вернула бы функция lower_bound (). Итератор, который вернула бы функция upper_bound (), содержит переменная- член second. Таким образом, переменная-член pos . first в этой программе эквивалентна локальной переменной beg, а переменная-член pos . second — локальной пере- менной end. Упражнения раздела 10.5.2 Упражнение 10.26. Напишите программу, которая заполняет контейнер типа multimap имена- ми авторов и названиями их работ. Используйте функцию f ind () для поиска и удаления элемен- та (при помощи функции erase ()). Убедитесь, что программа работает правильно даже тогда, когда искомый элемент в контейнере отсутствует. Упражнение 10.27. Повторите программу из предыдущего упражнения, но на этот раз для получения итераторов, обозначающих диапазон удаляемых элементов, используйте функцию equal_range(). Упражнение 10.28. Используя контейнер типа multimap из предыдущего упражнения, напиши- те программу, которая создает список авторов, чьи имена начинаются с очередной буквы алфави- та. Результат должен выглядеть примерно следующим образом. Author Names Beginning with 'A': Author, book, book, ... • • • Author Names Beginning with 'B': • • • Упражнение 10.29. Объясните назначение операнда pos. first->second, используемого в выражении вывода последней программы этого раздела. 10.6. Применение контейнеров: программа TextQuery В заключение этой главы реализуем простую программу для работы с текстом. Программа будет читать указанный пользователем файл, а затем позволит ему искать слова, которые могут в нем встретиться. Результатом запроса будет количе- ство найденных слов, а также список строк, в которых они присутствуют. Если в той же строке искомое слово встречается несколько раз, программа должна быть доста- точно “умна”, чтобы отобразить эту строку только один раз. Строки должны отобра- жаться по порядку, т.е. строка 7 должна быть перед строкой 9 и т.д. Например, прочитав файл, содержащий вводную часть этой главы, и получив для поиска слово “element”, программа должна вернуть примерно следующий результат. element occurs 125 times (line 62) element with a given key. (line 64) second element with the same key. (line 153) element |==| operator.
408 Часть II. Контейнеры и алгоритмы (line 250) the element type. (line 398) corresponding element. Далее следуют остальные 120 строк, в которых встречается слово “element”2. 10.6.1. Проект программы Наилучший способ начать проект программы — это составить перечень ее функ- циональных возможностей. Зная, какие именно функции необходимо обеспечить, значительно легче разобраться, какие именно структуры данных понадобятся и как можно было бы реализовать необходимые действия. Итак, начнем с требований, ко- торым должна удовлетворять разрабатываемая программа. 1. Программа должна позволить пользователю указать имя обрабатываемого фай- ла. Она должна загрузить содержимое файла в некое хранилище, чтобы иметь возможность отображать строку исходного текста, в которой присутствует иско- мое слово. 2. Программа должна разбить каждую строку на слова и запомнить все строки, в которых присутствует искомое слово. Отображаемые номера строк должны рас- полагаться в порядке возрастания и не содержать дубликатов. 3. Результат поиска должен содержать номера строк, в которых встречается иско- мое слово. 4. Чтобы отобразить текст, в котором встретилось слово, необходимо иметь воз- можность выбрать строку из исходного файла, соответствующую данному номе- ру строки. Структура данных Реализуем программу на базе простого класса, которому присвоим имя Text Query. Выполнить приведенные выше требования можно при помощи контей- неров разных типов. 1. Для сохранения копии всего исходного файла, используем вектор vector <string>. Каждая строка исходного файла будет элементом этого вектора. Та- ким образом, когда необходимо отобразить строку, ее можно выбрать из вектора, используя номер как индекс. 2. Номера строк, в которых найдено слово, будем хранить в наборе. Применение набора гарантирует, что строка будет представлена только одним элементом и что номера строк будут автоматически сохранены в порядке возрастания. 3. Чтобы связать каждое слово с набором номеров строк, в которых встречается ис- комое слово, используем карту. Таким образом, класс будет содержать две переменные-члена: вектор, содержа- щий исходный файл, и карту, связывающую каждое введенное слово с набором но- меров строк, в которых оно встречается. 2 Поскольку исходный текст книги написан в системе LaTeX, строки относительно корот- ки, а вокруг выделенных символов располагаются дескрипторы /. — Примеч. ред.
Глава 10. Ассоциативные контейнеры 409 Функции Требования определяют также и интерфейс создаваемого класса. Однако сначала примем одно немаловажное для проекта решение: функция, которая делает запрос, должна будет возвратить набор номеров строк. Какой же тип использовать для этой реализации? Как можно заметить, реализовать запрос довольно просто: чтобы получить свя- занный со словом набор, достаточно обратиться к карте по индексу. Единственный вопрос — как возвратить найденный набор. Надежней, конечно, возвратить копию найденного набора, однако при этом придется копировать каждый элемент набора. При обработке достаточно большого файла, копирование набора может обойтись очень дорого. Можно также возвратить пару итераторов набора или константную ссылку на набор. Для простоты будем возвращать копию, однако отметим, что это отнюдь не наилучшее решение, которое возможно придется пересмотреть, если ко- пирование будет осуществляться слишком долго. Первую, третью и четвертую задачи проекта решим при помощи функций-членов класса. Вторую задачу решим внутри класса. Исходя из поставленных задач, в интер- фейсе класса можно определить три следующие открытые (public) функции-члена. Функция read_file() получает поток ifstream&, из которого она читает строки, и сохраняет их в векторе. Завершив чтение файла, функция read_ f ile () создаст карту, в которой каждое слово связано с номером строки, в ко- торой оно присутствует. Функция run_query () получает строку и возвращает набор номеров строк, в которых присутствует содержимое переданной строки. Функция text_line О получает номер строки и возвращает текст, соответст- вующий этой строке в исходном файле. Ни функция run_query (), ни функция text_line () не изменяют объект, от имени которого они выполняется, поэтому их имеет смысл определить как кон- стантные функции-члены (раздел 7.7.1, стр. 286). Для обеспечения действий функции read_file(), необходимо также опреде- лить две закрытые функции-члена, которые будут читать исходный файл и созда- вать карту. Функция store_f ile () будет читать файл и сохранять данные в векторе. Функция build_map () разобьет каждую строку на слова и создаст карту, в ко- торой хранятся и номера строк, в которых встречается каждое слово. 10.6.2. Класс TextQuery Теперь, на основании проекта, можно создать класс TextQuery. class TextQuery { // как прежде public: // определения типов, упрощающие объявления typedef std::string::size_type str_size; typedef std::vector<std::string>::size_type line_no;
410 Часть II. Контейнеры и алгоритмы /* интерфейс: * функция read_file() создает внутреннюю структуру данных * для предоставленного файла * функция run_query() находит указанное слово и возвращает * набор строк, в которых оно встречается * функция text_line() возвращает запрошенную строку из * исходного файла ★ у void read_file(std::ifstream &is) { store_file(is); bui1d_map(); } std::set<line_no> run_query(const std::string&) const; std: .-string text_line (line_no) const; str_size size() const { return lines_of_text.size(); } void display_map(); // вспомогательная: отображает содержимое // карты private: // вспомогательная, используется void store_file(std::ifstream&); void bui1d_map(); функцией rea d_file() // сохраняет исходный файл II ассоциирует каждое слово с II набором номеров строк // запомнить весь исходный файл std::vector<std::string> lines_of_text; // карта слов и наборов строк, в которых они встречаются std::map< std::string, std::set<line_no> > word_map; // символы, используемые для отступов static std::string whitespace_chars; // канонизировать текст: удалить пунктуацию и сделает все // буквы строчными static std::string cleanup_str(const std::string&); Класс непосредственно отражает решения проекта. Единственная неописанная часть — это определение типа, которое создает псевдоним для типа s i ze_type вектора. По причинам, описанным в разделе 3.1 (стр. 103), имена всех библиотечных объек- тов в определении класса указаны полностью (т.е. с использованием части std::). Функция read_f ile () определена внутри класса. Она вызывает функцию store_ f ile () для чтения и сохранения исходного файла, а также функцию build_map () для создания карты из слов и номеров строк. Другие функции определим в разде- ле 10.6.4 (стр. 413). Но сначала, чтобы решить проблему с текстовым запросом, на- пишем программу, которая этот класс использует. Упражнения раздела 10.6.2 Упражнение 10.30. В функциях-членах класса Text Query использованы только те возможно- сти, которые уже были описаны. Напишите собственные версии функций-членов. (Подсказка: единственная сложная часть — это возвращение значения из функции run_query (), если на- бор номеров строк пуст. Решение заключается в том, чтобы создать и возвратить новый (временный) набор.)
Глава 10. Ассоциативные контейнеры 411 10.6.3. Применение класса TextQuery Приведенная ниже функция main() программы использует объект класса TextQuery для отработки запроса пользователя. Большая часть этой программы посвящена взаимодействию с пользователем: запрос следующего искомого слова и вызов функции print_resuits (), которая отображает результат (она будет реа- лизована позже). // программа имеет один аргумент, задающий файл с исходным текстом int main(int argc, char **argv) // открыть файл, к тексту которого осуществляется запрос ifstream infile; if (argc <2 II !open_file(infile, argv[l])) { cerr << "No input file!" << endl; return EXIT_FAILURE; } TextQuery tq; tq.read_file(infile) ; // создать карту запроса // цикл взаимодействия с пользователем: приглашение к // вводу искомого слова и отображение результата // цикл бесконечный; условие выхода находится внутри while (true) { cout << "enter word to look for, or q to quit: string s; cin >> s; // выход из цикла, если введен символ конца файла или 'q' if (!cin I I s == "q") break; // получить набор номеров строк, в которых // присутствует искомое слово set<TextQuery::line_no> Iocs = tq.run_query(s); // отобразить количество слов и все строки с ним, если есть print_results(Iocs, s, tq) ; } Предварительные действия Эта программа проверяет допустимость переданного в качестве аргумента argv [1] функции main () имени файла, пытаясь открыть его при помощи функции open_f ile () (раздел 8.4.3, стр. 325). Если поток допустим, значит, исходный файл открыт успешно. В противном случае формируется соответствующее сообщение и происходит выход из программы с возвращением значения EXIT_FAILURE (раздел 7.3.2, стр. 273), которое указывает, что произошла ошибка. Как только файл будет открыт, создать карту, обеспечивающую поддержку за- просов, не составит труда. Для этого создается локальная переменная по имени tq, которая будет содержать файл и необходимые структуры данных. TextQuery tq; tq.read_file(infile) ; // создать карту запроса Далее происходит вызов функции read_f ile () объекта tq класса TextQuery, которой передается файл, открытый функцией open_f ile (). После завершения работы функции read_file(), объект tq содержит две структуры данных: вектор, соответствующий исходному файлу, а также карту из
412 Часть II. Контейнеры и алгоритмы слов и наборов номеров строк. Эта карта содержит элементы для каждого отдельно- го слова в исходном файле. В элементах карты каждое слово связано с набором но- меров строк, в которых это слово присутствует. Осуществление поиска Поскольку необходимо обеспечить возможность поиска не только одного слова при каждом запуске программы, приглашение к вводу следует поместить в цикл while. // цикл взаимодействия с пользователем: приглашение к // вводу искомого слова и отображение результата // цикл бесконечный; условие выхода находится внутри while (true) { cout << "enter word to look for, or q to quit: string s; cin >> s; // выход из цикла, если введен символ конца файла или 'q' if (!cin I I s == "q") break; // получить набор номеров строк, в которых // присутствует искомое слово setcTextQuery::line_no> Iocs = tq.run_query(s); // отобразить количество слов и все строки с ним, если есть print_results(Iocs, s, tq) ; } В условии цикла while проверяется логический литерал true, который всегда остается истинным и делает цикл бесконечным. Выход из цикла осуществляет опе- ратор break после проверки состояния объекта cin и прочитанного искомого слова. Таким образом, выход из цикла происходит в случае, когда в объекте cin происхо- дит ошибка, или он получает символ конца файла, или когда пользователь вводит символ q (quit — выйти). Прочитанное искомое слово передается в качестве аргумента функции run_ query () объекта tq, что позволяет создать набор номеров строк, в которых это сло- во присутствует. Полученный набор Iocs, искомое слово и объект tq класса TextQuery передается функции print_results (), которая отображает результат работы программы. Отображение результатов Осталось определить функцию print_results (). void print_results(const set<TextQuery::line_no>& Iocs, const string& sought, const TextQuery &file) { // если слово найдено, отобразить количество его экземпляров // и все случаи применения typedef set<TextQuery::line_no> line_nums; line_nums::size_type size = locs.size(); cout « "\n" « sought << " occurs " << size << " " << make plural(size, "time", "s") « endl; // отобразить каждую строку, в которой есть искомое слово line_nums::const_iterator it = Iocs.begin(); for ( ; it != locs.end(); ++it) { cout << "\t(line " // не путать пользователя, начиная нумерацию строк с О
Глава 10. Ассоциативные контейнеры 413 « (*it) +.1 « ") " « file.text_line(*it) « endl; } } В начале функции расположено определение типа, упрощающего применение набора номеров строк. Отображение начинается с сообщения о количестве найден- ных пар, которое присваивается переменной size в результате вызова функции- члена size () набора Iocs. Вызов функции make_plural () (раздел 7.3.2, стр. 273) позволяет отобразить слово time или times, в зависимости от того, равно ли коли- чество найденных слов единице (т.е. в единственном или множественном числе). Самая сложная часть программы — это цикл for, в котором обрабатывается на- бор Iocs и отображаются номера строк и строки, в которых было найдено слово. Единственной сложностью здесь является изменение номеров строк на более при- вычные людям. При сохранении текста, первая строка имеет номер 0, что соответст- вует правилам нумерации элементов массивов и контейнеров в языке C++. Однако большинство пользователей полагает, что первой является строка номер 1, поэтому при отображении будем систематически добавлять единицу к реальным номерам хранимых строк, чтобы привести их к форме, более удобной для пользователя. Упражнения раздела 10.6.3 Упражнение 10.31. Каким будет результат выполнения функции main (), если искомое слово не найдено? 10.6.4. Создание функций-членов Теперь необходимо создать те функции-члены, которые не были определены внутри класса. Сохранение содержимого исходного файла Сначала необходимо прочитать файл, который пользователь хочет анализировать. При помощи функций классов string и vector, эта задача решается довольно легко. // чтение исходного файла: сохранить каждую строку как элемент // вектора lines_of_text void TextQuery::store_file(ifstream &is) { } Поскольку содержимое файла необходимо хранить построчно, используем для чтения функцию getline (). Для добавления каждой прочитанной строки в вектор lines_of_text используем функцию push_back (). Создание карты слов Каждый элемент вектора содержит строку текста. Чтобы создать карту из слов и номеров строк, каждую строку необходимо разбить на отдельные слова. Здесь снова используется объект класса istringstream, как и в программе на стр. 327.
414 Часть II. Контейнеры и алгоритмы // выявить разделенные пробелами слова в исходном векторе / / и поместить их в карту word_map наряду с номером строки void TextQuery::build_map() { // обработать каждую строку из исходного вектора for (line_no line_num = 0; line_num != lines_of_text.size(); ++line_num) { // line используется для чтения текста по одному слову istringstream line(lines_of_text[line_num]); string word; while (line >> word) // добавить в набор номер данной строки; // индексирование добавит слово в карту, если // его там еще нет word_map[cleanup_str(word)].insert(line_num); } } Цикл for перебирает вектор lines_of_text построчно. Сначала при по- мощи оператора ввода класса istringstream привяжем объект line класса istringstream к текущей строке, чтобы прочитать из нее каждое слово. Не забы- вайте, что этот оператор, подобно любой другой функции класса istream, игнори- рует пробелы. Таким образом, цикл while читает каждое отделенное пробелом сло- во из потока line в строковую переменную word. Последняя часть этой функции подобна использованной в программе подсчета слов. Для индексирования карты используем переменную word. Если слова в карте word_map еще нет, оператор индексирования добавит его, причем значением добав- ленного элемента будет пустой набор. Независимо от того, было ли слово добавлено, индексирование предоставит доступ к значению элемента, т.е. набору. Вызов его функции insert () добавит номер текущей строки. Если слово в той же строке встречается несколько раз, функция insert () никак не прореагирует. Поддержка запросов Фактически запрос осуществляет функция run_query (). set<TextQuery::line_no> TextQuery::run_query(const string &query_word) const { // Обратите внимание: чтобы избежать добавления слов в карту // word_map, здесь применяется функция find(), а не // индексирование 1 map<string, set<line_no> >::const—iterator loc = word_map.find(cleanup—str(query_word)); if (loc == word—map.end()) return set<line_no>(); // не найдено, вернуть пустой набор else // возвратить набор номеров строк для данного слова return loc->second; Функция run_query () получает ссылку на константную строку и использует ее значение при поиске строки в карте word_map. Если строка найдена, функция воз- вращает связанный с ней набор номеров строк. В противном случае она возвращает пустой набор.
Глава 10. Ассоциативные контейнеры 415 Применение набора, возвращенного функцией run_query () В результате выполнения, функция run_query () вернет набор номеров строк, в которых присутствует искомое слово. Кроме отображения количества случаев при- менения искомого слова, необходимо отобразить каждую строку, в которой оно при- сутствует. Это позволит сделать функция text line (). string TextQuery::text_line(line_no line) const { if (line < lines_of_text.size()) return 1ines_of_text[line]; throw std::out_of_range("line number out of } range"); Эта функция получает номер строки и возвращает соответствующую ему строку исходного текста. Поскольку код, который использует класс TextQuery, этого сде- лать не может (функция lines_of_text () объявлена закрытой (private)), не- обходимо сначала проверить, находится ли запрошенная строка в допустимом диа- пазоне. Если это так, возвращается соответствующая строка, а если нет — передается исключение out_of_range. Упражнения раздела 10.6.4 Упражнение 10.32. Переделайте программу TextQuery так, чтобы для хранения номеров строк вместо набора использовался вектор. Обратите внимание, поскольку строки сохраняются в порядке возрастания, новый номер строки в вектор можно добавить только тогда, когда по- следний элемент вектора не имеет тот же номер строки. Каковы преимущества и недостатки (с точки зрения эффективности и простоты разработки) этих двух решений? Какое из них пред- почтительнее и почему? Упражнение 10.33. Почему функция TextQuery: :text_iine не проверяет, является ли ее аргумент отрицательным? Резюме Элементы в ассоциативном контейнере упорядочены и доступны по ключу. Ассоциатив- ные контейнеры обеспечивают быстрый поиск и возвращение элементов. Применение ключа отличает их от последовательных контейнеров, доступ к элементам которых осуществляется по позиции. Контейнеры типа тар и multimap хранят элементы, которые являются парами ключ- значение. Для создания этих пар применяется библиотечный тип pair, определенный в заго- ловке utility. Обращение к значению итератора на элемент контейнера тар или multimap возвращает значение, которое является объектом типа pair. Первый элемент (first) объек- та типа pair представляет собой константный ключ, а второй элемент (second) — значение, связанное с этим ключом. Контейнеры типа set и multiset хранят только ключи. Контей- неры типа тар и set допускают наличие только одного элемента с определенным ключом. Контейнеры типа multimap и multiset допускают наличие нескольких элементов с одина- ковым ключом. Некоторые функции ассоциативных контейнеров совпадают с функциями последова- тельных контейнеров. Однако ассоциативные контейнеры обладают несколькими уникаль- ными функциями. Кроме того, в них переопределены некоторые функции, совпадающие с по-
416 Часть II. Контейнеры и алгоритмы следовательными контейнерами. Различия в функциях отражают применение в ассоциатив- ных контейнерах ключей. К элементам в ассоциативном контейнере можно обращаться при помощи итератора. Библиотека гарантирует, что итераторы позволят обратиться к элементам по ключу. Функ- ция begin () возвращает элемент с самым младшим ключом. Приращение этого итератора позволит перебрать все элементы в возрастающем порядке. Т ермины Ассоциативный контейнер (associative container). Контейнер, который обеспечивает бы- стрый доступ к хранимым в нем объектам по ключу. Ассоциативный массив (associative array). Массив, элементы которого проиндексированы по ключу, а не по позиции. Таким образом, ключ связан (ассоциирован) со значением. Контейнер тар (карта). Ассоциативный контейнер, аналогичный ассоциативному масси- ву. Подобно типу vector, тип тар является шаблоном класса. Но при создании карты необ- ходимо указать два типа: тип ключа и тип связанного с ним значения. В контейнере тар клю- чи уникальны, они не повторяются. Каждый ключ связан с определенным значением. Обра- щение к значению итератора карты возвращает объект типа pair, который содержит константный ключ и связанное (ассоциированное) с ним значение. Контейнер multimap. Ассоциативный контейнер, подобный контейнеру тар, но способ- ный содержать одинаковые ключи. Контейнер multiset. Ассоциативный контейнер, который содержит только ключи. В от- личие от набора, способен содержать одинаковые ключи. Контейнер set (набор). Ассоциативный контейнер, который содержит только ключи. Ключи в контейнере set не могут совпадать. Оператор *. Оператор обращения к значению, примененный к итератору контейнера тар, set, multimap или multiset возвращает объект типа value_type. Обратите внимание, типом value_type контейнера map и multimap является пара. Оператор [ ]. Оператор индексирования, примененный к контейнеру тар, получает ин- декс, типом которого должен быть key_type (или тип, допускающий преобразование в него). Возвращает значение типа mapped_type. Строгое сравнение (strict weak ordering). Отношения между ключами ассоциативного контейнера. При строгом сравнении, можно сравнить два любых значения и выяснить, кото- рое из них меньше. Если ни одно из значений не меньше другого, они считаются равными. См. раздел 10.3.1 (стр. 388). Тип key_type. Тип, определенный в шаблоне ассоциативного контейнера, которому со- ответствует тип ключей, используемых для сохранения и возвращения значения. У контейне- ра тар тип key_type используется для индексации. У контейнера set типы key_type и value_type совпадают. Тип mapped_type. Тип, определенный в шаблонах ассоциативных контейнеров тар и multimap, которому соответствует тип хранимых значений. Тип pair (пара). Тип, объект которого содержит две открытые переменные-члена по име- ни first (первый) и second (второй). Тип pair является шаблоном, при создании класса которого указывают два типа: тип первого и тип второго элемента. Тип value_type. Тип элемента, хранимого в контейнере. У контейнеров set и multiset, типы value_type и key_type совпадают. У контейнеров тар и multimap, этот тип пред- ставляет собой пару, первый элемент которой (first) имеет тип const key_type, а второй (second) — тип mapped_type.
ГЛАВА 11 Общие алгоритмы В ЭТОЙ ГЛАВЕ... 11.1. Краткий обзор 418 11.2. Первый взгляд на алгоритмы 421 11.3. Возвращаясь к итераторам 432 11.4. Структура общих алгоритмов 446 11.5. Алгоритмы, специфические для контейнеров 449 Резюме 451 Термины 452 В библиотечных контейнерах определен на удивление небольшой набор функ- ций. Вместо того, чтобы снабжать каждый контейнер большим количеством одина- ковых функций, библиотека предоставляет набор алгоритмов, большинство из кото- рых не зависит от конкретного типа контейнера. Эти алгоритмы называют “общими” (generic), поскольку применимы они и к контейнерам разных типов, и к разным ти- пам элементов. В этой главе рассматриваются не только общие алгоритмы, но и более подробно описаны итераторы. Некоторые стандартные функции определены в самих контейнерах. Главным об- разом они позволяют добавлять и удалять элементы, получить доступ к первому и последнему элементам, получать (а в некоторых случаях и обнулять) размер кон- тейнера, а также получать итераторы на первый элемент и элемент за пределами по- следнего. С элементами контейнера можно осуществить множество других не менее полез- ных операций. Последовательный контейнер можно отсортировать, найти опреде- ленный элемент, самый большой или самый маленький элемент и т.д. Чтобы не соз- давать каждую из этих функций как член контейнера каждого типа, стандартная библиотека определяет набор общих алгоритмов (generic algorithm). Алгоритмами они называются потому, что реализуют стандартные операции, а общими — потому что работают с контейнерами любых типов, включая массивы встроенных типов и, как будет продемонстрировано далее, с последовательностями других видов, а не только с такими библиотечными типами, как vector или list. Алгоритмы приме- нимы также к контейнерам, созданным самостоятельно, если они соответствуют со- глашениям стандартной библиотеки.
418 Часть II. Контейнеры и алгоритмы Большинство алгоритмов работают перебирая диапазон элементов, ограничен- ный двумя итераторами. Как правило, перебирая элементы диапазона алгоритм об- рабатывает каждый из них. Алгоритмы получают доступ к элементам при помощи итераторов, которые обозначают диапазон подлежащих обработке элементов. 11.1. Краткий обзор Предположим, что необходимо выяснить, существует ли в векторе целых чисел по имени vec определенное значение. Проще всего решить эту задачу при помощи библиотечной функции find (). // искомое значение int search_value - 42; // вызов, позволяющий выяснить, присутствует ли искомое значение vectorednt>::const iterator result = // отображение результата cout << "The value " << search_value << (result == vec.end() ? " is not present" : "is present") < < endl; При вызове функция find () получает два итератора и значение. Она проверяет каждый элемент в диапазоне (раздел 9.2.1, стр. 341), обозначенном его аргументами- итераторами. Как только равный переданному значению элемент будет найден, функция f ind () возвращает итератор на этот элемент. Если соответствующий эле- мент не найден, функция find () возвращает второй из переданных ей итераторов, что означает отказ. Таким образом, проверив равенство возвращенного итератора и второго аргумента можно выяснить, был ли элемент найден. Проверку осуществляет условный оператор (раздел 5.7, стр. 190), расположенный непосредственно внутри оператора вывода, который и сообщает, было ли найдено значение. Поскольку функция f ind () использует итераторы, ее можно применить для по- иска значения в любом контейнере. Рассмотрим пример применения функции f ind () для поиска значения в списке целых чисел по имени 1st. // вызов, позволяющий найти элемент в list<int>::const_iterator result = find(1st.begin(), lst.end(), cout << "The value " << search_value < < (result == 1st.end() ? " is not present" : " is « endl; списке search_value); present") Если бы не тип переменной result и итераторов, переданных функции find (), этот код был бы идентичен рассмотренному ранее коду поиска элемента вектора. Поскольку итераторы действуют подобно указателям, функцию find () можно применить для поиска и в массивах встроенных типов. int ia[6] = {27, 210, 12, 47, 109, 83}; int search_value = 83; int *result = find(ia, ia + 6, search_value); cout << "The value " << search_value < < (result == ia + 6 ? " is not present" : " is present") « endl;
Глава 11. Общие алгоритмы 419 Здесь функции find() передается указатель на первый элемент массива ia и указатель на элемент шестью позициями дальше от начала массива ia (т.е. следую- щий элемент после конца). Если возвращенный указатель равен ia + 6, это свиде- тельствует о том, что поиск закончился неудачей, в противном случае будет возвра- щен указатель на найденное значение. Когда необходимо указать диапазон, передают итераторы (или указатели) на пер- вый и следующий после последнего элементы этого диапазона. При следующем вызове функции find (), поиск осуществляется только среди элементов ia [ 1 ] и ia [2]. // искать только среди элементов ia[l] и ia[2] int *result = find(ia +1, ia + 3, search_value); Как работают алгоритмы Каждый общий алгоритм реализован как независимый от конкретных типов кон- тейнера. Алгоритмы в значительной степени (но не полностью) не зависят также от типов элементов, которые контейнер содержит. Чтобы разобраться в работе алго- ритмов, давайте более подробно рассмотрим функцию f ind (). Ее задачей является поиск указанного элемента в неотсортированной коллекции элементов. Концепту- ально, функция find () должна выполнить следующие действия. 1. Проверить по очереди каждый элемент. 2. Если значение элемента равно искомому значению, следует возвратить итератор на текущий элемент. 3. В противном случае перейти к проверке следующего элемента, повторяя этап 2 до тех пор, пока либо значение не будет найдено, либо не будут проверены все элементы. 4. Если конец коллекции достигнут, но значение не найдено, следует вернуть зна- чение, свидетельствующее о неудаче при поиске. Стандартные алгоритмы не зависят от типа Как уже упоминалось, алгоритм не зависит от типа контейнера. Алгоритм имеет одну косвенную зависимость, от типа элемента, поскольку ни должны допускать сравнение. К алгоритмам предъявляются следующие требования. 1. В нем должен быть предусмотрен способ перебора коллекции, позволяющий пе- ремещаться от одного элемента к следующему. 2. Необходим способ определения того, достигнут ли конец коллекции. 3. Необходим способ сравнения каждого элемента с искомым значением. 4. Необходим тип, который позволит обратиться к элементу внутри контейнера по позиции, а также указать, что элемент не был найден. Итераторы связывают алгоритмы с контейнерами Общие алгоритмы реализуют первое требование, применяя для перебора кон- тейнера итераторы (iterator). Все итераторы обладают функцией инкремента, по- зволяющей переходить от одного элемента к следующему, и оператором обраще- ния к значению, позволяющим получать доступ к значению элемента. За одним
420 Часть II. Контейнеры и алгоритмы исключением, рассматриваемым в разделе 11.3.5 (стр. 443), итераторы поддержи- вают также операторы равенства и неравенства, позволяющие выяснить, равны или нет два итератора. В большинстве случаев алгоритмы получают (по крайней мере) два итератора, которые обозначают диапазон элементов, с которыми алгоритм должен работать. Первый итератор относится к первому элементу, а второй указывает на элемент, следующий после последнего. Сам элемент, на который указывает второй итератор (упоминаемый также как итератор после конца (off-the-end iterator)), не исследует- ся, он служит исключительно как ограничитель при переборе. Итератор после конца позволяет также реализовать четвертое требование, обес- печивая удобный способ вернуть значение, указывающее на то, что элемент так и не был найден. То есть если значение не найдено, возвращается итератор после конца, а в противном случае возвращается итератор, указывающий на найденный элемент. Третье требование, сравнение со значением, реализуется одним из двух способов. Как правило, функция find О требует, чтобы для типа элемента был определен оператор ==. Алгоритм использует этот оператор для сравнения элементов. Если ис- пользуемый тип не поддерживает оператор ==, или если необходимо сравнить эле- менты иным способом, можно воспользоваться второй версией функции find(). Эта версия получает дополнительный аргумент, который является именем функции, используемой для сравнения элементов. Для достижения независимости от типа, алгоритмы никогда не полагаются на функции контейнера, вместо них для доступа и перебора элементов, всегда исполь- зуются итераторы. Реальный тип контейнера не важен, не важно даже то, содержит ли он элементы. Библиотека предоставляет более 100 алгоритмов. Подобно контейнерам, алго- ритмы имеют унифицированную архитектуру. В этой главе продемонстрировано применение алгоритмов, а также описаны принципы, общие для всех библиотечных алгоритмов. Классифицированный перечень всех алгоритмов приведен в приложе- нии А, “Библиотека” (стр. 840). Упражнения раздела 11.1 Упражнение 11.1. В заголовке algorithm определена функция count (), подобная функции find (). Она получает два итератора и значение, а возвращает количество обнаруженных в диа- пазоне элементов, обладающих искомым значением. Организуйте чтение в вектор последователь- ности целых чисел. Осуществите подсчет элементов с указанным значением. Упражнение 11.2. Повторите предыдущую программу, но чтение значений организуйте в список (list) строк. Фундаментальная концепция: алгоритмы никогда не используют функции контейнеров Общие алгоритмы никогда не используют функции контейнеров. Они работают ис- ключительно с итераторами. Тот факт, что алгоритмы оперируют итераторами, а не функциями контейнера, возможно, удивителен, но имеет глубоким смысл: когда ис- пользуются “обычные” итераторы, алгоритмы не способны измени! ь размер исходного контейнера. Как бу дет продемонстрировано, алгоритмы способны изменять значения
Глава 11. Общие алгоритмы 421 хранимых в контейнере элементов и перемещать их внутри контейнера. Однако они не способны ни добавлять, ни удалять сами элементы. Как будет продемонстрировано в разделе 11.3.1 (стр. 432). существуют специальные классы итераторов, которые способны на несколько большее, чем просто перебор эле- ментов. Они позволяют выполнять операции вставки. Когда алгоритм работает с од- ним из таких итераторов, возможен побочный эффект добавления элемента в контей-у пер, однако в самих алгоритмах это никогда не используется. 11.2. Первый взгляд на алгоритмы Прежде чем перейти к изучению структуры библиотеки алгоритмов, рассмот- рим несколько примеров. В этом разделе уже было продемонстрировано примене- ние функции find (), а теперь давайте используем несколько дополнительных ал- горитмов. Для применения общего алгоритма, в код следует подключить заголо- вок algorithm. ttinclude <algorithm> В библиотеке определен также набор обобщенных числовых алгоритмов, для ко- торых использованы те же соглашения, что и для общих алгоритмов. Чтобы исполь- зовать эти алгоритмы, в код необходимо подключить заголовок numeric. #include <numeric За небольшим исключением, все алгоритмы работают с диапазоном элементов. Далее этот диапазон будем называть “исходным диапазоном" (“input range”). Алго- ритмы, работающие с исходным диапазоном, всегда получают его в виде двух пер- вых параметров. Эти параметры являются итераторами, используемыми для обозна- чения первого и следующего после последнего подлежащего обработке элемента. Хотя большинство алгоритмов работают с одинаково обозначенным исходным диапазоном, они отличаются тем, как элементы этого диапазона используются. Проще всего подразделить алгоритмы на читающие, записывающие и реорганизую- щие порядок элементов. Примеры каждого вида этих алгоритмов будут рассмотрены в остальной части этого раздела. 11.2.1. Алгоритмы, только читающие элементы контейнера Много алгоритмов только читают значения элементов в исходном диапазоне, но никогда не записывают их. Одним из них является алгоритм поиска (find). Другим предназначенным только для чтения алгоритмом является accumulate, который определен в заголовке numeric. Предположим, что vec — это вектор целых чисел. // прибавляет к сумме элементов вектора vec число 42 int sum = accumulate(vec.begin(), vec.end(), 42); Приведенный выше код присваивает переменной sum число, равное сумме элемен- тов вектора vec плюс 42. Функция accumulate () получает три параметра. Первые два задают диапазон суммируемых элементов, а третий — исходное значение для сум- мы. Функция accumulate () инициализирует внутреннюю переменную исходным значением. Затем она добавляет к этому исходному значению значение каждого эле-
422 Часть II. Контейнеры и алгоритмы мента диапазона. Алгоритм возвращает результат суммирования. Тип значения, воз- вращаемого функцией accumulate (), соответствует типу третьего аргумента. Третий аргумент, который задает начальное значение, необходим для того, чтобы указать i функции accumulate () тип накапливаемых элементов. Следовательно, невозможно / избежать передачи соответствующего начального значения и связанного с ним типа. Тот факт, что для функции accumulate () не важен тип суммируемых элемен- тов, имеет два следствия. Во-первых, ей обязательно следует передать начальное значение, поскольку в противном случае функции accumulate () не будет извест- но, с какого значения начинать суммирование. Во-вторых, функции accumulate () необходимо указать тип элементов в контейнере, поэтому тип третьего аргумента должен совпадать или допускать преобразование в тип элементов контейнера. Внут- ри функции accumulate (), третий аргумент используется как отправная точка при последовательном суммировании элементов контейнера. Типы элементов должны допускать суммирование. Например, функцию accumulate О можно использовать для конкатенации элементов вектора строк. // конкатенация элементов контейнера v и присвоение // результата переменной sum string sum = accumulate(v.begin(), v.endf), string("")); В результате этого обращения каждый элемент вектора строк v1 будет добавлен к пустой строке. Обратите внимание, третий параметр здесь явно указан как объект класса string. Передача строки как символьного литерала привела бы к ошибке при компиляции. Если бы был передан строковый литерал, типом суммируемых значений оказался бы const char*, но оператор сложения класса string (раз- дел 3.2.3, стр. 110) для операндов типа string и const char* возвращает резуль- тат типа string, а не const char*. Применение функции f ind_f irst_of () Кроме функции find (), в библиотеке определены и другие, более сложные ал- горитмы поиска. Некоторые из них подобны функциям поиска класса string (раздел 9.6.4, стр. 371). Одной из них является функция find_first_of (). Этот алгоритм получает две пары итераторов, которые обозначают два диапазона элемен- тов. Он ищет в первом диапазоне соответствие любому элементу из второго и воз- вращает итератор на первый найденный элемент. Если соответствия не найдено, он возвращает конечный итератор первого диапазона. Предположим, что rosterl и roster2 являются списками (list) имен. Используя функцию f ind_f irst of () можно вычислить, сколько имен в обоих списках совпадает. // программа предназначена лишь для иллюстрации: // существуют и более простые способы решения этой проблемы size_t ent = 0; list<string>::iterator it - rosterl.begin(); // поиск в списке rosterl любых имен из списка roster2 while ((it - find_first_of(it, rosterl.end(), roster2.begin(), roster2.end())) ' В оригинале vec. — Примеч. ped.
Глава 11. Общие алгоритмы 423 rosterl.end()) { ++cnt; // соответствие найдено; инкремент итератора it обеспечит // просмотр остальной части списка rosterl + + i t; cout Found " << ent names on both rosters endl ; В данном случае функция find_first_of () ищет в списке roster2 любой элемент, который соответствует любому элементу из диапазона первого списка, т.е. осуществляется поиск элемента из диапазона от указанного итератором it до ука- занного итератором, возвращаемым функцией rosterl. end О . Функция возвра- щает первый элемент в этом диапазоне, который соответствует элементу во втором диапазоне. При первой итерации цикла while просматривается весь диапазон спи- ска rosterl. При второй и последующих итерациях просматривается только та часть списка rosterl, в которой соответствие еще не было найдено. В условии цикла while проверяется значение, возвращаемое функцией find_ f irst_of (), что позволяет выяснить, найдено ли соответствие имен. Если соответ- ствие найдено, происходит приращение значения счетчика ent. Происходит также приращение итератора it, что позволяет перейти к следующему элементу спи- ска rosterl. Как известно, если соответствие не будет найдено, функция f ind_ f irst_of () возвратит итератор, совпадающий с итератором, возвращаемым функ- цией rosterl.end(). Фундаментальная концепция. Т ипы итераторов, передаваемых в качестве аргументов Большинство общих алгоритмов работает с двумя итераторами, которые обозначают диапазон элементов в контейнере (или другой последовательности). Типы обозна- чающих диапазон аргументов должны точно соответствовать типам итераторов, обо- значающих диапазон: они должны указывать на элементы того же контейнера (или на элемент, следующий после конца того же контейнера) и, если они не равны, должна существовать возможность достичь второй итератор в результате многократного при- ращения первого. Некоторые алгоритмы, например функция f ind_f irst_of О, получают две пары итераторов. Типы итераторов в каждой паре должны точно соответствовать типу ите- раторов контейнеров, по с типом другой пары они не должны обязательно совпадать. В частности, элементы могут храниться в контейнерах разных видов. Но что является совершенно необходимым, так это возможность сравнить элементы из двух последо- вательностей. В приведенной программе типы контейнеров rosterl и roster2 не должны обяза- тельно совпадать: контейнер rosterl, например, может быть списком (list), а кон- тейнер roster2 — вектором (vector), двухсторонней очередью (deque) или любой другой последовательностью, описанной далее в этой главе. Но что является совер- шенно необходимым, так это возможность сравнить элементы из двух последователь- ностей при помощи оператора ==. Если контейнер rosterl представляет собой спи- сок типа list<string>, контейнер roster2 может быть вектором типа vector<char*>, поскольку для библиотечного типа string определен оператор == как для типа string, так и для типа char*.
424 Часть II. Контейнеры и алгоритмы Упражнения раздела 11.2.1 Упражнение 11.3. Примените функцию accumulate О для суммирования элементов вектора типа vector<int>. Упражнение 11.4. Если вектор v имеет тип vector<double>, в чем состоит ошибка вызова accumulates, begin () , v.end(), 0) (если она есть)? Упражнение 11.5. Что произойдет, если программа, использующая функцию f ind_f irst_of (), не будет осуществлять приращение итератора it? 11.2.2. Алгоритмы, записывающие элементы контейнера Некоторые алгоритмы способны записывать значения в элементы. Используя та- кие алгоритмы, следует соблюдать осторожность и предварительно удостовериться, что количество элементов, в которые алгоритм собирается внести изменения, по крайней мере, не превосходит количества существующих элементов. Некоторые алгоритмы осуществляют запись непосредственно в исходную после- довательность, а другие получают дополнительный итератор, который обозначает результат. Такие алгоритмы используют итератор назначения (destination iterator) для обозначения позиции, в которую следует осуществить запись. Существует и тре- тий вид алгоритмов, записывающих определенное количество элементов в ту же по- следовательность. Запись элементов в исходную последовательность Алгоритмы, осуществляющие запись в исходную последовательность, в принци- пе безопасны: они способны переписать лишь столько элементов, сколько их нахо- дится в указанном исходном диапазоне. Простейшим примером алгоритма, осуществляющего запись в свою исходную последовательность, является функция fill (). fill(vec.begin (), vec.end(), 0); // обнулить каждый элемент // присвоить половине подпоследовательности значение 10 fill(vec.begin(), vec.begin() + vec.size()/2, 10); Функции f ill () передают два итератора, обозначающих диапазон элементов, в которые следует записать копию ее третьего параметра. В результате данное значе- ние будет присвоено каждому элементу указанного диапазона. С учетом допустимо- сти исходного диапазона, запись произойдет совершенно безопасно. Алгоритм за- пишет значение только в те элементы, которые гарантированно существуют непо- средственно в исходном диапазоне. Алгоритмы не проверяют операции записи Функции f ill_n () передают итератор, количество и значение. Она присваивает предоставленное значение определенному количеству элементов начиная с указан- ного итератором. Функция fill_n() подразумевает, что для записи указано кор- ректное количество элементов. Довольно распространенной ошибкой новичков яв-
Глава 11. Общие алгоритмы 425 ляется вызов функции f ill_n () (или подобного алгоритма записи элементов) для контейнера, который не содержит никаких элементов. vectoreint> vec; // пустой вектор // катастрофа: попытка записи в 10 несуществующих элементов // вектора vec fill__n (vec .begin () , 10, 0); Этот вызов функции f ill_n () приведет к катастрофе. Здесь указано, что долж- ны быть записаны десять элементов, но вектор vec пуст, и никаких элементов в нем нет. Результат непредсказуем, но, вероятнее всего, произойдет серьезный отказ во время выполнения. Алгоритмы, осуществляющие запись в указанное количество элементов или по задан- ному итератору, никак не проверяют, достаточно ли велик контейнер, чтобы вместить все записываемые данные. Функция back_inserter () Один из способов проверки, имеет ли контейнер достаточно элементов для запи- си, подразумевает использование итератора вставки (insert iterator). Итератор вставки — это итератор, который позволяет добавлять элементы в базовый контей- нер. Как правило, при присвоении значения элементу контейнера при помощи ите- ратора, осуществляется присвоение тому элементу, на который указывает итератор. При присвоении с использованием итератора вставки, в контейнер добавляется но- вый элемент, равный правому значению. Более подробная информация об итераторе вставки приведена в разделе 11.3.1 (стр. 432). Однако для иллюстрации безопасного применения алгоритмов, записы- вающих данные в контейнер, используем функцию back_inserter (). Для приме- нения этой функции в код программы следует подключить заголовок iterator. Функция back_inserter () является адаптером итератора. Подобно адаптерам контейнера (раздел 9.7, стр. 376), адаптер итератора получает объект и создает новый объект, который подражает поведению его аргумента. В данном случае аргумен- том функции back_inserter () является ссылка на контейнер. Функция back_ inserter () создает объект итератора вставки, связанный с данным контейнером. Попытка присвоить значение элементу при помощи этого итератора, приводит к вы- зову функции push_back (), добавляющей элемент с данным значением в контей- нер. Функцию back inserter () можно использовать для создания итератора, ис- пользуемого в качестве итератора назначения функции f ill_n (). vector<int> vec; // пустой вектор II ok: функция back_inserter() создает итератор вставки, // который добавляет элементы в вектор vec fill_n(back_inserter(vec), 10, 0); // добавляет 10 элементов в vec Теперь каждый раз, когда функция f ill_n () записывает значение, она осуществ- ляет это при помощи итератора вставки, созданного функцией back_inserter (). В результате для вектора vec будет происходить вызов функции push_back (), до- бавляющей 10 элементов в конец вектора vec, каждый из которых получит значение 0.
426 Часть II. Контейнеры и алгоритмы Алгоритмы, осуществляющие запись с использованием итератора назначения Третий вид алгоритма осуществляет запись в неизвестное заранее количество элементов при помощи итератора назначения. Как и у функции f ill n (), итератор назначения указывает на первый элемент последовательности, который будет со- держать результат. Самым простым из таких алгоритмов является функция сору (). Этот алгоритм получает три итератора: первые два обозначают исходный диапазон, а третий указывает на элемент в последовательности назначения. Очень важно, чтобы переданный функции сору () контейнер назначения был по крайней мере того же размера, что и исходный диапазон. Предположим, что список ilst содержит целые числа. При помощи функции сору () его значения можно скопировать в вектор сле- дующим образом. vectoreint> ivec; // пустой вектор // скопировать элементы списка в вектор ivec сору(ilst.begin(), ilst.end(), back_inserter(ivec)); Функция copy () читает элементы исходного диапазона и записывает их в кон- тейнер назначения. Безусловно, этот код нельзя назвать очень эффективным: если необходимо соз- дать новый контейнер как копию уже существующего, обычно лучше использовать исходный диапазон непосредственно при инициализации создаваемого контейнера. // более эффективный способ копирования элементов списка vector<int> ivec(ilst.begin(), ilst.end О); Версии копирования Некоторые алгоритмы обладают т.н. версией “копирования”. Эти алгоритмы осуществляют некую обработку элементов исходной последовательности, но саму последовательность не изменяют. Они могут создавать новую последовательность, в которую и сохраняют результат обработки элементов исходной. Хорошим примером является алгоритм replace. Он осуществляет чтение и запись в исходную последовательность, заменяя текущее значение новым. Алгоритм получает четыре параметра: два итератора, обозначающих исходный диапазон, и два значения. Она заменяет вторым значением значение каждого элемента, которое равно первому. // заменить во всех элементах значение 0 на 42 replace(ilst.begin(), ilst.end(), 0, 42); Это обращение заменяет все значения 0 на 42. Если бы исходную последователь- ность следовало оставить неизменной, необходимо было бы применить функцию replace_copy (). Этой версии функции передают третий аргумент: итератор, ука- зывающий получателя откорректированной последовательности. // создать пустой вектор для хранения измененных значений vector<int> ivec; // использовать функцию back__inserter () для увеличения контейнера // назначения до необходимых размеров replace_copy(ilst.begin(), ilst.end(), back_inserter(ivec), 0, 42); После этого обращения, список ilst останется неизменным, а вектор ivec будет содержать копию его элементов, но со значением 42 вместо 0.
Глава 11. Общие алгоритмы 427 Упражнения раздела 11.2.2 Упражнение 11.6. Напишите программу, которая при помощи функции f ill_n () обнуляет зна- чения последовательности целых чисел. Упражнение 11.7. Укажите, есть ли в приведенных ниже программах ошибки, и, если они есть, исправьте их. (a) vector<int> vec; list<int> 1st; int i; while (cin >> i) 1st.push_back(i); copy(1st.begin(), lst.end(), vec.begin()); (b) vector<int> vec; vec.reserve(10); fill_n(vec.begin(), 10, 0) ; Упражнение 11.8. Как уже упоминалось, алгоритмы не изменяют размер контейнеров, с которы- ми они работают. Почему применение функции back_inserter () является исключением из этого правила? 11.2.3. Алгоритмы, переупорядочивающие элементы контейнера Предположим, что необходимо проанализировать слова, использованные в набо- ре детских рассказов. При этом возможно, понадобится узнать, сколько слов содер- жат по 6 или больше символов. Каждое слово следует учесть только единожды, неза- висимо от того, сколько раз оно встречается в одном или в нескольких рассказах. При отображении слова должны быть отсортированы по размеру, а те, что имеют одинаковый размер, — в алфавитном порядке. Подразумевается, что исходный текст каждой книги был введен и сохранен в век- торе строк по имени words. Как же подсчитать количество слов? Для этого необхо- димо предпринять следующее. 1. Устранить дубликаты каждого слова. 2. Упорядочить слова на основании их размера. 3. Подсчитать слова, размер которых равен 6 символов или более. На каждом из этих этапов можно использовать общие алгоритмы. Для иллюстрации поставленной задачи, используем в качестве исходного текста следующую простую историю. the quick red fox jumps over the slow red turtle В результате обработки этого текста программа должна отобразить следующий результат. 1 word 6 characters or longer Устранение дубликатов Предположим, что исходный текст уже находится в векторе по имени words. В пер- вую очередь из вектора words необходимо устранить повторяющиеся слова. // сортировка слов в алфавитном порядке позволяет найти дубликаты sort(words.begin(), words.end());
428 Часть II. Контейнеры и алгоритмы /* устранить повторяющиеся слова: * функция unique() переупорядочивает слова так, чтобы каждое из * них присутствовало только один раз в начальной части вектора ★ words и возвращает итератор на элемент, следующий после * диапазона уникальных значений; ★ для удаления неуникальных элементов используется функция erase() * класса vector * / vector<string>::iterator end_unique - unique(words.begin(), words.end()); words.erase(end_unigue, words.end()); Исходной вектор содержит копию всех слов рассказа. Сначала вектор следует от- сортировать. Функции sort () передают два итератора, обозначающих диапазон подлежащих сортировке элементов. Для сравнения элементов используется опера- тор <. В данном случае следует отсортировать весь вектор. После обращения к функции sort () элементы вектора располагаются следую- щим образом. fox jumps over quick red red slow the the turtle Обратите внимание, слова red и the повторяются. Применение функции unique () После сортировки вектора words следует решить следующую проблему: оста- вить только одну копию каждого использованного в тексте слова. Для решения этой проблемы прекрасно подходит алгоритм unique. Функции unique () передают два итератора, обозначающие диапазон элементов. Она реорганизует элементы указан- ного диапазона так, чтобы смежные копии элементов оказались удалены, и возвра- щает итератор, обозначающий конец диапазона уникальных значений. После обращения к функции unique () содержимое вектора выглядит так, как показано на рис 11.1. words fox jumps over quick red slow the turtle the turtle t last_word (Следующий элемент после уникальных) Рис. 11.1. Содержимое вектора words Обратите внимание, размер вектора words не изменился. Он все еще содержит 10 элементов; только порядок этих элементов теперь иной. Обращение к функ- ции unique () “удаляет” смежные копии элементов. Слово “удаляет” помещено в кавычки потому, что на самом деле функция unique () ничего не удаляет. Она просто переписывает смежные копии так, чтобы в начале последовательности оказались элементы лишь с уникальными значениями. Итератор, возвращенный функцией unique (), указывает следующий элемент после конца диапазона уни- кальных значении.
Глава 11. Общие алгоритмы 429 Применение функций контейнера для удаления элементов Для удаления совпадающих элементов можно воспользоваться функцией кон- тейнера erase (). Приведенное выше обращение к функции erase (), удаляет эле- менты от с указанного итератором end_unique и до конца контейнера words. Те- перь контейнер words содержит 8 уникальных слов из исходного текста. Алгоритмы никогда непосредственно не изменяют размер контейнера. Если элементы 1 необходимо добавить или удалить, следует использовать функции контейнера. Следует заметить, что обращение к функции erase () окажется безопасным даже в том случае, когда вектор не содержит совпадающих слов. В этом случае функция unique () возвратит итератор, совпадающий с возвращенным функцией words. end (). Таким образом, оба аргумента функции erase () будут иметь одинаковое значение, а следовательно, обрабатываемый ей диапазон окажется пуст. Удаление пустого диапазона не приводит ни к какому результату, поэтому программа будет работать правильно даже тогда, когда в исходном тексте нет повторяющихся слов. Создание необходимых вспомогательных функций Следующая задача заключается в подсчете слов, имеющих длину 6 и более сим- волов. Для решения этой проблемы используем два дополнительных общих алго- ритма: stable_sort и count_if. Чтобы применить каждый из этих алгоритмов, понадобятся вспомогательные функции, известные как предикаты (predicate). Пре- дикат — это функция, которая выполняет некую проверку и возвращает значение, свидетельствующее об успехе или отказе. Первый необходимый предикат используется для сортировки элементов по раз- меру. То есть необходимо определить функцию, которая сравнивает две строки и возвращает логическое значение (тип bool), указывающее, является ли первая из них короче второй. // функция сравнения, используемая при сортировке слов по длине bool isShorter(const string &sl, const string &s2) return si.size() < s2.size(); Также нужна еще одна функция, которая позволяет выяснить, равна ли длина данного слова 6 символам или более. // выяснить, равна ли длина данного слова 6 символам или более bool GT6(const string &s) return s.size() >= 6; } Хоть эта функция и решает проблему, ее возможности ограниченны: размер слов жестко задан внутри самой функции. Если необходимо будет искать слова другой длины, придется создать еще одну функцию. Функцию сравнения легко можно сде- лать более универсальной, если передать ей два параметра: строку и размер. Однако функция, передаваемая функции count_if (), получает только один аргумент, по- этому более общий подход в этой программе неприменим. Более эффективное реше- ние этой части программы описано в разделе 14.8.1 (стр. 561).
430 Часть II. Контейнеры и алгоритмы Алгоритмы сортировки В библиотеке определены четыре разных алгоритма сортировки, из которых для сортировки слов в алфавитном порядке использован самый простой, sort. Кроме алгоритма sort, в библиотеке определен также алгоритм stable_sort. Он обеспе- чивает упорядочивание равных элементов. Как правило, о порядке равных элемен- тов в отсортированной последовательности можно не заботиться. В конце концов, они ведь равны. Но в данном случае под “равными” подразумеваются элементы со значениями одинаковой длины. Элементы одинаковой длины предстоит еще вы- строить в алфавитном порядке. Вызов функции stable_sort () позволяет распо- ложить элементы с одинаковыми значениями длины в алфавитном порядке. Функции sort () и stable_sort () имеют перегруженные версии. В одной из версий для сравнения значений элементов используется оператор < их собственного типа. Именно эта версия функции sort () использовалась для сортировки вектора words перед поиском совпадающих элементов. Второй версии передают третий па- раметр: имя предиката, используемого при сравнении элементов. Эта функция должна получать два аргумента того же типа, что и у элемента, а возвращать значе- ние, которое может быть использовано в условии. Эта вторая версия используется при передаче функции isShorter () при сравнении элементов. // сортировка по размеру, с обеспечением алфавитного порядка // слов одинакового размера stable_sort(words.begin(), words.end(), isShorter); После этого вызова вектор words будет отсортирован согласно значениям разме- ра элементов, причем слова одинаковой длины будут расставлены в алфавитном по- рядке, как показано на рис. 11.2. words fox red the over slow jumps quick turtle Рис. 11.2. Расположение элементов век- тора words после сортировки Подсчет слов размером 6 символов и более Теперь, переупорядочив вектор по размеру слов, осталось подсчитать, сколько из них имеют размер в 6 символов и более. Эту задачу решает алгоритм count_if. vector<string>::size_type wc = count_if(words.begin(), words.end(), GT6); Функция count_if () обрабатывает диапазон элементов, указанный двумя пер- выми параметрами. Она передает каждое прочитанное значение функции предиката, указанной третьим аргументом. Эта функция должна получать один аргумент типа элемента, а возвращать — значение, которое может быть проверено в условии. Алго- ритм возвращает количество элементов, для которых переданная функция вернула значение true. В данном случае функция count_if () передает каждое слово функции GT6 (), которая возвращает логическое значение true, если слово имеет размер 6 символов или более.
Глава 11. Общие алгоритмы 431 Теперь все вместе Рассмотрев отдельные части программы, соберем их вместе. // функция сравнения, используемая при сортировке слов по длине bool isShorter(const string &sl, const string &s2) { return si.size() < s2.size(); // выяснить, равна ли длина данного слова 6 символам или более bool GT6(const string &s) { return s.sizeO >= 6; int main() { vector<string> words; // скопировать содержимое каждой книги в один вектор string next_word; while (cin >> next_word) { // добавить содержимое следующей книги в конец words words.push_back(next_word); } // сортировка слов в алфавитном порядке позволяет // найти дубликаты sort(words.begin(), words.end()); / * * устранить повторяющиеся слова: * функция unique() переупорядочивает слова так, чтобы каждое из * них присутствовало только один раз в начальной части вектора * words и возвращает итератор на элемент, следующий после * диапазона уникальных значений; * для удаления неуникальных элементов используется функция erase() * класса vector * у vector<string>::iterator end_unique = unique(words.begin(), words.end()); words.erase(end_unique, words.end()); // сортировка по размеру, с обеспечением алфавитного // порядка слов одинакового размера stable_sort(words.begin(), words.end(), isShorter); vector<string>::size_type wc = count_if(words.begin(), words.end(), GT6); cout << wc << " " << make_plural(wc, "word", "s") << " 6 characters or longer" << endl; return 0; } Задачу вывода слов, отсортированных по размеру, оставляем читателю в качестве упражнения. Упражнения раздела 11.2.3 Упражнение 11.9. Реализуйте программу подсчета слов размером 4 символов или более, которая отображает список уникальных слов. Проверьте программу на примере использованного ранее файла исходного текста. Упражнение 11.10. В библиотеке определена функция find_if (). Подобно функции find (), функция find_if () получает два итератора, обозначающие рабочий диапазон. Подобно функ- ции count_if (), она получает также третий параметр, назначающий предикат, применяемый для проверки каждого элемента диапазона. Функция find_if О возвращает итератор, который
432 Часть II. Контейнеры и алгоритмы указывает на первый элемент, для которого функция2 возвратила значение, отличное от нуля. Если такого элемента нет, функция возвращает итератор равный второму аргументу. Используйте функ- цию f ind_if () для модификации той части программы, где осуществляется подсчет слов, раз- мер которых равен 6 символам или более. Упражнение 11.11. Почему алгоритмы не изменяют размер контейнеров? Упражнение 11.12. Почему необходимо использовать функцию контейнера erase (), а не опре- делять общий алгоритм, который был бы способен удалять элементы из контейнера? 11.3. Возвращаясь к итераторам Как отмечалось в разделе 11.2.2 (стр. 424), в библиотеке определены итераторы, не зависящие от специфического контейнера. Фактически существует три дополни- тельных вида итераторов. Итератор вставки (insert iterator). Этот итератор связан с контейнером и при- меняется для вставки элементов в контейнер. Итератор ввода-вывода (iostream iterator). Этот итератор может быть связан как с потоком ввода, так и с потоком вывода, и применяется он для перебора связан- ного потока ввода-вывода. Реверсивный итератор (reverse iterator). Этот итератор перемещается назад, а не вперед. Для каждого контейнера определен собственный тип reverse_iterator. Такой итератор возвращают функции rbegin () и rend (). Эти типы итераторов определены в заголовке iterator. В этом разделе рассматривается каждый из этих видов итераторов, а также их применение в общих алгоритмах. Здесь также описано, как и когда использовать итератор типа const_iterator. 11.3.1. Итераторы вставки В разделе 11.2.2 (стр. 424) уже было продемонстрировано применение функции back_inserter () для создания итератора, который добавляет элементы в контей- нер. Функция back_inserter () является примером адаптера вставки. Адаптер вставки (inserter), или адаптер inserter, — это адаптер итератора (раздел 9.7, стр. 376), получающий контейнер и возвращающий итератор, который позволяет вставлять элементы в указанный контейнер. Присвоение при помощи итератора вставки приводит к добавлению нового элемента. Существует три вида адаптеров вставки, которые отличаются позицией добавляемых элементов. Адаптер back_inserter создает итератор, используемый функцией push_back (). Адаптер front_inserter создает итератор, используемый функцией push_ front() . Адаптер inserter создает итератор, используемый функцией insert (). Кро- ме имени контейнера, адаптеру inserter передают второй аргумент: итератор, указывающий позицию, перед которой должна начаться вставка. 2 Вероятно, имеется в виду предикат. — Примеч. ред.
Глава 11. Общие алгоритмы 433 Адаптер f rontinserter требует наличия функции push f ront () Адаптер front_inserter работает аналогично адаптеру back_inserter: он создает итератор, который рассматривает присвоение как обращение к функции push_f ront () своего базового контейнера. Адаптер front_inserter можно использовать только тогда, когда контейнер обла- 1 дает функцией push_f ront (). Применение функции f ront_inserter () для век- тора или другого контейнера, который не имеет функции push_front о, является ошибкой. Адаптер inserter создает итератор, обеспечивающий вставку в указанном месте Адаптер inserter обеспечивает более общую форму. Он получает имя контей- нера и итератор, обозначающий позицию вставки. // итератор позиции в списке ilst list<int>::iterator it = find(ilst.begin(), ilst.end(), 42); // вставить замещающую копию ivec в указанную позицию ilst replace_copy(ivec.begin(), ivec.end(), inserter(ilst, it), 100, 0); Сначала при помощи функции find () выбирается элемент в списке ilst. При обращении к функции replace_copy () используется адаптер inserter, который вставит элементы в список ilst непосредственно перед элементом, обозначенным итератором, который возвращен функцией find (). В результате обращения к функ- ции replace_copy () элементы вектора ivec будут скопированы с заменой каждо- го значения 100 на 0. Элементы будут вставлены непосредственно перед элементом, обозначенным итератором it. При создании адаптера inserter следует указать позицию вставки новых эле- ментов. Элементы всегда добавляются перед позицией, обозначенной аргументом- итератором адаптера вставки. Можно предположить, что использовав для контейнера адаптер inserter и ите- ратор, возвращаемый функцией begin (), можно получить результат, аналогичный применению адаптера f ront_inserter. Однако адаптер inserter ведет себя со- вершенно не так, как адаптер front_inserter. При использовании адаптера f ront_inserter, элементы всегда добавляются перед текущим первым элементом контейнера. При использовании адаптера inserter, элементы добавляются перед указанной позицией. Если эта позиция первоначально соответствует первому эле- менту, после вставки она перестанет быть позицией в начале контейнера. list<int> ilst, ilst2, ilst3; // пустые списки // после этого цикла ilst содержит: 1234 for (list<int>::size_type i = 0; i != 4; ++i) ilst.push_front(i) ; // после копирования ilst2 содержит: 4321 copy(i1 st.begin(), ilst.end(), front_inserter(ilst2)); // после копирования ilst3 содержит: 1234 copy(i1st.begin(), ilst.end(), inserter(ilst3, ilst3.begin()));
434 Часть II. Контейнеры и алгоритмы При копировании в список ilst2, элементы всегда добавляются перед текущим элементом. При копировании в список ilst3, элементы добавляются в фиксиро- ванную позицию. Сначала эта позиция является началом списка, но по мере добав- ления элементов она перемещается. Как уже отмечалось в разделе 9.3.3 (стр. 345), применение адаптера front_inserter приводит к расположению вставленных элементов в обратном по- рядке. Упражнения раздела 11.3.1 Упражнение 11.13. Объясните различия между тремя итераторами вставки. Упражнение 11.14. Напишите программу, которая использует функцию repiace_copy () для копирования последовательности элементов из одного контейнера в другой с заменой ука- занных значений новыми. Напишите программу, использующую адаптеры inserter, back_ inserter и front_inserter. Объясните, почему в каждом случае получился разный поря- док элементов последовательности. Упражнение 11.15. В библиотеке алгоритмов определена функция unique_copy (), которая используется подобно функции unique (), за исключением того, что ей передают третий итера- тор, указывающий последовательность для копирования уникальных элементов. Напишите про- грамму, которая использует функцию unique_copy () для копирования уникальных элементов из списка в первоначально пустой вектор. 11.3.2. Итераторы ввода-вывода Хотя классы ввода-вывода и не являются контейнерами, существуют итераторы, которые применимы к объектам ввода-вывода: итератор istream_iterator читает поток ввода, а итератор ostream_iterator записывает в поток вывода. Эти итера- торы рассматривают соответствующий поток как последовательность элементов оп- ределенного типа. Используя потоковый итератор, общие алгоритмы можно приме- нить для чтения данных из потоковых объектов (или для записи в них). Для потоковых итераторов определены только самые простые функции: инкре- мент, обращение к значению и присвоение. Кроме того, два итератора istream можно сравнить на равенство и неравенство, а итераторы ostream такого сравнения не поддерживают. Определение потоковых итераторов Потоковый итератор (stream iterator) — это шаблон класса. Например, итератор istream_iterator может быть определен для любого класса, у которого опреде- лен оператор ввода (>>). Аналогично, итератор ostream_iterator может быть определен для любого класса, обладающего оператором вывода (<<). Таблица 11.1. Конструкторы итератора ввода*вывода istream_iterator<T> Создает итератор istream_iterator, позволяющий читать in (strm); объекты типа т из потока ввода strm
Глава 11. Общие алгоритмы 435 Окончание табл. 11.1 istream iterator<T> in(strm); Создает итератор istream_iterator, позволяющий читать объекты типа т из потока ввода strm istream_iterator<T> in; ostream iterator<T> in(strm); Итератор istream_iterator, указывающий на ЭЛвМвНТ ПОСЛв конца Создает итератор ostream_iterator, позволяющий записы- вать объекты типа т в поток вывода strm ostream_iterator<T> in(strm, delim); Создает итератор ostream_iterator, позволяющий записы- вать объекты типа т в поток вывода strm, используя delim как разделитель между элементами, delim — это символьный массив с нулевым символом в конце При создании потокового итератора необходимо указать тип объектов, которые итератор будет читать или записывать. istream_iterator<int> cin_.it (cin); // читать из cin целые числа istream_iterator<int> end_of_stream; // конечный итератор // запись объектов класса Sales_item в объект out file // класса ofstream // каждый элемент сопровождается пробелом ofstream outfile; ostream_iterator<Sales_item> output(outfile, " "); Итератор ostream_iterator следует связать с определенным потоком, что можно сделать при создании итератора istream_iterator. В качестве альтерна- тивы, при создании итератора можно не передавать никаких аргументов. В результа- те получится значение, которое можно использовать как указывающее на элемент после конца. Для итератора ostream_iterator такое значение не предусмотрено. При создании итератора ostream_iterator можно (но это не обязательно) предоставить второй аргумент, который задает разделитель, используемый при за- писи элементов в поток вывода. Разделитель должен быть символьной строкой в стиле С. В конце этой строки должен быть нулевой символ, в противном случае ре- зультат будет непредсказуемым. Операции с итераторами istream_lterator Таблица 11.2. Операции с итераторами istream iterator * i t It->mem + + it it + + Равенство (неравенство) двух итераторов istream_iterator. Итераторы долж- ны быть предназначены для чтения объектов одинакового типа. Два итератора равны, если оба они указывают на конечное значение. Два итератора “не на конец потока” равны, если они созданы с использованием того же потока ввода Возвращает значение, прочитанное из потока То же, что и (* i t) . mem. Возвращает член mem класса, объект которого был про- читан из потока Перемещает итератор для чтения следующего значения из потока ввода, при помощи оператора > > для типа элемента. Как обычно, префиксная версия перемещает поток и возвращает ссылку на итератор после приращения. Постфиксная версия переме- щает поток, но возвращает прежнее значение
436 Часть II. Контейнеры и алгоритмы При создании, итератор istream_iterator связывается с потоком и устанав- ливается так, чтобы первое обращение к его значению привело к чтению из потока первого элемента. Итератор istream_iterator можно использовать, например, для чтения в век- тор элементов со стандартного устройства ввода. istream_iterator<int> in_iter(cin); // читать из cin целые числа istream_iterator<int> eof; // итератор конца потока // читать файл до конца, сохраняя прочитанное в вектор vec while (in_iter != eof) // инкремент перемещает поток на следующее значение II обращение к значению читает следующий объект из II потока istream vec.push_back(*in_iter++); Этот цикл читает целые числа из объекта cin и сохраняет их в векторе vec. При каждой итерации цикла проверяется, не равен ли итератор in_iter итератору eof. Этот итератор был создан как пустой итератор istream_iterator, соответствую- щий концу файла. Связанный с потоком итератор станет равен конечному итератору при достижении потоком конца файла или в случае ошибки. Самая трудная часть этой программы — аргумент функции push_back (), в ко- тором используется обращение к значению и постфиксный оператор инкремента. Согласно правилам приоритета (раздел 5.5, стр. 188), результат инкремента окажет- ся операндом оператора обращения к значению. Приращение итератора istream_ iterator передвинет поток. Однако в выражении использован постфиксный ин- кремент, который возвращает прежнее значение итератора. В результате инкремента из потока будет прочитано следующее значение, а итератор окажется возвращен на предыдущее прочитанное значение. Чтобы получить это значение, достаточно обра- титься к значению данного итератора. Но интересней всего то, что эту программу можно переписать следующим образом. istrearn_iterator<int> in_iter'(cin); istream_iterator<int> eof; vector<int> vec(in_iter, eof); // читать из cin целые числа // итератор конца потока // создать вектор vec из // диапазона, указанного // итераторами Здесь вектор vec создается из диапазона элементов, обозначенного двумя итера- торами. Это итераторы istream_iterator, а следовательно диапазон получен при чтении из связанного с ним потока. В результате применения этого конструктора данные из объекта cin будут считываться до тех пор, пока не встретится конец фай- ла или не будет введено значение, тип которого отличается от типа int. Именно прочитанные элементы и используются для создания вектора vec. Применение итераторов ostream iterator и istreamiterators Итератор ostream_iterator можно использовать для записи последователь- ности значений в поток способом, аналогичным используемому при присвоении по- следовательности значений элементам контейнера. // записывать строки на стандартное устройство вывода по одной ostream_iterator<string> out_iter(cout, "\n"); // читать строки co стандартного устройства ввода
Глава 11. Общие алгоритмы 437 istream_iterator<string> in_iter(cin), eof; // читать, пока не достигнут eof, и записывать прочитанное на // стандартное устройство вывода while (in_iter != eof) // записать значение in_iter на стандартное устройство вывода, // а затем прирастить итератор, чтобы получить из объекта cin // следующее значение *out_iter++ = *in_iter++; Эта программа читает слова из объекта cin и записывает их в отдельные строки при помощи объекта cout. Сначала создается итератор типа ostream_iterator, предназначенный для за- писи в объект cout строк, причем после каждой из них следует символ новой стро- ки. Затем создается второй итератор типа istream_iterator, который использу- ется для чтения строк из объекта cin. Цикл while работает аналогично предыду- щему примеру. Однако теперь вместо сохранения прочитанного значения в вектор, оно записывается в объект cout, для чего подлежащее отображению значение при- сваивается итератору out_iter. Присвоение происходит аналогично описанному в программе на стр. 232, где один массив был скопирован в другой. Здесь в результате обращения к значениям обоих итераторов, значение правой части присваивается левой, а затем происходит приращение каждого из итераторов. В результате прочитанное будет записано в объект cout, а последующее приращение каждого итератора приведет к чтению из объекта cin следующего значения. Применение итераторов lstream_iterator с типами классов Итератор istream_iterator можно создать для любого типа (класса), у кото- рого есть оператор ввода (>>). Например, итератор istream_iterator можно ис- пользовать для чтения и суммирования последовательности объектов класса Sales_item. istream_iterator<Sales_item> item_iter(cin), eof; Sales_item sum; // изначально пустой объект класса Sales_item sum = *item_iter++; // прочитать первую транзакцию в объект sum // и получить следующую запись while (item_iter != eof) { if (item_iter->same_isbn(sum)) sum = sum + * item it ex- cise { cout << sum << endl; sum = *item_iter; } ++item_iter; // прочитать следующую транзакцию cout « sum << endl; // не забудьте отобразить последнюю запись Эта программа связывает итератор item_iter с объектом cin, чтобы с его по- мощью читать объекты класса Sales_item. Затем программа читает первую запись в объект sum. sum = *item_iter++; // прочитать первую транзакцию в объект sum // и получить следующую запись Для получения первой записи со стандартного устройства ввода и ее присвоения объекту sum, здесь используется оператор обращения к значению. Последующее
438 Часть II. Контейнеры и алгоритмы приращение итератора заставляет поток прочитать следующую запись со стандарт- ного устройства ввода. Цикл while выполняется до тех пор, пока объект cin не достигнет конца файла. Внутри цикла while, ISBN только что прочитанной записи сравнивается с ISBN объекта sum. В условии цикла while использован оператор стрелки, позволяющий обратиться к значению итератора istream и, получив доступ к только что прочи- танному объекту, выполнить его функцию-член same_isbn () для объекта sum. Если ISBN совпадают, происходит приращение значения общего количества в объ- екте sum. В противном случае осуществляется отображение текущего значения объек- та sum и присвоение ему копии значения только что прочитанной транзакции. По- следний оператор цикла осуществляет приращение итератора, что в данном случае приводит к чтению следующей транзакции со стандартного устройства ввода. Цикл продолжается до тех пор, пока не произойдет ошибка или не встретится конец файла. Перед выходом из программы следует отобразить последнее введенное значение. Ограничения потоковых итераторов Потоковые итераторы имеют несколько важных ограничений. Итератор ostream_iterator не позволяет читать, а итератор istream_ iterator не позволяет осуществлять запись. Как только итератору ostream_iterator будет присвоено значение, запись окажется совершена. Впоследствии это значение невозможно будет изменить, только присвоить другое значение. Кроме того, каждое отдельное значение ите- ратора ostream_iterator, как и следовало ожидать, используется для вывода только один раз. Итератор ostream_iterator не имеет оператора - >. Применение потоковых итераторов в алгоритмах Как известно, алгоритмы работают с итераторами. Поскольку потоковые итера- торы поддерживают по крайней мере некоторые из функций итераторов, их можно применить в некоторых из общих алгоритмов. Например, можно прочитать числа со стандартного устройства ввода и записать на стандартное устройство вывода те из них, которые не повторяются (т.е. являются уникальными). istream_iterator<int> cin_it(cin); // читать из cin целые числа istream_iterator<int> end_of_stream; // конечный итератор // инициализировать vec данными со стандартного устройства ввода vector<int> vec(cin_it, end_of_stream); sort(vec.begin(), vec.end() ) ; // запись в cout целых чисел с использованием " " как разделителя ostream_iterator<int> output(cout, " "); // запись на стандартное устройство вывода только уникальных // элементов вектора vec unique_copy(vec.begin(), vec.end(), output); Предположим, введены следующие числа. 23 109 45 89 6 34 12 90 34 23 56 23 8 89 23 Программа отобразит следующий результат. 6 8 12 23 34 45 56 89 90 109
Глава 11. Общие алгоритмы 439 Программа создает вектор vec с использованием двух итераторов, input и end_of_stream. В результате такой инициализации он будет читать целые числа из объекта cin до тех пор, пока не встретится конец файла или не произойдет ошиб- ка. Прочитанные значения сохраняются в векторе vec. Как только ввод будет закончен и вектор vec окажется инициализирован значе- ниями, произойдет вызов функции sort (), сортирующей введенные числа. После обращения к функции sort () повторяющиеся элементы окажутся рядом. Здесь использована функция unique_copy (), являющаяся копирующей верси- ей функции unique. Она копирует уникальные значения из исходного диапазона в итератор назначения. В данном обращении итератором назначения является создан- ный ранее итератор вывода. В результате из вектора vec в cout будут скопированы лишь уникальные значения, после каждого из которых добавлен пробел. Упражнения раздела 11.3.2 Упражнение 11.16. Перепишите программу со стр. 436 так, чтобы при записи содержимого фай- ла на стандартное устройство вывода использовался алгоритм сору. Упражнение 11.17. Используйте для инициализации вектора целых чисел два итератора istream_iterator. Упражнение 11.18. Напишите программу, читающую последовательность целых чисел со стан- дартного устройства ввода при помощи итератора istream_iterator. Используя итератор ostream_iterator, запишите все нечетные числа в один файл. Каждое значение должен со- провождать пробел. Также при помощи итератора ostream_iterator запишите четные числа во второй файл. Каждое из этих значений должно быть помещено в отдельную строку. 11.3.3. Реверсивные итераторы Реверсивный итератор (reverse iterator) — это итератор, перебирающий контей- нер в обратном направлении, т.е. от последнего элемента к первому. Реверсивный итератор инвертирует смысл инкремента (и декремента): оператор ++ переводит ре- версивный итератор на предыдущий элемент, а оператор-на следующий. Напомним, что для каждого контейнера определены функции-члены begin () и end (). Эти функции-члены возвращают итераторы на первый и следующий после последнего элементы контейнера соответственно. Для контейнеров определены также функции-члены rbegin () и rend (), которые возвращают реверсивные ите- раторы на последний элемент контейнера и элемент перед его началом. Подобно обычным итераторам, существуют константные и неконстантные реверсивные ите- раторы. Взаимное положение этих четырех итераторов на гипотетическом векторе vec представлено на рис. 11.3. Предположим, что вектор vec содержит числа от 0 до 9, расположенные в по- рядке возрастания.
440 Часть II. Контейнеры и алгоритмы vec.begin() ► vec.end() vec.rend() < vec.rbegin() Рис. 11.3. Взаимное положение итераторов, возвращае- мых функциями begin (), end (), rbegin () и rend () Следующий цикл for отображает элементы в обратном порядке. // реверсивный итератор отображает элементы в обратном порядке vector<int>::reverse_iterator r_iter; for (r_iter = vec.rbegin(); // итератор r_iter указывает на II последний элемент r_iter != vec.rend(); // функция rend() возвращает итератор // на элемент перед началом ++r_iter) // декремент итератора на один элемент cout << *r_iter << endl; // отображает 9,8, 7,...О Хотя смысл оператора декремента реверсивного итератора может показаться не- правильным, этот оператор позволяет применять для обработки контейнера стан- дартные алгоритмы. Например, передав функции sort () два реверсивных итерато- ра, вектор можно отсортировать в порядке убывания. // сортирует вектор vec в "нормальном" порядке sort(vec.begin(), vec.end()); // обратная сортировка: самый маленький элемент располагается II в конце вектора vec sort(vec.rbegin(), vec.rend()); Реверсивным итераторам необходим оператор декремента Нет ничего удивительно в том, что реверсивный итератор можно создать только из такого класса итератора, для которого определены операторы -- и ++. В конце концов, задача реверсивного итератора заключается в переборе последовательности назад. Итераторы всех стандартных контейнеров поддерживают как инкремент, так и декремент. Однако потоковые итераторы к ним не относятся, поскольку невоз- можно перемещать поток в обратном направлении. Следовательно, создать из пото- кового итератора реверсивный итератор невозможно. Отношения между реверсивными и другими итераторами Предположим, что существует объект line класса string (строка), который со- держит разделенный запятыми список слов. Используя функцию f ind () можно ото- бразить, например, первое слово строки line. // найти первый элемент в списке, разделенном запятыми string::iterator comma = find(line.begin(), line.end(), cout << string(line.begin(), comma) << endl; Если в строке line есть запятая, итератор comma будет указывать на нее, а в про- тивном случае он окажется равен итератору, возвращаемому функцией line. end (). При выводе содержимого строки от позиции line. begin () до позиции comma, будут отображены символы от начала до запятой, или вся строка, если запятых в ней нет.
Глава 11. Общие алгоритмы 441 Но если понадобится последнее слово в списке, вместо обычных можно исполь- зовать реверсивные итераторы. // найти последний элемент в списке, разделенном запятыми string::reverse_iterator rcomma = find(line.rbegin(), line.rend(), 1 , ' ) ; Поскольку функции find () в качестве аргументов передаются результаты вы- полнения функций rbegin () и rend (), поиск начинается с последнего символа в строке line в обратном порядке. По завершении поиска, если запятая найдена, ите- ратор rcomma будет указывать на последнюю запятую в строке, т.е. первую запятую с конца. Если запятой нет, итератор rcomma окажется равен итератору, возвращае- мому функцией line. rend (). Весьма интересна та часть, в которой осуществляется вывод найденного слова. Попытка прямого вывода создает несколько странный результат. // ошибка: создаст слово в обратном порядке cout << string(line.rbegin(), rcomma) << endl; Например, если введена сторока “FIRST,MIDDLE, LAST”, будет получен резуль- тат “TSAL”! Эту проблему иллюстрирует рис. 11.4. Здесь реверсивные итераторы исполь- зуются для перебора строки в обратном порядке. Чтобы получать правильный ре- зультат, необходимо реверсивные итераторы, rcomma и возвращаемый функцией line. rbegin (), преобразовывать в обычные итераторы, перемещаемые вперед. Итератор, возвращаемый функцией line . rbegin (), преобразовывать не нужно, поскольку заранее известно, что результат этого преобразования будет равен ите- ратору, возвращаемому функцией line.endO. Для преобразования итератора rcomma можно применить функцию-член base (), которой обладает каждый ревер- сивный итератор. // ок: получить прямой итератор, и читать до конца строки cout « string (rcomma .base () , line.endO) « endl; С учетом того, что введены те же данные, в результате будет отображено слово “LAST”, как и ожидалось. begin() comma rcomma.base() end() FIRST, MIDDLE, LAST rcomma rbegin() Puc. 11.4. Отношения между реверсивными и обычными итераторами Объекты, представленные на рис. 11.4, наглядно иллюстрируют взаимоотноше- ния между обычными и реверсивными итераторами. Например, итераторы rcomma и возвращаемый функцией rcomma.base () указывают на разные элементы, также как и возвращаемые функциями line.rbegin() и line.endO. Эти различия вполне обоснованны: они позволяют гарантировать возможность одинаковой обра- ботки диапазона элементов при перемещении как вперед, так и назад.
442 Часть II. Контейнеры и алгоритмы Тот факт, что реверсивные итераторы предназначены для представления диапазонов и & 1 что эти диапазоны являются асимметричными, очень важен, т.к. вследствие данного об- стоятельства при инициализации или присвоении реверсивному итератору простого ите- ратора, полученный в результате итератор не будет указывать на тот же элемент, что и исходный. Упражнения раздела 11.3.3 Упражнение 11.19. Напишите программу, которая использует итераторы reverse_iterator для отображения содержимого вектора в обратном порядке. Упражнение 11.20. Теперь отобразите элементы в обратном порядке, используя обычные ите- раторы. Упражнение 11.21. Используйте функцию find() для поиска в списке целых чисел последнего элемента со значением о. Упражнение 11.22. С учетом того, что вектор содержит 10 элементов, скопируйте в список диа- пазон его элементов от позиции 3 до позиции 7 в обратном порядке. 11.3.4. Константные итераторы Внимательный читатель, вероятно, заметил, что в программе на стр. 418, где ис- пользуется функция find(), итератор result был определен как имеющий тип const_iterator. Это было сделано потому, что данный итератор не предполага- лось использовать для изменения элементов контейнера. С другой стороны, для хранения итератора, возвращаемого функцией f ind_ first_of О, на стр. 422 был использован обычный, неконстантный итератор, не- смотря на то, что в этой программе также не предполагалось изменять элементы контейнера. В данном случае различие состоит в некоторых нюансах и заслуживает отдельного пояснения. Дело в том, что во втором случае итератор используется как аргумент функции find_first_of(). Исходный диапазон здесь определен итератором it и итератором, возвращенным при обращении к функции rosterl. end (). Алгоритмы требуют, чтобы типы обо- значающих диапазон итераторов точно совпадали. Тип итератора, возвращаемого функцией rosterl. end (), зависит от типа контейнера rosterl. Если этот кон- тейнер является константным объектом, итератор следует объявить как const_ iterator, а в противном случае необходим итератор обычного типа. В данной про- грамме список rosterl не был константным, поэтому функция end () вернула обыч- ный итератор. Если бы итератор it был определен как имеющий тип const_iterator, обра- щение к функции f ind_f irst_of () привело бы ошибке во время компиляции. Дело в том, что типы итераторов, используемых для обозначения диапазона, оказа- лись бы неидентичными. Итератор it имел бы тип const_iterator, а итератор возвращенный функцией rosterl. end (), — тип iterator.
Глава 11. Общие алгоритмы 443 11.3.5. Пять категорий итераторов Общий набор функций определен для всех итераторов, однако некоторые из них обладают несколько большими возможностями, чем другие. Например, итератор ти- па ostream_iterator поддерживает только инкремент, обращение к значению и присвоение. Итераторы векторов поддерживают эти функции, а также декремент, операторы сравнения и арифметические операторы. Таким образом, итераторы можно классифицировать на основании набора функций, которыми они обладают. Аналогично, по виду требуемой от итератора функции можно классифицировать алгоритмы. Для некоторых алгоритмов поиска достаточен итератор, обеспечиваю- щий лишь приращение и чтение, а для алгоритмов сортировки необходим итератор, позволяющий читать, записывать и осуществлять произвольный доступ к элемен- там. Необходимые алгоритмам возможности итераторов можно разделить на пять категорий. Эти пять категорий соответствуют пяти категориям итераторов, которые представлены в табл. 11.3. Таблица 11.3. Категории итераторов Итератор ввода Итератор вывода Прямой итератор Двунаправленный итератор Итератор произвольного доступа Обеспечивает чтение, но не запись; поддерживает только инкремент Обеспечивает запись, но не чтение; поддерживает только инкремент Обеспечивает чтение и запись; поддерживает только инкремент Обеспечивает чтение и запись; поддерживает инкремент и декремент Обеспечивает чтение и запись; поддерживает все арифметические опера- ции итераторов 1. Итератор ввода (input iterator) позволяет читать элементы контейнера, но запи- си не гарантирует. Итератор ввода обязательно должен поддерживать следую- щий минимум функций. • Операторы равенства и неравенства (==, ! =), используемые для сравнения двух итераторов. • Префиксный и постфиксный инкременты (++), используемые для перемеще- ния итератора. • Оператор обращения к значению (* *), позволяющий прочитать элемент. Опе- ратор обращения к значению может быть применен только к операнду, распо- ложенному справа от оператора присвоения. • Оператор стрелки (->), равнозначный выражению (*it) .member. То есть обращение к значению итератора и доступ к члену класса объекта. Итераторы ввода могут быть использованы только последовательно; после прира- щения итератора ввода вернуться к прежнему элементу уже невозможно. К общим алгоритмам, предполагающим только этот уровень поддержки, относятся find и accumulate. Библиотечный тип istream_iterator является итератором ввода. 2. Итератор вывода (output iterator) можно рассматривать как итератор ввода, об- ладающий дополнительными функциональными возможностями. Итератор вы- вода применяется для записи в элемент, но чтения он не гарантирует. Для итера- торов вывода обязательны следующие функции.
444 Часть II. Контейнеры и алгоритмы • Префиксный и постфиксный инкременты (++), используемые для перемеще- ния итератора. • Оператор обращения к значению (*) может быть применен только к операнду, расположенному слева от оператора присвоения. Присвоение при обращении к значению итератора вывода позволяет осуществить запись в элемент. Вполне возможен случай, когда итераторы вывода потребуют, чтобы значение каждого итератора было записано только один раз. При использовании итерато- ра вывода, оператор * должен быть использован один и только один раз для дан- ного значения итератора. Как правило, итераторы вывода используют как третий аргумент алгоритма, указывающий позицию начала записи. Например, алгоритм сору получает итератор вывода как его третий параметр при копировании эле- ментов из исходного диапазона в диапазон назначения, указанный итератором вывода. Итератором вывода является ostream_iterator. 3. Прямой итератор (forward iterator) позволяет читать и записывать данные в контейнер. Они перемещаются по последовательности только в одном направле- нии. Прямые итераторы поддерживают все операции итераторов ввода и итера- торов вывода. Кроме того, они позволяют читать и записывать значение в тот же элемент несколько раз. Прямой итератор можно скопировать, чтобы запомнить место в последовательности и вернуться к нему позже. Прямой итератор необхо- дим общему алгоритму replace. 4. Двунаправленный итератор (bidirectional iterator) позволяет читать и записы- вать данные в контейнер в обоих направлениях. Кроме всех функций прямого итератора, двунаправленный итератор поддерживает также префиксный и пост- фиксный декременты (--). К общим алгоритмам, которым необходим двуна- правленный итератор, относится reverse. Все библиотечные контейнеры пре- доставляют итераторы, которые соответствуют требованиям как минимум дву- направленных итераторов. 5. Итератор прямого доступа (random-access iterator) обеспечивает доступ к лю- бой позиции внутри контейнера в любой момент. Эти итераторы обладают всеми функциональными возможностями двунаправленных итераторов. Кроме того, они обладают следующими функциями. • Операторы сравнения <,<=,> и >=, позволяющие сравнить относительные позиции двух итераторов. • Операторы сложения и вычитания (+, +=, - и -=), обеспечивающие арифме- тические действия между итератором и целочисленным значением. В резуль- тате получается итератор, перемещенный внутри контейнера вперед (или на- зад) на соответствующее количество элементов. • Оператор вычитания (-), применяемый к двум итераторам, позволяет полу- чить дистанцию между двумя итераторами. • Оператор индексирования iter [п], равнозначный выражению * (iter + п). К общим алгоритмам, предполагающим наличие итераторов прямого доступа, относятся алгоритмы сортировки. Итераторы вектора (vector), двухсторонней
Глава 11. Общие алгоритмы 445 очереди (deque) и строки (string) также являются итераторами прямого дос- тупа и используются как указатели при доступе к элементам обычного массива. За исключением итераторов вывода, категории итераторов представляют собой своего рода иерархию: любой итератор более высокой категории применим там, где необходим итератор менее высокой категории. Алгоритм, которому необходим ите- ратора ввода, можно использовать как с итератором ввода, так и с прямым, двуна- правленным итератором или итератором прямого доступа. Но алгоритм, предпола- гающий наличие итератора прямого доступа, можно применить только с итератором прямого доступа. Типы тар (карта), set (набор) и list (список) обладают двунаправленными итераторами. Итераторы типов string (строка), vector (вектор) и deque (двух- сторонняя очередь) являются итераторами прямого доступа, подобно указателям массива. Итератор istream_iterator является итератором ввода, а итератор ostream_iterator — итератором вывода. Фулда ментальная концепция. Ассоциативные контейнеры и алгоритмы Хотя контейнеры типа тар и set обладают двунаправленными итераторами, для ассоциативных контейнеров применимы не все алгоритмы. Дело в том, что ключи в ассоциативных контейнерах являются константами. Следовательно, любой алго- ритм, который осуществляет запись в элементы последовательности, не может быть использован для ассоциативного контейнера. Связанный с ассоциативным контей- нером итератор можно использовать как аргумент функции, использующей его только для чтения. Когда приходится иметь дело с алгоритмами, итераторы ассоциативных контейнеров це- * и > 1 лесо°бразно рассматривать в качестве итераторов ввода, которые поддерживают также / декремент, а не в качестве полнофункциональных двунаправленных итераторов. Стандарт C++ определяет для каждого параметра-итератора общих и числовых алгоритмов минимально допустимую категорию итератора. Например, алгоритм find, реализующий однопроходный, допускающий только чтение перебор контей- нера, требует как минимум итератор ввода. Функция replace () предполагает на- личие двух итераторов, которые являются, по крайней мере, прямыми итераторами. Первые два параметра функции replace_copy () должны быть, по крайней мере, прямыми итераторами, а третий параметр, указывающий получателя, должен быть не менее, чем итератором вывода. Каждый реально используемый в качестве параметра итератор должен принад- лежать к категории, которая, согласно определенной иерархии, по крайней мере не ниже, чем предусмотренный минимум. Передача менее мощного итератора приведет к ошибке, а более мощного — нет. Ошибка, связанная с передачей алгоритму итератора недопустимой категории, не будет гарантированно обнаружена во время компиляции.
446 Часть II. Контейнеры и алгоритмы Упражнения раздела 11.3.5 Упражнение 11.23. Перечислите пять категорий итераторов и операции, которые каждый из них поддерживает. Упражнение 11.24. Итератором какой категории обладает список? А вектор? Упражнение 11.25. Итераторы какой категории нужны алгоритму сору? А алгоритмам reverse И unique? Упражнение 11.26. Объясните, почему каждый из следующих фрагментов кода ошибочен. Укажи- те ошибки, которые будут обнаружены при компиляции. (a) string sa[10]; const vector<string> file_names(sa, sa+6); vector<string>::iterator it = file_names.begin()+2; (b) const vector<int> ivec; fill(ivec.begin(), ivec.end(), ival); (c) sort(ivec.begin(), ivec.rend()); (d) sort(ivecl.begin(), ivec2.end()); 11.4. Структура общих алгоритмов Подобно тому, как все контейнеры спроектированы по одинаковой схеме, в осно- ве алгоритмов лежит общий проект. Понимание принципов, заложенных в проект библиотеки, облегчает изучение алгоритмов и способствует их более эффективному применению. Поскольку существует более 100 алгоритмов, имеет смысл изучить их структуру, а не запоминать каждый в отдельности. Фундаментальным свойством любого алгоритма является вид (или виды) итера- торов, которыми он оперирует. Для каждого алгоритма определен итератор, который может быть передан каждому из его параметров. Если параметр должен быть итера- тором прямого доступа, им может оказаться итератор контейнера vector и deque или даже указатель массива. Итераторы на других контейнерах с такими алгоритма- ми не могут быть использованы. Второй способ классификации алгоритмов уже был представлен в начале главы, т.е. согласно действиям, которые они способны осуществлять с элементами. Некоторые алгоритмы осуществляют только чтение, т.е. они оставляют значения элементов и их порядок неизменными. Другие присваивают новые значения определенным элементам. Третьи перемещают значения из одного элемента в другой. Как будет продемонстрировано в остальной части этого раздела, существует две дополнительные схемы алгоритмов: одна схема определена параметрами, которые передаются алгоритмам, в вторая — именами функций и соглашениями перегрузки. 11.4.1. Параметрическая схема алгоритмов Эта классификация алгоритмов основана на соглашениях об именах параметров. Понимание этих соглашений поможет в изучении новых алгоритмов: зная, что озна- чает имя данного параметра, можно догадаться, какие операции выполняет данный
Глава 11. Общие алгоритмы 447 алгоритм. Большинство алгоритмов получают параметры в одной из следующих четырех форм. алл(beg, алг(beg, алг(beg, алг(beg, end, другие параметры) ; end, dest, другие параметры); end, beg2, другие параметры); end, beg2, end2, другие параметры); Здесь алг — это имя алгоритма, a beg и end — параметры, обозначающие диапа- зон элементов, с которыми алгоритм работает. Обычно этот диапазон называют ис- ходным диапазоном (input range) алгоритма. Исходный диапазон необходим почти всем алгоритмам, а наличие других параметров зависит от конкретных обстоя- тельств. Как правило, остальные параметры, dest, beg2 и end2, также являются итераторами. Во всех алгоритмах эти итераторы играют одинаковую роль. Кроме них, некоторые алгоритмы получают дополнительные параметры (также являющие- ся неитераторами). Алгоритмы с одним итератором назначения Параметр dest (destination — назначение) — это итератор, обозначающий получате- ля, используемого для хранения результата. Подразумевается, что алгоритмы способны безопасно записать в контейнер назначение стольких элементов, сколько необходимо. При вызове этих алгоритмов очень важно удостовериться, что контейнер назначения достаточно велик, чтобы содержать полученный результат. Как правило, контейнер на- 2^'лк значения предоставляет итератор вставки или итератор ostream_iterator. Если вызов этих алгоритмов осуществляется с итератором контейнера, подразумевается, что контейнер имеет столько элементов, сколько необходимо. Если dest является итератором контейнера, алгоритм записывает свой результат в уже существующие элементы контейнера. Как правило, итератор dest связан с итера- тором вставки (раздел 11.3.1, стр. 432) или итератором ostream_iterator. Итератор вставки добавляет элементы в контейнер, гарантируя достаточную емкость. Итератор ostream_iterator осуществляет запись в поток вывода, а следовательно, тоже не создает никаких проблем независимо от количества записываемых элементов. Алгоритмы с двумя итераторами, указывающими исходную последовательность Алгоритмы, получающие один параметр (Ьед2) или два параметра (Ьед2 и end2), используют эти итераторы для обозначения второго исходного диапазона. Как пра- вило, для выполнения необходимых действий эти алгоритмы используют элементы второго диапазона вместе с элементами исходного. Когда алгоритм получает пара- метры Ьед2 и end2, эти итераторы обозначают весь второй диапазон. То есть алго- ритм получает два полностью определенных диапазона: исходный диапазон, обозна- ченный итераторами beg и end, а также второй, исходный диапазон, обозначенный итераторами Ьед2 и end2. Алгоритмы, получающие только итератор Ьед2 (но не end2), рассматривают итератор Ьед2 как указывающий на первый элемент во втором исходном диапазоне. Конец этого диапазона не определен. В этом случае алгоритмы подразумевают, что
448 Часть II. Контейнеры и алгоритмы диапазон, который начинается с элемента, указанного итератором Ьед2, имеет, по крайней мере, такой же размер, что и диапазон, обозначенный итераторами beg и end. Подобно алгоритмам, осуществляющим запись при помощи итератора dest, алгорит- мы, получающие итератор Ьед2 в качестве единственного параметра, подразумевают, что последовательность, которая начинается с элемента, указанного итератором beg2, имеет такой же размер, что и диапазон, обозначенный итераторами beg и end. 11.4.2. Соглашения об именовании алгоритмов В библиотеке использовано несколько наборов совершенно однозначных согла- шений об именовании для функций и их перегруженных версий. Первый применя- ется к алгоритмам, которые проверяют элементы в исходном диапазоне, а второй — к алгоритмам, переупорядочивающим элементы внутри исходного диапазона. Различия между версиями, которые получают значение и предикат Достаточно много алгоритмов работает, проверяя элементы в их исходном диапа- зоне. Как правило, эти алгоритмы используют один из стандартных операторов сравнения, либо ==, либо <. Большинство алгоритмов использует вторую версию, которая позволяет самостоятельно переопределять используемый оператор, а не создавать специальную функцию проверки или сравнения. Алгоритмы, которые переупорядочивают элементы контейнера, используют опе- ратор <. Для этих алгоритмов определена вторая, перегруженная версия функции, получающей дополнительный параметр, указывающий другую функцию, которая и применяется для упорядочивания элементов. sort(beg, end); // применение для сортировки элементов // оператора < sort(beg, end, comp); // применение для сортировки элементов II функции по имени comp По умолчанию для сравнения значений алгоритмы используют оператор ==. Эти алгоритмы обладают второй, одноименной (а не перегруженной) версией с парамет- ром, который является предикатом (раздел 11.2.3, стр. 429). Алгоритмы, получающие предикат, имеют суффикс _if. find(beg, end, val); // найти в исходном диапазоне первый // экземпляр значения val find_if(beg, end, pred); // найти первый экземпляр, для которого II функция pred возвращает значение true Обе эти функции (алгоритма) находят в исходном диапазоне первый элемент с определенным значением. Алгоритм find ищет указанное значение, а алгоритм f ind_if — значение, для которого функция по имени pred возвратит значение, от- личное от нуля. Алгоритмы предоставляют одноименную версию, а не перегруженную, потому что обе они получают то же самое количество параметров. В случае переупорядочиваю- щих алгоритмов, устранить неоднозначность обращения очень просто: достаточно ориентироваться исключительно на количество параметров. Для алгоритмов поиска определенных элементов, количество параметров остается одинаковым и в случае
Глава 11. Общие алгоритмы 449 передачи искомого значения, и в случае передачи предиката. Следовательно, при пе- регрузке становится вполне возможной некоторая неоднозначность (раздел 7.8.2, стр. 295). Поэтому, хоть и очень редко, для подобных алгоритмов библиотека пре- доставляет две одноименные версии, а не перегруженные. Различия между копирующими и не копирующими версиями Независимо от того, проверяет ли алгоритм элементы, он может изменить порядок элементов внутри исходного диапазона. По умолчанию такие алгоритмы записывают переупорядоченные элементы обратно в их исходный диапазон. Эти алгоритмы также обладают второй, одноименной версией, которая записывает полученный ре- зультат по назначению. К именам этих алгоритмов добавляется окончание _сору. reverse(beg, end); reverse_copy(beg, end, dest); Функция reverse (), как и следует из ее имени, меняет порядок элементов ис- ходной последовательности на обратный. Первая версия меняет порядок элемен- тов на обратный непосредственно в исходной последовательности. Вторая версия, reverse_copy (), копирует переупорядоченные элементы в последовательность начиная с элемента, указанного итератором dest. Упражнения раздела 11.4.2 Упражнение 11.27. В библиотеке определены следующие алгоритмы. replace(beg, end, old_val, new_val); replace_if(beg, end, pred, new_val); replace_copy(beg, end, dest, old_val, new_val); replace_copy_if(beg, end, dest, pred, new_val); Опишите выполняемые ими операции на основании только имен и параметров. Упражнение 11.28. Предположим, что 1st — это контейнер, который содержит 100 элементов. Объясните, что выполняет следующий фрагмент кода, и устраните все ошибки, которые здесь, возможно, присутствуют. vector<int> vecl; reverse_copy(1st.begin(), lst.end(), vecl.begin()); 11.5. Алгоритмы, специфические для контейнеров Итераторы контейнера list (списка) являются двунаправленными, но не обес- печивающими произвольный доступ. Поскольку контейнер list не обеспечивает произвольного доступа к элементам, для него нельзя использовать алгоритмы, кото- рые предполагают наличие итераторов прямого доступа. К ним относятся алгорит- мы, связанные с сортировкой. Существуют и другие общие алгоритмы, такие как merge, remove, reverse и unique, которые к спискам применимы, но выполняют- ся довольно медленно. Эти алгоритмы могут выполняться быстрее, если воспользо- ваться преимуществами, предоставляемыми контейнером list.
450 Часть II. Контейнеры и алгоритмы Если воспользоваться внутренней структурой списка, можно создать и намного более быстрые алгоритмы. Чтобы не полагаться исключительно на общие функции, в библиотеке определен довольно сложный набор функций, специфических для спи- ска, которые подходят и для других последовательных контейнеров. Эти специфи- ческие для контейнера типа list функции перечислены в табл. 11.4. В данной таб- лице не указаны общие алгоритмы, которые используют двунаправленные или ме- нее мощные итераторы, вследствие чего они одинаково эффективно выполняются как со списками, так и с другими контейнерами. Таблица 11.4. Функции, специфические для списка 1st.merge(lst2) 1st.merge(lst2, comp) 1st.remove(val) 1st.remove_if(unaryPred) 1st.reverse() 1st.sort 1st.splice(iter, 1st.splice(iter, 1st.splice(iter, lst2) lst2, iter2) beg, end) 1st.unique() 1st.unique(binaryPred) Объединяет элементы списков ist2nlst. Оба списка должны быть отсортированы. Элементы из списка 1st2 уда- ляются, и после объединения список 1st2 оказывается пус- тым. Возвращает тип void. В первой версии используется оператор <, а во второй — указанная функция сравнения При помощи функции 1st. erase () удаляет каждый эле- мент, значение которого равно переданному значению, или для которого указанный предикат возвращает значение, от- личное от нуля. Возвращает тип void Меняет порядок элементов списка 1st на обратный Сортирует элементы списка 1st Перемещает элемент (элементы) из списка ist2 в список 1st непосредственно перед элементом в списке 1st, ука- занным итератором iter. Перемещенный элемент (элементы) из списка 1st2 удаляются. Первая версия пере- мещает все элементы из списка 1st2 в список 1st. После этого список ist2 оказывается пустым. Списки 1st и 1st2 не могут совпадать. Вторая версия перемещает только те элементы списка 1st2, которые указаны итератором iter 2. В данном случае, списки lst2nlst могут совпа- дать. То есть в данном случае функцию spl ice () вполне можно применить для перемещения элементов внутри списка. Третья версия перемещает элементы в диапазоне, указанном итераторами beg И end. Как обычно, итераторы beg и end должны относиться к допустимому диапазону. Итераторы м о- гут относиться к диапазону в любом списке, включая список 1st. Если итераторы beg и end относятся к списку 1st, а итератор iter относится к элементу в этом диапазоне, ре- зультат выполнения функции окажется непредсказуемым При помощи вызова функции 1st. erase () удаляет рас- положенные рядом элементы с одинаковыми значениями. Для определения равенства элементов, в первой версии исполь- зуется оператор ==, а во второй — указанный предикат Для контейнеров типа list всегда предпочтительнее использовать специализиро- ванные версии функций, а не общие алгоритмы.
Глава 11. Общие алгоритмы 451 Большинство специфических для списка алгоритмов подобны, но не идентичны их общим аналогам. 1.remove(val); 1.гemove_i f(pred); 1.reverse(); 1.sort(); 1.sort(comp); 1.unique(); 1.unique(comp); // удаляет из 1 все экземпляры val // удаляет из 1 все экземпляры, для которых pred // возвращает true // меняет в 1 порядок элементов на обратный // использует для сравнения элементов // опера тор < типа элемента // использует comp для сравнения элементов // используя оператор == удаляет одинаковые // смежные элементы // использует comp для удаления одинаковых // смежных элементов Существует два критически важных различия между функциями, специфиче- скими для списков, и их общими аналогами. Одно из различий заключается в том, что версии функций remove () и unique () для списка изменяют исходный кон- тейнер, т.е. указанные элементы фактически удаляются. Например, вызов функции list: : unique () удалит из списка второй и последующие совпадающие элементы. В отличие от соответствующих общих алгоритмов, функции, специфические для списков, способны добавлять и удалять элементы. Еще оно различие заключается в том, что функции списка merge () и splice () разрушительны для их аргументов. Когда используется общая версия функции merge (), объединенная последовательность записывается в контейнер, указанный итератором назначения, а две исходные последовательности остаются неизменными. В случае функции merge () класса list, список, переданный к качестве аргумента, обнуляется: его элементы перемещаются в объединенный список, для которого и была вызвана функция merge (). Упражнения раздела 11.5 Упражнение 11.29. Переделайте приведенную в разделе 11.2.3 (стр. 427) программу, которая удаляет совпадающие слова, таким образом, чтобы в ней использовался список, а не вектор. Резюме Одним из наиболее важных аспектов в процессе стандартизации языка C++ стало созда- ние и распространение стандартной библиотеки. Библиотеки контейнеров и алгоритмов яв- ляются краеугольным камнем стандартной библиотеки. В библиотеке определено более 100 алгоритмов. К счастью, алгоритмы имеют унифицированную архитектуру, которая сущест- венно упрощает их изучение и применение. Алгоритмы не зависят от типа контейнера: как правило, они работают с последовательно- стью элементов, которые могут храниться в контейнере библиотечного типа, встроенном мас- сиве или даже в специально созданной последовательности для чтения или записи в поток. Эта независимость от типа достигается благодаря использованию итераторов. Первые два ар- гумента, передаваемые большинству алгоритмов, представляют собой итераторы, обозна- чающие диапазон элементов. Дополнительными аргументами может быть итератор, указы-
452 Часть II. Контейнеры и алгоритмы вающий контейнер для записи результата, или два итератора, обозначающие вторую исход- ную последовательность. Итераторы можно разделить на категории согласно операциям, которые они поддержива- ют. Существует пять категорий итераторов: ввода, вывода, прямой, двунаправленный и про- извольного доступа. Итератор принадлежит к определенной категории, если он поддерживает операции, обязательные для итератора данной категории. Подобно тому, как итераторы классифицируются по операциям, алгоритмы разделены на категории согласно свойствам итераторов, необходимых для их параметров. Алгорит- мам, которые только читают последовательность, зачастую хватает возможностей лишь итераторов ввода, а алгоритмам, которые осуществляют запись, необходим, как минимум, итератор вывода и т.д. Алгоритмы, осуществляющие поиск значений, зачастую имеют вторую версию, исполь- зующую для поиска элемента предикат, возвращающий значение, отличное от нуля. У таких алгоритмов имя второй версии имеет суффикс _if. Аналогично, большинство алгоритмов обладают т.н. копирующей версией. Они записывают преобразованные элементы в последо- вательность вывода, а не обратно в исходный диапазон. Имена таких версий заканчиваются суффиксом _if. Третья схема связана с назначением алгоритмов: чтение, запись или переупорядочивание элементов. Сами алгоритмы никогда не изменяют размер обрабатываемых последовательно- стей. (Если аргумент является итератором вставки, он мог бы добавлять элементы, но сам ал- горитм этого не делает.) Алгоритмы могут копировать элементы из одной позиции в другую, но не могут добавлять или удалять элементы. Термины Адаптер back_inserter. Адаптер итератора, который, получив ссылку на контейнер, создает итератор вставки, используемый функцией push_back () для добавления элементов в указанный контейнер. Адаптер front_inserter. Адаптер итератора, который, получив ссылку на контейнер, создает итератор вставки, используемый функцией push_f ront () для добавления элемен- тов в начало указанного контейнера. Адаптер inserter. Адаптер итератора, который, получив итератор и ссылку на контей- нер, создает итератор вставки, используемый функцией insert () для добавления элементов непосредственно перед элементом, указанным данным итератором. Двунаправленный итератор (bidirectional iterator). Поддерживает те же операции, что и прямые итераторы, плюс способность использовать оператор - - для перемещения по после- довательности назад. Итератор istream_iterator. Потоковый итератор, обеспечивающий чтение из потока ввода. Итератор ostream_iterator. Потоковый итератор, обеспечивающий запись в поток вывода. Итератор ввода (input iterator). Итератор, позволяющий читать, но не записывать эле- менты. Итератор вставки (insert iterator). Итератор, который использует функцию контейнера для вставки элементов, а не их перезаписи. Когда итератору вставки присваивается значение, в последовательность добавляется новый элемент с данным значением. Итератор вывода (output iterator). Итератор, позволяющий записывать, но не читать эле- менты. Итератор после конца (off-the-end iterator). Итератор, отмечающий конец диапазона элементов последовательности. Итератор после конца используется как ограничитель, он
Глава 11. Общие алгоритмы 453 указывает на элемент, следующий после последнего элемента в диапазоне. Этот итератор может относиться к несуществующему элементу, поэтому обращаться к его значению ни в коем случае нельзя. Итератор прямого доступа (random-access iterator). Поддерживает те же самые операции, что и двунаправленный итератор, плюс способность использовать операторы сравнения для выяснения позиций двух итераторов относительно друг друга, а также способность осущест- влять с итераторами арифметические действия, обеспечивая таким образом произвольный доступ к элементам. Категории итераторов (iterator categories). Концептуальная организация итераторов на основании поддерживаемых ими операций. Категории итераторов составляют иерархию, в которой более мощные итераторы предоставляют те же операции, что и менее мощные. Пока итератор обеспечивает, по крайней мере, достаточный уровень операций, он вполне приме- ним. Например, некоторым алгоритмам требуются только итераторы ввода. Такие алгоритмы могут быть применены к любому другому итератору, который обладает возможностями не ниже, чем у итератора вывода. Алгоритмы, которым необходимы итераторы прямого доступа, применимы только для тех итераторов, которые поддерживают операции прямого доступа. Общий алгоритм (generic algorithm). Алгоритм, не зависящий от типа контейнера. Потоковый итератор (stream iterator). Итератор, который может быть связан с потоком. Предикат (predicate). Функция, которая возвращает значение типа bool (логическое) или допускающее преобразование в него. Зачастую используется общими алгоритмами для проверки элементов. Используемые библиотекой предикаты являются либо унарными (получающими один аргумент) либо бинарными (получающими два аргумента). Прямой итератор (forward iterator). Итератор, позволяющий читать и записывать элемен- ты, но не поддерживающий оператор - -. Реверсивный итератор (reverse iterator). Итератор, позволяющий перемещаться по после- довательности назад. У этих итераторов операторы ++ и - - имеют противоположный смысл.

Ill Абстракция, классы и ДАННЫЕ В ЭТОЙ ЧАСТИ... Глава 12. Классы Глава 13. Управление копированием Глава 14. Перегрузка операторов и преобразования Классы являются самым важным элементом большинства программ на языке C++. Они позволяют создавать собственные типы данных, которые специально предна- значены для решения конкретных проблем. В результате приложения получаются более простыми и понятными. Хорошо проработанные классы могут быть столь же легки в применении, как и встроенные типы. В классе определены и данные, и функции для работы с ними. Переменные-члены хранят состояние (данные) объекта класса, а функции выполняют операции, в которых используются значения переменных-членов. Классы позволяют отделить реализацию и интерфейс. Интерфейс содержит всю необходимую информацию об операциях, ко- торые способен осуществлять класс, а подробности его реализации должны быть из- вестны только разработчику класса. Подобное разделение уменьшает воздействие факторов, усложняющих программирование и приводящих к ошибкам. Классы зачастую называют абстрактными типами данных (abstract data type). Абстрактный тип данных объединяет данные (состояние) и манипулирующие ими функции в единый блок. Внутренние действия класса можно рассматривать как аб- стракцию, поскольку отнюдь не всегда необходимо знать, как именно работает класс. Абстрактные типы данных являются основой объектно-ориентированного и общего программирования. Глава 12, “Классы”, начинается подробным описанием определения классов. Здесь изложены фундаментальные правила применения любых классов: область ви- димости класса, сокрытие данных и конструкторы, а также некоторые из дополни- тельных возможностей классов: дружественные отношения, неявный указатель this, статические и переменные данные-члены. Именно классы языка C++ определяют действия, осуществляемые при инициа- лизации, копировании, присвоении и удалении объектов. В этом отношении язык C++ отличается от многих других языков, большинство которых не позволяют раз-
456 Часть III. Абстракция, классы и данные работникам классов контролировать эти операции. Этим темам посвящена глава 13, “Управление копированием”. В главе 14, “Перегрузка операторов и преобразования”, рассматривается пере- грузка операторов, позволяющая операндам, имеющим тип класса, использовать операторы, аналогичные встроенным. Перегрузка операторов — это один из спосо- бов, позволяющий создавать новые типы в языке C++, которые в применении столь же интуитивно понятны, как и встроенные. Здесь также представлен еще один спе- циальный вид функций-членов — функции преобразования, которые обеспечивают неявное преобразование объектов класса. Компилятор применяет эти функции пре- образования в тех же условиях и по тем же причинам, что и функции преобразова- ния встроенных типов.
12 Классы В ЭТОЙ ГЛАВЕ... 12.1. Определение и объявление классов 458 12.2. Неявный указатель this 468 12.3. Область видимости класса 473 12.4. Конструкторы 480 12.5. Дружественные отношения 493 12.6. Статические члены класса 496 Резюме 501 Термины 501 Классы в языке C++ используются для определения собственных абстрактных типов данных (abstract data type). Определяя собственные типы данных, которые соответствуют конкретным решаемым задачам, можно упростить создание, отладку и модификацию программ. В этой главе продолжается описание классов, начатое в главе 2, “Переменные и базовые типы”, и главе 5, “Выражения”. Здесь более подробно рассматриваются воз- можности абстрактных данных, позволяющие скрыть внутреннее содержимое объ- екта, обеспечивая тем не менее доступ к его открытым функциям. Кроме того, в этой главе рассматривается область видимости класса, конструкто- ры и указатель this. Здесь также представлены три новые возможности класса: дружественные отношения, статические и переменные данные-члены. Классы являются важнейшим компонентом языка C++. Ранние версии языка имели название “С with Classes” (С с классами), что подчеркивало главенствующую роль классов. По мере развития языка, возможности классов совершенствовались. Основная задача при разработке языка заключалась в предоставлении программи- стам таких возможностей, которые позволили бы им создавать собственные типы, столь же простые в применении и интуитивно понятные, как и встроенные. В этой главе описана большая часть фундаментальных особенностей классов.
458 Часть III. Абстракция, классы и данные 12.1. Определение и объявление классов Классы в программах этой книги используются начиная уже с главы 1, “Первые шаги”. Все использованные ранее библиотечные типы, такие как vector, istream и string, являются классами. Были также определены собственные простые классы, такие как Sales__item и TextQuery. Напомним, класс Sales_item выглядел таким образом. class Sales_item { public: // операции, допустимые для объектов класса Sales_item double avg_price() const; bool same_isbn(const Sales_item &rhs) const { return isbn == rhs.isbn; } // стандартный конструктор необходим для инициализации // членов встроенного типа Sales_item(): units_sold(0), revenue(0.0) { } private: std::string isbn; unsigned units_sold; double revenue; double Sales_item::avg_price() const if (units_sold) return revenue/units_sold; else return 0; 12.1.1. Определение класса Частично определение классов уже было представлено при их создании в разде- лах 2.8 (стр. 85) и 7.7 (стр. 284). Важнее всего то, что класс определяет не только новый тип, но и новую область видимости. Члены класса В каждом классе может быть определено любое количество членов. Это могут быть данные, функции или определения типов. Класс может содержать любое количество разделов public (открытые), private (закрытые) и protected (защищенные). Маркеры доступа public и private уже использовались: члены класса, определенные в разделе public, доступны любому коду, использующему объект данного класса, а определенные в разделе private доступным только другим членам класса. Раздел protected более подробно рас- сматривается в главе 15, “Объектно-ориентированное программирование”, при об- суждении наследования. Все члены класса должны быть объявлены внутри него, и добавить новый член извне в уже определенный класс невозможно.
Глава 12. Классы 459 Конструкторы При создании объекта класса, компилятор автоматически использует для его инициализации конструктор (раздел 2.3.3, стр. 71). Конструктор (constructor) — это специальная функция-член, имя которой совпадает с именем класса. Его задача за- ключается в гарантированном обеспечении необходимым исходным значением каж- дой переменной-члена. Конструктор может обладать списком инициализирующих значений (раз- дел 7.7.3, стр. 289) для переменных-членов объекта. // стандартный конструктор необходим для инициализации // членов встроенного типа Sales_item(): units_sold(0), revenue(0.О) { } Список инициализирующих значений конструктора (constructor initializer list) — это список имен членов класса и соответствующих им исходных значений. Он рас- полагается непосредственно после списка параметров конструктора и начинается с двоеточия. Функции-члены Функции-члены должны быть объявлены и могут быть (но необязательно) опре- делены внутри класса. Функции, определенные внутри класса, по умолчанию счи- таются встраиваемыми (inline) (раздел 7.6, стр. 282). Когда функции-члены определяются вне класса, необходимо указать, что они нахо- дятся в области видимости данного класса. В определении Sales_item: :avg_price используется оператор области видимости (scope operator) (раздел 1.2.2, стр. 30), позволяющий указать, что это определение функции avg_price () класса Sales_ item. Функции-члены неявно получают дополнительный аргумент, который связывает функцию с объектом, от имени которого происходит вызов функции. trans.avg_price() В приведенном выше примере происходит вызов функции avg_price () объекта по имени trans. Если объект trans принадлежит классу Sales_item, функция trans () вполне сможет обратиться к внутреннему члену класса Sales_item. Функции-члены могут быть объявлены константными. Для этого после списка параметров достаточно поместить ключевое слово const. double avg_price() const; Константный член класса не может изменять значения переменных-членов объекта, с которым он работает. Ключевое слово const должно присутствовать и в объявлении, и в определении. Когда оно указано только в одном случае, компи- лятор сообщает об ошибке. Упражнения раздела 12.1.1 Упражнение 12.1. Создайте класс по имени Person, способный хранить имя и адрес человека. Для хранения каждого из этих элементов используйте объекты класса string. Упражнение 12.2. Создайте для класса Person конструктор, которому передаются две строки.
460 Часть III. Абстракция, классы и данные Упражнение 12.3. Создайте функции, возвращающие при обращении имя и адрес. Должны ли эти функции быть константными? Объясните, почему. Упражнение 12.4. Укажите, какие из членов класса Person следует объявить как public, а какие как private. Объясните, почему. 12.1.2. Абстракция данных и инкапсуляция В основе концепции классов лежат абстракция данных (data abstraction) и инкап- суляция (encapsulation). Абстракция данных — это методика программирования (и проектирования), пред- полагающая разделение интерфейса и реализации. Разработчик класса должен поза- ботиться о его реализации, но программисты, использующие класс, не обязаны знать подробности его внутренней конструкции. Чтобы использовать класс, достаточно знать его интерфейс, т.е. что именно он делает, а не как устроен. Инкапсуляция — это термин, который описывает методику объединения низко- уровневых элементов для создания нового, высокоуровневого объекта. Одной из форм инкапсуляции является функция: множество выполняемых в ней действий объединено в одном большом объекте, которым и является функция. Инкапсуляция элементов скрывает детали реализации: функцию можно вызвать и не получая ни- какого доступа к операторам, которые ее составляют. Точно так же и класс является инкапсулирующей сущностью: он представляет собой объединение нескольких чле- нов, причем в хорошо проработанных классах реализующие его члены скрыты. Если рассмотреть библиотечный тип vector, можно найти примеры и абстрак- ции данных, и инкапсуляции. Абстракцией здесь является использование интерфей- са, описывающего доступные для выполнения операции. Инкапсуляция заключается в том, что здесь нет никакого доступа к деталям реализации способов хранения эле- ментов. Массив напротив, будучи подобным вектору, не использует ни абстракции, ни инкапсуляции. Массивом можно манипулировать непосредственно, обращаясь к области памяти, в которой он хранится. Маркеры доступа улучшают абстракцию и инкапсуляцию Чтобы определить абстрактный интерфейс класса, в языке C++ используются маркеры доступа (access label) (раздел 2.8, стр. 88). Класс может содержать любое количество маркеров доступа. Члены класса, определенные после маркера public, доступны для всех частей программы. Абстрактные данные класса определяются как открытые (public) члены. Члены класса, определенные после маркера private, доступны лишь коду внутри класса. Раздел private инкапсулирует (т.е. скрывает) реализацию от кода, который использует данный класс. Не существует никаких ограничений на то, как часто может использоваться мар- кер доступа. Каждый маркер задает уровень доступа определяемых далее членов класса. Указанный уровень доступа остается в силе до тех пор, пока не встретится следующий маркер доступа или закрывающая фигурная скобка тела класса.
Глава 12. Классы 461 Определять члены класса можно и до указания маркера доступа. Уровень досту- па членов класса, определенных после открывающей фигурной скобки тела класса и перед первым маркером доступа, зависит от того, как именно класс определен. Если класс определен с ключевым словом struct, его члены, определенные перед пер- вым маркером доступа, будут открытыми, а если класс определен с ключевым сло- вом class, они будут закрытыми1. Совет. Конкретные и абстрактные типы Не все типы должны быть абстрактными. Библиотечный класс pair можно считать показательным примером хорошо проработанного вспомогательного класса, который является конкретным (concrete), а не абстрактным. Конкретный класс — это класс, ко- торый предоставляет свою реализацию, а не скрывает ее. Некоторые классы, например pair, действительно не имеют никакого абстрактного интерфейса. Класс pair предназначен для того, чтобы объединить две переменные- члена в один объект. Здесь нет никакой необходимости скрывать переменные-члены и никаких преимуществ это не дает. Сокрытие членов только усложнило бы примене- ние такого класса, как pair. Даже в этом случае подобные классы зачастую имеют функции-члены. В частности, для любого класса, обладающего переменными-членами встроенного или составного типа, имеет смысл определить конструктор (конструкторы) для их инициализации. Пользователь класса может самостоятельно инициализировать переменные-члены или присвоить им значение, однако наличие конструктора повысит устойчивость класса к ошибкам. Роль программиста Программисты имеют тенденцию называть людей, которые будут запускать на выполнение их приложения, “пользователями" (“users”). Приложения разрабатыва- ют и усовершенствуют в ответ на отзывы тех, кто в конечном счете их “использует" (“use”). Классы некоторым образом похожи на приложения: разработчик класса его проектирует и реализует для “пользователей” этого класса. В данном случае “пользователь” — это программист, а не конечный пользователь приложения. Успешными бывают лишь те приложения, которые не только эффективно реша- ют актуальную для пользователей задачу, но и удобны в применении. Аналогично, хорошо проработанный вспомогательный класс должен удовлетворять потребно- стям пользователей класса и быть удобным в применении. Различие между разработчиками и пользователями класса соответствует также различию между пользователями и разработчиками приложений. Пользователей за- ботит только то, чтобы приложение соответствовало их потребностям, а его цена — возможностям. Аналогично, пользователей класса интересует лишь их интерфейс. Хорошие разработчики определяют интерфейс класса так, чтобы он был интуитивно понятным и простым в использовании. А реализация заботит пользователей лишь постольку, поскольку она влияет на применение класса. Если реализация слишком медлительна или создает трудности при использовании, это вряд ли обрадует поль- 1В этом и заключается единственное принципиальное различие между классами и струк- турами. — Примеч. ред.
462 Часть III. Абстракция, классы и данные зователей класса. Но что касается хорошо работающих классов, то их реализация волнует только их разработчика. В случае простых приложений, разработчик и пользователь класса может быть одним и тем же лицом. Но даже в таком случае полезно различать эти роли. При проектировании интерфейса класса, разработчик класса должен думать о его про- стоте и удобстве для пользователя класса, а при использовании класса программист не должен думать о том, как именно этот класс работает. “Пользователями” программисты, работающие с языком C++, зачастую называют как пользователей приложения, так и пользователей класса. Понять, о каких именно “пользователях” идет речь, помогает контекст. Когда го- ворят о “пользовательском коде” или “пользователе класса Sales_item”, подразу- мевают программиста, который использует класс при создании приложения. Когда речь идет о “пользователе” приложения bookstore, имеют в виду менеджера книж- *-> 2 ного магазина, который использует приложение . Фундаментальная концепция. Преимущества абстракции данных и инкапсуляции Абстракция данных и инкапсуляция предоставляют два важных преимущества. • Внутренняя организация класса защищена от непреднамеренных ошибок пользователя, которые могли бы повредить состояние объекта. • Реализация класса может быть впоследствии изменена, в ответ на изменение требований или при устранении ошибок, но это не потребует внесения измене- ний в код на пользовательском уровне. Определяя переменные-члены класса только в разделе private, автор класса остан-, ляет за собой свободу впоследствии вносить в них изменения. При изменении реали- зации, чтобы выявить влияние внесенных изменений, достаточно исследовать только код класса. Если данные открыты public), любая функция будет способна непосред- ственно обратиться к переменным-членам и изменить текущее состояние объекта. В этом случае пришлось бы находить и переписывать все те участки кода, которые пола- гались на пережнее поведение программы. Аначошчно, если внутреннее состояние класса закрыто, изменение данных-членов может осуществить весьма ограниченное количество функций. Данные оказываются защищены о г ошибок, которые может совершить пользователь. Если произойдет ошибка, которая повреждает состояние объекта, область ее поиска будет ограничена, поскольку изменить закрытые данные может только функция-член, а следовательно, там и следует искать ошибку. Ограничение области поиска ошибок значительно по- вышает работоспособность программы и облегчает ее обслуживание. Если данные закрыты и интерфейс функций-членов не изменился, пользовательские функции, которые манипулируют объектами класса, менять не потребуется. ? Пользователем может быть также код другого приложения или даже сетевой ресурс. — Примеч. ред.
Глава 12. Классы 463 Поскольку измененное определение класса находится в файле заголовка, который под- ключен в текст каждого файла исходного кода, для внесения в них изменений достаточно будет перекомпилировать каждый файл, использующий этот класс. Упражнения раздела 12.1.2 Упражнение 12.5. Какие маркеры доступа могут быть доступны классам в языке C++? Какие виды членов должны быть определены после каждого маркера доступа? Существуют ли ограничения от- носительно количества используемых маркеров доступа внутри определения класса? Упражнение 12.6. Чем классы определенные с помощью ключевого слова class, отличаются от классов, определенных с помощью ключевого слова struct? Упражнение 12.7. Что такое инкапсуляция? Чем она полезна? 12.1.3. Подробнее об определении классов Созданные ранее классы были довольно просты, однако они позволили проде- монстрировать основы применения классов в языке C++. Но необходимо рассмот- реть еще несколько важных аспектов создания классов, которым и будет посвящена остальная часть этого раздела. Несколько переменных-членов одинакового типа Как уже отмечалось, переменные-члены класса объявляются аналогично обыч- ным переменным. При объявлении обычных переменных и переменных-членов класса общим является случай, когда несколько переменных-членов имеют одина- ковый тип. Такие переменные-члены могут быть перечислены в одном объявлении. Например, можно определить тип (класс) по имени Screen, который будет пред- ставлять окно (window) на экране компьютера. Класс Screen будет обладать пе- ременной членом типа string, предназначенной для хранения содержимого окна, и тремя переменными-членами типа string: : size_type, которые предназначены для хранения номера символа, на котором в данный момент находится курсор, а также высоты и ширины окна. Члены этого класса можно определить следующим образом, class Screen { public: // функции-члены интерфейса private: std::string contents; std::string::size_type cursor; std::string::size_type height, width; }; Применение определения типов для рационализации классов Кроме определения данных и функций членов, класс позволяет также определять собственные локальные имена для таких типов. Абстракция класса Screen достиг- нет более высокого уровня, если для типа std: :string: :size_type применить определение типа. class Screen { public: // функции-члены интерфейса
464 Часть III. Абстракция, классы и данные typedef std::string::size_type index; private: std::string contents; index cursor; int height, width; }; Имена типов, которые определены внутри класса, подчиняются стандартным правилам доступа, как и все остальные члены класса. Определение типа index рас- положено в разделе public, поскольку пользователи должны использовать это имя. Пользователи класса Screen не должны знать, что для его реализации использован класс string. Определение типа index скрывает эту деталь реализации класса Screen. Объявив данный тип открытым (public), можно позволить пользователям класса использовать это имя. Функции-члены можно перегружать Еще один способ сделать классы более простыми и понятными заключается в уменьшении количества функций-членов. В частности, ни один из рассмотренных ранее классов не нуждался в перегруженных версиях своих функций-членов. Одна- ко подобно обычным функциям (не членам класса), функция-член вполне может быть перегружена (раздел 7.8, стр. 291). За исключением перегруженных операторов (раздел 14.9.5, стр. 579), для кото- рых существуют специальные правила, перегрузка функций-членов класса приво- дит лишь к созданию других функций-членов данного класса. Функция-член класса никак не связана ни с обычными функциями (не членами класса), ни с функциями, объявленными в других классах, и не может их перегружать. Пере- груженные функции-члены подчиняются тем же правилам, что и обычные функ- ции: две перегруженные функции-члена не могут иметь одинаковое количество и тип параметров. Для выявления используемой при обращении версии функции- члена, используется тот же механизм распознавания (раздел 7.8.2, стр. 295), что и для обычных перегруженных функций. Определение перегруженных функций-членов Для демонстрации перегрузки, снабдим класс Screen двумя перегруженными функциями-членами, которые возвращают указанный символ окна. Одна версия бу- дет возвращать символ, на котором в данный момент находится курсор, а вторая — символ, указанный рядом и столбцом. class Screen { public: typedef std::string::size_type index; // возвратить символ, на котором в данный момент находится // курсор char get() const { return contents[cursor]; } char get(index ht, index wd) const; // остальные члены класса private: std::string contents; index cursor; index height, width; );
Глава 12. Классы 465 Подобно любой перегруженной функции, выбор используемой версии осуществля- ется на основании количества и/или типов переданных при обращении аргументов. Screen myscreen; char ch - myscreen.get(); // вызов Screen::get() ch = myscreen.get(0, 0); // вызов Screen::get (index, index) Явное определение встраиваемых функций-членов Определенные внутри класса функции-члены, такие, например, как не получаю- щая никаких аргументов версия функции get (), автоматически считаются встраи- ваемыми (inline). То есть при создании исполняемого кода компилятор попытается встроить ее содержимое по месту вызова (раздел 7.6, стр. 282). Функцию-член мож- но также объявлять встраиваемой явно. class Screen { public: typedef std::string::size type index; // определенная внутри объявления класса функция является // встраиваемой неявно char get() const { return contents [cursor]; } // определенная вне объявления класса функция может быть // объявлена встраиваемой явно inline char get(index ht, index wd) const; // ключевое слово inline может и не быть указано в объявлении // класса, а применено позже index get_cursor() const; } ; // когда функция объявлена встраиваемой в объявлении класса, / / повторять ее объявление как встраиваемой в определении не нужно char Screen::get(index r, index c) const { index row = r * width; // вычислить положение ряда return contents[row + с]; // смещение с позволяет получить // указанный символ } // функция не была объявлена встраиваемой в объявлении класса, // а здесь, в определении, была inline Screen::index Screen::get_cursor() const { return cursor; } Функцию-член можно объявить встраиваемой в составе ее объявлении внутри тела класса. В качестве альтернативы, функцию можно объявить встраиваемой при ее определении вне тела класса. То есть ключевое слово inline применимо как в объявлении, так и в определении. Преимуществом определения встраиваемых функций вне класса является повышение удобочитаемости кода класса. Подобно другим встраиваемым функциям, определение встраиваемой функции-члена должно присутствовать в каждом использующем ее файле исходного кода. Определение встраиваемой функции-члена, располагаемое вне тела класса, как правило, имеет смысл помещать в тот же файл заголовка, в котором определен сам класс. Упражнения раздела 12.1.3 Упражнение 12.8. Определите функцию Sales_item: :avg_price () как встраиваемую.
466 Часть III. Абстракция, классы и данные Упражнение 12.9. Напишите собственную версию рассматриваемого в этом разделе класса screen, снабдив его конструктором, который создает объект класса Screen, получив значение высоты, ширины и содержимого экрана. Упражнение 12.10. Объясните, что представляет собой каждый член следующего класса. class Record { typedef std::size_t size; Record(): byte_count(0) { } Record(size s): byte_count(s) { } Record(std::string s): name(s), byte_count(0) { } size byte_count; std: .-string name; public: size get_count() const { return byte_count; } std::string get_name() const { return name; } }; 12.1.4. Объявление и определение класса Закрывающая фигурная скобка завершает определение класса. По завершении определения, все члены класса должны быть известны. В результате объем памя- ти, необходимы для хранения объекта данного класса, также будет известен. В ка- ждом файле исходного кода класс может быть определен только один раз. Если класс определен в несколькими файлах, его определение в каждом файле должно быть идентичным. Поместив определение класса в файл заголовка, можно гарантировать, что в каж- дом использующем его файле исходного кода определения класса будут идентичны- ми. Применение защиты заголовка (раздел 2.9.2, стр. 93) гарантирует, что даже не- однократное подключение заголовка в тот же файл исходного кода не приведет к дублированию определения класса. Класс можно объявлять, но не определять. class Screen; // объявление класса Screen Подобное объявление, иногда называемое предварительным объявлением (forward declaration), вводит имя Screen в программу и указывает, что имя Screen принад- лежит классу. После предварительного объявления но до определения, класс Screen является незавершенным типом (incomplete3 type). То есть известно, что Screen является классом, но не известно, члены каких именно типов он содержит. Использование незавершенного типа весьма ограниченно. Объекты такого типа не могут 1 быть определены. Незавершенный тип можно использовать только для определения ука- / зателей или ссылок, а также для объявления (но не определения) функций, которые ис- пользуют этот тип как параметр или тип возвращаемого значения. Прежде чем можно будет создать объект класса, он должен быть полностью опре- делен. Класс должен быть определен, а не только объявлен, поскольку компилятору должен быть известен объем памяти, который необходимо зарезервировать для хра- нения объекта данного класса. Аналогично, прежде чем ссылка или указатель будут использованы для доступа к объекту класса, сам класс должен быть уже определен. 3 В оригинале incompete — некомпетентным. — Примеч. ред.
Глава 12. Классы 467 Использование объявлений класса для членов класса Переменная-член может быть определена как имеющая тип класса только тогда, когда определение класса уже существует. Если тип незавершен, переменная-член может быть только указателем или ссылкой на объект данного класса. Поскольку класс еще не может считаться определенным до завершения его тела, он не может иметь и переменные-члены его собственного типа. Однако класс уже считается объявленным, как только его имя будет упомянуто. Следовательно, класс может иметь переменные-члены, которые являются указателями или ссылками на его собственный тип. class LinkScreen { Screen window; LinkScreen *next; LinkScreen *prev; }; Как правило, предварительное объявление класса используют в случае, когда необходи- 1 мо создать взаимозависящие классы. Пример такого случая будет приведен в разде- ле 13.4 (стр. 517). Упражнения раздела 12.1.4 Упражнение 12.11. Определите два класса, х и Y, где класс х имеет указатель на класс Y, а класс y имеет объект класса х. Упражнение 12.12. Объясните различие между объявлением и определением класса. Когда при- меняется объявление класса? Когда применяется определение класса? 12.1.5. Объекты класса При определении класса фактически определяется тип. Как только класс опреде- лен, можно создавать объекты его типа. При создании (определении) объекта, как правило, резервируется и область памяти для его хранения, а при определении типов этого (обычно) не происходит. class Sales_item { public: // операции, допустимые для объектов класса Sales_item private: std::string isbn; unsigned units_sold; double revenue; }; Здесь определен новый тип, но область памяти для его хранения не резерви- руется. Sales_item item; При создании объекта компилятор резервирует область памяти, достаточную для хранения объекта класса Sales_item. Имя item является ссылкой на эту область. Каждый объект имеет собственную копию переменных-членов класса. Изменение
468 Часть III. Абстракция, классы и данные значений переменных-членов объекта item никак не повлияет на значения пере- менных-членов других объектов класса Sales_item4. Определение объектов класса После определения класса, его можно использовать двумя способами: использовать имя класса непосредственно, как имя типа; использовать ключевое слово class или struct, сопровождаемое именем класса. Sales_item iteml; // инициализированный по умолчанию объект // клаоса Sa 1es_item class Sales_item iteml; // эквивалентное определение объекта iteml Эти два способа создания объекта класса эквивалентны. Второй способ унаследо- ван из языка С и также допустим в языке C++. Как правило, в языке C++ использу- ется первый способ, он более быстрый и простой. Почему определение класса завершается точкой с запятой Как уже отмечалось на стр. 87, определение класса завершается точкой с запятой. Точка с запятой необходима потому, что за определением класса может следовать список определений объектов. А определение, как обычно, должно завершаться точ- кой с запятой. class Sales_item { /* ... */ }; class Sales_item { /* ... */ } accum, trans; Вообще, определение объекта как часть определения класса считается не самой лучшей идеей. Такая форма записи значительно менее очевидна. Она способна вве- кыхю&мм сти читателей в заблуждение, поскольку объединяет в одном операторе определе- 1 ние двух разных сущностей: класса и его объекта. 12.2. Неявный указатель this Как уже упоминалось в разделе 7.7.1 (стр. 286), функции-члены обладают допол- нительным неявный параметром, который является указателем на текущий объект класса. Этот неявный параметр, this, связан с объектом, от имени которого вызвана функция-член. Указывать параметр this функции-члена не нужно, компилятор сам применит его неявно. В теле функции-члена указатель this можно (но необяза- тельно) использовать явно. Компилятор воспринимает неуточненную ссылку на член класса так, как будто ее предваряет указатель this. Когда используется указатель thi s Хотя внутри функции-члена явно обращаться к указателю this, как правило, не нужно, есть один случай, когда это необходимо. Речь идет об обращении к объекту в целом, а не к его члену. Чаще всего указатель this используется в случае, когда функция должна возвратить ссылку на объект, от имени которого она была вызвана. 4 Если в нем отсутствуют статические (static) переменные-члены, разумеется. — При- меч. ред.
Глава 12. Классы 469 Класс Screen является хорошим примером класса, в котором могли бы найти применение функции, возвращающие ссылки. На настоящий момент класс Screen обладает только двумя функциями get (), а значит, вполне логично добавить сле- дующие функции: две функции set (), позволяющие присвоить значение указанному символу или символу на котором находится курсор; функцию move (), перемещающую курсор в новую позицию, указанную двумя значениями. Было бы очень удобно, если бы пользователь получил возможность объединить эти действия в одно выражение. // переместить курсор в указанную позицию и присвоить II символу значение myScreen.move(4,0).set('#'); Этот оператор эквивалентен следующим операторам. myScreen.move(4,0); myScreen.set('#'); Возвращение ссылки на указатель thi s Чтобы обеспечить возможность вызова функций move () и set () в одном выра- жении, каждая из них должна возвращать ссылку на объект, от имени которого она выполняется. class Screen { public: // функции-члены интерфейса Screen^ move(index г, index с); Screen& set(char); Screen& set(index, index, char); // другие члены как и прежде Обратите внимание, что типом возвращаемого значения этих функций является Screen&, т.е. они возвращают ссылку на объект собственного класса. Каждая из этих функций возвращает объект, от имени которого они вызваны. Чтобы получить доступ к объекту, используется указатель this. Реализация этих двух новых функ- ций приведена ниже. Screen& Screen::set(char с) contents[cursor] = c; return *this; Screen& Screen::move(index r { index c) index row = r * width; // положение ряда cursor = row + c; return *this; Единственной интересной частью этих функций является оператор return. В ка- ждом случае функция возвращает тип *this. В этих функциях this является
470 Часть III. Абстракция, классы и данные указателем на неконстантный объект класса Screen. Подобно любым другим указа- телям, для доступа к объекту, на который указывает указатель this, достаточно об- ратиться к его значению. Возвращение ссылки на указатель thi s из константной функции-члена В обычной (неконстантной) функции-члене указатель this имеет тип константно- го указателя (раздел 4.2.5, стр. 151) на тип класса. Можно изменить значение, на кото- рое указывает указатель this, но изменить хранимый в нем адрес нельзя. В констант- ной функции-члене указатель this является константным указателем на констант- ный объект типа класса. В этом случае нельзя изменить ни объект, на который указывает указатель this, ни хранимый в нем адрес. Из константной функции-члена нельзя возвратить обычную ссылку на объект класса. Кон- стантная функция-член может возвратить *this только как константную ссылку. В класс Screen можно, например, добавить функцию display (). Эта функция должна выводить содержимое на переданный объект класса ostream. С логической точки зрения эта функция должна быть константной. Отображение содержимого не изменяет объект. Если функцию display () сделать константным членом класса Screen, указатель this внутри функции display () окажется константным, т.е. const screen* const. Однако, подобно функциям move () и set (), функции display () желательно обеспечить возможность вызова в одном выражении с другими функциями. // переместите курсор в указанную позицию, присвоить символ II и отобразить на экран myScreen.move(4,0).set('#').display(cout); Здесь подразумевается, что функция display () возвращает ссылку на объект класса Screen и получает ссылку на объект класса ostream. Если функция display () является константным членом класса, типом возвращаемого ей значе- ния окажется const screen&. К сожалению, здесь есть проблема. Если определить функцию display () как константный член класса, ее можно было бы вызывать для неконстантного объекта, но она окажется непригодной для большого выражения. Следующий код был бы не- допустимым. Screen myScreen; // этот код некорректен, если функция-член display() константна II функция display!) возвращает константную ссылку, а функция set () // использовать константу не может myScreen.display().set('*'); Проблема заключается в том, что это выражение использует функцию set () для объекта, возвращаемого функцией display (). Но этот объект является константой, поскольку функция display () возвращает его как константный, а функцию set () для константного объекта вызвать нельзя.
Глава 12. Классы 471 Перегрузка на основании константности Для решения этой проблемы необходимо определить две функции display (): ту, которая является константой, и ту, которая константой не является. Функции- члены вполне можно перегружать исходя из того, являются ли они константными или нет, причем по тем же причинам, по которым функцию можно перегружать исходя из того, является ли ее параметр указателем на константу (раздел 7.8.4, стр. 301). Константный объект может использовать только константный член класса. Неконстантный объект может использовать любой член класса, но неконстантная версия подходит лучше. Таким образом, можно определить закрытую функцию-член по имени do_ display (), которая и будет фактически осуществлять вывод объекта класса Screen. Каждая из версий функции display () будет обращаться к этой функции, а затем возвращать используемый объект. class Screen { public: // функции-члены интерфейса. II функцию display () можно перегружать исходя из того, является // ли объект константой или нет Screen& display(std::ostream &os) { do_display(os); return *this; } const Screen& display(std::ostream &os) const { do_display(os); return *this; } private: // вывод объекта класса Screen осуществляет одна функция, II которую и вызывают обе версии функции display() void do_display(std::ostream &os) const { os « contents; } // как прежде } ; Теперь при использовании функции display () в составе большого выраже- ния будет применяться ее неконстантная версия, а при отображении константного объекта — константная версия. Screen myScreen(5,3); const Screen blank(5, 3); myScreen.set(’#').display(cout); // вызов неконстантной версии blank.display(cout); // вызов константной версии Изменяемые переменные-члены Иногда необходимо обеспечить возможность изменять значение переменной- члена класса даже внутри константной функции-члена. Такие переменные-члены следует объявлять изменяемыми. Изменяемая переменная-член (mutable data member) — это член класса, который никогда не становится константным, даже если он принадлежит константному объ- екту. Соответственно, константная функция-член вполне может изменить значение такой переменной. Чтобы сделать переменную-член изменяемой, применяется клю- чевое слово mutable, которое должно предшествовать ее объявлению. class Screen { public: // функции-члены интерфейса private:
472 Часть III. Абстракция, классы и данные mutable size_t access_ctr; // обеспечивает возможность II изменения даже в константных // объектах // другие переменные-члены, как и прежде Давайте снабдим класс Screen новой переменной-членом по имени access_ ctr, которая будет изменяемой. Эта переменная будет использована для подсчета количества обращений к функциям-членам класса Screen. void Screen::do_display(std::ostream& os) const { ++access_ctr; // счетчик обращений к любым функциям-членам os << contents; Несмотря на то, что функция do_display () является константной, она вполне способна увеличить значение переменной access_ctr. Дело в том, что этот член класса объявлен как mutable, т.е. любая функция-член, включая константные, мо- жет изменить ее значение. Совет: чаще используйте закрытые вспомогательные функции Некоторые читатели могут удивиться: зачем дополнительно создавать отдельную функцию do_display () ? В конце концов, обращение к функции do_display () не намного проще, чем осуществляемое внутри нее действие. Зачем же она нужна? При- чин здесь несколько. 1. Всегда желательно избегать нескольких экземпляров одного кода 2. По мере развития класса, функция display () может стать значительно более сложной, а следовательно, преимущества одой, а не нескольких копий кода станут более очевидными. 3. Во время разработки в тело функции display (), вероятно, придется добавить отладочный код, который в финальной версии будет удален. Это будет проще сделать в случае, когда весь отладочный код находится в одной функции do_display(). 4. Поскольку функция do_display() объявлена встраиваемой (inline), при создании исполняемого кода компилятор и так вставит ее содержимое по месту вызова, поэтому вызов функции не повлечет за собой никаких потерь времени и ресурсов. Обычно в хорошо спроектированных программах на языке C++ существует большое количество маленьких вспомогательных функций, таких как do_display (), которые выполняют всю основную работу, когда их использует набор других функций 5 Весьма существенное требование. Впоследствии, при отладке, крайне сложно догадаться, почему код, прекрасно работающий в одних условиях, отказывается работать в других. Дело в том, что в одну копию кода необходимое изменение было внесено, а во вторую, прочно забы- тую, нет. — Примеч. ред.
Глава 12. Классы 473 Упражнения раздела 12.2 Упражнение 12.13. Усовершенствуйте класс Screen таким образом, чтобы он содержал функ- ции move (), set () и display (). Проверьте класс, выполнив следующее выражение. // переместить курсор в указанную позицию, присвоить символ // и отобразить на экран myScreen.move(4,0).set('#').display(cout); Упражнение 12.14. Обращаться внутри класса к его членам через указатель this вполне допус- тимо, но несколько избыточно. Обсудите все “за” и “против” явного применения указателя this для получения доступа к членам класса. 12.3. Область видимости класса Каждый класс создает новую, собственную область видимости и уникальный тип. Объявления членов класса внутри тела класса представляют их имена в области ви- димости класса. Два разных класса имеют две разные области видимости. Даже если два класса имеют совершенно одинаковый список членов, они имеют разные 5, ‘ 1 типы. Члены каждого класса отличны от членов любого другого класса (или любой другой области видимости). Рассмотрим пример. class First { public: int memi; double memd; class Second { public: int memi; double memd; First objl; Second obj 2 = objl; // ошибка: objl и obj 2 имеют разные типы Использование членов класса Извне области видимости класса, к его членам можно обращаться только через объект или при помощи указателя, используя точечный оператор или оператор стрелки соответственно. Левый операнд этих операторов является объектом класса или указателем на объект класса. Имя члена класса, которое следует за оператором, должно быть объявлено в области видимости соответствующего класса. Class obj; // Class - некоторый класс Class *ptr = &obj; // member - переменная-член этого класса ptr->member; // доступ к члену member объекта, на который // указывает указатель ptr obj.member; // доступ к члену member объекта по имени obj // memfcn() - функция-член этого класса ptr->memfсп(); // вызов функции-члена memfen() для объекта, на
474 Часть III. Абстракция, классы и данные // который указывает указатель pkr obj.memfсп() ; // вызов функции-члена memfen() для объекта // по имени obj Для доступа к некоторым из членов класса используется оператор обращения, а для доступа к другим — оператор области видимости (: :Как правило, для обра- щения к переменной или функции-члену следует указать объект. К членам, которые являются определением типа, например screen: : index, можно обратиться при помощи оператора области видимости. Область видимости и определение члена класса Определения элементов “ведут себя” так, как будто они находятся в области ви- димости класса, даже если они находятся вне тела класса. Напомним, что в опреде- лении члена класса, расположенного вне тела класса, следует указать класс, к кото- рому он принадлежит. double Sales_item::avg_price() const { if (units_sold) return revenue/units_sold; else return 0; } Для указания того, что определение функции-члена avg_price () принадлежит области видимости класса Sales_item, здесь использовано ее полностью опреде- ленное имя Sales_item: : avg_price. Полностью определенное имя члена класса в определении позволяет выяснить область видимости класса, к которому он при- надлежит. Когда определение находится в области видимости класса, к переменным- членам revenue и units_sold можно обращаться без такой формы записи, как this->revenue и this- >units_sold. Списки параметров и тела функций находятся в области видимости класса Когда функция-член определена вне тела класса, список параметров и тело функции-члена располагаются после имени члена класса. Они считаются опреде- ленными внутри области видимости класса, а следовательно, способны обращаться к другим членам данного класса без уточнения принадлежности. Рассмотрим, напри- мер, определение в классе Screen версии функции get () с двумя параметрами. char Screen::get(index r, index c) const { index row - r * width; // вычислить положение ряда return contents[row + с]; // смещение с позволяет получить II указанный символ } Для параметров функции get () здесь использовано имя типа index, опреде- ленное внутри класса Screen. Поскольку список параметров расположен в области видимости класса Screen, нет никакой необходимости применять синтаксис Screen: : index. Хоть это и не очевидно, но определение находится в текущей об- ласти видимости класса. Аналогично, здесь вполне законно использованы имена index, width и contents, объявленные внутри класса Screen.
Глава 12. Классы 475 Тип возвращаемого функцией значения не всегда находится в области видимости класса В отличие от типов параметров, тип возвращаемого значения указан перед име- нем члена класса. Если функция определена вне тела класса, используемое для ука- зания типа возвращаемого значения имя оказывается вне области видимости класса. Когда для указания типа возвращаемого значения используется тип, определенный внутри класса, он должен быть указан полностью. Рассмотрим, например, функцию get_cursor(). class Screen { public: typedef std::string::size_type index; index get_cursor() const; }; inline Screen::index Screen::get_cursor() const { return cursor; } Возвращаемое значение этой функции имеет тип index, имя которого определе- но внутри класса Screen. Если определение функции get_cursor () находится вне тела класса, вне области видимости класса окажется весь код, который предва- ряет имя функции. Поэтому имя, используемое для указания типа возвращаемого значения, тоже окажется вне области видимости класса. Чтобы указать тип по имени index, который определен внутри класса Screen, необходимо использовать его полное имя Screen: : index. Упражнения раздела 12.3 Упражнение 12.15. Перечислите части текста программы, которые находятся в области видимо- сти класса. Упражнение 12.16. Что произойдет, если определить функцию get_cursor () следующим об- разом, index Screen::get_cursor() const { return cursor; } 12.3.1. Поиск имен в области видимости класса В рассмотренных до сих пор программах поиск имен (name lookup) (процесс по- иска объявления, соответствующего данному имени) был относительно прост. 1. Сначала поиск объявления осуществляется в том блоке кода, в котором исполь- зуется имя. Причем рассматриваются только те имена, объявления которых рас- положены перед местом применения. 2. Если имя не найдено, поиск продолжается в иерархии областей видимости начи- ная с текущей. Если объявление так и не найдено, происходит ошибка. В программах на языке C++ все имена следует объявлять прежде, чем они будут использованы.
476 Часть III. Абстракция, классы и данные Может показаться, что области видимости класса “ведут себя” несколько по- иному, но в действительности они подчиняются этому же правилу. Сомнения могут возникнуть и относительно способа, применяемого для поиска имен внутри функ- ций, определенных непосредственно в теле класса. Фактически определение класса обрабатывается в два этапа. 1. Сначала компилируются объявления членов класса. 2. И только после просмотра объявлений всех членов класса компилируются сами опре- деления. Безусловно, используемые в области видимости класса имена необязательно должны быть именами только членов класса. При поиске имен в области видимости класса могут быть найдены имена, объявленные и в других областях видимости. Ес- ли имя, используемое в области видимости класса, не принадлежит к именам членов класса, его поиск продолжается в областях видимости, окружающих класс. Поиск имен для объявлений членов класса Поиск имен, используемых в объявлениях членов класса, осуществляется в сле- дующем порядке. Сначала рассматриваются объявления членов класса, предваряющие имя. Если имя не найдено, рассматриваются объявления той области видимости, в которой класс определен, т.е. расположенные непосредственно перед определе- нием класса. Рассмотрим пример. typedef double Money; class Account { public: Money balance () { return bal; } private: Money bal; } ; При обработке объявления функции balance (), компилятор ищет сначала объяв- ление имени Money в области видимости класса Account. Компилятор просматривает только те объявления, которые расположены перед местом применения имени Money. Поскольку его объявление как члена класса не найдено, компилятор ищет имя в гло- бальной области видимости. Объявление имени Money будет найдено перед определе- нием класса Account. Найденное глобальное определение типа Money будет приме- нено для типа возвращаемого значения функции balance () и переменной-члена bal. Имена типов, определенных внутри класса, будут обнаружены прежде, чем они окажутся использованными для типа переменных-членов или в качестве типа возвращаемого зна- чения, либо типа параметра функции-члена. Компилятор обрабатывает объявления членов класса в порядке, соответствую- щем их расположению внутри класса. Как обычно, имя должно быть определено прежде, чем оно будет применено. Кроме того, если имя используется в качестве имени типа, оно не может быть переопределено.
Глава 12. Классы 477 typedef double Money; class Account { public: Money balance () { return bal; } // используется глобальное // определение типа Money private: // ошибка: нельзя изменить смысл имени Money typedef long double Money; Money bal; } ; Поиск имени в определениях членов класса Поиск имени, используемого в теле функции-члена, осуществляется следующим образом. 1. Объявления в локальных областях видимости функции-члена. 2. Если объявление имени не найдено внутри функции-члена, просматриваются объявления всех членов класса. 3. Если объявление имени не найдено внутри класса, просматриваются объявле- ния, расположенные во внешних областях видимости. При поиске имен в областях видимости, члены класса следуют обычным правилам В примерах, демонстрирующих поиск имен, приходится применять подходы, считаю- щиеся плохой практикой программирования. Плохой стиль программирования несколь- ких следующих программ применен преднамеренно. Следующая функция использует одинаковое имя и для параметра, и для члена класса (подобного следует избегать). Здесь это сделано преднамеренно, чтобы про- демонстрировать правила поиска имен. // Примечание: этот код предназначен лишь для демонстрации II неверного подхода II Использование одинаковых имен для параметра и члена класса - не II самая лучшая идея int height; class Screen { public: void dummy_fcn(index height) { cursor = width * height; // Который height? Параметр } private: index cursor; index height, width; }; При поиске объявления имени height, используемого в функции dummy_f сп (), компилятор сначала просматривает локальную область видимости самой функции, где определен параметр height. Поэтому в теле функции dummy_fсп () будет ис- пользован параметр height.
478 Часть III. Абстракция, классы и данные В данном случае параметр height скрывает глобальную переменную height. Несмотря на то, что член класса скрыт, его все равно можно использовать. Достаточно 1 указать его полное имя, включающее имя класса, либо явно применить указатель this. Если обычные правила поиска необходимо изменить, можно поступить следую- щим образом. // плохой подход: имена, локальные для функций-членов, не должны II скрывать имена переменных-членов класса void dummy_fcn(index height) { cursor = width * this->height; // переменная-член height // альтернативный способ указания переменной-члена cursor = width * Screen::height; // переменная-член height После поиска в области видимости функции, осуществляется поиск в области видимости класса Если необходимо использовать переменную-член по имени height, параметру следует присвоить другое имя. // хороший подход: не используйте имена переменных-членов для // параметров или других локальных переменных void dummy_fcn(index ht) { cursor - width * height; // переменная-член height Теперь когда компилятор будет искать имя height, внутри функции он его не найдет. Затем компилятор просмотрит класс Screen. Поскольку имя height ис- пользуется внутри функции-члена, компилятор просмотрит все объявления членов класса. Несмотря на то, что объявление имени height расположено после места его использования внутри функции dummy_f cn (), компилятор решает, что оно отно- сится к переменной-члену height. После поиска в области видимости класса, продолжается поиск в окружающей области видимости Если компилятор не находит имя в функции или в области видимости класса, он ищет его в окружающей области видимости. В данном случае оно объявлено в глобальной области видимости, перед определением класса Screen. Однако этот объект был скрыт. Несмотря на то, что глобальный объект был скрыт, его все равно можно использовать. q| Достаточно указать оператор принадлежности к глобальной области видимости. // плохой подход: не скрывайте необходимые имена, которые II определены в окружающих областях видимости void dummy_fcn (index height) { cursor = width * ::height; // Который height? Глобальный
Глава 12. Классы 479 Поиск имен распространяется по всему файлу, где они были применены Когда член класса определен вне определения класса, третий этап поиска его имени происходит не только в объявлениях глобальной области видимости, кото- рые расположены непосредственно перед определением класса Screen, но и рас- пространяется на остальные объявления в глобальной области видимости. Рас- смотрим пример. class Screen { public: void setHeight(index); private: index height; }; Screen::index verify(Screen::index); void Screen::setHeight(index var) { // var: относится к параметру II height: относится к члену класса /! verify: относится к глобальной функции height - verify(var); } Обратите внимание, что объявление глобальной функции verify () невидимо до определения класса Screen. Однако третий этап поиска имени подразумевает просмотр тех объявлений окружающей области видимости, которые расположены перед определением члена класса, и объявление глобальной функции verify () оказывается найдено. Упражнения раздела 12.3.1 Упражнение 12.17. Что произойдет, если поместить определение типа в последнюю строку клас- са Screen? Упражнение 12.18. Объясните код, приведенный ниже. Укажите, какое из определений, туре или initvai, будет использовано для каждого из имен. Если здесь есть ошибки, найдите их и укажите способ исправления. typedef string Type; Type initVal(); class Exercise { public: typedef double Type; Type setVal(Type); Type initVal(); }; Type Exercise::setVal(Type parm) { val = parm + initVal(); }
480 Часть III. Абстракция, классы и данные Определение функции-члена setvai () является ошибкой. Внесите изменения, необходимые для того, чтобы класс Exercise использовал глобальное определение типа туре и глобальную функцию initVal (). 12.4. Конструкторы Конструктор (constructor) (раздел 2.3.3, стр. 71) — это специальная функция- член, которая выполняется каждый раз, когда создается новый объект класса. Задача конструктора заключается в гарантированном обеспечении необходимым исходным значением каждой переменной-члена. Определение конструктора уже было проде- монстрировано в разделе 7.7.3 (стр. 288). class Sales_item { public: // операции, допустимые для объектов класса Sales_item // стандартный конструктор необходим для инициализации // членов встроенного типа Sales_item(): units_sold(0), revenue(O.O) { } Для инициализации переменных-членов units_sold и revenue этот конструк- тор использует список инициализирующих значений. Переменная-член isbn неявно инициализируется стандартным конструктором класса string как пустая строка. Имя конструктора совпадает с именем класса. Для конструктора не может быть указан тип возвращаемого значения. Подобно любой другой функции, конструктор может иметь любое количество параметров. Конструкторы можно перегружать Класс может иметь любое количество конструкторов. При этом очень важно, что- бы их списки параметров не совпадали. Но как выяснить, сколько и каких именно конструкторов следует определить? Обычно конструкторы отличаются способом, позволяющим пользователю инициализировать переменные-члены объекта. Например, вполне логично было бы снабдить класс Sales_item двумя дополни- тельными конструкторами: тем, который позволил бы пользователю предоставить исходное значение для isbn, и тем, который позволил бы инициализировать объект данными, прочитанными из объекта класса istream. class Sales_item { // другие члены, как и прежде public: // дополнительные конструкторы, инициализирующие объект II данными из объекта класса string и объекта класса istream Sales_item(const std::string&); Sales_item(std::istream&); Sales_item(); };
Глава 12. Классы 481 Используемый конструктор определяют переданные аргументы Теперь в классе определены три конструктора. При создании новых объектов можно использовать любой из них. // применение стандартного конструктора: // isbn - пустая строка; unit s_sold и revenue равны О Sales_item empty; // isbn задан явно; units_sold и revenue равны О Sales_item Primer_3rd_Ed("0-201-82470-1"); // читает значения со стандартного устройства ввода II в переменные-члены isbn, units_sold и revenue Sales_item Primer_4th_ed(cin); Используемый для инициализации объекта конструктор определяет тип (или ти- пы) переданного ему аргумента. При создании объекта empty никакого инициали- зирующего значения не передано, поэтому используется стандартный конструктор. Для инициализации объекта Primer_3rd_Ed конструктору передан один строко- вый аргумент (0-201-82470-1), а при инициализации объекта Primer_4th_ed конструктору передана ссылка на объект cin класса istream. Вызов конструктора происходит автоматически Компилятор6 самостоятельно запускает конструктор каждый раз, когда создается объект класса. // конструктор, получающий строку, используется для создания II объекта с инициализированной переменной-членом isbn Sales_item Primer_2nd_ed("0-201-54848-8"); // стандартный конструктор используется для создания в памяти II не инициализированного объекта Sales_item *р = new Sales_item(); В первом случае получающий строку конструктор инициализирует объект Primer_2nd_ed. Во втором случае, в динамически распределяемой памяти соз- дается новый пустой объект класса Sales_item. При условии, что размещение в памяти прошло успешно, впоследствии объект инициализируется стандартным конструктором. Конструкторы константных объектов Конструктор не может быть объявлен константным (раздел 7.7.1, стр. 286). class Sales_item { public: Sales_item() const; // ошибка }; В константных конструкторах нет никакой необходимости. Когда создается константный объект класса, для его инициализации используется обычный конст- руктор. Задача конструктора заключается в инициализации объекта. Конструктор используется для инициализации объекта независимо от того, является ли объект константным. 6 Вероятно, все же исполняемый код программы, ведь C++ не является интерпретатором и для работы созданных на нем программ компилятор уже не нужен. — Примеч. ред.
482 Часть III. Абстракция, классы и данные Упражнения раздела 12.4 Упражнение 12.19. Создайте один или несколько конструкторов, которые позволят пользователю класса задавать исходные значения для всех или ни для одной из переменных-членов данного класса, class NoName { public: // конструктор (ы) располагаются здесь ... private: std::string *pstring; int ival; double dval; }; Объясните, как было принято решение о количестве и типе аргументов необходимых конструкторов. Упражнение 12.20. Выберите одну из следующих абстракций (или собственную). Определите, ка- кие данные необходимы в таком классе. Предоставьте соответствующий набор конструкторов. Объясните принятые решения. (a) Book (b) Date (с) Employee (d) Vehicle (е) Object (f) Tree 12.4.1. Список инициализирующих значений конструктора Подобно любой другой функции, конструктор имеет имя, список параметров и тело. В отличие от других функций, конструктор может также содержать список инициализирующих значений. // в конструкторах рекомендуется использовать список // инициализирующих знанений Sales_item::Sales_item(const string &book): isbn(book), units_sold(0), revenue(0.0) { } Список инициализирующих значений конструктора начинается с двоеточия, за которым следует разделяемый запятыми список переменных-членов и предназна- ченных для их инициализации значений внутри круглых скобок. Этот конструктор инициализирует переменную-член isbn значением параметра book, а переменные- члены units_sold и revenue — значением 0. Подобно любой функции-члену, конструкторы могут быть определены внутри или вне класса. Список инициали- зирующих значений конструктора располагается только в его определении, но не в объявлении. Список инициализирующих значений конструктора — это весьма удобная возможность, которой многие достаточно опытные программисты, работающие с языком C++, не умеют пользоваться. Одной из сложностей при овладении списком инициализирующих значений конструктора является то, что его вполне можно пропустить и присвоишь необхо- димые значения переменным-членам внутри тела конструктора. Конструктор класса Sales_item, например, можно переписать так, чтобы он получал строку следую- щим образом.
Глава 12. Классы 483 // допустимый, но не самый лучший способ создания конструктора: // список инициализирующих значений конструктора отсутствует Sales_item::Sales_item(const string &book) { Этот конструктор присваивает значения переменным-членам класса Sales_item, а не инициализирует их. Независимо от наличия или отсутствия яв- ной инициализации, переменная-член isbn инициализируется прежде, чем выпол- няется конструктор. Для инициализации переменной-члена isbn, этот конструктор неявно использует стандартный конструктор класса string. Таким образом, на мо- мент выполнения тела конструктора переменная-член isbn уже содержит значение. Присвоение нового значения внутри тела конструктора потребует перезаписи этого значения. С концептуальной точки зрения работу конструктора можно разделить на две фазы: фазу инициализации и фазу обычных вычислений. К фазе вычислений отно- сятся все операторы, расположенные внутри тела конструктора. Переменные-члены класса всегда инициализируются на фазе инициализации, независи- I мо от того, указаны ли они в списке инициализации конструктора или нет. Инициализация завершается прежде, чем начинается фаза вычислений. Каждая переменная-член, которая не упомянута в списке инициализации конст- руктора явно, инициализируется по тем же правилам, что и обычные переменные (раздел 2.3.4, стр. 73). Стандартный конструктор класса инициализирует перемен- ные-члены класса. Исходные значения переменных-членов встроенного или состав- ного типа зависят от области видимости инициализируемого объекта: в локальной области видимости они остаются неинициализированными, а в глобальной инициа- лизируются нулевым значением. Две версии конструктора sales_item (), созданные в этом разделе, решают од- ну задачу: независимо от того, применен ли список инициализации конструктора или значения переменным-членам были присвоены внутри тела конструктора, конеч- ный результат остается тем же. После того как конструктор завершает свою работу, три переменные-члена содержат необходимые значения. Различие между конструк- торами заключается в следующем: версия, использующая список инициализации, инициализирует переменные-члены объекта, а версия без списка инициализации осуществляет присвоение значений переменным-членам в теле конструктора. На- сколько существенно это различие, зависит от типа переменной-члена. Иногда применение списка инициализации конструктора неизбежно Если переменная-член класса не упомянута в списке инициализации конструк- тора, для ее инициализации компилятор неявно использует стандартный конструк- тор типа переменной-члена. Если этот тип (класс) не имеет стандартного конструк- тора, попытка компилятора использовать его потерпит неудачу. В таких случаях для инициализации переменной-члена список инициализации следует применить в обя- зательном порядке.
484 Часть III. Абстракция, классы и данные Некоторые переменные-члены должны быть инициализированы в списке инициализации ' 1 конструктора. Для таких переменных-членов присвоение в теле конструктора не сработа- ет- Переменные-члены, являющиеся объектами класса, не имеющего стандартного кон- структора, а также переменные-члены, являющиеся константой или ссылкой, должны быть инициализированы в списке инициализации конструктора независимо от их типа. Поскольку члены встроенного типа неявно не инициализируются, может пока- заться, что не имеет особого значения, инициализированы они или им присвоено значение. За исключением двух случаев, применение списка инициализации эквива- лентно присвоению значения переменной, не являющейся членом класса как по ре- зультату, так и по эффективности. Например, следующий конструктор ошибочен. class ConstRef { public: ConstRef(int ii); }; // отсутствует явный список инициализации конструктора // ошибка: переменная ri не инициализирована ConstRef::ConstRef(int ii) // приевоения: II ok // ошибка: нельзя присвоить значение константе / / присвоение переменной ri, которая не была // связана с объектом Не забывайте, что константные объекты или объекты ссылочного типа следует инициализировать, а не присваивать им значения. К моменту, когда тело конструк- тора начинает выполняться, инициализация уже завершена. Единственной возмож- ностью обеспечить значениями константу или ссылку, является список инициализа- ции конструктора. Корректный конструктор выглядит следующим образом. // ок: явная инициализация констант и ссылок ConstRef::ConstRef(int ii): i(ii), ci(i), ri(ii) { } Совет. Используйте списки инициализации конструктора Бо многих классах различие между инициализацией и присвоением связано исклю- чительно с вопросом эффективности: зачем инициализировать переменную-член и присваивать ей значение, когда ее достаточно просто инициализировать. Однако важнее всего то, что некоторые переменные-члены обязательно должны быть ини- циализированы. Список инициализации обязательно следует использовать для всех переменных-членов, 1 которые являются константами, ссылками и объектами классов, не имеющих стандартно- му/ го конструктора. Как правило, применение списка инициализации конструктора позволяет избе- жать ошибки при компиляции, если среди переменных-членов класса встретится та- кая, которая требует наличия списка инициализации.
Глава 12. Классы 485 Порядок инициализации переменных-членов класса Нет ничего удивительного в том, что каждая переменная-член присутствует в списке инициализации конструктора только один раз. В конце концов, зачем пере- менной-члену два исходных значения? Но что на самом деле неожиданно, так это то, что список инициализации конструктора задает только значения, используемые для инициализации переменных-членов, но не определяет порядок, в котором осуществ- ляется инициализация. Порядок инициализации переменных-членов задает их рас- положение при определении. Сначала инициализируется первая переменная-член, затем следующая и т.д. Порядок инициализации зачастую не имеет значения. Но если одна из переменных- ;| членов инициализируется с учетом значения другой, порядок их инициализации критиче- ски важен. Рассмотрим следующий класс. public: // ошибка во время выполнения: i инициализируется прежде j X (int val): j(val), i(j) { } }; В данном случае список инициализации конструктора написан так, чтобы ини- циализировать переменную-член j значением val, а затем использовать перемен- ную-член j для инициализации переменной-члена i. Но переменная-член i ини- циализируется первой. В результате попытка инициализации переменной-члена i осуществляется в момент, когда переменная-член j еще не имеет значения! Некоторые компиляторы достаточно интеллектуальны, чтобы распознать опас- ность и выдать предупреждение о том, что переменные-члены в списке инициализа- ции конструктора расположены в порядке, отличном от порядка их объявления. Элементы списка инициализации конструктора имеет смысл располагать в том же порядке, в котором переменные-члены объявлены. Кроме того, старайтесь по возможности избегать применения одних переменных-членов для инициализации других. Вообще, можно достаточно просто избежать любых проблем, связанных с поряд- ком выполнения инициализации. Достаточно использовать параметры конструктора вместо переменных-членов объекта. Конструктор класса X, например, лучше было бы написать следующим образом. X(int val): i(val), j(val) { } В этой версии порядок инициализации переменных-членов i и j не имеет значения. Список инициализации может содержать любые выражения Список инициализации может содержать выражения любой сложности. Напри- мер, класс Sales_item можно снабдить новым конструктором, который получает сроку для инициализации переменной-члена isbn, а также беззнаковое целое число для обозначения количества проданных книг и число типа double для цены.
486 Часть III. Абстракция, классы и данные Sales_item(const std::string &book, int ent, double price): isbn(book), units_sold(ent), revenue(cnt * price) { } В этом списке инициализации используется параметр revenue, представ- ляющий выручку за проданные книги. Его значение вычисляется в результате ум- ножения цены на количество. Инициализация переменных-членов, являющихся объектами класса При инициализации переменной-члена, являющейся объектом класса, необходи- мо задать аргументы, которые будут переданы одному из конструкторов класса этой переменной-члена. Можно использовать любой из конструкторов этого класса. На- пример, класс Sales_item может инициализировать переменную-член isbn при помощи любого из конструкторов класса string (раздел 9.6.1, стр. 365). Вместо пустой строки, например, значением по умолчанию для переменной-члена isbn мож- но назначить очень большое значение ISBN, которое в реальности невозможно. То есть переменную-член i sbn можно инициализировать строкой из десяти девяток. // альтернативное определение стандартного конструктора // класса Sales__item Sales_item(): isbn(10, '9'), units_sold(0), revenue(0.0) {} В этом списке инициализации использован тот конструктор класса string, ко- торому передают количество и символ, формирующий строку. Упражнения раздела 12.4.1 Упражнение 12.21. Напишите стандартный конструктор, использующий список инициализации для класса, который содержит переменные-члены следующих типов: const string, int, double* и if streams. Инициализируйте строку именем класса. Упражнение 12.22. Этот список инициализации содержит ошибку. Найдите и устраните ее. struct X { X (int i, int j): base(i), rem(base % j) { } int rem, base; } ; Упражнение 12.23. Предположим, что существует класс по имени NoDefault, который обла- дает конструктором, получающим аргумент типа int, но не стандартным конструктором. Создай- те класс с, который имеет переменную-член типа NoDefault. Создайте для класса с стандарт- ный конструктор. 12.4.2. Аргументы по умолчанию и конструкторы Давайте вернемся к определениям стандартного конструктора и конструктора, получающего строку. Sales_item(const std::string &book): isbn(book), units_sold(0), revenue(0.0) { } Sales_item(): units_sold(0), revenue(O.O) { } Эти конструкторы почти одинаковы: единственное различие заключается в том, что конструктор, получающий строковый параметр, использует его для инициали- зации переменной-члена isbn. Стандартный конструктор, применяемый для ини- циализации переменной-члена isbn, неявно использует стандартный конструктор класса string.
Глава 12. Классы 487 Эти конструкторы можно объединить, снабдив строковую переменную-член ар- гументом по умолчанию. class Sales_item { public: // аргумент по умолчанию для book - пустая строка Sales item(const std::string &book - ""): isbn(book), units_sold(0), revenue(O.O) Sales_item(std::istream &is); // как прежде } ; Здесь определено два конструктора, в одном из которых параметру предоставлен аргумент по умолчанию. Конструктор, получающий заданный по умолчанию аргумент для одного строкового параметра, будет использован в любом из следующих случаев. Sales_item empty; Sales_item Primer_3rd_Ed("0-201-82470-1"); В случае объекта empty, используется значение аргумента по умолчанию, в слу- чае объекта Primer_3rd_ed — значение, указанное явно. Каждая версия конструктора этого класса предоставляет одинаковый интерфейс: оба они инициализируют объект класса Sales_item переданным строковым значе- нием или значением, принятым по умолчанию, если никакого значения не передано. Предпочтительнее использовать аргумент по умолчанию, поскольку это уменьшает количество повторяющегося кода. Упражнения раздела 12.4.2 Упражнение 12.24. Используя версию класса Sales_item с двумя конструкторами (стр. 487), один из которых имеет аргумент по умолчанию для единственного строкового параметра, укажите, какой из конструкторов будет применен для инициализации каждой из следующих переменных, и перечислите значения переменных-членов для каждого из объектов. Sales_item first_item(cin); int main() { Sales_item next; Sales_item last("9-999-99999-9"); } Упражнение 12.25. Теоретически вполне возможно, что потребуется предоставить конструктору, по- лучающему ссылку на объект класса istream, объект cin как заданный по умолчанию аргумент. Напишите объявление конструктора, которое использует объект cin как аргумент по умолчанию. Упражнение 12.26. Могут ли конструкторы, получающие строку и ссылку на объект класса istream, иметь аргументы по умолчанию? Если нет, то почему? 12.4.3. Стандартный конструктор Стандартный конструктор (default constructor) используется в случае, когда при создании объекта не предоставляются инициализирующие значения. Конструк- тор, который обладает аргументами по умолчанию для всех параметров, также рас- сматривается как стандартный конструктор.
488 Часть III. Абстракция, классы и данные Синтезируемый стандартный конструктор Если в классе определен хотя бы один конструктор, компилятор не будет автома- тически создавать стандартный конструктор. В основе этого правила лежит предпо- ложение, что если класс должен контролировать инициализацию объекта в одном случае, то вполне вероятно, что ему это потребуется и во всех остальных случаях. Компилятор создает стандартный конструктор автоматически только то- гда, когда в классе не определены никакие конструкторы. Синтезируемый стандартный конструктор (synthesized default constructor) ини- циализирует переменные-члены согласно тем же правилам, которые используются при инициализации обычных переменных7. Переменные-члены, являющиеся объек- тами другого класса, инициализируются их собственным стандартным конструкто- ром. Переменные-члены встроенного или составного типа, такие как указатели и массивы, инициализируются только для тех объектов, которые определены в гло- бальной области видимости. Объекты встроенных или составных типов, определен- ные в локальной области видимости, остаются неинициализированными. Когда класс содержит переменные-члены встроенного или составного типа, не сле- дует полагаться на синтезируемый стандартный конструктор. Необходимо опреде- лить собственный конструктор, инициализирующий такие переменные-члены. Кроме того, каждый конструктор должен предоставить инициализирующие значе- ния для переменных-членов встроенного или составного типа. Конструктор, который не инициализирует переменные-члены встроенных или составных типов, оставляет их в неопределенном состоянии, а применение такого объекта для любых целей, отлич- ных от присвоения значения, является ошибкой. Если каждый конструктор класса яв- но присваивает каждой переменной-члену заранее известное значение, его функции- члены смогут отличить пустой объект от объекта, имеющего реальные значения. Как правило, классу необходим стандартный конструктор В некоторых случаях компилятор самостоятельно применяет стандартный кон- структор. Если класс не имеет стандартного конструктора, он не может быть исполь- зован в таком контексте. Для демонстрации ситуаций, когда необходим стандартный конструктор, рассмотрим класс NoDefault, который не имеет собственного стан- дартного конструктора, но имеет конструктор, получающий аргумент типа string. Поскольку конструктор в классе определен, компилятор не будет создавать для него стандартный конструктор. Тот факт, что у класса NoDefault нет никакого стан- дартного конструктора, имеет следующие последствия. 1. Каждый конструктор каждого класса, в состав которого входит переменная-член типа NoDefault, должен инициализировать ее явно, передав конструктору класса NoDefault исходное строковое значение. 2. Компилятор не будет создавать стандартный конструктор для классов, в состав которых входит переменная-член типа NoDefault. Если таким классам необхо- 7 Имеется в виду конструктор, предоставляемый компилятором. — Примеч.ред.
Глава 12. Классы 489 димы значения по умолчанию, их следует указать явно, а конструктор должен явно инициализировать переменные-члены типа NoDefault. 3. Тип NoDefault не может быть использован как тип элемента для динамически распределяемого массива. 4. При статическом распределении массива элементов типа NoDefault, необхо- дима явная инициализация каждого элемента. 5. Контейнер типа vector, содержащий объекты класса NoDefault, не может ис- пользовать конструктор, которому передают только размер вектора, без исход- ных значений элементов. Практически всегда, когда определены другие конструкторы, классу необходим стандартный конструктор. Как правило, исходные значения, присвоенные перемен- ным-членам в стандартном конструкторе, должны указывать, что объект “пуст”. Применение стандартного конструктора Распространенной ошибкой среди программистов, плохо знакомых с языком C++, являет- ся объявление объекта, инициализированного стандартным конструктором следующим образом. // Упс! Это объявление функции, а не создание объекта Sales_item myobj () ; Компиляция объявления myobj пройдет без сообщения об ошибке. Но при по- пытке использовать объект myobj, компилятор сообщит, что он не можем приме- нить синтаксис доступа к элементу для функции! Sales_item myobj(); // ok: но определена функция, а не объект if (myobj.same_isbn(Primer_3rd_ed)) // ошибка: myobj - функция Проблема заключается в том, что компилятор интерпретирует определение объекта myobj как объявление функции, которая не принимает никаких параметров и воз- вращает объект типа Sales_item. Но это вовсе не то, что ожидалось! Правильный способ создания объекта при помощи стандартного конструктора не подразумевает наличия пустых круглых скобок. // ок: определение объекта класса ... Sales_item myobj; Код, приведенный ниже, прекрасно работает. // ок: создание неименованного, пустого объект класса Sales_item // и применение его для инициализации объекта myobj Sales_item myobj = Sales_item(); Здесь создается и инициализируется объект класса Sales_item, который и ис- пользуется для инициализации объекта класса myobj. Компилятор инициализирует объект класса Sales_item при помощи его стандартного конструктора. Упражнения раздела 12.4.3 Упражнение 12.27. Какое из следующих выражений неверно (если есть)? Почему? (а) Класс должен обладать по крайней мере одним конструктором. (Ь) Стандартный конструктор — это конструктор без параметров.
490 Часть III. Абстракция, классы и данные (с) Если у переменных-членов класса по умолчанию нет никаких значений, то класс не должен об- ладать стандартным конструктором. (d) Если в классе не определен стандартный конструктор, компилятор создаст его автоматически, инициализируя каждую переменную-член значением по умолчанию соответствующего типа. 12.4.4. Неявное преобразование Как упоминалось в разделе 5.12 (стр. 204), язык C++ автоматически осуществля- ет преобразование некоторых встроенных типов. Можно также определить способ неявного преобразования объекта одного класса в объект данного, а также способ неявного преобразования объекта данного класса в объект другого класса (типа). Определение преобразования объекта из текущего класса в другой, будет продемон- стрировано в разделе 14.9 (стр. 566). Для определения неявного приведения (преоб- разования) к типу класса, достаточно определить соответствующий конструктор. Конструктор, который может быть вызван с одиночным аргументом, вполне позволяет оп- ределить неявное преобразование из типа параметра к типу класса. Давайте вернемся к версии класса Sales_item, в которой определено два конст- руктора. class Sales_item { public: // аргумент по умолчанию для book - пустая строка Sales_item(const std::string &book = ""): isbn(book), units_sold(0), revenue(O.O) { } Sales_item(std::istream &is); // как прежде }; Каждый из этих конструкторов определяет неявное преобразование. Следова- тельно, объект класса string или istream можно использовать там, где ожидается объект класса Sales_item. string null_book = "9-999-99999-9"; // ok: создание объекта класса Sales_item с нулевыми значениями // переменных-членов units_sold и revenue, а также значением isbn, // равным заданному для строки null_book item.same_isbn(null_book); Эта программа использует объект класса string как аргумент функции same_ isbn () класса Sales_item. В качестве аргумента эта функция ожидает объект класса Sales_item. Чтобы создать новый объект класса Sales_item из строки null_book, компилятор использует ту версию конструктора класса Sales_item, которая получает объект класса string. Этот вновь созданный (временный) объект класса Sales_item и передается функции same_isbn (). Желательно ли такое поведение, зависит от конкретных обстоятельств. В данном случае это хорошая идея. Строка nul l_book, вероятнее всего, соответствует несуще- ствующему ISBN, и при обращении функция same_isbn () вполне может выяснить, не является ли объект item класса Sales_item нулевым. С другой стороны, пользо- ватель может по ошибке вызвать функцию same_isbn () для объекта null_book.
Глава 12. Классы 491 Существенно более проблематичным является преобразование типов из istream в Sales__item. // ок: применение конструктора istream класса Sales_item для // создания объекта, передаваемого функции same_isbn() item.same_isbn(cin); Этот код неявно преобразовывает объект cin в объект класса Sales_item. Пре- образование осуществляет та версия конструктора класса Sales_item, которая по- лучает объект класса istream. Данный конструктор создает временный объект класса Sales_item, в который и осуществляется чтение со стандартного устройст- ва ввода. Затем этот объект передается функции same_isbn (). Этот объект класса Sales_item является временным (раздел 7.3.2, стр. 273). Пока не завершится работа функции same_isbn (), доступа к этому объекту нет. Таким образом, приходится создать объект, который после использования удаляет- ся. Безусловно, такой подход почти всегда является ошибочным. Предотвращение неявных преобразований, осуществляемых конструктором Объявив конструктор с использованием ключевого слова explicit, можно пре- дотвратить его применение в контексте, требующем неявного преобразования. class Sales_item { public: // аргумент по умолчанию для book - пустая строка explicit Sales_item(const std::string &book - isbn(book), units_sold(0), revenue(O.O) { } explicit Sales_item(std::istream &is); // как прежде } ; Ключевое слово explicit используется только в объявлениях конструкторов внутри класса. В определении вне тела класса его не повторяют. // ошибка: ключевое слово explicit допустимо только для // объявлений конструкторов в заголовке класса explicit Sales_item::Sales_item(istream& is) { is >> *this; // применение оператора ввода класса Sales_item // для чтения значений переменных-членов } Теперь ни один из конструкторов не применим для неявного создания объекта класса Sales_i tem. Ни один из приведенных выше примеров теперь не сработает, item.same_isbn(null_book); // ошибка: конструктор, получающий II объект класса string, теперь объявлен // как explicit item.same_isbn(cin); // ошибка: конструктор, получающий // объект класса istream, теперь // объявлен как explicit Когда конструктор объявлен явным (т.е. с использованием ключевого слова explicit), компилятор не будет использовать его как оператор преобразования.
492 Часть III. Абстракция, классы и данные Применение конструкторов для явного преобразования Конструктор, объявленный как explicit, может быть использован для преобра- зования. string null_book = ”9-999-99999-9"; // ок: создание объекта класса Sales_item с нулевыми значениями // переменных-членов units_sold и revenue, а также значением isbn, // равным заданному для строки null_book item.same_isbn(Sales_item(null_book)); В этом коде получающий строку null_book конструктор создает объект класса Sales_item явно. Несмотря на то, что конструктор объявлен как explicit, он вполне применим. Объявление конструктора явным запрещает только неявное его применение. Для явного создания временного объекта применим любой конструктор. Обычно конструкторы с одним параметром следует объявлять явными, если для ино- го нет вполне очевидных причин (т.е. необходимо обеспечить неявное преобразова- ние). Объявление конструктора явным позволит избежать ошибок, а когда пользова- телю понадобится преобразование, он сможет создать объект явно. Упражнения раздела 12.4.4 Упражнение 12.28. Объясните, следует ли объявить явным конструктор класса Saies_item, получающий аргумент типа string. Каковы преимущества объявления конструктора явным? Ка- ковы недостатки? Упражнение 12.29. Объясните, что делает следующий код. string null_isbn = "9-999-99999-9"; Sales_item nulll(null_isbn); Sales_item null("9-999-99999-9"); Упражнение 12.30. Откомпилируйте следующий код. f(const vector<int>&); int main() { vector<int> v2; f(v2); // должно сработать f (42); // вероятно, ошибка return 0; } Что можно сказать о конструкторе вектора, исходя из ошибки во втором обращении к функции f () ? О чем бы свидетельствовал успех обращения? 12.4.5. Явная инициализация переменных-членов класса Хотя большинство объектов инициализируются запуском соответствующего конструктора, переменные-члены простых неабстрактных классов можно инициали- зировать непосредственно. Когда класс не обладает конструкторами и все его пере- менные-члены являются открытыми (public), их можно инициализировать анало- гично элементам массива. struct Data {
Глава 12. Классы 493 }; // vail.ival = 0; vail.ptr = 0 Data vail = { 0, 0 }; // val2.ival = 1024; // val2 .pkr = "Anna Livia Plurabelle” Data val2 = { 1024, "Anna Livia Plurabelle" }; Инициализирующие значения располагаются в порядке объявления соответст- вующих переменных-членов. Следующий пример содержит ошибку, поскольку пе- ременная-член ival объявлена прежде, чем ptr. // ошибка: нельзя использовать значение "Anna Livia Plurabelle" для II инициализации целочисленной переменной-члена ival Data val2 = { "Anna Livia Plurabelle", 1024 }; Эта форма инициализации унаследована от языка С и поддерживается для со- вместимости с программами С. Явная инициализация переменных-членов объекта класса имеет три существенных недостатка. 1. Все переменные-члены класса должны быть открытыми (publ iс). 2. Задача по инициализации каждой переменной-члена каждого объекта возлагает- ся на программиста. Это весьма утомительно и приводит к ошибкам, поскольку очень просто забыть переменную-член или предоставить значение несоответст- вующего типа. 3. При добавлении или удалении переменной-члена, все случаи инициализации придется найти и исправить. Применение конструкторов является наилучшим выбором практически в любой си- туации. Снабдив класс стандартным конструктором, компилятору можно позволить автоматически запускать его, что гарантирует правильную инициализацию каждого объекта класса до его первого применения. Упражнения раздела 12.4.5 Упражнение 12.31. Переменные-члены класса pair являются открытыми (public), однако приведенный ниже код не компилируется. Почему? pair<int, int> р2 = {0, 42); // почему не компилируется? 12.5. Дружественные отношения Иногда имеет смысл позволить некоторым функциям, не являющимся членами класса, обращаться к его закрытым переменным-членам, которые остаются недос- тупными извне. Перегруженные операторы, например операторы ввода и вывода, зачастую нуждаются в доступе к закрытым переменным-членам класса. По причи- нам, рассматриваемым в главе 14, “Перегрузка операторов и преобразования”, эти операторы не могут быть членами класса. Но даже не являясь членами класса, они остаются частью его интерфейса. Механизм дружественных отношений (friend mechanism) позволяет классу пре- доставить доступ к его закрытым членам, указанным функциям или классам. Объ- явление дружественных отношений начинается с ключевого слова friend. Оно мо- жет располагаться только внутри определения класса. Объявления дружественных
494 Часть III. Абстракция, классы и данные отношений могут располагаться в любом месте класса: дружественные функции не являются членами класса, поэтому раздел, в котором они объявлены, никак не влия- ет на степень их доступа. Как правило, объявления дружественных отношений имеет смысл группировать в на- чале или в конце определения класса. Пример дружественных отношений Предположим, что, кроме класса Screen, существует некий диспетчер, который управляет группой окон на экране. Вполне логично, что этому классу понадобится доступ к внутренним данным объектов класса Screen, которыми он управляет. Класс окон Screen должен позволить классу диспетчера окон Window_Mgr (от window manager) обращаться к его переменным-членам следующим образом. class Screen { // члены класса Window_Mgr смогут обращаться к закрытым II членам класса Screen friend class Window_Mgr; // ... остальное как раньше в классе Screen }; Функции-члены класса Window_Mgr смогут обращаться непосредственно к за- крытым переменным-членам класса Screen. Например, класс Window_Mgr может обладать следующей функцией перемещения окон (объектов класса Screen). Window_Mgr& Window_Mgr::relocate(Screen::index r, Screen::index c, Screen& s) { // корректное обращение к переменным height и width s.height += r; s.width += c; return *this; } Без объявления дружественных отношений этот код был бы ошибочным: ему не удалось бы воспользоваться переменными-членами height и width параметра s. Поскольку класс Screen объявил класс Window_Mgr дружественным, все члены класса Screen будут доступны для функций класса Window_Mgr. Дружественной может быть обычная функция, не являющаяся членом класса, функция-член другого класса, определенного ранее, или весь класс. Функции-члены дружественного класса получают доступ даже к закрытым переменным-членам того класса, который объявил его дружественным. Как сделать дружественной функцию-член другого класса Чтобы не объявлять весь класс Window_Mgr дружественным классу Screen, можно указать, что лишь его функции-члену relocate () будет предоставлен дос- туп к закрытым данным. class Screen { // класс Window_Mgr следует определить до класса Screen friend Window Mgr& Window_Mgr::relocate(Window_Mgr::index, Window_Mgr::index,
Глава 12. Классы 495 Screens); // ... остальное как раньше в классе Screen }; При объявлении функции-члена дружественной, следует уточнить, какому имен- но классу она принадлежит. Объявление дружественных отношений и область видимости Взаимозависимость между объявлениями дружественных отношений и опреде- лением дружественных классов или функций требует соблюдения некоего порядка их расположения, обеспечивающего правильную структуру классов. В предыдущем примере класс Window_Mgr должен был быть определен прежде объявления друже- ственных отношений. В противном случае класс Screen не смог бы найти имя функции-члена класса Window_Mgr и объявить ее дружественной. Однако сама функция relocate () не может быть определена до тех пор, пока не определен класс Screen: в конце концов, он и был объявлен дружественным, чтобы обращать- ся к членам класса Screen. Как правило, чтобы сделать функцию-член дружественной классу, содержащему необходимые данные, этот класс должен быть уже определен. С другой стороны, класс или функция не являющаяся членом класса, необязательно должны быть объ- явлены, чтобы стать дружественными. Г~—Объявление дружественных отношений представляет имя класса или функции, не являю- 1: | щейся членом класса, в окружающей области видимости. Кроме того, дружественная / функция может быть определена внутри класса. Область видимости функции экспортиру- ется в область видимости, окружающую определение класса. Представленные в дружественном классе имена классов и функций (определения или объявления) применяются так, как будто они были объявлены ранее. class X { friend class Y; friend void f() { /* вполне допустимо определить дружественную функцию в теле класса */ } Y *ymem; // ok: объявление класса Y, указанного // дружественным в классе X void g() { return ::f(); } // ok: объявление функции f(), I/ представленной в классе X }; Перегруженные функции и дружественные отношения Класс должен объявить дружественной каждую из версий перегруженных функ- ций, которым необходимо предоставить доступ к закрытым данным. // перегруженные функции storeOn () extern std::ostream& storeOn(std::ostream &, Screen &); extern BitMap& storeOn(BitMap &, Screen &); class Screen { // версия функции storeOn () для параметра типа ostream // способна обращаться к закрытым переменным-членам объектов // класса Screen friend std::ostream& storeOn(std::ostream &, Screen &) ; / / / / ... };
496 Часть III. Абстракция, классы и данные В классе Screen дружественной объявлена та версия функции storeOn (), ко- торой передают параметр типа ostream&. Версия функции storeOn () с парамет- ром типа BitMap& никаких особых прав доступа к переменным-членам класса Screen не получит. Упражнения раздела 12.5 Упражнение 12.32. Что такое дружественная функция? Что такое дружественный класс? Упражнение 12.33. Когда полезны дружественные отношения? Обсудите все преимущества и не- достатки дружественных отношений. Упражнение 12.34. Определите функцию, которая, не являясь членом класса, суммирует два объекта класса Saies_item. Упражнение 12.35. Определите функцию, которая, не являясь членом класса, читает данные из Объекта класса istream В Объект класса Sales_item. 12.6. Статические члены класса Иногда всем объектам определенного класса необходимо предоставить доступ к глобальному объекту. Возможно, в программе необходимо подсчитать количество созданных на настоящий момент объектов определенного класса, либо глобальный объект может быть указателем на функцию обработки ошибок класса или указате- лем на область в динамической памяти, предназначенную для объектов этого класса. Однако применение глобальных объектов противоречит принципам инкапсуля- ции: такие объекты предназначены для обеспечения реализации специфических аб- стракций. Значение глобального объекта может изменить обычный пользователь- ский код. Чтобы не создавать глобальный объект, доступный любому коду, в классе имеет смысл определить статическую переменную-член (static member). Обычные, нестатические, переменные-члены существуют в каждом объекте клас- са. В отличие от обычных переменных-членов, статические существует независимо от конкретных объектов класса. Каждая статическая переменная-член — это объект, связанный с классом, а не объект класса. Кроме совместно используемых статических переменных-членов, в классе можно также определить статические функции-члены. Статическая функция-член (static member function) не имеет параметра this. В отличие от нестатических, к статиче- ским членам класса можно обращаться непосредственно. Преимущества применения статических членов класса Применение статических членов класса имеет три преимущества перед глобаль- ными переменными8. 1. Имя статического члена класса находится в области видимости класса. Таким образом удается избежать конфликтов имен с членами других классов или гло- бальных объектов. 8 Напомню, что слова “объект” и “переменная” фактически являются синонимами, как и слова “класс”и “тип”. — Примеч.ред.
Глава 12. Классы 497 2. Принципы инкапсуляции не нарушаются. Статический член класса может быть закрытым, а глобальный объект — нет. 3. Читая программу, очень просто выяснить, какому классу принадлежит статиче- ский член. Это позволяет лучше понять намерения программиста. Определение статических членов Чтобы сделать член класса статическим, его объявление следует предварить клю- чевым словом static. Статические члены подчиняются обычным правилам досту- па, т.е. они могут быть открытыми или закрытыми. Давайте рассмотрим пример простого класса, представляющего банковский счет (Account). Каждый счет имеет баланс (или сумму (amount)) и владельца (owner). Каждый счет пополняется ежемесячно, но применяемая к каждому счету процент- ная ставка (rate) всегда остается той же. Этот класс можно записать следующим образом. class Account { public: // здесь расположены функции интерфейса void applyint() { amount += amount * interestRate; } static double rate() { return interestRate; } static void rate(double); // задать новую процентную ставку private: std::string owner; double amount; static double interestRate; static double initRate(); }; Каждый объект этого класса имеет две переменные-члена: owner и amount. Эти- переменные-члены не являются статическими. Здесь есть общий объект interestRate, который совместно используется всеми объектами класса Account. Использование статических членов класса К статическому члену класса можно обратиться непосредственно из класса, ис- пользуя оператор области видимости, или косвенно, при помощи объекта, ссылки или указателя на объект данного класса. Account acl; Account *ас2 = &ас1; // эквивалентные способы вызова статической функции-члена rate() double rate; rate = acl.rate(); // при помощи объекта класса Account II или ссылки на него rate = ac2->rate(); // при помощи указателя на объект II класса Account rate = Account::rate(); // непосредственно из класса, при помощи // оператора области видимости Функция-член класса может обратиться к статической переменной-члену класса как и к любой другой, без использования оператора области видимости. class Account { public: // здесь расположены функции интерфейса void applyint() { amount += amount * interestRate; } };
498 Часть III. Абстракция, классы и данные Упражнения раздела 12.6 Упражнение 12.36. Что такое статический член класса? Каковы преимущества статических чле- нов? Чем они отличаются от обычных членов класса? Упражнение 12.37. Напишите собственную версию класса Account. 12.6.1. Статические функции-члены Рассматриваемый класс Account имеет две статические функции-члена по име- ни rate, причем одна из них определена внутри класса. При определении статиче- ских членов класса вне тела класса, повторно указывать ключевое слово static не нужно, оно применяется только с объявлением внутри класса. void Account::rate(double newRate) { interestRate = newRate; } Статические функции не имеют указателя thi в Статический член класса является частью именно класса, а не отдельного объек- та. Следовательно, статическая функция-член не имеет указателя this. Явное или неявное обращение к указателю this приводит к ошибке при компиляции. Поскольку статический член класса не принадлежит отдельному объекту, стати- ческая функция-член не может быть объявлена константной. В конце концов, объ- явление функции-члена константной — это обещание не изменять объект, членом которого является функция. И наконец, статические функции-члены не могут быть также объявлены виртуальными. Более подробная информация о виртуальных функциях приведена в разделе 15.2.4 (стр. 597). Упражнения раздела 12.6.1 Упражнение 12.38. Создайте класс по имени Foo, который имеет одну переменную-член типа int. Снабдите класс конструктором, который получает значение типа int и инициализирует им переменную-член. Создайте функцию-член, которая возвращает значение этой переменной-члена. Упражнение 12.39. Используя определенный в предыдущем упражнении класс Foo, создайте другой класс, ваг, с двумя статическими переменными-членами: типа int и типа Foo. Упражнение 12.40. Используя классы из двух предыдущих упражнений, добавьте в класс ваг две статические функции-члена. Первая статическая функция, Foovai (), должна возвращать значение статического члена класса ваг типа Foo. Вторая, caiisFooVai (), должна возвра- щать количество обращений к функции Foovai (). 12.6.2. Статические переменные-члены Статические переменные-члены могут быть любого типа. Они могут быть кон- стантами, ссылками, массивами, объектами класса и т.д. Статические переменные-члены должны быть определены (только один раз) вне тела класса. В отличие от обычных переменных-членов, статические переменные- члены конструктор не инициализирует, это необходимо сделать при определении.
Глава 12. Классы 499 Наилучший способ гарантировать, что объект будет определен только один раз, — это разместить определение статических переменных-членов в том же файле, кото- рый содержит определение невстраиваемых функций-членов класса. Статические члены класса определяют точно так же, как и другие: сначала указы- вают имя типа, а затем полностью определенное имя члена класса. Например, переменную-член interestRate можно определить следующим образом. // определить и инициализировать статический член класса double Account::interestRate = initRate(); Этот оператор определяет статический объект по имени interestRate, кото- рый является членом класса Account и имеет тип double. Подобно другим членам класса, определение статического находится в области видимости того класса, где определено его имя. В результате статическую функцию-член initRate () можно использовать для инициализации переменной rate непосредственно, без уточнения класса. Обратите внимание, несмотря на то, что функция-член initRate () являет- ся закрытой, ее можно использовать для инициализации объекта interestRate. Определение переменной-члена interestRate, подобно любому другому опреде- лению, находится в области видимости класса, а следовательно, имеет доступ к за- крытым членам класса. При обращении к статическому члену класса вне тела класса, подобно любому другому Ha I члену класса, необходимо указать класс, в котором он определен. Но ключевое слово static используется только при объявлении внутри тела класса. В определении ключе- вое слово static не используется. Целочисленные статические константные переменные-члены отличаются от остальных Обычно статические члены класса, подобно обычным переменным-членам, не могут быть инициализированы в теле класса, поэтому их, как правило инициализи- руют при определении. Исключением из этого правила являются константная статическая (static const) переменная-член целочисленного типа. Она может быть инициализиро- вана внутри тела класса, если инициализирующее значение является констант- ным выражением, class Account { public: static double rate() { return interestRate; } static void rate(double); // задать новую процентную ставку private: static const int period = 30; // процент задают каждые 30 дней double daily_tbl[period]; // ok: period - константное выражение }; Константная статическая переменная-член целочисленного типа, инициализи- руемая константным значением, является константным выражением. Таким обра- зом, оно может быт использовано там, где ожидается константное выражение, на- пример, при указании количества элементов массива daily_tbl.
500 Часть III. Абстракция, классы и данные Когда константная статическая переменная-член инициализируется в теле класса, она 1 все равно должна быть определена вне определения класса. Когда инициализация осуществляется внутри класса, в определении члена класса не следует задавать исходное значение. // определение статического члена класса без инициализации; // исходное значение задано внутри определения класса const int Account::period; Статические члены не являются частью объектов класса Обычные члены класса являются частью каждого объекта данного класса. Стати- ческие члены существуют независимо от конкретного объекта, поскольку они не яв- ляются частью объекта класса. Поскольку статические переменные-члены не при- надлежат конкретному объекту, их можно применять такими способами, которые недоступны для нестатических переменных-членов. Например, типом статической переменной-члена может быть класс, которому данная переменная-член принадлежит. Нестатическая переменная-член не может быть объявлена как указатель или ссылка на объект ее класса. class Ваг { public private: static Bar meml; // ok Bar *mem2 ; / / ok9 Bar mem3; // ошибка Кроме того, статическая переменная-член применима как аргумент по умолчанию. class Screen { public: // bkground ссылается на статический член класса II объявлено позже, в определении класса Screen& clear(char = bkground); private: static const char bkground = Нестатическая переменная-член неприменима в качестве аргумента по умолча- нию потому, что ее значение не может быть использовано независимо от объекта, ча- стью которого она является. При попытке использовать нестатическую переменную- член в качестве аргумента по умолчанию произойдет ошибка, поскольку на момент получения ее значения самого объекта еще не существует. Упражнения раздела 12.6.2 Упражнение 12.41. Используя классы Foo и ваг, созданные в упражнении раздела 12.6.1 (стр. 498), инициализируйте статическую переменную-член класса Foo. Инициализируйте цело- численный член класса значением 2о, а переменную-член типа Foo нулевым значением. 9 Вероятно, тоже ошибка. — Примеч. ред.
Глава 12. Классы 501 Упражнение 12.42. Какие из следующих объявлений и определений статических переменных- членов являются ошибочными? Объясните, почему. / / exampl е. h class Example { public: static double rate = 6.5; static const int vecSize = 20; static vector<double> vec(vecSize); }; // example. C #include "example.h" double Example::rate; vector<double> Example::vec; Резюме Классы — это фундаментальной компонент языка C++. Классы позволяют определять новые, собственные типы, которые наилучшим образом приспособлены к задачам конкрет- ного приложения. Это позволяет сделать программы более компактными и простыми в мо- дификации. Классы базируются на абстракции данных (способность определять данные и функции- члены) и инкапсуляции (возможность защитить члены класса от общего доступа). Функции- члены определяют интерфейс класса. Инкапсуляцию класса обеспечивает возможность объ- явления данных и функций реализации закрытыми (private). В классе могут быть определены конструкторы — специальные функции-члены, контро- лирующие инициализацию объектов. Конструкторы могут быть перегружены. Каждый кон- структор должен инициализировать все переменные-члены. Для инициализации переменных- членов конструкторы могут использовать список инициализации — набор пар имен перемен- ных-членов и их исходных значений. Классы могут предоставлять доступ к своим закрытым членам некоторым другим классам или функциям. Для этого другой класс или функция объявляются дружественными. Классы позволяют объявлять переменные-члены изменяемыми (mutable) или статиче- скими (static). Изменяемая переменная-член никогда не становится константой — ее зна- чение может быть изменено даже внутри константной функции-члена. Статической может быть как функция, так и переменная-член. Статические члены класса существуют независимо от объектов данного класса. Термины Абстрактный тип данных (abstract data type). Структура данных, которая использует ин- капсуляцию для сокрытия ее реализации. Это позволяет программистам, использующим класс, воспринимать его абстрактно, т.е. знать, что он делает, но не заботиться о том, как именно он это делает. Для определения абстрактных типов данных в языке C++ используют- ся классы. Абстракция данных (data abstraction). Технология программирования, связанная с ин- терфейсом класса. Абстракция данных позволяет программистам игнорировать детали реали- зации класса, интересуясь лишь его возможностями. Абстракция данных является основой как объектно-ориентированного, так и общего программирования.
502 Часть III. Абстракция, классы и данные Дружественные отношения (friend). Механизм, при помощи которого класс предоставля- ет доступ к своим закрытым членам. Дружественными могут быть объявлены как классы, так и отдельные функции. Дружественные классы и функции имеют те же права доступа, что и члены самого класса. Закрытые члены класса (private member). Члены класса, определенные после маркера доступа private, доступны только членам данного класса и дружественным классам или функциям. Используемые классом переменные-члены и вспомогательные функции, которые не являются частью интерфейса класса, обычно объявляют закрытыми (private). Изменяемая переменная-член (mutable data member). Переменная-член, которая никогда не становится константой, даже когда является членом константного объекта. Значение изме- няемой переменной-члена вполне может быть изменено внутри константной функции. Инкапсуляция (encapsulation). Разделение реализации и интерфейса. Инкапсуляция скрывает детали реализации класса. В языке C++ инкапсуляция предотвращает доступ обыч- ного пользователя класса к его закрытым членам. Класс (class). Механизм языка C++, позволяющий создавать собственные абстрактные типы данных. Классы могут содержать как данные, так и функции. Класс определяет новый тип и новую область видимости. Ключевое слово class. Следующие после ключевого слова class объявления класса считаются по умолчанию закрытыми (private). Ключевое слово struct. Следующие после ключевого слова struct объявления струк- туры считаются по умолчанию открытыми (public). Конкретный класс (concrete class). Класс, который предоставляет пользователям свою реализацию. Константная функция-член (const member function). Функция-член, которая не может изменять обычные (т.е. нестатические и неизменяемые) переменные-члены объекта. Указа- тель this константного члена класса является указателем на константу. Функция-член мо- жет быть перегружена на основании того, является ли она константной или нет. Конструктор преобразования (conversion constructor). Неявный конструктор, который может быть вызван с одиночным аргументом. Конструктор преобразования используется для неявного преобразования типа аргумента в тип класса. Маркер доступа (access label). Маркеры public и private определяют, будут ли объяв- ленные далее члены класса доступны и для пользователей класса, а не только для дружест- венных классов. Каждый маркер устанавливает степень защиты от доступа для тех членов классов, которые объявлены ниже и до следующего маркера. Маркеры могут присутствовать в классе любое количество раз. Незавершенный тип (incomplete type). Тип, который уже объявлен, но еще не определен. Использовать незавершенный тип для определения члена класса или переменной нельзя. Од- нако ссылки или указатели на незавершенные типы вполне допустимы. Область видимости класса (class scope). Каждый класс определяет область видимости. Область видимости класса сложнее, чем другие области видимости, поскольку определенные внутри тела класса функции-члены могут использовать имена, которые появятся уже после определения. Объявление класса (class declaration). Класс может быть объявлен прежде, чем он будет определен. Объявление класса содержит ключевое слово class (или struct), за которым следует имя класса, сопровождаемое точкой с запятой. Класс, который объявлен но не опре- делен, называется незавершенным типом. Открытый член класса (public member). Члены класса, определенные после маркера дос- тупа public, доступны любому пользователю класса. Обычно в разделе public определяют интерфейс класса, т.е. только те функции, которые должны быть доступны пользователю. Поиск имени (name lookup). Процесс поиска объявления используемого имени.
Глава 12. Классы 503 Предварительное объявление (forward declaration). Объявление имени еще не опреде- ленного класса. Используется, как правило, для обеспечения возможности обращения к объ- явлению класса до его определения. См. незавершенный тип. Синтезируемый стандартный конструктор (synthesized default constructor). Компилятор самостоятельно создает (синтезирует) стандартный конструктор для классов, у которых не определено никаких конструкторов. Этот конструктор инициализирует переменные-члены типа класса запуская их стандартные конструкторы, а переменные-члены встроенных типов остаются неинициализированными. Список инициализации конструктора (constructor initializer list). Перечень исходных зна- чений переменных-членов класса. Инициализация переменных-членов класса значениями списка осуществляется прежде, чем выполняется тело конструктора. Переменные-члены класса, которые не указаны в списке инициализации, инициализируются неявно, при помощи их стандартного конструктора. Стандартный конструктор (default constructor). Конструктор без параметров. Статический член класса (static member). Данные или функции-члены класса, которые не являются частью конкретного объекта и совместно используются всеми объектами данного класса. Функция-член (member function). Член класса, который является функцией. Обычные функции-члены связаны с объектом класса при помощи неявного указателя this. Статиче- ские функции-члены с объектом не связаны и указателя this не имеют. Функции-члены вполне могут быть перегружены, если версии функции различаются количеством или типом параметров. Явный конструктор (explicit constructor). Конструктор с одним аргументом, который, од- нако, не может быть использован для неявного преобразования. Чтобы сделать конструктор явным, его объявление следует предварить ключевым словом explicit.

ГЛАВА 13 Управление копированием В ЭТОЙ ГЛАВЕ... 13.1. Конструктор копий 13.2. Оператор присвоения 13.3. Деструктор 13.4. Пример обработки сообщения 13.5. Работа с указателями Резюме Термины 506 512 514 517 522 532 533 Для каждого типа данных, как встроенного, так и класса, определен набор функ- ций (возможно, пустой), применимых для объекта этого типа. Два значения типа int можно сложить, можно вызвать для контейнера типа vector функцию size () и т.д. Эти функции определяют, как можно использовать объекты данного типа. Тип определяет также то, как именно происходит создание объекта. Инициали- зацию объектов класса осуществляют его конструкторы. Тип также задает способ копирования, присвоения и удаления объектов. Класс задает эти действия при по- мощи специальных функций-членов: конструктора копий, оператора присвоения и деструктора. Эти операции и рассматриваются в данной главе. При определении нового класса явно или неявно определяются действия, осуще- ствляемые при копировании, присвоении и удалении его объектов. Для этого необ- ходимо определить специальные функции-члены: конструктор копий, оператор при- своения и деструктор. Если конструктор копий или оператор присвоения не опреде- лен явно, компилятор сделает это сам. Конструктор копий (copy constructor) — это специальный конструктор с одним параметром (как правило, константным), являющимся ссылкой на объект данного класса. Конструктор копий используется явно, когда вновь создаваемый объект инициализируется копией объекта того же типа, или неявно, когда объект этого класса передается функции или возвращается ей. Деструктор (destructor) — это противоположность конструктора: он применяется автоматически, когда объект выходит из области видимости или когда объект удаляет- ся из динамически распределяемой памяти. Деструктор используется для освобож- дения ресурсов, занятых при создании объекта, а также во время его существования.
506 Часть III. Абстракция, классы и данные Независимо от того, определен ли в классе собственный деструктор, компилятор авто- матически запустит деструкторы его нестатических переменных-членов. Более подробная информация о перегрузке операторов приведена в следующей главе, а здесь рассматривается только оператор присвоения (assignment operator). Подобно конструкторам, оператор присвоения может быть перегружен, что позволя- ет задать разные типы для правого операнда. Та версия, чей правый операнд имеет тип текущего класса, является специальной: если ее нет, компилятор создаст ее самостоятельно. Все эти функции вместе (конструктор копий, оператор присвоения и деструктор) называют управлением копированием (copy control). Компилятор реализует эти функции автоматически, но в классе можно определить и их собственные версии. Управление копированием — это важнейшая часть определения любого класса C++. У программистов, плохо знакомых с языком C++, зачастую возникают затруднения при ( ) необходимости задать действия, происходящие при копировании, присвоении или уда- wJy лении объектов. Это незнание обусловлено тем, что задавать их не обязательно, ведь ^1/ма^ компилятор вполне может сделать это самостоятельно, хотя результат этих действий может быть не совсем таким, как хотелось бы. Зачастую синтезируемые компилятором функции управления копированием прекрасно работают и выполняют именно те действия, которые необходимы. Но при работе с некоторыми классами рассчитывать на стандартное поведение нецелесооб- разно и даже опасно. Как правило, при реализации функций управления копирова- нием, сложнее всего выяснить, когда именно следует переопределять стандартные версии. Одним из таких случаев, когда определение собственных функций управле- ния копированием необходимо, является ситуация, где класс обладает переменной- членом, представляющей собой указатель. 13.1. Конструктор копий Конструктор с одним параметром (как правило, константным), являющимся ссылкой на объект данного класса, называется конструктором копий. Подобно стан- дартному конструктору, конструктор копий может быть вызван компилятором не- явно. Конструктор копий используется в следующих целях: для явной или неявной инициализации одного объекта значением другого объ- екта того же класса; для копирования объекта, передаваемого функции в качестве аргумента; для копирования объекта, возвращаемого из функции; для инициализации элементов последовательного контейнера; для инициализации элементов массива из списка инициализирующих значений. Способы создания объекта Напомним, что язык C++ поддерживает две формы инициализации (раздел 2.3.3, стр. 71): прямая инициализация и инициализация копии. При инициализации копии используется знак =, а при прямой инициализации инициализирующее значение помещают в круглые скобки.
Глава 13. Управление копированием 507 При инициализации объектов класса, различие между прямой инициализацией и копированием заключается в нюансах. Прямая инициализация подразумевает непо- средственный вызов конструктора с соответствующими аргументами. Инициализа- ция копии подразумевает применение конструктора копий. Ранее инициализация копии уже была использована в конструкторе, создающем временный объект (раз- дел 7.3.2, стр. 273). Затем, чтобы скопировать временный объект в только что соз- данный, был использован конструктор копий. string string string string null_book = "9-999-99999-9"; dots(10, '.'); empty_copy = stringO; empty_direct; // инициализация копии // прямая инициализация // инициализация копии // прямая инициализация Что касается объектов классов, инициализация копии применяется только при передаче одного аргумента или при явном создании временного объекта, предназна- ченного для копирования. Когда создается объект dots, конструктор класса string получает количество символов и символ, используемый для инициализации строки dots. Создавая стро- ку null_book, компилятор сначала создает временный объект, вызвав тот из конст- рукторов класса string, который получает параметр в виде символьной строки в стиле С. Затем, для инициализации строки null_book как копии этого временного объекта, компилятор использует конструктор копий класса string. При инициализации объектов empty_copy и empty_direct происходит вызов стандартного конструктора класса string. В первом случае стандартный конструк- тор создает временный объект, который впоследствии используется конструктором копий для инициализации строки empty_copy. Во втором случае стандартный кон- структор применяется непосредственно для строки empty_direct. Язык C++ поддерживает инициализацию копий прежде всего для совместимости с языком С. Компилятор вполне способен (но необязательно должен) пропустить конструктор копий и создать объект непосредственно. Как правило, различие между прямой инициализацией и инициализацией копий заключается скорее в вопросах низкоуровневой оптимизации. Но для таких типов, которые копирования не поддерживают, или при использовании конструктора, яв- ляющегося неявным (раздел 12.4.4, стр. 491), это различие весьма существенно, ifstream filei("filename"); // ok: прямая инициализация ifstream file2 = "filename"; // ошибка: конструктор копий является // закрытым // Эта инициализация сработает в случае, если // конструктор Sales—item(const string&) не является явным Sales_item item = string("9-999-99999-9"); Инициализация объекта f ilel вполне корректна. В классе if stream определен конструктор, который может быть вызван со строкой в стиле С. Этот конструктор и используется для инициализации объекта f ilel. В эквивалентной на первый взгляд инициализации объекта f ile2 использована инициализация копии. Но это не наилучший подход. Поскольку нельзя скопировать объект класса ввода-вывода (раздел 8.1, стр. 313), использовать инициализацию ко- пий для объектов этих классов невозможно. От того, будет ли инициализация объекта item удачной, зависит версия исполь- зуемого класса Sales_item. В некоторых версиях конструктор, получающий аргу-
508 Часть III. Абстракция, классы и данные мент типа string, объявлен как explicit. Если конструктор объявлен явным, при инициализации произойдет сбой. Если конструктор не объявлен явным, инициали- зация отлично сработает. Параметры и возвращаемые значения Как известно, если тип параметра не является ссылочным (раздел 7.2.1, стр. 256), аргумент копируется. Аналогично, возвращение значения нессылочного типа (раздел 7.3.2, стр. 273) подразумевает его копирование оператором return. Когда параметр или возвращаемое значение имеет тип класса, его копию создает конструктор копий. Рассмотрим, например, функцию make_plural () со стр. 273. // для копирования возвращаемого значения используется II конструктор копий; / / параметры являются ссылками, поэтому они не копируются string make__plural (size_t, const strings, const strings); Чтобы возвратить данное слово во множественном числе, эта функция неявно использует конструктор копий класса string, а параметры, являющиеся констант- ными ссылками, он не может скопировать. Инициализация элементов контейнера Для инициализации элементов последовательного контейнера используется кон- структор копий. Например, инициализировать контейнер можно передав один па- раметр, задающий его размер (раздел 3.3.1, стр. 116). Для создания элементов кон- тейнера здесь используется и стандартный конструктор, и конструктор копий. // здесь будет использован один стандартный конструктор II класса string и пять конструкторов копий класса string vector<string> svec(5); Сначала, при инициализации вектора svec, компилятор использует стандартный конструктор класса string для создания временного значения. Затем, для копиро- вания временного значения в каждый элемент вектора svec, используется конст- руктор копий. Существует общее правило (раздел 9.1.1, стр. 335), согласно которому рекомендует- ся создавать пустой контейнер и добавлять в него элементы по мере того, как их значения становятся известны, если не предполагается использовать заданное по умолчанию исходное значение элементов контейнера. Конструкторы и элементы массива Если для элементов массива, представляющих собой объекты класса, не предос- тавлены исходные значения, для инициализации каждого элемента будет использо- ван стандартный конструктор. Но если явно предоставить исходные значения эле- ментов массива, указав их в заключенном в фигурные скобке списке (раздел 4.1.1, стр. 135), для задания значений каждого элемента будет использована инициализа- ция копий. Элемент каждого типа создается с присущим ему значением, а впослед- ствии, при копировании необходимого значения соответствующему элементу, ис- пользуется конструктор копий.
Глава 13. Управление копированием 509 Sales_item primer_eds [] { string("0-201-16487-6"), string("0-201-54848-8"), string("0-201-82470-1"), Sales_item() Значение, которое применяется при вызове для элемента конструктора с одним ар- гументом, может быть указано непосредственно, как и в случае инициализации первых трех элементов. Но если никаких аргументов не указано, как это сделано при инициа- лизации последнего элемента, следует использовать полный синтаксис конструктора. Упражнения раздела 13.1 Упражнение 13.1. Что такое конструктор копий? Когда он используется? Упражнение 13.2. Допустим, что при компиляции второго случая инициализации происходит ошибка. Что можно сказать об определении вектора? vector<int> vl(42); // ok: 42 элемента со значением 0 vector<int> v2 = 42; // ошибка: о чем свидетельствует эта ошибка? Упражнение 13.3. Предположим, что класс Point обладает открытым (public) конструктором копий. Укажите каждый случай использования конструктора копий в этом фрагменте кода. Point global; Point foo_bar(Point arg) { Point local = arg; Point *heap = new Point(global); *heap = local; Point pa[ 4 ] = { local, *heap }; return *heap; } 13.1.1 . Синтезируемый конструктор копий Если конструктор копий в классе не определен, компилятор создаст его само- стоятельно. В отличие от синтезируемого стандартного конструктора (раздел 12.4.3, стр. 488), конструктор копий создается даже тогда, когда определены другие конст- рукторы. Синтезируемый конструктор копий (synthesized copy constructor) осуще- ствляет почленную инициализацию (memberwise initialize) нового объекта в качестве копии исходного объекта. Почленная инициализация подразумевает, что компилятор последовательно копи- рует каждый нестатический член класса существующего объекта в создаваемый. Син- тезируемый конструктор копий непосредственно копирует значения членов класса, имеющих встроенный тип. Для копирования члена класса, типом которого является другой класс, применяется конструктор копий этого класса. Исключением являются элементы массива. Несмотря на то, что в обычных условиях скопировать массив нельзя, если класс имеет член, который является массивом, синтезируемый конст- руктор копий скопирует и массив. Для этого он скопирует каждый элемент массива. Упрощенная концепция почленной инициализации предполагает наличие синте- зируемого конструктора копий, в котором каждая переменная-член указана в списке инициализации конструктора. Рассмотрим, например, приведенный ниже класс Sales item, который имеет три переменные-члена.
510 Часть III. Абстракция, классы и данные class Sales_item { // другие члены как и прежде private: std::string isbn; unsigned units_sold; double revenue; } ; Синтезируемый конструктор копий класса Sales_item мог бы выглядеть сле- дующим образом. Sales_item::Sales_item(const Sales_item &orig): isbn(orig.isbn), // использует конструктор копий // класса string units_sold (orig.units_sold), // копирует orig,units_sold revenue(orig.revenue) // копия orig.revenue { } // пустое тело 13.1.2 . Определение собственного конструктора копий Конструктор копий — это конструктор, который получает один параметр, яв- ляющийся ссылкой (обычно константной) на класс. class Foo { public: Foo(); // стандартный конструктор Foo(const Foo&); // конструктор копий }; Как правило, параметр является константной ссылкой, хотя вполне можно соз- дать конструктор копий, получающий неконстантную ссылку. Поскольку конструк- тор копий (неявно) используется при передаче объектов в функции или возвраще- нии из них, его обычно не следует объявлять как explicit (раздел 12.4.4, стр. 491). Конструктор копий должен копировать члены объекта, переданного в качестве ар- гумента, в создаваемый объект. В большинстве случаев синтезируемый конструктор копий осуществляет именно те действия, которые необходимы. Объекты классов, члены которых имеют встроен- ный тип или тип класса (но не тип указателя), зачастую вполне могут быть скопиро- ваны без явного определения конструктора копий. Но некоторые классы должны контролировать то, что происходит при копирова- нии объектов. Такие классы зачастую имеют переменную-член, которая является указателем или представляет другой ресурс, создаваемый в конструкторе. Другие классы требуют, чтобы при создании каждого нового объекта были выполнены неко- торые действия. И в обоих этих случаях следует определить конструктор копий. Зачастую самой трудной задачей при определении конструктора копий является собственно осознание того, что он необходим. После этого конструктор копий опреде- ляется подобно любому другому конструктору: он имеет то же имя, что и у класса, не имеет возвращаемого значения, но может (и должен) использовать список инициали- зации конструктора, чтобы инициализировать члены вновь создаваемого объекта, а также осуществлять внутри тела функции любые другие необходимые действия.
Глава 13. Управление копированием 511 В последующих разделах будут приведены примеры классов, в которых конст- рукторы копий необходимы. В разделе 13.4 (стр. 517) рассматриваются два класса, которым явный конструктор копий необходим для выполнения действий, связанных с работой простого приложения обработки сообщений. Классы, члены которых яв- ляются указателями, рассматриваются в разделе 13.5 (стр. 522). Упражнения раздела 13.1.2 Scanned Ц Digrol Упражнение 13.4. Используя приведенный ниже набросок класса, напишите конструктор ко- пий, который копирует все его элементы. Скопируйте объект, на который указывает указатель pstring, а не сам указатель. struct NoName { NoName() : pstring(new std::string), i(0), d(0) { } private: std::string *pstring; • • int i; double d; }; Упражнение 13.5. Какое из определений классов, скорее всего, нуждается в конструкторе копий? (а) Класс Point3w, содержащий четыре переменные-члена типа float. (b) Класс Matrix, фактическая матрица которого создается в динамической памяти внутри кон- структора, а удаляется внутри деструктора. (с) Класс Payroll, каждый объект которого обладает уникальным идентификатором. (d) Класс word, содержащий строку, вектор строк и пару с координатами. Упражнение 13.6. Параметр конструктора копий необязательно должен быть константой, но он должен быть ссылкой. Объясните смысл этого ограничения. Объясните, почему, например, сле- дующее определение неработоспособно. Sales__item: : Sales_item (const Sales_item rhs); 13.1.3 . Предотвращение копирования В некоторых классах для того, чтобы они вообще могли существовать, необходи- мо предотвратить копирование. Например, такие классы, как iostream, не допус- кают копирования (раздел 8.1, стр. 313). Казалось бы, чтобы запретить копирование, можно было бы просто не создавать конструктор копий. Но если.не определить кон- структор копий, компилятор создаст его самостоятельно. Чтобы предотвратить копирование, конструктор копий следует явно объявить закрытым З&ь i (private). Когда конструктор копий закрыт, пользовательскому коду не будет позволено копи- ровать объекты данного класса. Компилятор отклонит любую попытку сделать копию. Однако дружественные функции и члены самого класса вполне смогут сделать копию. Если необходимо запретить копирование даже для друзей и внутренних чле- нов класса, конструктор копий следует объявить (как private), но при этом не оп- ределять его.
512 Часть III. Абстракция, классы и данные Функцию-член вполне можно объявить, но не определять. Однако любая попытка применения неопределенного члена класса приведет к отказу во время компоновки. Объявив (но не определив) закрытый конструктор копий, можно предотвратить лю- бую попытку скопировать объект класса. Независимо от того, попытается ли сделать копию пользовательский код, функция-член класса или дружественная функция, во время компоновки произойдет отказ и такой код будет помечен как ошибочный. Большинству классов необходим как конструктор копий, так и стандартный конструктор Классы, у которых нет стандартного конструктора и/или конструктора копий, имеют серьезные ограничения в применении. Объекты классов, не допускающих ко- пирование, могут быть переданы функции (и возвращены из нее) только по ссылке. Кроме того, они не могут быть использованы как элементы контейнера. Как правило, стандартный конструктор и конструктор копий имеет смысл все же оп- ~ ределить (явно или неявно). Компилятор создает синтезируемый стандартный кон- структор только тогда, когда нет никаких других конструкторов. Следовательно, если * определен конструктор копий, стандартный конструктор придется тоже определять. 13.2. Оператор присвоения Класс способен управлять не только инициализацией объектов, он может также определять действия, осуществляемые при присвоении объектов. Sales_item trans, accum; trans = accum; Подобно конструкторам копий, компилятор самостоятельно синтезирует опера- тор присвоения, если в классе не определен его собственный оператор. Перегрузка оператора присвоения Прежде чем приступить к изучению синтезируемого оператора присвоения, не- обходимо сказать несколько слов о перегруженных операторах (overloaded operator), подробно рассматриваемых в главе 14, “Перегрузка операторов и преобразования”. Перегруженные операторы — это функции, которые имеют имя operator, за ко- торым следует символ определяемого оператора. Следовательно, чтобы переопреде- лить оператор присвоения, необходимо определить функцию по имени operators Подобно любой другой функции, функция-оператор имеет тип возвращаемого зна- чения и список параметров. Количество параметров в списке параметров (включая неявный параметр this, если оператор является членом класса) должно совпадать с количеством операндов оператора. Оператор присвоения является бинарным, по- этому его функция имеет два параметра: первый параметр соответствует левому операнду, а второй — правому. Большинство операторов могут быть определены двояко: в качестве функции не члена класса или в качестве функции члена класса. Когда оператор является функ- цией-членом, ее первым операндом будет неявный указатель this. Некоторые опе- раторы, включая оператор присвоения, должны быть членами класса, для которого оператор определен. Поскольку оператор присвоения должен быть членом класса,
Глава 13. Управление копированием 513 его параметр this уже связан с указателем на левый операнд. Следовательно, опе- ратор присвоения получает единственный параметр, который является объектом то- го же класса. Обычно правый операнд передается как константная ссылка. Тип возвращаемого оператором присвоения значения должен быть, как и у операторов присвоения встроенных типов (раздел 5.4.1, стр. 184), ссылкой на его левый операнд. Следовательно, оператор присвоения возвращает ссылку на собст- венный класс. Например, оператор присвоения класса Sales_item мог бы быть объявлен сле- дующим образом. class Sales_item { public: // другие члены как и прежде II эквивалент синтезируемого оператора присвоения Sales_item& operators(const Sales_item &); }; Синтезируемый оператор присвоения Синтезируемый оператор присвоения (synthesized assignment operator) работает аналогично синтезируемому конструктору копий. Он выполняет почленное присвое- ние (memberwise assignment): каждый член класса правого объекта будет присвоен соответствующему члену левого. За исключением массивов, присвоение значений каждого члена класса происходит обычным для его типа способом. У массивов каж- дый элемент присваивается отдельно. Например, синтезируемый оператор присвоения класса Sales_item выглядел бы примерно следующим образом. // эквивалент синтезируемого оператора присвоения Sales_item& Sales_item::operator=(const Sales_item &rhs) { isbn = rhs.isbn; // вызов string::operator= units_sold = rhs.units_sold; // применение оператора присвоения // встроенного типа int revenue = rhs.revenue; // применение оператора присвоения // встроенного типа double return *this; Синтезируемый оператор присвоения по очереди присваивает значение каждой переменной-члена объекта, используя соответствующий оператор присвоения встроенного типа или класса. Оператор возвращает указатель this на объект, яв- ляющийся левым операндом. Взаимосвязь копирования и присвоения Классы, использующие синтезируемый конструктор копий, обычно используют и синтезируемый оператор присвоения. В рассматриваемом классе Sales_item не нужен ни собственный конструктор копий, ни оператор присвоения: прекрасно сра- ботают и их синтезируемые версии. Но в классе вполне может быть определен его собственный оператор присвоения. Как правило, если класс нуждается в конструкторе копий, ему потребуется и опера- тор присвоения.
514 Часть III. Абстракция, классы и данные Фактически, операции копирования и присвоения следует рассматривать в неразрывной 1 взаимосвязи. Если необходима одна из них, то почти наверняка понадобится и другая. Примеры классов, нуждающихся в определении собственных операторов при- своения, приведены в разделах 13.4 (стр. 517) и 13.5 (стр. 522). Упражнения раздела 13.2 Упражнение 13.7. Когда в классе следует определять оператор присвоения? Упражнение 13.8. Укажите, нуждались ли классы, перечисленные в упражнении раздела 13.1.2 (стр. 511), в операторе присвоения. Упражнение 13.9. Первое упражнение в разделе 13.1.2 (стр. 511) содержит определение класса NoName. Укажите, нуждается ли этот класс в операторе присвоения. Если да, реализуйте его. Упражнение 13.10. Определите класс Employee, который содержит имя служащего и его уни- кальный идентификатор. Снабдите класс стандартным конструктором и конструктором, получаю- щим строку, которая представляет имя служащего. Если класс нуждается в конструкторе копий или операторе присвоения, реализуйте и эти функции. 13.3. Деструктор Одна из задач конструктора заключается в автоматическом резервировании ре- сурсов. Например, конструктор может создать буфер или открыть файл. Зарезерви- рованный в конструкторе ресурс необходимо соответствующим образом автомати- чески освободить. Деструктор (destructor) — это специальная функция-член, при- меняемая для осуществления всех действий, необходимых для освобождения занятых ресурсов. Она является противоположностью конструктора класса. Когда происходит вызов деструктора Вызов деструктора происходит автоматически, при каждом удалении объекта класса. // р указывает на объект, созданный стандартным конструктором Sales_item *р = new Sales_item; { // новая область видимости Sales_item item(*p); // конструктор копий копирует *р в item delete р; // вызов деструктора для объекта, на II который указывает указатель р // выход из локальной области видимости; !/ вызов деструктора для объекта item Переменные, такие как item, удаляются автоматически, когда они выходят из области видимости. Следовательно, деструктор для объекта item выполняется в момент, когда встречается закрывающая фигурная скобка. Объект, который создается в динамической памяти, удаляется только тогда, когда удаляется последний указывающий на него указатель. Если не удалить (при помощи оператора delete) указатель на объект в динамической памяти, деструктор не бу- дет вызван для этого объекта. Объект сохранится навсегда1, что приведет к утечке 1 До перезагрузки компьютера. — Примеч. ред.
Глава 13. Управление копированием 515 памяти (memory leak). Кроме того, все используемые внутри объекта ресурсы также не будут освобождены. Вызов деструктора не происходит, когда ссылка или указатель на объект выходят из об- j ласти видимости. Деструктор выполняется только тогда, когда удаляется указатель на объект, расположенный в динамически распределяемой памяти, или когда из области видимости выходит реальный объект (а не ссылка на объект). Когда контейнер удаляется, деструкторы выполняются также для его элементов, имеющих тип класса, будь то библиотечный контейнер или встроенный массив. Sales_item *р = new Sales_item[10]; // создание в динамической // памяти vector<Sales_item> vec(p, р + 10); // локальный объект delete [] р; // удаление массива; деструктор выполняется // для каждого элемента // вектор vec выходит из области видимости; /! деструктор выполняется для каждого элемента Элементы контейнера всегда удаляются в обратном порядке: сначала элемент с индексом size () - 1, затем с индексом size () - 2 и так до элемента [0], кото- рый удаляется последним. Когда необходим явный деструктор Для большинства классов явный деструктор не нужен. В частности, класс обла- дающий конструктором, необязательно должен иметь собственное определение де- структора. Деструкторы необходимы только тогда, когда для них есть работа. Обыч- но они используются для освобождения ресурсов, которые были зарезервированы в конструкторе или появились за время существования объекта. Существует весьма полезное эмпирическое правило: если класс нуждается в дест- рукторе, потребуются также оператор присвоения и конструктор копий. Это правило зачастую называют "правилом трех” (Rule of Three), поскольку когда необходим дест- руктор, необходимы все три элемента управления копированием. Задача деструктора не ограничена только освобождением ресурсов. На самом де- ле деструктор способен выполнять любые действия, которые разработчик класса желает выполнить в последний момент существования объекта этого класса. Синтезируемый деструктор В отличие от конструктора копий и оператора присвоения, компилятор всегда создает деструктор. Синтезируемый деструктор удаляет каждый нестатический член класса, причем в порядке, обратном их созданию. Следовательно, удаление перемен- ных-членов осуществляется в порядке, обратном их объявлению в классе. Для каж- дой переменной-члена, имеющей тип класса, синтезируемый деструктор вызывает деструктор ее класса. Попытка удаления переменной-члена встроенного или составного типа никаких последст- вий не имеет. В частности, синтезируемый деструктор не удаляет объект, на который указывает переменная-член, являющаяся указателем.
516 Часть III. Абстракция, классы и данные Как создать деструктор Рассматриваемый класс Sales_item— это хороший пример класса, который не резервирует никаких ресурсов, а следовательно, не нуждается в собственном дест- рукторе. Но в резервирующих ресурсы классах, как правило, следует определить де- структор, чтобы освободить эти ресурсы. Деструктор — это функция-член, имя ко- торой совпадает с именем класса, но предваряется символом тильды (~). Она не имеет возвращаемого значения и не получает никаких аргументов. Поскольку дест- руктор не имеет параметров, его нельзя перегрузить. Таким образом, хотя в классе можно определить несколько конструкторов, деструктор может быть только один и он применяется ко всем объектам данного класса. Важное различие между деструктором и конструктором копий (или оператором присвоения) заключается в том, что даже при наличии собственного деструктора, син- тезируемый деструктор все равно выполняется. Например, для класса Sales_item можно создать следующий пустой деструктор. class Sales_item { public: // пусто; здесь нет никаких действий по удалению II переменных-членов, это осуществляется автоматически ~Sales_item() { } // другие члены как и прежде } При удалении объектов класса Sales_item, этот деструктор ничего не делает. После того как он завершает работу, выполняется синтезируемый деструктор, кото- рый и удаляет все переменные-члены класса. Синтезируемый деструктор удаляет переменную-член типа string, вызывая деструктор класса string, который осво- бождает область памяти, используемой для хранения переменной isbn. Перемен- ные-члены units_sold и revenue имеют встроенный тип, поэтому для их удале- ния синтезируемый деструктор ничего не делает. Упражнения раздела 13.3 Упражнение 13.11. Что такое деструктор? Что делает синтезируемый деструктор? Когда создает- ся синтезируемый деструктор? Когда в классе необходимо определить собственный деструктор? Упражнение 13.12. Укажите, нуждается ли класс NoName, упомянутый в упражнении разде- ла 13.1.2 (стр. 511), в собственном деструкторе. Если да, реализуйте его. Упражнение 13.13. Укажите, нуждается ли в деструкторе класс Employee, определенный в уп- ражнениях на стр. 514. Если да, реализуйте его. Упражнение 13.14. Хороший способ изучения управления копированием и конструкторов заклю- чается в определении простого класса, каждая из рассматриваемых функций-членов которого отображает на экране свое имя. struct Exmpl { Exmpl() { std::cout << "Exmpl()" << std::endl; } Exmpl(const Exmpl&) { std::cout << "Exmpl(const Exmpl&)" << std::endl; } }; Напишите класс, подобный классу Exmpl, снабдив его функциями-членами для управления копи- рованием и другими конструкторами. Затем напишите программы, использующие объекты класса, подобного Exmpl, различными способами: с передачей при создании нессылочных и ссылоч-
Глава 13. Управление копированием 517 ных параметров, с созданием в динамически распределяемой памяти, с размещением в кон- тейнере и т.д. Изучение того, когда и какие из конструкторов и функций-членов управления копи- рованием используются, поможет лучше понять их концепцию. Упражнение 13.15. Сколько вызовов деструктора выполняется в следующем фрагменте кода? void fen(const Sales_item *trans, Sales_item accum) { Sales_item iteml(*trans), item2(accum); if (!iteml.same_isbn(item2)) return; if (iteml.avg_price() <= 99) return; else if (item2.avg_price() <= 99) return; } 13.4. Пример обработки сообщения В качестве примера рассмотрим класс, который должен управлять копированием и осуществлять некоторые вспомогательные действия, необходимые для двух клас- сов, которые могли бы быть использованы в приложении обработки сообщений. Это классы Message и Folder, представляющие, соответственно, сообщение электрон- ной почты (или другое) и папки, в которых это сообщение могло бы присутствовать. Предположим, что сообщение может присутствовать в нескольких папках. Класс Message будет обладать функциями-членами save () и remove (), которые сохра- няют или удаляют данное сообщение из указанной папки. Чтобы не помещать отдельную копию сообщения в каждую папку, объект класса Message будет содержать набор (set) указателей на объекты класса Folder (папки), в которых присутствует сообщение. Каждый объект класса Folder также будет хранить указатели на объекты класса Message, т.е. сообщения, которые папка содержит. Реализуемая структура данных представлена на рис. 13.1. set<Folder*> folders Рис. 13.1. Конструкция классов Message и Folder При создании нового объекта класса Message, определяется содержимое сообще- ния, но не папка. Чтобы поместить сообщение в папку, применяется функция save (). При копировании объекта класса Message осуществляется копирование как со- держимого исходного сообщения, так и набора указателей на объект класса Folder. Необходимо также добавить указатель на этот объект класса Message в каждый из объектов класса Folder, который указан в исходном объекте класса Message. Присвоение одного объекта класса Message другим происходит аналогично ко- пированию: после присвоения оба объекта сообщения будут иметь одинаковое со- держимое и одинаковый набор папок. Сначала необходимо удалить существующее сообщение (левый операнд) из всех папок, в которых он был указан до присвоения.
518 Часть III. Абстракция, классы и данные Как только старое сообщение будет удалено, содержимое и набор папок из правого операнда следует скопировать в левый. Указатель на левый операнд класса Message необходимо также добавить в каждую папку, указанную в наборе. При удалении объекта класса Message, необходимо модифицировать каждый объект класса Folder, указатель на который он содержит. Когда сообщение оказы- вается удалено, указатели на него больше не имеют смысла, поэтому их следует уда- лить из каждого набора указателей объектов класса Folder, перечисленных в набо- ре удаленного сообщения. Рассмотрев этот список операций, можно придти к выводу, что деструктор и опе- ратор присвоения совместно используют действия по удалению сообщения из спи- ска папок, содержащих его. Аналогично, конструктор копий и оператор присвоения совместно используют действия по добавлению сообщения в список папок. Для ре- шения этих задач определим две закрытые вспомогательные функции. Класс Message С учетом анализа приведенного выше проекта, класс Message можно предвари- тельно реализовать следующим образом. class Message { public: // папки автоматически инициализируются пустым набором Message(const std::string &str = contents (str) { } // управление копированием: необходимость манипулировать // указателями на данный объект класса Message в объектах // класса Folder, указатели на которые он содержит Message(const Message&); Message& operator=(const Message&); -Message(); // добавить/удалить данное сообщение из набора папок, // определенных в объекте сообщения void save(Folder&); void remove(Folder&) ; private: std::string contents; // фактический текст сообщения std::set<Folder*> folders; // папки, содержащие данное I / сообщение // Вспомогательные функции, используемые конструктором копий, / / оператором присвоения и деструктором: // добавляет это сообщение в папку, которая указана параметром void put_Msg_in_Folders(const std::set<Folder*>&); // удаляет данное сообщение из каждой папки void remove_Msg_from_Folders(); }; В классе определены две переменные-члена: contents, которая является строкой, содержащей фактическое содержимое сообщения, и folders — контейнер типа set (набор), содержащий указатели на папки, в которых присутствует данное сообщение. Конструктор получает один параметр типа string, представляющий собой со- держимое сообщения. Конструктор сохраняет копию сообщения в переменной- члене contents и (неявно) инициализирует контейнер типа set объекта класса Folder как пустой набор. Этот конструктор обладает аргументом по умолчанию, которым является пустая строка. Следовательно, этот конструктор считается также стандартным конструктором класса Message.
Глава 13. Управление копированием 519 Вспомогательные функции реализуют действия, совместно используемые функ- циями-членами управления копированием. Функция put_Msg_in_Folders () до- бавляет копию ее сообщения в папки, которые содержат указатель на данное сооб- щение. После завершения работы этой функции, каждая указанная параметром пап- ка будет содержать указатель и на это сообщение. Данная функция будет использована как конструктором копий, так и оператором присвоения. Функцию remove_Msg_f rom_Folders () использует деструктор и оператор присвоения. Она удаляет указатель на данное сообщение из каждой папки, указатель на которую содержит переменная-член folders. Управление копированием класса Message При копировании объекта класса Message, в каждую содержащую его папку не- обходимо добавить вновь созданное сообщение. Этих действий синтезируемый кон- структор выполнить не сможет, поэтому придется определить собственный конст- руктор копий. Message::Message(const Message &m): contents(m.contents), folders(m.folders) { // добавить данное сообщение в каждую папку, // на которую указывает m put_Msg_in_Folders(folders); Конструктор копий инициализирует переменные-члены нового, объекта копиями значений переменных-членов исходного. Кроме инициализации обычных перемен- ных-членов (с этим справился бы и синтезируемый конструктор копий), необходимо также перебрать набор folders и добавить указатель на данный объект класса Message в набор указателей каждого перечисленного в нем объекта класса Folder. Конструктор копий использует функцию put_Msg_in_Folder () для этих же целей. При создании собственного конструктора копий, необходимо явно скопировать значения всех переменных-членов. Явно определенный конструктор копий ничего не копирует ав- томатически. Подобно любым другим конструкторам, если переменная-член типа класса не инициализирована явно, для ее инициализации используется стандартный конст- руктор. В конструкторе копий, для инициализации переменных-членов, стандарт- ный конструктор копий классов переменных-членов не используется. Функция-член put_Msg_in_Folders () Функция-член put_Msg_in_Folders () перебирает указатели переменной- члена folders параметра rhs. Она содержит указатели на все объекты класса Folder, относящиеся к rhs. Указатель на данный объект класса Message необхо- димо добавлять в каждую из этих папок. Для этого функция перебирает в цикле набор rhs. folders, обращаясь к функ- ции-члену класса Folder по имени addMsg (). Эта функция осуществляет все дей- ствия, необходимые для добавления указателя на объект класса Message в набор объекта класса Folder.
520 Часть III. Абстракция, классы и данные // добавить это сообщение в папки, на которые указывает rhs void Message::put_Msg_in_Folders(const set<Folder*> &rhs) { for(std::set<Folder*>::const—iterator beg = rhs.begin(); beg != rhs.end(); ++beg) (*beg)->addMsg(this); // *beg указывает на Folder Единственной сложной частью этой функции является обращение к функции addMsg(). (*beg)->addMsg(this); // *beg указывает на Folder Все начинается с обращения к значению итератора (*beg), которое возвращает указатель на объект класса Folder. Затем к указателю на объект класса Folder применяется оператор стрелки, позволяющий выполнить его функцию-член addMsg (). Ей передается указатель this на объект класса Message, который необ- ходимо добавлять в набор объекта класса Folder. Оператор присвоения класса Message Оператор присвоения несколько сложнее, чем конструктор копий. Подобно кон- структору копий, он должен присвоить содержимое (переменная-член contents) и модифицировать набор folders так, чтобы они соответствовали правому операнду. Он должен также добавить это сообщение в каждую из папок, на которую указывает rhs. В этой части присвоения можно использовать функцию put_Msg_in_Folders (). Прежде чем приступить к копированию данного сообщения, указатели на него сле- дует удалить из каждого набора объектов класса Folder, содержащего его в настоящее время. Для этого понадобится перебрать набор folders объекта класса Message, найти каждый объект класса Folder, содержащий указатель на него, и удалить этот указатель. Эти действия выполнит функция remove_Msg_f rom_Folders (). С учетом того, что все необходимые действия выполнят функции remove_ Msg_f rom_Folders () и put_Msg_in_Folders (), собственно оператор присвое- ния довольно прост. Message& Message::operator=(const Message &rhs) { if (&rhs != this) { remove_Msg_from_Folders(); // модифицировать существующие contents folders // папки contents = rhs.contents; // скопировать содержимое из rhs folders = rhs.folders; // скопировать указатели папок // из rhs // добавить данное сообщение в каждую папку rhs put_Msg_in_Folders(rhs.folders) ; return *this; Оператор присвоения начинается с проверки того, не являются ли левый и пра- вый операнды одним и тем же объектом. Причины этой проверки станут очевидны при рассмотрении остальной части функции. Предположим, что операнды являются разными объектами. В этом случае происходит вызов функции remove_Msg_f rom_ Folders (), удаляющей данное сообщение из набора folders каждого объекта класса Folder. Как только это будет сделано, происходит присвоение значений
Глава 13. Управление копированием 521 переменных-членов contents и folders правого операнда соответствующим пе- ременным-членам текущего объекта. И наконец, происходит вызов функции put_Msg_ in_Folders (), добавляющей указатель на это сообщение в каждый объект класса Folder, указанный в наборе rhs . folders. Теперь, когда стали вполне понятны действия, осуществляемые функцией remove_ Msg_f rom_Folders (), стало очевидно, почему оператор присвоения начинается с проверки объектов на совпадение. Присвоение подразумевает предварительное удаление значений переменных-членов левого операнда и последующее при- своение им значений соответствующих переменных-членов правого. Если объект является тем же самым, удаление переменных-членов левого операнда уничтожит и значения правого! Крайне важно обеспечить корректную работу оператора присвоения даже в том случае, когда объект по ошибке присваивается сам себе. Обычно для этого достаточно прове- рить, не происходит ли попытка самоприсвоения. Функция-член remove_Msg_from_Folders О Реализация функции remove_Msg_from_Folders () во всем подобна реализа- ции функции put_Msg_in_Folders (), за исключением того, что здесь происходит вызов функции remMsg (), удаляющей данное сообщение из каждой папки, указан- ной в наборе folders. // удалить данное сообщение из соответствующих папок void Message::remove_Msg_from_Folders() { // удалить данное сообщение из соответствующих папок for(std::set<Folder*>::const_iterator beg = folders.begin(); beg != folders.end(); ++beg) (*beg)->remMsg(this); // *beg указывает на Fol // *beg указывает на Folder } Деструктор класса Message Последняя функция управления копированием, которую осталось реализовать, — это деструктор. Message::-Message() { remove_Msg_from_Folders(); При наличии функции remove_Msg_f rom_Folders (), создать деструктор во- все не сложно, поскольку она позволяет очистить набор folders. Система автома- тически вызывает деструктор класса string, чтобы освободить переменную-член contents, и деструктор контейнера set, чтобы освободить память, зарезервирован- ную для переменной-члена folders. Таким образом, единственная задача деструкто- ра класса Message заключается в вызове функции remove_Msg_f rom_Folders (). Зачастую оператору присвоения необходимы те же действия, что и конструктору ко- пий или деструктору. В таких случаях эти действия имеет смысл поместить в закры- (ем тые вспомогательные функции.
522 Часть III. Абстракция, классы и данные Упражнения раздела 13.4 Упражнение 13.16. Создайте класс Message, описанный в этом разделе. Упражнение 13.17. Добавьте в класс Message функции аналогичные функциям addMsgO и remMsg () класса Folder. Эти функции МОГЛИ бы иметь имена addFldr И remFldr. Они должны получать указатель на объект класса Folder и добавлять (удалять) его в набор указате- лей folders. Эти функции можно сделать закрытыми (private), поскольку использовать их предполагается только внутри реализации класса Folder. Упражнение 13.18. Создайте соответствующий класс Folder. Этот класс должен обладать на- бором типа set<Message*>, элементы которого являются указателями на объекты класса Message. Упражнение 13.19. Добавьте в классы Message и Folder функции-члены save() и remove (). Эти функции должны получать объект класса Folder и добавлять (или удалять) ука- затель на него в (из) наборы объектов класса Folder, содержащие указатель на данный объект класса Message. Операция должна также модифицировать объект класса Folder таким обра- зом, чтобы он обладал указателем на данный объект класса Message. Для этого можно восполь- зоваться функциями addMsg () или remMsg (). 13.5. Работа с указателями В этой книге постоянно рекомендуется использовать стандартную библиотеку. Одной из причин является то, что применение стандартной библиотеки в современ- ных программах на языке C++ значительно уменьшает потребность в указателях. Однако существует еще достаточно много приложений, которые все еще требуют применения указателей, особенно в реализации классов. Классы, которые содержат указатели, требуют осторожного обращения при управлении копированием. Дело в том, что при копировании указателя фактически копируется лишь содержащийся в нем адрес, а сам объект, на который он указывает, нет. Одним из первых решений, принимаемых разработчиком при проектировании класса, обладающего членом-указателем, является способ его применения. После копи- рования одного указателя в другой, оба указателя указывают на тот же объект. В этом случае оба указателя можно использовать для изменения того же объекта. В результа- те при помощи одного из указателей вполне можно удалить основной объект, причем второй указатель все еще будет указывать на уже не существующий объект. По умолчанию переменная-член, которая является указателем, ведет себя анало- гично объекту указателя. Однако функции управления копированием могут реали- зовать разные стратегии копирования членов классов, представляющих собой указа- тели. В большинстве классов языка C++ используется один из трех следующих под- ходов в работе с членами-указателями. 1. Указатель-член класса может быть присвоен подобно обычному указателю. Та- кие классы не требуют никаких специальных приемов управления копировани- ем, но имеют все недостатки, связанные с указателями. 2. Класс может реализовать так называемый “интеллектуальный указатель" (smart pointer). Объект, на который указывает такой указатель, может быть использо- ван совместно, но потеря указателей в классе предотвращается.
Глава 13. Управление копированием 523 3. Класс может иметь поведение, подобное значению (valuelike behavior). Объект, на который указывает указатель, окажется индивидуальным, и управлять каж- дый объект класса будет собственным экземпляром. В этом разделе рассматриваются три класса, которые реализуют каждый из этих подходов управления указателями-членами. Простой класс с указателем-членом Чтобы продемонстрировать проблемы, возникающие при реализации просто- го класса, рассмотрим пример, в котором содержится указатель на переменную типа int. // класс, членом которого является указатель, который ведет себя // как обычный указатель class HasPtr { public: // копирование переданного значения HasPtr(int *р, int i): ptr(p), val(i) { } // константные функции-члены, возвращающие значение / / указанной переменной-члена int *get_ptr() const { return ptr; } int get_int() const { return val; } // неконстантные функции-члены, изменяющие II указанную переменную-член void set_ptr(int *р) { ptr = р; } void set_int(int i) { val = i; } // возвращает или изменяет значение, на которое указывает // указатель; допустимо для константных объектов int get_ptr_val() const { return *ptr; } Конструктор HasPtr () имеет два параметра, значения которых он копирует в переменные-члены объекта класса HasPtr. Класс предоставляет простые констант- ные функции доступа: функция get_int () возвращают значение переменной чле- на типа int, а функция get_ptr (), — соответственно, указатель. Функции-члены set_int () и set_ptr () позволяют изменять значения этих переменных-членов, т.е. присвоить новое значение переменной-члену типа int, а также присвоить указа- телю адрес другого объекта. Здесь также определены функции-члены get_ptr_ val () и set_ptr_val (), которые возвращают и, соответственно, устанавливают значение объекта, на который указывает указатель. Стандартный конструктор копий, оператор присвоения и указатели-члены Поскольку в классе не определен собственный конструктор копий, при копиро- вании одного объекта класса HasPtr в другой, копируются обе переменные-члена. int obj = 0; HasPtr ptrl(&obj, 42); // int* - указатель на obj, val - 42 HasPtr ptr2(ptrl); // int* - указатель на obj, val - 42
524 Часть III. Абстракция, классы и данные После копирования, указатели объектов ptrl и ptr2 содержат адрес того же объекта, а их переменные-члены типа int — одинаковые значения. Но поведение каждого из этих двух членов класса совершенно разное, поскольку значение указа- теля отличается от значения объекта, на который он указывает. Хотя после копиро- вания значения переменных-членов типа int и одинаковы, но они вместе с тем не- зависимы, чего нельзя сказать об указателях, ведь указывают они на один объект. Классы, обладающие переменной-членом в виде указателя и использующие стандарт- ные, синтезируемые компилятором функции управления копированием, обладают всеми недостатками, присущими обычным указателям. В частности, такой класс не способен избежать создания потерянных указателей. Указатели совместно используют один объект После копирования арифметического значения, полученная копия никак не за- висит от оригинала и ее изменение никак не повлияет на оригинал. ptrl.set_int(0); ptr2.get_int(); ptrl.get_int(); // изменяет val только в ptrl // возвращает 42 // возвращает О При копировании указателя, адреса в разных объектах будут принадлежать то- му же оригинальному объекту. Если происходит вызов функции set_ptr_val () любого из объектов, будет изменен оригинальный объект, который используется ими обоими. ptrl.set_ptr_val(42); ptr2.get_ptr_val(); // изменяет значение объекта, указатели на // который содержат оба объекта, ptrl и ptr2 // возвращает 42 Когда два указателя указывают на тот же объект, любой из них можно использо- вать для изменения значения совместно используемого объекта. Потерянные указатели Поскольку данный класс копирует сами указатели, он создает для пользователя потенциальную проблему: объект класса HasPtr хранит указатель, который ему был присвоен. То есть пользователь должен гарантировать, что объект, указатель на ко- торый содержит объект класса HasPtr, не будет удален на протяжении всего време- ни его существования. int *ip = new int(42); // создание в динамически распределяемой / / памяти переменной типа int и ее // инициализация значением 42 HasPtr ptr(ip, 10); // HasPtr указывает на тот же объект, / / что и ip delete ip; // объект, на который указывает ip, удален ptr.set_ptr_val(0); // катастрофа: объект, на который // указывает HasPtr, не существует! Проблема здесь в том, что указатель ip и указатель внутри объекта ptr указы- вают на тот же объект. Когда этот объект удаляется, указатель внутри объекта класса HasPtr остается, но указывает уже на недопустимый объект. Однако нет никакого способа узнать, допустим ли еще объект или уже нет.
Глава 13. Управление копированием 525 Упражнения раздела 13.5 Упражнение 13.20. С учетом того, что здесь используется первоначальная версия класса HasPtr, которая базируется на стандартных функциях управления копированием, опишите, что происходит в следующем коде, int i - 42; HasPtr pl(&i, 42); HasPtr p2 = pl; cout << p2.get_ptr_val() << endl; pl.set_ptr_val(0) ; cout « p2.get_ptr_val() « endl; Упражнение 13.21. Что произойдет в случае, если снабдить класс HasPtr деструктором, кото- рый удаляет его член-указатель? 13.5.1. Определение классов интеллектуальных указателей В предыдущем разделе был создан простой класс, который содержал указатель и переменную типа int. Переменная-член класса, которая представляла собой указа- тель, вела себя подобно любому другому указателю. Все изменения, внесенные в объект на который указывал указатель, отражались на всех совместно используемых объектах. Если пользователь удалял этот объект, указатель в остальных объектах рассматриваемого класса становился потерянным. То есть его переменная-член со- держала указатель на несуществующий объект. В качестве альтернативы обычному указателю, можно определить переменную- член такого типа, который иногда называют классом интеллектуального указателя (smart pointer). Интеллектуальный указатель ведет себя подобно обычному указате- лю, но обладает рядом дополнительных возможностей. В данном случае возложим на интеллектуальный указатель ответственность за удаление совместно используе- мого объекта. Пользователи динамически создают объект в распределяемой памяти и передают его адрес новому объекту класса HasPtr. При помощи простого указате- ля пользователь вполне может обратиться к объекту, но он не должен удалять его. В классе HasPtr удаление объекта будет происходить только тогда, когда проверка покажет, что удаляется последний указатель на него. В других случаях класс HasPtr будет вести себя подобно обладающему простым указателем. В частности, при копировании объекта класса HasPtr, указатель копии будет указывать на тот же объект, что и указатель исходного. Если изменить этот объект при помощи одной из копий, изменение отразится и на других. Для удаления указателя новому классу HasPtr понадобится деструктор. Но этот деструктор не будет удалять указатель безоговорочно. Если два объекта класса HasPtr указывают на один основной объект, его нельзя удалять до тех пор, пока не будут удалены оба объекта HasPtr. Чтобы написать такой деструктор, необходим способ, позволяющий выяснить, является ли текущий объект класса HasPtr по- следним из тех, которые указывают на данный объект.
526 Часть III. Абстракция, классы и данные Счетчик пользователей Общепринятым подходом, используемым для создания интеллектуальных указа- телей, является применение счетчика пользователей (use count). Подобный указате- лю класс ассоциирует счетчик с объектом, на который он указывает. Счетчик поль- зователей отслеживает количество объектов класса, совместно использующих ука- затель. Когда значение счетчика пользователей становится равным нулю, объект можно удалять. Счетчик пользователей иногда называют также счетчиком ссылок (reference count). Каждый раз, когда создается новый объект класса, указатель инициализируется и счетчику пользователей назначается значение 1. Когда объект создается как копия другого объекта, конструктор копий копирует указатель и увеличивает значение счет- чика пользователей. При присвоении объекта, оператор присвоения уменьшает зна- чение счетчика пользователей объекта, который представляет собой левый операнд (а также удаляет объект, если значение счетчика пользователей становится равным нулю), и увеличивает значение счетчика пользователей объекта, являющегося правым операндом. И наконец, при вызове деструктора значение счетчика пользователей уменьшается и основной объект удаляется, если оно становится равным нулю. Остается одна проблема: где поместить счетчик пользователей? Счетчик не мо- жет принадлежать непосредственно объекту класса HasPtr. Чтобы объяснить при- чину, давайте рассмотрим, что произойдет в этом случае, int obj; HasPtr pl(&obj, 42); HasPtr p2(pl); // pl и p2 указывают на тот же объект типа int HasPtr p3(pl); // pl, р2 и рЗ указывают на тот же объект типа int Если счетчик пользователей хранит сам объект класса HasPtr, то как его пра- вильно модифицировать при создании объекта рЗ? Его значение можно увеличить в объекте pl и скопировать в объект рЗ, но как тогда модифицировать счетчик в объекте р2? Класс счетчика пользователей Существует два классических способа реализации счетчика пользователей, один из которых использован здесь, а другой рассматривается в разделе 15.8.1 (стр. 630). В данном случае предстоит определить отдельный конкретный класс, который ин- капсулирует счетчик пользователей и соответствующий указатель. // закрытый класс, предназначенный для использования только I/ классом HasPtr class U_Ptr { friend class HasPtr; int *ip; size_t use; U_Ptr(int *p) : ip(p), used) { } ~U_Ptr() { delete ip; } } ; Все члены этого класса являются закрытыми. Поскольку предоставлять доступ к классу U_Ptr обычным пользователям не предполагается, ни один из его членов не объявлен открытым. Класс HasPtr объявлен дружественным, чтобы его члены име- ли доступ к членам класса U_Ptr.
Глава 13. Управление копированием 527 Сам класс весьма прост, хотя положенная в его основу концепция считается до- вольно сложной. Класс U_Ptr содержит указатель и счетчик пользователей. Каж- дый объект класса HasPtr содержит указатель на класс U_Ptr. Счетчик пользова- телей будет отслеживать количество объектов класса HasPtr, содержащих указате- ли на каждый объект класса U_Ptr. Определенными для класса U_Ptr функциями являются только его конструктор и деструктор. Конструктор копирует указатель, который деструктор удаляет. Конструктор инициализирует также счетчик пользова- телей значением 1, поскольку первый объект класса HasPtr уже содержит указа- тель на объект класса U_Ptr. Предположим, что был создан объект класса HasPtr, указатель которого дол- жен указывать на целочисленное значение 4 2. Схематически объекты изображены на рис. 13.2. int *р = new int(42); int Рис. 13.2. Объекты после их создания После копирования эти объекты будут выглядеть так, как это показано на рис. 13.3. int *р = new int(42); int Рис. 13.3. Объекты после копирования Применение класса счетчика пользователей Новый класс HasPtr содержит указатель на объект класса U_Ptr, который в свою очередь содержит указатель на реальный объект класса int. Чтобы отразить тот факт, что теперь класс содержит указатель на объект класса U_Ptr, а не на цело- численную переменную, класс HasPtr придется переделать. Сначала рассмотрим функции-члены управления копированием и конструкторы. /* Класс интеллектуального указателя: владеет объектом в * динамически распределяемой памяти, с которым он связан * Пользовательский код должен создать объект в динамически * распределяемой памяти и инициализировать им объект класса * HasPtr, но он не должен удалять его; это сделает класс HasPtr ★ / class HasPtr {
528 Часть III. Абстракция, классы и данные public: // HasPtr владеет указателем; р должен быть создан в // динамической памяти HasPtr(int *р, int i): ptr(new U_Ptr(p)), val(i) { } // скопировать переменные-члены и увеличить значение счетчика // пользователей HasPtr(const HasPtr &orig): ptr(orig.ptr), val(orig.val) { ++ptr->use; } HasPtr& operator=(const HasPtr&); // если счетчик пользователей равен нулю, удалить объект U_Ptr -HasPtr() { if (--ptr->use == 0) delete ptr; } private: U_Ptr *ptr; // указывает на класс счетчика пользователей U_Ptr int val; }; Конструктор класса HasPtr получает указатель и целочисленное значение. Пе- реданный в качестве параметра указатель используется при создании нового объекта класса U_Ptr. После завершения работы конструктора HasPtrО, объект класса HasPtr содержит указатель на только что созданный объект класса U_Ptr. Этот объект класса U_Ptr содержит переданный ему указатель. В этом вновь созданном объекте класса U_Ptr, счетчик пользователей имеет значение 1, а значит, указывает, что на него ссылается только один объект класса HasPtr. Конструктор копий копирует переменные-члены его параметра и увеличивает значение счетчика пользователей. После завершения работы конструктора, вновь созданный объект указывает на тот же объект класса U_Ptr, что и исходный, а зна- чение счетчика пользователей увеличивается на единицу. Деструктор проверяет значение счетчика пользователей в объекте класса U_Ptr. Если оно равно 0, значит это последний объект класса HasPtr, который указывает на данный объект класса U_Ptr. В таком случае деструктор -HasPtr () удаляет свой указатель на объект класса U_Ptr. В результате удаления указателя произойдет вызов деструктора класса U_Ptr, который в свою очередь удалит основной объект типа int. Присвоение и счетчики пользователей Оператор присвоения немного сложнее, чем конструктор копий. HasPtr& HasPtr::operator^(const HasPtr &rhs) { ++rhs.ptr->use; // сначала инкремент счетчика // пользователей rhs if (--ptr->use == 0) delete ptr; // если счетчик пользователей имеет значение // 0, удалить этот объект ptr = rhs.ptг; // скопировать объект U_Ptr val = rhs.val; // скопировать переменную-член int return *this; ) Сначала увеличивается значение счетчика пользователей правого операнда. За- тем происходит декремент и проверка счетчика пользователей этого объекта. По- добно деструктору, если это последний объект, указывающий на объект класса U_Ptr, происходит его удаление, что в свою очередь приводит к удалению основно- го объекта типа int. После декремента (а возможно, и удаления) существующего значения в левом операнде, происходит копирование в этот объект указателя из rhs. Как обычно, оператор присвоения возвращает ссылку на этот объект.
Глава 13. Управление копированием 529 Данный оператор присвоения также “принимает меры” против самоприсвоения. Инкре- мент значения счетчика пользователей rhs происходит до декремента счетчика пользо- вателей левого операнда. Если левый и правый операнды являются тем же объектом, в ходе выполнения оператора присвоения произойдет сначала инкремент, а затем декремент счетчика пользователей объекта класса U_Ptr. Изменение других членов класса Другие члены класса, которые обращаются к указателю на целое число, также не- обходимо изменить, поскольку теперь доступ к нему осуществляется косвенно через указатель объекта класса U_Ptr. class HasPtr { public: // функции управления копированием и конструкторы как прежде // функции доступа следует изменить так, чтобы они обращались к II значению через объект класса U_Ptr int *get_ptr() const { return ptr->ip; } int get_int() const { return val; } // изменить соответствующие переменные-члены void set_ptr(int *p) { ptr->ip = p; } void set_int(int i) { val = i; } // возвращает или изменяет значение, на которое указывает // указатель; допустимо для константных объектов // Примечание: *ptr->ip эквивалентно *(ptr->ip) int get_ptr_val() const { return *ptr->ip; } void set_ptr_val(int i) { *ptr->ip = i; } private: U_Ptr *ptr; // указывает на класс счетчика пользователей U_Ptr int val; }; Функции доступа, которые возвращают и устанавливают значение переменной- члена типа int, остаются неизменными, а работающие с указателем придется изме- нить так, чтобы они обращались к нему через объект класса U_Ptr. При копировании объектов класса HasPtr, переменная-член типа int ведет себя как и в первой версии класса. Его значение копируется и переменные-члены остают- ся независимыми. Переменные-члены, являющиеся указателями, в копии и исход- ном объекте содержат адрес того же основного объекта. Изменение, сделанное в од- ном из объектов, повлияет на значение всех объектов класса HasPtr. Но о потерян- ных указателях пользователи класса HasPtr волноваться уже не должны. Класс HasPtr сам позаботится об освобождении объекта, он также проследит за тем, что- бы объект существовал до тех пор, пока имеется хотя бы один объект класса HasPtr, содержащий указатель на него. Совет. Манипулирование переменными-членами, которые являются указателями Объекты, обладающие переменными-членами в виде указателем, зачастую требуют определения функций-членов управления копированием. Если положиться на син- тезируемые версии, пользователь класса может столкнуться с некоторыми пробле- мами Пользователи вынуждены будут, гарантировать, что объект, указатель на
530 Часть III. Абстракция, классы и данные который используется, будет существовать, по крайней мере, до тех пор, пока име- ются указатели на него. Чтобы использовать класс, членами которого являются указатели, необходимо определить все три функции-члена управления копированием: конструктор копий, оператор при- своения и деструктор. Эти функции-члены позволят определить поведение переменной- члена как подобное указателю (pointer!ike) или как подобное значению (valuelike). Класс, подобный значению, предоставляет каждому объекту его собственную копию основных значений, указанных членами-указателями. Конструктор копий создает но- вый элемент и копирует значение объекта. Оператор присвоения удаляет существую- щий объект и копирует значение своего правого операнда в левый. Деструктор удаля- ет объект. В качестве альтернативы поведению, подобному значению или указателю, в некото- рых классах используются т.н. “интеллектуальные указатели’. Объекты этих классов совместно используют то же основное значение, чем обусловлено поведение, подобное указателю. Но чтобы избежать проблем, присущих обычным указателям, они исполь- зуют функции управления копированием. Для реализации поведения интеллектуаль- ного указателя, класс должен гарантировать, что основной объект будет существовать до тех пор, пока не будет удален последний из указателей на него. Подсчет пользова- телей (раздел 13.5.1, стр. 525) — это общепринятый способ реализации класса интел- лектуального указателя. Счетчик пользователей отслеживает количество копий ука- зателя на то же основное значение. Конструктор копий копирует указатель из прежне- го объекта в новый и увеличивает значение счетчика пользователей. Оператор присвоения уменыпает значение счетчика пользователей у левого операнда и увели- чивает у правого. Если счетчик пользователей левого операнда равен нулю, оператор присвоения должен удалить объект, на который он указывает. И наконец, оператор присвоения копирует указатель из правого операнда в левый. Деструктор осуществ- ляет декремент счетчика пользователей и удаляет основной объект, если значение счетчика равно нулю. Эти способы управления указателями применяются настолько часто, что все програм- мисты, использующие классы с членами-указателями, должны быть с ними знакомы. Упражнения раздела 13.5.1 Упражнение 13.22. Что такое счетчик пользователей? Упражнение 13.23. Что такое интеллектуальный указатель? Чем класс интеллектуального указате- ля отличается из того, который реализует поведение обычного указателя? Упражнение 13.24. Реализуйте собственную версию счетчика пользователей для класса HasPtr. 13.5.2. Определение классов подобных значению Совершенно иной подход решения проблемы указателей-членов подразумевает использование семантики значений (value semantics). Классы с семантикой значений создают объекты, которые ведут себя подобно арифметическим типам: при копиро-
Глава 13. Управление копированием 531 вании такого объекта получаются новая, совершенно независимая копия. Измене- ния, внесенные в копию, никак не влияют на исходный объект и наоборот. Хорошим примером класса, подобного значению, является класс string. Чтобы сделать поведение указателя-члена подобным значению, необходимо ко- пировать объект, на который указывает указатель, каждый раз, когда копируется объект класса HasPtr. ★ * Класс HasPtr ведет себя подобно значению, несмотря на наличие * указателя- члена: * Каждый раз, при копировании объекта класса HasPtr создается * новая копия основного объекта int, на который указывает ptr. ★ / // нет смысла передавать указатель, если объект, на который он // указывает, все равно предполагается скопировать // сохранить указатель на копию переданного объекта HasPtr(const int &р, int i): ptr(new int(p)), val(i) {} // скопировать переменные-члены и увеличить значение счетчика // пользователей HasPtr(const HasPtr &orig): ptr(new int (*orig.ptr)), val(orig.val) { } HasPtr& operator=(const HasPtr&); ~HasPtr() { delete ptr; } friend ostream& operator«(ostream&, const HasPtr&); // функции доступа следует изменить так, чтобы они обращались к // значению через объект Ptr int get_ptr_val() const { return *ptr; } int get_int() const { return val; } // изменить соответствующие переменные-члены void set_ptr(int *p) { ptr = p; } void set_int(int i) { val = i; } // возвращает или изменяет значение, на которое указывает II указатель; допустимо для константных объектов int *get_ptr() const { return ptr; } void set_ptr_val(int p) const { *ptr = p; } private: int *ptr; // указывает на int int val; }; Конструктор копий больше не копирует указатель. Теперь он создает новый объ- ект типа int и инициализирует его значением исходного. Таким образом, каждый объект будет обладать собственной, независимой копией значения типа int. По- скольку каждый объект использует собственную копию, деструктор безоговорочно удаляет указатель. Оператор присвоения не должен создавать новый объект. Он должен присвоить новое значение только переменной типа int, указатель на которую содержит объект, а не сам указатель. HasPtr& HasPtr::operator=(const HasPtr &rhs) { // Примечание: Каждый объект класса HasPtr должен содержать II указатель на гарантированно существующую II переменную типа int; известно, что ptr не может II быть нулевым указателем *ptr = *rhs.ptr; // копирование значения, на которое // указывает указатель
532 Часть III. Абстракция, классы и данные // копирование целочисленного значения Другими словами, изменяется значение переменной, на которую указывает ука- затель, а не сам указатель. Как обычно, оператор присвоения должен корректно сработать даже тогда, когда объект v й 1 присваивается сам себе. В данном случае операции безопасны в принципе, даже если левый и правый операнды являются тем же объектом. Таким образом, нет никакой необ- ходимости в явной проверке операндов на равенство. Упражнения раздела 13.5.2 Упражнение 13.25. Что такое класс, подобный значению? Упражнение 13.26. Реализуйте собственную версию подобного значению класса HasPtr. Упражнение 13.27. В подобном значению классе HasPtr определена каждая из функций- членов управления копированием. Опишите, что произойдет в случае, если в классе: (а) определен конструктор копий и деструктор, но не определен оператор присвоения; (Ь) определен конструктор копий и оператор присвоения но не деструктор; (с) определен деструктор, но не конструктор копий и не оператор присвоения. Упражнение 13.28. Реализуйте для приведенных ниже классов стандартный конструктор и необ- ходимые функции-члены управления копированием. a) class TreeNode { public: private: std::string value; int count; TreeNode *left; TreeNode *right; }; (b ) class BinStrTree { public: private: TreeNode *root; }; Резюме Кроме определения операций, допустимых для выполнения с объектами, класс позволяет задать также действия, осуществляемые при копировании, присвоении и удалении объектов. Эти действия определяют специальные функции-члены: конструктор копий, оператор при- своения и деструктор. Обычно их называют функциями “управления копированием”. Если в классе отсутствует одна или несколько из этих функций, компилятор синтезирует их автоматически. Синтезируемые функции осуществляют почленную инициализацию, при- своение или удаление: т.е. синтезируемая функция по очереди перебирает каждую перемен- ную-член и вызывает соответствующую функцию копирования, присвоения или удаления класса данной переменной-члена. Если член класса имеет тип класса, синтезируемая функ- ция вызывает соответствующую функцию этого класса (например, конструктор копий вызы- вает конструктор копий члена класса, деструктор вызывает его деструктор и т.д.). Если пере- менная-член имеет встроенный тип или является указателем, ее копирование или присвоение осуществляется непосредственно; для удаления переменных-членов встроенных типов или указателей деструктор ничего не делает. Если член класса является массивом, его элементы копируются, присваиваются и удаляются в соответствии с типом его элементов.
Глава 13. Управление копированием 533 В отличие от конструктора копий и оператора присвоения, синтезируемый деструктор создается и выполняется независимо от того, определен ли в классе его собственный деструк- тор. Синтезируемый деструктор выполняется после деструктора, определенного в классе, ес- ли он есть. Самая трудная задача при определении функций управления копированием зачастую заключается в выяснении того, что они действительно необходимы. Класс, резервирующий память или другие ресурсы, почти всегда требует определения функций-членов управления копированием, позволяющих контролировать распределенный ресурс. Если класс нуждается в деструкторе, он почти наверняка потребует определения кон- структора копий и оператора присвоения. Термины Деструктор (destructor). Специальная функция-член, которая освобождает занятую объ- ектом память, когда он выходит из области видимости или удаляется. Компилятор автомати- чески удаляет каждый член класса. При удалении переменных-членов типа класса использу- ются их собственные деструкторы, а при удалении переменных-членов встроенного или со- ставного типа конструктор ничего не делает. В частности, объект, на который указывает указатель-член класса, автоматически деструктором не удаляется. Интеллектуальный указатель (smart pointer). Класс, который ведет себя подобно указате- лю, но обеспечивает и другие функциональные возможности. Как правило, интеллектуаль- ный указатель получает указатель на объект в динамической памяти и берет на себя ответст- венность за его удаление. Создает объект пользователь, а удаляет его объект класса интеллек- туального указателя. Для манипулирования указателем на совместно используемый объект, класс интеллектуального указателя требует, чтобы использующий его класс реализовал функции-члены управления копированием. Используемый объект удаляется только тогда, когда удаляется последний интеллектуальный указатель на него. Наиболее популярным спо- собом реализации классов интеллектуального указателя является счетчик пользователей. Конструктор копий (copy constructor). Конструктор, который инициализирует новый объект как копию другого объекта того же типа. При передаче объекта в функцию или из функции, конструктор копий применяется неявно. Если конструктор копий не определен яв- но, компилятор синтезирует его самостоятельно. Оператор присвоения (assignment operator). Оператор присвоения может быть перегру- жен, чтобы определить действия, осуществляемые при присвоении одного объекта класса другому объекту того же класса. Оператор присвоения должен быть членом класса, а также должен возвращать ссылку на его объект. Компилятор самостоятельно синтезирует оператор присвоения, если в классе он не определен явно. Перегруженный оператор (overloaded operator). Функция, которая переопределяет один из операторов языка C++, предназначенный для работы с объектами данного класса. В этой главе описано определение лишь оператора присвоения, а более подробно перегрузка опера- торов рассматривается в главе 14, “Перегрузка операторов и преобразования”. Почленная инициализация (memberwise initialization). Термин, используемый для описа- ния работы синтезируемого конструктора копий. Конструктор копий последовательно копи- рует значения каждого члена класса прежнего объекта в новый. Члены встроенного или со- ставного типа копируются непосредственно. Для копирования переменных-членов, имеющих тип класса, используется конструктор копий используемого класса.
534 Часть III. Абстракция, классы и данные Почленное присвоение (memberwise assignment). Термин, используемый для описания работы синтезируемого оператора присвоения. Оператор присвоения последовательно при- сваивает значения каждого члена класса прежнего объекта новому. Члены встроенного или составного типа присваивается непосредственно. Для присвоения переменных-членов, имею- щих тип класса, используется оператор присвоения используемого класса. Правило трех (Rule of Three). Сокращенное название эмпирического правила: если класс нуждается в нестандартном деструкторе, ему наверняка понадобится собственный конструк- тор копий и оператор присвоения. Семантика значения (value semantics). Описание поведения функций управления копи- рованием классов, которое подобно копированию значений арифметического типа. Копии подобных значению объектов независимы: изменения, внесенные в копию, никак не отобра- жаются на исходном объекте. В подобном значению классе, обладающем указателем-членом, необходимо определить собственные функции-члены управления копированием. Функции управления копированием копируют также объект, на который указывает указатель. Подоб- ные значению классы, типами членов которого являются только другие подобные значению классы или встроенные типы, вполне могут полагаться на синтезируемые функции-члены управления копированием. Синтезируемый конструктор копий (synthesized copy constructor). Конструктор копий, создаваемый (синтезируемый) компилятором для классов, у которых конструктор копий не определен явно. Синтезируемый конструктор копий осуществляет почленную инициализа- цию нового объекта значениями существующего. Синтезируемый оператор присвоения (synthesized assignment operator). Версия оператора присвоения, создаваемого (синтезируемого) компилятором для классов, у которых он не оп- ределен явно. Синтезируемый оператор осуществляет почленное присвоение правого операн- да левому. Счетчик пользователей (use count). Программный подход, используемый при реализации функций-членов управления копированием. Счетчик пользователей хранится наряду с со- вместно используемым объектом. Для обслуживания счетчика пользователей и указателя на совместно используемый объект создается отдельный класс. Конструкторы, отличные от кон- структора копий, устанавливают состояние совместно используемого объекта и инициализи- руют счетчик пользователей значением 1. При каждом создании новой копии объекта (в кон- структоре копий или операторе присвоения), значение счетчика пользователей увеличивает- ся. При удалении объекта (деструктором или когда он является левым операндом оператора присвоения), происходит декремент счетчика пользователей. Оператор присвоения и дест- руктор проверяют, не достиг ли счетчик пользователей значения 0, и если это так, они удаля- ют объект. Счетчик ссылок (reference count). То же, что и счетчик пользователей. Управление копированием (copy control). Специальные функции-члены, которые оп- ределяют действия, осуществляемые при копировании, присвоении и удалении объектов класса. Если эти функции не определены в классе явно, компилятор синтезирует их само- стоятельно.
ГЛАВА 14 Перегрузка операторов И ПРЕОБРАЗОВАНИЯ В ЭТОЙ ГЛАВЕ... 14.1. Определение перегруженного оператора 536 14.2. Операторы ввода и вывода 543 14.3. Арифметические операторы и операторы отношения 548 14.4. Операторы присвоения 550 14.5. Оператор индексирования 551 14.6. Операторы доступа к членам класса 553 14.7. Операторы инкремента и декремента 556 14.8. Оператор вызова функции объекта 560 14.9. Преобразования и типы классов 566 Резюме 582 Термины 583 Как упоминалось в главе 5, “Выражения”, для встроенных типов данных в языке C++ определено множество операторов и автоматических преобразований. Эти средства позволяют программистам создавать разнообразные выражения, где ис- пользуются разные типы данных. Язык C++ позволяет переопределять смысл операторов, применяемых для объ- ектов класса. Он позволяет также определять для класса функции преобразования типов. Функции преобразования класс-тип используются (при необходимости) по- добно встроенным функциям преобразования, для неявного преобразования объекта одного типа в объект другого. Перегрузка операторов (operator overloading) позволяет программисту создавать собственные версии операторов для операндов типа класса. В главе 13, “Управление копированием”, была особо подчеркнута важность оператора присвоения, а также представлено его определение. Впервые применение перегруженных операторов было продемонстрировано в главе 1, “Первые шаги”, когда операторы сдвига (>> и <<) ис- пользовались в программе для ввода и вывода, а оператор суммы (+) — для объеди- нения данных двух объектов класса Sales_item. В этой главе речь пойдет о том, как создавать эти перегруженные операторы.
536 Часть III. Абстракция, классы и данные Используя перегрузку операторов, в объектах класса можно переопределить боль- шинство операторов, представленных в главе 5, “Выражения”. Разумное применение перегрузки операторов позволяет сделать использование класса столь же интуитивно понятным, как и встроенного типа. Стандартная библиотека, например, предоставляет несколько перегруженных операторов для контейнерных классов. Сюда относятся: оператор индексирования, позволяющий получить доступ к данным элементов, а так- же операторы * и - >, позволяющие обратиться к значениям итераторов контейнера. Тот факт, что операторы этих библиотечных типов унифицированы, позволяет ис- пользовать их подобно встроенным массивам и указателям. Возможность использова- ния в программах обычных выражений, а не именованных функций, существенно уп- рощает код. Сравните, например, следующие фрагменты кода. cout << "The sum of " << vl << " and " << v2 << " is " << vl + v2 << endl; Это же выражение, но с использованием именованных функций ввода-вывода, выглядело бы следующим образом. // гипотетическое выражение с использованием именованных функций // ввода-вывода cout.print("The sum of ").print(vl). 14.1. Определение перегруженного оператора Перегруженный оператор (overloaded operator) — это функция со специальным именем, состоящим из ключевого слова operator, сопровождаемого символом оп- ределяемого оператора. Подобно любой другой функции, перегруженный оператор имеет тип возвращаемого значения и список параметров. Sales_item operator+(const Sales_item&, const Sales_item&); Здесь объявлен оператор суммы (+), который осуществляет “сложение” двух объ- ектов класса Sales_item и возвращает копию объекта класса Sales_item. За исключением оператора вызова функции, перегруженный оператор имеет столько параметров (включая неявный указатель this функций-членов), сколько и операндов. Оператор вызова функции получает неограниченное количество операндов. Имена перегруженных операторов Список операторов, которые могут быть перегружены, приведен в табл. 14.1. Те операторы, которые не могут быть перегружены, перечислены в табл. 14.2. Таблица 14.1. Операторы, которые могут быть перегружены + - * / % & | ~ ! << >> == != && | | + = —— /— %— = &= | = *= «= »= [] о -> ->* new new [] delete delete []
Глава 14. Перегрузка операторов и преобразования 537 Новый оператор создать нельзя, даже связав его с другим допустимым символом. Например, даже не стоит пытаться создать оператор operator**, осуществляющий возведение в степень. Перегрузка операторов new и delete описана в главе 18, “Специализированные инструменты и технологии”, (стр. 784). Таблица 14.2. Операторы, которые не могут быть перегружены Операнд перегруженного оператора должен иметь тип класса Смысл оператора для встроенного типа не может быть изменен. Например, опе- ратор суммы встроенного целочисленного типа не может быть переопределен. // ошибка: невозможно переопределить встроенный оператор типа int int operator+(int, int); Кроме того, для встроенных типов данных не могут быть определены дополни- тельные операторы. Например, не может быть определен оператор operator+, опе- рандами которого являются два массива. По крайней мере один из операндов перегруженного оператора должен иметь тип класса 1 или перечисления (раздел 2.7, стр. 84). Это правило позволяет обойти требование, со- гласно которому перегруженный оператор не может переопределять смысл операторов для объектов встроенного типа. Приоритет и порядок неизменны При переопределении оператора не может быть изменен ни его приоритет (раздел 5.10.1, стр. 193), ни порядок, ни количество операндов. Независимо от типа операндов и назначения операторов, выражение х == у + z; всегда связывает ар- гументы у и z с оператором operator+, а результат передает в качестве правого операнда оператору operator==. Четыре символа (+,-,* и &) используются как для унарных, так и для бинарных операторов. Перегружены могут быть как унарные, так и бинарные версии этих опе- раторов. Количество операндов позволяет выяснить, какой именно из операторов переопределен. Аргументы по умолчанию для перегруженных операторов недопус- тимы, за исключением оператора вызова функции operator (). Порядок вычисления по сокращенной схеме не сохраняется Перегруженные операторы не гарантируют порядка обработки операндов. В ча- стности, не гарантируется сохранение порядка обработки операндов встроенного ло- гического оператора AND, логического оператора OR (раздел 5.2, стр. 176) и опе- ратора запятая (раздел 5.9, стр. 192). Оба операнда перегруженной версии опера- торов && и | | всегда обрабатываются, причем порядок, обработки этих операндов непредсказуем. Порядок выполнения операндов оператора запятая также не опреде- лен. Поэтому не рекомендуется перегружать операторы &&, | | и оператор запятая. Член класса или не член класса Перегруженные операторы могут быть определены как обычные функции (не члены класса) и как функции-члены класса.
538 Часть III. Абстракция, классы и данные Перегруженные функции, которые являются членами класса, могут иметь на один пара- 1 метр больше, чем общее количество операндов. Такие функции-члены обладают неявным ^У! параметром this, который связан с первым операндом. Перегруженный унарный оператор не имеет ни одного явного параметра, если это функция-член, и один параметр — если это функция, не являющаяся членом класса. Аналогично, перегруженный бинарный оператор имеет один параметр, если это функция-член класса, и два параметра — если это обычная функция. Давайте на примере класса Sales_item рассмотрим бинарные операторы, кото- рые являются и не являются членами класса. Известно, что класс обладает операто- ром суммы. Поскольку оператор суммы существует, следует также определить и оператор составного присвоения (+=). Этот оператор добавит значение одного объ- екта Sales_item в другой. Как правило, арифметические операторы и операторы отношения определяют как обычные функции, а операторы присвоения — как функции-члены. // бинарный оператор как член класса: левый операнд - неявный // указатель this Sales_item& Sales_item::operator*^(const Sales_item&); // бинарный оператор не член класса: каждый операнд должен быть !/ объявлен как параметр Sales_item operator*(const Sales_item&, const Sales_item&); Оба оператора, сумма и составное присвоение являются бинарными, но количе- ство параметров у них указано разное и причина здесь в указателе this. Когда оператор является функцией-членом, указатель this указывает на левый операнд. Таким образом, для оператора operator* (не члена класса) указано два параметра (обе ссылки на константные объекты класса Sales_item). Несмотря на то, что оператор составного присвоения (член класса) тоже является бинарным, он получает только один (явный) параметр. Когда используется оператор, под левым операндом автоматически подразумевается объект, на который указывает указатель this, а под правым — единственный параметр функции. Обратите внимание, оператор составного присвоения возвращает ссылку, а опе- ратор суммы — объект класса Sales_item. Это соответствует различию в типах возвращаемого значения аналогичных операторов арифметических типов: сумма возвращает r-значение, а составное присвоение — ссылку на левый операнд. Перегрузка операторов и дружественные отношения Когда операторы определяют как обычные функции (не члены класса), их зачас- тую объявляют дружественными для класса (раздел 12.5, стр. 493), с объектами ко- торого он работает. Далее в этой главе будут указаны две причины, по которым опе- раторы имеет смысл определять как обычные функции. В таких случаях оператор зачастую нуждается в доступе к закрытым переменным-членам класса. Класс Sales_item снова послужит примером ситуации, когда некоторые опе- раторы следует сделать дружественными. Определим один оператор как член класса и три оператора как обычные функции. Этим трем операторам (не членам класса) понадобится доступ к закрытым переменным-членам, поэтому они объявлены дру- жественными.
Глава 14. Перегрузка операторов и преобразования 539 class Sales_item { friend std::istream& operator>> (std::istream&, Sales_item&); friend std::ostream& operator<< (std::ostream&, const Sales_item&); public: Sales_item& operator+=(const Sales_item&); } ; Sales_item operator+(const Sales_item&, const Sales_item&); To, что операторам ввода и вывода необходим доступ к закрытым данным, не должно удивлять. В конце концов, они читают и записывают их значения. С другой стороны, нет никакой необходимости объявлять оператор сложения дружествен- ным. Он может быть реализован как открытый член класса operator+=. Использование перегруженных операторов Перегруженный оператор можно использовать точно так же, как и оператор встроенного типа. Предположим, что iteml и item2 — это объекты класса Sales_ item. Их сумму можно вывести на экран точно так же, как и сумму двух перемен- ных типа int. cout << iteml + item2 << endl; В этом выражении неявно использован вызов оператора operator+, определен- ного для класса Sales_item. Перегруженный оператор можно вызвать таким же образом, как и обычную функцию: указав ее имя и передав соответствующее количество аргументов необхо- димого типа. // эквивалентное обращение к оператору не члену класса cout << operator+(iteml, item2) « endl; Это обращение имеет тот же результат, что и предыдущее — отображение суммы объектов iteml и item2. Вызов оператора функции-члена класса осуществляется тем же способом, что и любой другой функции-члена: указывается имя ее объекта, точка или оператор стрелки, имя функции и необходимое количество аргументов соответствующего ти- па. В случае бинарного оператора, необходимо передать один операнд. iteml += item2; // выражение, основанное на "вызове" iteml.operator+=(item2); // эквивалентное обращение к оператору // функции-члену класса Каждый из этих операторов добавляет к значению объекта iteml значение объекта item2. В первом случае вызов перегруженного оператора происходит неяв- но, с использованием синтаксиса выражения. Во втором случае происходит явный вызов оператора функции-члена объекта i t eml. Упражнения раздела 14.1 Упражнение 14.1. Чем перегруженный оператор отличается от встроенного оператора? Что об- щего у перегруженного и встроенного операторов? Упражнение 14.2. Напишите для класса Saies_item объявления перегруженных операторов ввода, вывода, суммы и составного присвоения.
540 Часть III. Абстракция, классы и данные Упражнение 14.3. Объясните, что выполняет следующая программа, с учетом того, что конструк- тор класса Saies_item получает строку и не объявлен явным (explicit). Объясните, что произойдет, если этот конструктор объявить явным. string null_book = " 9-999-99999-9"; Sales_item item(cin); item += null_book; Упражнение 14.4. У типов string и vector определен перегруженный оператор ==, который может быть использован при сравнении объектов этих типов. Укажите, какая из версий опера- тора == применяется в каждом из следующих выражений. string s; vector<string> svecl, svec2; "cobble" == "stone" svecl[0] == svec2[0]; svecl == svec2 14.1.1. Проект перегруженного оператора Проектируя класс с перегруженными операторами, имеет смысл учитывать не- сколько весьма полезных правил. Не следует перегружать операторы со стандартным смыслом Операторы присвоения, обращения к адресу (address of) и запятой имеют стандарт- ный смысл для операндов типа класса. Если класс не содержит их перегруженной вер- сии, компилятор определит собственную версию каждого из этих операторов. Синтезируемый оператор присвоения (раздел 13.2, стр. 512) осуществляет по- членное присвоение: для каждой переменной-члена он по очереди применяет оператор присвоения ее собственного класса. Стандартные операторы обращения к адресу (&) и запятая (, ) выполняются для объектов класса тем же способом, что и для объектов встроенных типов. Опера- тор обращения к адресу возвращает адрес области памяти занятой объектом, к которому он применяется. Оператор запятой обрабатывает каждое выражение слева направо и возвращает значение самого правого операнда. Встроенные логические операторы AND (&&) и OR (| | ) осуществляют вычис- ление по сокращенной схеме (раздел 5.2, стр. 176). Переопределенные операторы теряют способность к вычислению по сокращенной схеме. Смысл этих операторов может быть изменен, достаточно переопределить их для данного класса. Как правило, перегрузка таких операторов, как запятая, обращение к адресу или ло- гические операторы AND и OR, является плохой идеей. Они обладают стандартным смыслом, который может измениться при определении собственных версий. Иногда необходимо определять собственную версию оператора присвоения. В этом случае она должна вести себя аналогично синтезируемым операторам: после при- своения значения левого и правого операндов должны быть равны, а возвращать оператор должен ссылку на левый операнд. Перегруженный оператор присвоения должен дополнять возможности стандартного, а не изменять его смысл.
Глава 14. Перегрузка операторов и преобразования 541 Для объектов классов большинство операторов не имеет никакого смысла Операторы, которые не являются операторами присвоения, обращения к ад- ресу и запятой, не имеют никакого смысла, когда применяются к операнду, типом которого является класс (если класс не имеет их перегруженных версий). При про- ектировании класса необходимо решить, какие из операторов он должен поддер- живать (и должен ли вообще их поддерживать). При проектировании набора операторов класса, сначала имеет смысл разработать его открытый интерфейс. Определив интерфейс, можно решить, какие из операто- ров следует перегрузить. Хорошими кандидатами являются те из функций, задача которых логически соответствует операторам. Функция, проверяющая равенство, должна быть оператором operator==. Для ввода и вывода обычно используют перегруженные операторы сдвига. Функция, проверяющая, не является ли объект пустым, соответствует логиче- скому оператору NOT, т.е. оператору operator!. Внимание! Будьте осторожны при использовании перегруженных операторов! Каждый оператор имеет некий смысл, когда он используется для встроенных типов. Бинарный оператор +, например, всегда означает сумму. Вполне логично и удобно применять в классе бинарный оператор + для аналогичной функции. Например, биб- лиотечный тип string, в соответствии с соглашением, общепринятым для множества языков программирования, использует оператор + для конкатенации, т.е. добавления содержимого одной строки в другую. Перегруженные операторы полезней всего тогда, когда смысл встроенного оператора логически соответствует функции текущего класса. Применение перегруженных опе- раторов вместо именованных функций позволяет сделать программы более простыми, естественными и интуитивно понятными. Злоупотребление перетруженными опера- торами, а также придание не свойственного им смысла, сделает класс неудобным в применении. На практике, вполне очевидные случаи противоестественной перегрузки операторов довольно редки. Например, ни один ответственный программист не переопределил бы оператор operator+ для вычитания. Зато очень часто предпринимаются попытки не- ким образом приспособить “обычный” оператор, который к данному классу неприме- ним. Операторы следует использовать только для тех функций, которые будут одно- значно поняты пользователями. Оператор с неоднозначным смыслом, например ра- венство, может быть интерпретирован по-разному. Когда смысл перегруженного оператора не очевиден, лучше использовать вместо него именованную функцию. Кроме того, имеет смысл применять именованные функции вместо операторов для функций, которые используются редко. Если функ- ция необычна, краткость оператора ей не нужна.
542 Часть III. Абстракция, классы и данные Составные операторы присвоения Если класс обладает арифметическим (раздел 5.1, стр. 173) или побитовым (раз- дел 5.3, стр. 179) оператором, его, как правило, имеет смысл снабдить соответствую- щими составными операторами. Например, в классе Sales_item определен опера- тор +. Вполне логично было бы также определить и оператор +=. Само собой разуме- ется, оператор += должен быть определен так, чтобы он вел себя аналогично встро- енным операторам, т.е. осуществлял составное присвоение: сначала сумма (+), а затем присвоение (=). Операторы равенства и отношения Классы, которые предполагается использовать для ключей ассоциативного кон- тейнера, должны обладать оператором <. По умолчанию ассоциативные контейнеры используют оператор < класса ключа. Даже если объекты класса предполагается хранить только в последовательном контейнере, для него следует определить опера- тор равенства (==) и оператор меньше чем (<). Дело в том, что большинство алго- ритмов предполагают их наличие. Алгоритм sort, например, использует оператор <, а алгоритм find — оператор ==. Если в классе определен оператор равенства, в нем имеет смысл определить и опе- ратор неравенства ! =. Пользователи класса подразумевают, что если его объекты мож- но сравнивать на равенство, их можно сравнивать и на неравенство. Это же касается и остальных операторов отношения. Если в классе определен оператор <, вероятнее все- го, потребуется определить и все остальные операторы отношения (>,>=,<и<=). Выбор обычной функции или члена класса При проектировании перегруженных операторов для класса, необходимо при- нять решение, должен ли каждый из них быть членом класса или обычной функцией (не членом класса). В некоторых случаях выбора нет; оператор должен быть членом класса. В других случаях можно принять во внимание несколько эмпирических пра- вил, которые помогут принять решение. Приведенный ниже список критериев мо- жет оказаться полезен в ходе принятия решения о том, следует ли сделать оператор функцией-членом класса или обычной функцией. Операторы присвоения (=), индексирования ( [] ), обращения ( () ) и доступа к члену класса (- >) следует определять как функции-члены класса. При попытке определения любого из этих операторов как обычной функции, произойдет ошибка во время компиляции. Подобно оператору присвоения, составные операторы (присвоения с суммой и т.д.), как правило, должны быть членами класса. В отличие от присвоения, они необязательно должны быть членами класса: компилятор никак не отреагирует на определение составного оператора как обычной функции. Другие операторы, которые изменяют состояние своего объекта или жестко свя- заны с данным классом (например инкремент, декремент и обращение к значе- нию), должны быть членами класса. Симметричные операторы, такие как арифметические, операторы равенства, реля- ционные и побитовые, лучше определять как обычные функции, не члены класса.
Глава 14. Перегрузка операторов и преобразования 543 Упражнения раздела 14.1.1 Упражнение 14.5. Перечислите операторы, которые должны быть членами класса. Упражнение 14.6. Объясните, должен ли каждый из следующих операторов быть членом класса и почему? (а) + (Ь) += (с) ++ (d) -> (е) « (f) && (g) == (h) () 14.2. Операторы ввода и вывода Классы, поддерживающие операции ввода-вывода, обычно создают таким обра- зом, чтобы они предоставляли тот же интерфейс, который определен в библиотеке iostream для встроенных типов. Таким образом, большинство классов обладает перегруженными операторами ввода и вывода. 14.2.1. Перегрузка оператора вывода << Для того чтобы оператор был совместим с библиотекой ввода-вывода, его первый па- I раметр должен быть ссылкой на объект класса ostream, а второй — ссылкой на кон- ^yl стантный объект класса. Оператор должен возвращать ссылку на свой параметр ^-''•"--2^ ostream. Общий каркас перегруженного оператора вывода выглядит следующим образом. // общий каркас перегруженного оператора вывода ostream& operator <<(ostream& os, const ClassType &object) { // действия по подготовке объекта // собственно вывод объекта os << // ... // возвращение объекта класса ostream return os; } Первый параметр — это ссылка на объект класса ostream, осуществляющий вы- вод. Объект класса ostream не константен, поскольку запись в поток изменяет его состояние. Параметр является ссылкой, поскольку невозможно скопировать объект класса ostream. Вторым параметром обычно является константная ссылка на класс, объект ко- торого необходимо вывести. Параметр представляет собой ссылку во избежание копирования аргумента. Но он может быть константной ссылкой, поскольку вы- вод объекта (обычно) не должен изменять его. Сделав параметр константной ссылкой, одно определение можно использовать для вывода как константных, так и неконстантных объектов. Типом возвращаемого значения является ссылка на объект класса ostream. Обычно значение представляет собой объект класса ostream, для которого приме- няется оператор вывода.
544 Часть III. Абстракция, классы и данные Оператор вывода класса Salesitem Теперь можно написать оператор вывода для класса Sales_item. ostream& operator<< (ostream& out, const Sales_.it em& s) { return out; } Отображение объекта класса Sales_item влечет за собой необходимость отобра- жения значений всех его трех переменных-членов, а также вычисления средней цены (average price). Каждый элемент отделяется табуляцией. После вывода значений опе- ратор возвращает ссылку на использованный для этого объект класса ostream. Операторы вывода обеспечивают минимум форматирования По поводу оператора вывода разработчики классов вынуждены принимать серь- езное решение: какую степень форматирования он должен обеспечивать? Как правило, операторы вывода должны отображать содержимое объекта с мини- мальным форматированием. Символ новой строки они добавлять не должны. Операторы вывода встроенных типов данных форматирования практически не обеспечивают и символ новой строки не отображают. При таком подходе к обработ- ке встроенных типов, пользователи ожидают, что и операторы вывода класса будут вести себя аналогично. Ограничивая оператор вывода отображением лишь содер- жимого объекта, можно позволить пользователю самостоятельно определить, какое дополнительное оформление ему необходимо. В частности, оператор вывода не должен отображать символ новой строки. Если оператор будет отображать символ новой строки, пользователь не сможет вывести содержимое объекта с описывающим его текстом в одной строке. Но если оператор вывода самостоятельно не форматиру- ет текст, это может сделать пользователь. Операторы ввода-вывода не должны быть функциями-членами класса Почему для обеспечения соответствия операторов ввода и вывода соглашениям библиотеки iostream, их следует определять как обычные функции, а не как функ- ции-члены класса? Этот оператор нельзя сделать членом самого класса. В этом случае левый опе- ранд должен был бы быть объектом данного класса. Рассмотрим пример. // если бы оператор operator<< был членом класса Sales_item Sales_item item; item « cout; Этот способ применения оператора противоположен обычному способу, исполь- зуемому для других типов. Чтобы обеспечить нормальное применение оператора, левый операнд должен иметь тип ostream. Это значит, что если оператор должен быть членом класса, он
Глава 14. Перегрузка операторов и преобразования 545 должен быть членом класса ostream. Но этот класс является частью стандартной библиотеки. Однако никто не может добавлять члены в библиотечный класс. Таким образом, если перегруженные операторы необходимо использовать для операций ввода-вывода собственных классов, их следует определять как обычные функции (не члены класса). Обычно операторы ввода-вывода читают или записы- вают данные в том числе и закрытых переменных-членов. Следовательно, операторы ввода-вывода должны быть дружественными для класса. Упражнения раздела 14.2.1 Упражнение 14.7. Определите оператор вывода для следующего класса checkoutRecord. class CheckoutRecord { public: private: double book_id; string title; Date date_borrowed; Date date_due; pair<string,string> borrower; vector< pair<string, string>* > wait_list; }; Упражнение 14.8. Напишите (по выбору) оператор вывода для любого из следующих классов, упоминавшихся в упражнениях раздела 12.4 (стр. 480). (a) Book (b) Date (с) Employee (d) Vehicle (е) Object (f) Tree 14.2.2. Перегрузка оператора ввода >> Подобно оператору вывода, первый параметр оператора ввода является ссылкой на поток, из которого осуществляется чтение данных, и возвращать он должен ссыл- ку на тот же поток. Его второй параметр представляет собой неконстантную ссылку на объект, в который предстоит прочитать данные. Второй параметр должен быть неконстантным, поскольку задачей оператора ввода и является собственно запись данных в этот объект. Однако важнейшее, хоть и менее очевидное, различие между операторами ввода и выво- да заключается в том, что при работе с операторами ввода возрастает вероятность воз- никновения ошибок и ввода конца файла. Оператор ввода класса Sales_item Оператор ввода класса Sales_item выглядит следующим образом. istream& operator>>(istream& in, Sales_item& s) { double price; in » s.isbn » s.units_sold >> price; // проверить успех ввода данных if (in) s.revenue = s.units_sold * price;
546 Часть III. Абстракция, классы и данные else s = Sales_item(); // ввод неудачен: вернуть объект в // стандартное состояние return in; } Из потока, указанного параметром типа istreams, этот оператор читает три сле- дующих значения: значение типа string соответствует переменной-члену isbn пе- реданного в качестве второго параметра объекта класса Sales_item, значение типа unsigned соответствует его переменной-члену units_sold, а значение типа double — локальной переменной price. Когда чтение завершается успехом, опера- тор использует переменную price и переменную-член units_sold для вычисле- ния значения переменной-члена revenue объекта. Ошибки во время ввода Оператор ввода класса Sales_item читает ожидаемые значения и проверяет, не произошла ли при этом ошибка. В данном случае возможны следующие ошибки. 1. Любая из операций чтения может окончиться неудачей, если введено неподхо- дящее значение. Например, после чтения ISBN оператор ввода подразумевает, что следующие два значения будут числовыми данными. Но если будут введены нечисловые данные, поток окажется недопустим и все последующее попытки чтения из него потерпят неудачу. 2. Во время любой из операций чтения может встретиться конец файла или про- изойти другая ошибка потока ввода. Чтобы не проверять каждую часть прочитанных данных, можно проверить со- стояние потока в целом и только потом использовать прочитанные данные. // проверить успех ввода данных if (in) s.revenue = s.units_sold * price; else s = Sales_item(); // ввод неудачен: вернуть объект в // стандартное состояние Если не будет прочитан хотя бы один из элементов данных, переменная-член price останется неинициализирована. Следовательно, перед ее использованием следует проверить, допустим ли еще поток ввода. Если это так, осуществляется вы- числение значения переменной revenue. В случае ошибки ничего страшного не произойдет, поскольку будет возвращен пустой объект класса Sales_item. Для этого при помощи стандартного конструктора создается новый, неименованный объект класса Sales_item, который и присваивается объекту. В результате при- своения переменная-член isbn объекта s будет содержать пустую строку, а пере- менные-члены revenue и units_sold — нули. Обработка ошибок ввода Если в операторе ввода происходит ошибка, имеет смысл удостовериться в том, что объект все еще находится в корректном, пригодном для использования со- стоянии. Это особенно важно в случае, когда объект был частично записан, прежде чем произошла ошибка.
Глава 14. Перегрузка операторов и преобразования 547 Например, оператор ввода класса Sales_item мог бы успешно прочитать новый ISBN, а затем столкнуться с ошибкой потока. Ошибка после чтения ISBN приведет к тому, что значения переменных-членов units_sold и revenue останутся неиз- менными со времен прежнего состояния объекта. В результате эти данные будут связаны с совершенно другим ISBN. В этом операторе удается избежать приведения переданного в качестве параметра объекта в недопустимое состояние. Для этого, в случае ошибки, ему присваивается пустой объект класса Sales_item. Тот пользователь, которому необходимо знать, было ли чтение успешным, может проверить допустимость потока ввода. Если пользо- ватель решит проигнорировать возможность ошибки ввода, ничего страшного не про- изойдет, объект все равно останется во вполне допустимом состоянии, ведь все его пе- ременные-члены будут определены. Кроме того, объект не сможет получить вводящие в заблуждение данные, ведь в случае ошибки его содержимое будет обнулено. Проектируя оператор ввода, очень важно решить, что делать в случае ошибки и как вновь сделать объект доступным. Оповещение об ошибке Кроме обработки возможных ошибок, оператор ввода вполне может устанавли- вать некий флаг состояния (раздел 8.2, стр. 314), переданного ему в качестве пара- метра объекта класса istream. В данном случае оператор ввода очень прост, поэто- му здесь можно заботиться лишь о тех ошибках, которые могут произойти только во время чтения. Если чтение прошло успешно, оператор ввода сработает корректно, поэтому нет никакой необходимости в дополнительной проверке. Однако в некоторых случаях операторы ввода нуждаются в дополнительных проверках. Например, данный оператор ввода мог бы проверять, соответствует ли прочитанный i sbn определенному формату. Данные могут быть прочитаны успеш- но, но интерпретировать их как ISBN будет невозможно. В таких случаях оператор ввода мог бы устанавливать флаг состояния, указывающий на произошедший отказ, даже несмотря на то, что с технической точки зрения чтение прошло успешно. Обычно оператор ввода должен установить только флаг failbit. Установка флага eofbit подразумевает конец файла, а флаг badbit — соответственно, сбой потока. Однако установку этих флагов лучше оставить самой библиотеке ввода-вывода. Упражнения раздела 14.2.2 Упражнение 14.9. Опишите поведение оператора ввода класса Saies_item, если ему переда- ны следующие данные. (а) 0-201-99999-9 10 24.95 (Ь) 10 24.95 0-210-99999-9 Упражнение 14.10. Какую ошибку содержит следующий оператор ввода класса Saies_item? istream& operator>>(istream& in, Sales_item& { double price; in >> s.isbn >> s.units_sold >> price; s.revenue = s.units_sold * price; return in; }
548 Часть III. Абстракция, классы и данные Что произойдет, если передать этому оператору данные из предыдущего упражнения? Упражнение 14.11. Определите оператор ввода для класса CheckoutRecord, определенного в упражнениях из раздела 14.2 (стр. 543). Создайте для него механизм обработки ошибок ввода. 14.3. Арифметические операторы и операторы отношения Обычно арифметические операторы и операторы отношения определяют как функции не члены класса, как и оператор суммы класса Sales_itern. // подразумевается, что оба объекта относятся к тому же isbn Sales_item operator+(const Sales_item& Ihs, const Sales_item& rhs) { Sales_item ret(Ihs); // скопировать Ihs в локальный объект, // который и будет возвращен ret += rhs; // добавить содержимое в rhs return ret; // возвратить ret как значение ) Фактически оператор суммы не изменяет состояние операндов, поэтому оба опе- ранда объявлены как ссылки на константные объекты. Вместо этого он создает и возвращает новый объект класса Sales_itern, который инициализирован как копия объекта Ihs. Чтобы добавить в результат значение объекта rhs, здесь использован составной оператор присвоения с суммой класса Sales_item. Обратите внимание, что для совместимости со встроенным оператором, данный оператор суммы возвращает r-значение, а не ссылку. Как правило, арифметические операторы создают новое значение, которое явля- ется результатом обработки двух его операндов. Это значение отличается от значе- ний каждого из операндов и сохраняется в локальной переменной. Возвращение ссылки на эту переменную во время выполнения программы было бы ошибкой. Классы, в которых определен арифметический оператор и соответствующий ему со- ставной оператор, обычно реализуют арифметический оператор при помощи со- ставного. Самый простой и наиболее эффективный способ реализации арифметического оператора (например +) заключается в использовании составного оператора (напри- мер +=). Рассмотрим, например, операторы класса Sales_item. Если реализовать оператор += за счет вызова оператора +, оператор += вынужден будет напрасно соз- дать и уничтожить временный объект, предназначенный для хранения промежуточ- ного результата оператора +. Упражнения раздела 14.3 Упражнение 14.12. Напишите операторы класса Sales_item так, чтобы + фактически реали- зовал сумму, а оператор += лишь обращался к нему. Укажите недостатки этого подхода по срав- нению со способом, описанным в этом разделе.
Глава 14. Перегрузка операторов и преобразования 549 Упражнение 14.13. Укажите, какие другие арифметические операторы могли бы понадобиться классу Sales_item (если они нужны)? 14.3.1. Операторы равенства Обычно классы языка C++ используют оператор равенства для проверки эквива- лентности объектов. То есть, как правило, он осуществляет сравнение каждой пере- менной-члена обоих объектов и признает их равными, если все значения одинаковы. В соответствии с этой концепцией, оператор равенства проектируемого класса Sales item должен сравнить ISBN двух объектов, а также значения их остальных переменных, inline bool operator==(const Sales_item &lhs, const Sales_item &rhs) { // должен быть дружественным для класса Sales_item return Ihs.units_sold == rhs.units_sold && Ihs.revenue == rhs.revenue && Ihs.same_isbn(rhs); inline bool operator!=(const Sales_item &lhs, const Sales_item &rhs) return I(Ihs rhs); // I= определен на базе operator== Определение этих функций тривиально. Однако важнее всего принципы, кото- рые здесь используются. Если в классе определен оператор ==, то два объекта могут содержать одинако- вые данные. Если в классе определен оператор, позволяющий выяснить равенство двух объ- ектов данного класса, его функция должна иметь имя operator==. Не стоит изобретать для нее другое имя, поскольку пользователи ожидают, что для срав- нения объектов можно использовать именно оператор ==. Кроме того, это гораз- до проще, чем каждый раз запоминать новые имена. Если в классе определен оператор ==, имеет смысл определить и оператор operator! =. Пользователи вполне резонно будут полагать, что если они могут выяснить равенство, то они смогут выяснить и неравенство. Определяя операторы равенства и неравенства почти всегда имеет смысл ис- пользовать один из них для создания другого. Один оператор должен фактиче- ски сравнивать объекты, а второй — использовать его в своих целях. Классы, в которых определен оператор operator==, гораздо проще использовать со AanjSЦ стандартной библиотекой. Некоторые алгоритмы, такие как find, по умолчанию исполь- Дзуют оператор ==. Если оператор == в классе определен, такие алгоритмы к нему можно применять без всякой дополнительной подготовки.
550 Часть III. Абстракция, классы и данные 14.3.2. Операторы отношения Классы, для которых определен оператор равенства, зачастую обладают операто- рами отношения. В частности, это связано с тем, что ассоциативные контейнеры и некоторые из алгоритмов используют оператор меньше (operators ). Вполне резонно предположить, что класс Sales_item должен поддерживать операторы отношения, хотя это и не обязательно. Причины не столь очевидны, по- этому рассмотрим их подробнее. Как будет продемонстрировано в главе 15, “Объектно-ориентированное про- граммирование”, для хранения транзакций класса Sales_item может быть исполь- зован ассоциативный контейнер. Помещая объекты в контейнер, их следует упоря- дочить по ISBN, чтобы впоследствии уже не возвращаться к этой проблеме. Но если определить оператор operators для сравнения только ISBN, может воз- никнуть противоречие с уже определенным оператором ==. Если есть две транзакции с одинаковым ISBN, ни одна из них не будет меньше другой. Но если количество про- данных экземпляров у них будет разным, такие объекты окажутся не равны. Однако объекты, ни один из которых не меньше другого, логично было бы считать равными. Поскольку логически определение оператора < не всегда согласуется с определе- нием оператора ==, его лучше вообще не определять. Как будет продемонстрировано в главе 15, “Объектно-ориентированное программирование”, для сравнения объек- тов класса Sales_item при сохранении в ассоциативном контейнере, можно ис- пользовать отдельную именованную функцию. Ассоциативные контейнеры (а также некоторые из алгоритмов) по умолчанию используют оператор <. Как правило, операторы отношения, подобно операторам равенства, имеет смысл определять как обычные функции (не члены класса). 14.4. Операторы присвоения В разделе 13.2 (стр. 512) уже рассматривалось присвоение одного объекта класса другому объекту того же класса. В виде параметра оператор присвоения получает объект класса. Обычно параметр имеет тип константной ссылки на класс, но может иметь и тип класса. Если такой оператор не определен явно, он будет синтезирован компилятором. Оператор присвоения класса должен быть его членом, чтобы компи- лятор не создавал его синтезируемую версию. Дополнительные операторы присвоения могут быть определены для класса, но они должны отличаться типом правого операнда. Например, библиотечный класс string обладает тремя версиями оператора присвоения. Кроме оператора присвое- ния, которому в качестве правого операнда передают константную ссылку на объект класса string, в классе string определен оператор присвоения, получающий сим- вольную строку в стиле С или тип char как правый операнд. Их можно использо- вать следующим образом. string car ("Volks") car = "Studebaker" // string = const char* string model; model = 'T'; // string = char
Глава 14. Перегрузка операторов и преобразования 551 Для обеспечения этих операций, класс string содержит следующие функции-члены. // операторы присвоения класса string class string { public: string& operator=(const string &) ; strings operator=(const char *); strings operator=(char); } ; ' Операторы присвоения могут быть перегружены. В отличие от составных операторов ;1 присвоения, каждый оператор присвоения, независимо от типа параметра, должен быть ^Х/ определен как функция-член. Оператор присвоения должен возвращать ссылку на значение указателя this Операторы присвоения класса string возвращают ссылку на класс string, что соответствует поведению операторов присвоения встроенных типов. Кроме того, по- скольку оператор присвоения возвращает ссылку, нет никакой необходимости соз- давать и впоследствии удалять временные копии результирующего объекта. Обычно возвращаемое значение является ссылкой на левый операнд. Рассмотрим, например, определение составного оператора присвоения класса Sales item. // подразумевается, что оба объекта относятся к тому же isbn Sales_item& Sales_item::operator+=(const Sales_item& rhs) { units_sold += rhs.units_sold; revenue += rhs.revenue; return *this; } Обычно операторы присвоения и составные операторы присвоения должны возвра- щать ссылку на левый операнд. Упражнения раздела 14.4 Упражнение 14.14. Определите версию оператора присвоения класса Saies_item, который позволит присвоить значение переменной-члену isbn. Упражнение 14.15. Определите оператор присвоения класса CheckoutRecord, представлен- ного в упражнениях из раздела 14.2 (стр. 543). Упражнение 14.16. Нужно ли в классе CheckoutRecord определять другие операторы при- своения? Если да, объясните, какие типы операндов должны быть использованы и почему. Реали- зуйте операторы присвоения для этих типов. 14.5. Оператор индексирования Классы, которые представляют собой контейнеры, способные возвращать от- дельные элементы, обычно обладают оператором индексирования (operator []).
552 Часть III. Абстракция, классы и данные Примерами библиотечных классов, в которых определен оператор индексирования, являются классы string и vector. Оператор индексирования должен быть определен как функция-член класса. Обеспечение доступа для чтения и записи Одной из сложностей при определении оператора индексирования является то, что он должен правильно работать, будучи как левым, так и правым операндом опе- ратора присвоения. Чтобы присутствовать слева от оператора присвоения, он дол- жен возвращать 1-значение, которое можно получить определив тип возвращаемого значения как ссылку. То есть возвращающий ссылку оператор индексирования при- меним с обеих сторон оператора присвоения. Кроме того, оператор индексирования имеет смысл адаптировать как к констант- ным, так и к неконстантным объектам. Но при применении к константному объекту должна быть возвращена константная ссылка, а она непригодна для использова- ния в качестве объекта, которому присваивается значение. Ге Обычно в обладающем оператором индексирования классе определяют две версии: неконстантную, которая возвращает неконстантный элемент, и константную, которая (ем возвращает константную ссылку. Прототип оператора индексирования В приведенном ниже классе определен оператор индексирования. Для простоты предполагается, что данные класс Foo хранит в векторе типа vectore int >. Foo(): data(lOO) { for (int i = 0; i !- 100; ++i) data[i] = i; } int ^operator[](const size_t); const int &operator[](const size_t) const; // другие члены интерфейса private: vector<int> data; // другие данные-члены и закрытые вспомогательные функции Сам оператор индексирования можно представить примерно следующим образом. return data[index]; // нет проверки диапазона индекса } const int& Foo::operator[](const size_t index) const { return data[index]; // нет проверки диапазона индекса Упражнения раздела 14.5 Упражнение 14.17. Определите оператор индексирования, который возвращает имя из вектора wait_iist класса checkoutRecord из упражнения раздела 14.2 (стр. 545).
Глава 14. Перегрузка операторов и преобразования 553 Упражнение 14.18. Укажите все преимущества и недостатки реализации данного оператора ин- дексирования. Упражнение 14.19. Предложите альтернативные способы определения данного оператора. 14.6. Операторы доступа к членам класса Чтобы создать класс, подобный указателю, например итератор, язык C++ позво- ляет перегружать операторы обращения к значению (*) и стрелки (- >). Оператор стрелка (arrow) должен быть определен как функция-член класса. Оператор 1 обращения к значению (dereference) необязательно должен быть членом класса, но, как правило, его тоже определяют как функцию-член. Создание безопасного указателя Операторы обращения к значению и стрелки зачастую используются в классах, реализующих интеллектуальный указатель (раздел 13.5.1, стр. 525). Предположим, например, что необходимо создать класс, объект которого будет имитировать указа- тель на объект класса Screen, описанного в главе 12, “Классы”. Присвоим этому классу имя screenPtr. Класс screenPtг будет подобен уже описанному классу HasPtr. Ожидается, что пользователи класса screenPtг будут передавать ему указатель на объект клас- са Screen, созданный в динамически распределяемой памяти. Объект класса screenPtг будет хранить этот указатель и удалять основной объект в момент удале- ния последнего объекта класса screenPtr. Кроме того, класс screenPtr не будет обладать стандартным конструктором. Это гарантирует, что объект класса screenPtr всегда будет относиться к объекту класса Screen. В отличие от встроенного указа- теля, несвязанный объект класса screenPtr не может существовать. Таким обра- зом, приложения могут использовать объекты класса screenPtr без проверки того, относятся ли они к существующему объекту класса Screen. Подобно классу HasPtr, класс screenPtr будет обладать счетчиком пользова- телей. Таким образом, необходимо определить вспомогательный класс, который бу- дет хранить указатель и связанный с ним счетчик пользователей. // закрытый класс, используемый только классом ScreenPtr class ScrPtr { friend class ScreenPtr; Screen *sp; size_t use; ScrPtr(Screen *p): sp(p), use(l) { } -ScrPtr() { delete sp; } } ; Этот класс похож на класс U_Ptr и решает ту же задачу. Объект класса scrPtr содержит указатель и счетчик его пользователей. Класс screenPtr объявлен дру- жественным, чтобы он мог обращаться к счетчику пользователей. Манипулировать счетчиком пользователей будет класс screenPtr. / * * Интеллектуальный указатель: пользователь передает указатель на * созданный в динамически распределяемой памяти объект класса
554 Часть III. Абстракция, классы и данные * Screen, который будет автоматически удален при удалении * последнего объекта класса ScreenPtr class ScreenPtr { public: // стандартного конструктора нет: объект класс ScreenPtrs // должен быть связан с основным объектом ScreenPtr(Screen *р): ptr(new ScrPtr(p)) { } // скопировать переменные-члены и увеличить значение счетчика // пользователей ScreenPtr(const ScreenPtr &orig): ptr(orig.ptr) { ++ptr->use; } ScreenPtr& operator=(const ScreenPtr&); // если счетчик пользователей равен нулю, удалить объект ScrPtr -ScreenPtr() { if (--ptr->use -= 0) delete ptr; } private: ScrPtr *ptr; // указывает на класс счетчика !/ пользователей ScrPtr Поскольку стандартного конструктора нет, каждый объект класса screenPtr дол- жен быть создан при передаче инициализирующего значения. Инициализирующим зна- чением может быть другой объект класса screenPtr или указатель на расположенный в динамической памяти объект класса Screen. Чтобы сохранить этот указатель и свя- занный с ним счетчик пользователей, конструктор создает новый объект класса scrPtr. Попытка создать объект класса screenPtr без инициализирующего значения приведет к ошибке. ScreenPtr Pl; // ошибка: класс ScreenPtr не имеет // стандартного конструктора ScreenPtr ps(new Screen(4, 4)); // ok: ps указывает на копию // объекта myScreen Поддержка операторов, характерных для указателя Фундаментальными операторами указателя являются обращение к значению и стрелка. Снабдить класс этими операторами можно следующим образом. class ScreenPtr { public: // конструктор и функции управления копированием как прежде Screen &operator*() { return *ptr->sp; } Screen *operator->() { return ptr->sp; } const Screen ^operator*() const { return *ptr->sp; } const Screen *operator->() const { return ptr->sp; } private: ScrPtr *ptr; // указывает на класс счетчика // пользователей ScrPtr Перегрузка оператора обращения к значению Оператор обращения к значению (dereference operator) — это унарный оператор. В данном случае он определен как функция-член класса, поэтому явных параметров он не имеет. Оператор возвращает ссылку на тот объект класса Screen, на который указывает объект класса screenPtr. Как и в случае с оператором индексирования, необходимы и константная и не- константная версии оператора обращения к значению. Они отличаются типом воз-
Глава 14. Перегрузка операторов и преобразования 555 вращаемого значения: константная версия возвращает ссылку на константу, что по- зволяет предотвратить случайное изменение пользователем основного объекта. Перегрузка оператора стрелки Оператор стрелка (operator arrow) несколько необычен. Он может быть бинар- ным оператором, получающим объект и имя члена класса, к значению которого осу- ществляется обращение. Вопреки правилам, оператор стрелки не получает никаких явных параметров. Второго параметра нет потому, что правый операнд оператора - > не является вы- ражением. Это скорее идентификатор, который соответствует члену класса. Не суще- ствует вполне очевидного и эффективного способа передачи идентификатора функции как параметра. Вместо этого компилятор выполняет работу по выборке члена класса. Правила приоритета сделают запись point->action () ; эквивалентной записи (point - >action) () ;. Другими словами, необходимо получить результат вычисления выражения point - >act ion. Компилятор обрабатывает этот код следующим образом. 1. Если point — указатель на объект класса, который имеет член по имени action, компилятор создаст код обращения к члену action данного объекта. 2. В противном случае, если point — объект класса, в котором определен опера- тор operator->, запись point->action будет воспринята как эквивалент point. operator-> () ->action. То есть сначала будет выполнен оператор operator - > () для объекта point, а затем повторены три этих этапа, с исполь- зованием результата выполнения оператора operator- > для объекта point. 3. В противном случае код является ошибочным. Применение перегруженного оператора стрелки Объект класса screenPtr можно использовать для доступа к членам класса Screen следующим образом. screenPtr р(&myScreen); // копирует основной объект класса Screen p->display(cout); Поскольку р это объект класса screenPtr, смысл выражения p->display эквива- лентен (р. operator- > () ) - >display. При обработке выражения р. operator- > () происходит вызов оператора operator-> класса screenPtr, который возвращает указатель на объект класса Screen. Этот указатель используется для доступа и за- пуска функции-члена display () того объекта, на который указывает объект класса screenPtr. Ограничения на возвращение из перегруженного оператора стрелки Перегруженный оператор стрелки должен возвратить либо указатель на объект класса, либо объект класса, в котором определен его собственный оператор стрелки. Если типом возвращаемого значения является указатель, к этому указателю при- меняется встроенный оператор стрелки. Компилятор обращается к значению указа- теля и выбирает указанный член класса из полученного в результате объекта. Если
556 Часть III. Абстракция, классы и данные в классе, на объект которого указывает указатель, этот член не определен, происхо- дит ошибка компиляции. Если возвращаемое значение является другим объектом типа класса (или ссыл- кой на такой объект), оператор применяется рекурсивно. Компилятор проверяет, имеет ли класс возвращенного объекта член в виде оператора стрелки, и если это так, применяет данный оператор. В противном случае компилятор сообщает об ошибке. Этот процесс продолжается до тех пор, пока не будет возвращен либо указатель на объект с данным членом класса, либо некое другое значение (тогда код будет при- знан ошибочным). Упражнения раздела 14.6 Упражнение 14.20. В наброске класса screenPtr был объявлен, но не определен оператор присвоения. Реализуйте оператор присвоения для класса screenPtr. Упражнение 14.21. Определите класс, который содержит указатель на объект класса screenPtr. Создайте для этого класса перегруженный оператор стрелки. Упражнение 14.22. Чтобы выяснить равенство или неравенство двух указателей, в классе интел- лектуального указателя, вероятно, потребуется определить операторы равенства и неравенства. Добавьте ЭТИ операторы В класс screenPtr. 14.7. Операторы инкремента и декремента Оператор инкремента (increment) (++) и декремента (decrement) (--) обычно реализуют для таких классов итераторов, которые обеспечивают поведение, подоб- ное указателю на элементы последовательности. Например, можно определить класс, который, являясь указателем на массив, обеспечивает проверку доступа к его элементам. В идеале, этот класс проверки указателя должен быть применим к масси- вам любого типа, как будет описано в главе 16, “Шаблоны и общее программирова- ние”, когда дело дойдет до шаблонов классов. А сейчас создадим класс, работающий с массивами целых чисел. * класс интеллектуального указателя: проверяет доступ к элементам * и передает исключение out_of_range при попытке доступа к * несуществующему элементу * создает и удаляет массив пользователь class CheckedPtr { public: // стандартного конструктора нет: объект класс CheckedPtrs // должен быть связан с основным объектом CheckedPtr(int *b, int *e): beg(b), end(e), curr(b) { } // функции обращения к значению и инкремента private: int * int* beg; end; curr ; // указатель на начало массива // следующий элемент после конца массива // текущая позиция внутри массива Подобно классу screenPtr, этот класс не имеет стандартного конструктора. При создании объекта класса CheckedPtr, пользователь должен предоставить ука-
Глава 14. Перегрузка операторов и преобразования 557 затель на массив. Класс CheckedPtr содержит три переменные-члена: beg, указа- тель на первый элемент массива; end, указатель на следующий элемент после конца массива; и curr, указатель на текущий элемент массива. Конструктор получает два указателя: на начало массива и на следующий элемент после его конца. Этими указателями конструктор инициализирует переменные- члены beg и end. Переменная-член curr инициализируется указателем на первый элемент. Определение операторов инкремента и декремента В языке C++ не требуется, чтобы операторы инкремента или декремента были обя- зательно членами класса. Но поскольку эти операторы изменяют состояние объекта, ^комеидуем Для которого они применяются, их имеет смысл сделать членами его класса. Однако прежде чем определять перегруженные операторы инкремента и декре- мента для класса CheckedPtr, необходимо рассмотреть еще одну тему. Для встро- енных типов уже определены префиксная и постфиксная версии операторов инкре- мента и декремента. Нет ничего удивительного в том, что для собственных классов также понадобится определить префиксные и постфиксные версии этих операторов. Сначала рассмотрим префиксные версии, а затем реализуем и постфиксные. Определение префиксных версий операторов инкремента и декремента Объявления префиксных версий операторов'могут выглядеть следующим образом. class CheckedPtr { public: CheckedPtr& operator++(); CheckedPtr& operator--(); // другие члены, как и прежде }; // префиксные операторы Чтобы соответствовать встроенным, префиксные операторы должны возвращать ссылку на объект после инкремента или декремента. Проверяя равенство значений переменных-членов curr и end, этот оператор ин- кремента гарантирует, что с его помощью нельзя выйти за границу массива. Если инкремент переместит указатель curr на позицию end, оператор передаст исключе- ние out_of_range, в противном случае произойдет приращение значения пере- менной-члена curr и возвращение ссылки на объект. // префикс: возвращает ссылку на объект после инкремента // или декремента CheckedPtr& CheckedPtr::operator++() { if (curr == end) throw out_of_range ("increment past the end of CheckedPtr"); ++curr return *this; // переместить текущую позицию
558 Часть III. Абстракция, классы и данные Оператор декремента ведет себя точно так же, за исключением того, что он уменьшает значение переменной-члена curr и проверяет, не равно ли оно значению переменной-члена beg. CheckedPtr& CheckedPtr::operator--() { if (curr == beg) throw out_of_range ("decrement past the beginning of CheckedPtr"); --curr; // переместить текущую позицию на один элемент назад return *this; } Дифференциация префиксных и постфиксных операторов При определении префиксных и постфиксных операторов возникает одна про- блема: каждый из них получает одинаковое количество параметров того же типа. При обычной перегрузке невозможно отличить префиксную и постфиксную версии оператора. Чтобы решать эту проблему, функциям постфиксных операторов передают до- полнительный (неиспользуемый) параметр типа int. При использовании пост- фиксного оператора, компилятор присваивает этому параметру аргумент 0. Хотя постфиксная функция вполне может использовать этот дополнительный параметр, как правило, так не поступают. Этот параметр не нужен для работы, обычно выпол- няемой постфиксным оператором. Его основная задача заключается в том, чтобы от- личить определение постфиксной версии функции от префиксной. Определение постфиксных операторов Теперь в класс CheckedPtr можно добавить постфиксные операторы. class CheckedPtr { public: // инкремент и декремент CheckedPtr operator++(int); CheckedPtr operator--(int); // другие члены как и прежде } ; // постфиксные операторы Для совместимости со встроенными операторами, постфиксные операторы должны возвращать прежнее значение (существовавшее до декремента или инкремента). Оно должно быть возвращено как значение, а не как ссылка. Постфиксные операторы могут быть реализованы следующим образом. // постфикс: инкремент/декремент объекта, но возвратить следует / / неизмененное значение CheckedPtr CheckedPtr::operator++(int) { // здесь проверка не нужна, ее выполнит префиксный инкремент CheckedPtr ret(*this); // сохранить текущее значение ++*this; // на один элемент вперед, проверку // осуществляет оператор инкремента return ret; // возврат сохраненного значения }
Глава 14. Перегрузка операторов и преобразования 559 CheckedPtr CheckedPtr::operator--(int) ( // здесь проверка не нужна, ее выполнит префиксный декремент CheckedPtr ret(*this); // сохранить текущее значение --*this; // на один элемент назад, проверку // осуществляет оператор декремента return ret; // возврат сохраненного значения } Постфиксные версии операторов несколько отличаются от префиксных. Они должны запоминать текущее состояние объекта перед его изменением. В этих опера- торах создается локальный объект класса CheckedPtr, который инициализируется как копия объекта, на который указывает указатель this. То есть ret — это копия данного объекта в исходном состоянии. Сохраняя копию текущего состояния объекта, оператор вызывает его собствен- ный префиксный оператор, который и осуществляет, соответственно, инкремент или декремент. ++*this Это вызов префиксного оператора инкремента для данного объекта класса CheckedPtr. Он проверяет, допустимо ли приращение, а затем либо увеличивает значение переменной-члена curr, либо передает исключение. Если исключение пе- редано не было, функция постфиксного инкремента завершает работу и возвращает исходную копию объекта, хранимую в переменной ret. Таким образом, текущее значение объекта увеличится, но возвращено будет прежнее, неизменное значение. Поскольку эти операторы используют вызов префиксных версий, нет никакой необходимости в проверке того, что значение переменной-члена curr соответствует допустимому диапазону. Проверка и передача исключения (при необходимости) происходят внутри соответствующего префиксного оператора. Поскольку параметр типа int не используется, имя ему присваивать не нужно. Явный вызов постфиксных операторов Как было продемонстрировано на стр. 539, перегруженный оператор можно вы- звать явно, а не использовать его в составе выражения. При явном вызове функции постфиксной версии необходимо передать значение для целочисленного аргумента. CheckedPtr parr(ia, ia + size); // ia указывает на массив целых // чисел parr.operator++(0); // вызов постфиксного II оператора operator++ parr.operator++(); // вызов префиксного // оператора operator++ Переданное значение обычно игнорируется, но оно позволяет предупредить ком- пилятор о том, что требуется именно постфиксная версия оператора. Ш Обычно имеет смысл определять и префиксную, и постфиксную версию операто- ру^ , ра. Классы, в которых определены только префиксные или только постфиксные /Рюмеидуем версии оператора, весьма удивят пользователя, который привык использовать любую из форм.
560 Часть III. Абстракция, классы и данные Упражнения раздела 14.7 Упражнение 14.23. Класс CheckedPtr реализует указатель на массив целых чисел. Определи- те для этого класса перегруженные операторы индексирования и обращения к значению. Опера- торы должны гарантировать корректность объекта класса CheckedPtr, т.е. недопустимость об- ращения к значению или применение индекса к элементу после конца массива. Упражнение 14.24. Должны ли определенные в предыдущем упражнении операторы обращения к значению и индексирования “пресекать” попытки индексирования и обращения к значению эле- мента перед началом массива? Объясните ответ. Упражнение 14.25. Чтобы вести себя подобно указателю на массив, класс CheckedPtr дол- жен реализовать операторы равенства и отношения, позволяющие выяснить, равны ли два объекта класса CheckedPtr между собой или один из них меньше другого. Добавьте эти операторы в класс CheckedPtr. Упражнение 14.26. Определите для класса screenPtr операторы суммы и разницы, реали- зующие арифметические операции над указателями (раздел 4.2.4, стр. 147). Упражнение 14.27. Укажите весе преимущества и недостатки разрешения передачи конструктору класса CheckedPtr пустого массива в качестве аргумента. Упражнение 14.28. Константные версии операторов инкремента и декремента не были опреде- лены. Почему? Упражнение 14.29. Не был также реализован оператор стрелки. Почему? Упражнение 14.30. Создайте версию класса CheckedPtr для массива объектов класса screen. Реализуйте для этого класса перегруженные операторы инкремента, декремента, обращения к значению и стрелки. 14.8. Оператор вызова функции объекта Оператор вызова функции (function-call operator) может быть перегружен. Как правило, оператор обращения (call operator) перегружают для тех классов, которые предоставляют некую функцию. Например, можно определить структуру по имени abslnt, которая инкапсулирует функцию преобразования значения типа int в его абсолютное значение. Это очень простой класс. Он содержит только одну функцию: оператор вызова функции. Этот оператор получает один параметр и возвращает абсолютное значение полученного аргумента. Для применения оператора обращения достаточно передать объекту класса спи- сок аргументов, как и при вызове функции. int i = -42; abslnt absObj; // объект, для которого определен оператор вызова // функции unsigned int ui = absObj(i); // вызов abslnt::operator(int) Несмотря на то, что absObj — это объект, а не функция, его вполне можно “вызвать”. В результате будет выполнен перегруженный оператор обращения, определенный
Глава 14. Перегрузка операторов и преобразования 561 в классе объекта absObj. Этот оператор получает значение типа int, обрабатывает его и возвращает полученное абсолютное значение. Оператор вызова функции следует объявить как функцию-член. В классе можно опреде- 1 лить несколько версий оператора обращения, каждый из которых отличается количеством / ^*>7 или типом параметров. Объект класса, в котором определен оператор обращения, зачастую называют объектом функции (function object), т.е. объектом, действующим подобно функции. Упражнения раздела 14.8 Упражнение 14.31. Определите объект функции, реализующий условный оператор (объект функ- ции должен получать три параметра). Он должен проверить значение первого параметра и, если оно соответствует логическому значению true, возвратить значение второго параметра, а в про- тивном случае — значение третьего параметра. Упражнение 14.32. Сколько операндов может получать перегруженный оператор вызова функции? 14.8.1. Использование объектов функции с библиотечными алгоритмами Объекты функции чаще всего используются как аргументы для общих алго- ритмов. Вернемся, например, к проблеме, которая была решена в разделе 11.2.3 (стр. 427). Рассматриваемая программа анализировала слова в наборе статей и под- считывала, сколько из них имели размер шесть и более букв. При этом использова- лась функция, которая проверяла длину переданной строки (больше или равна шес- ти символам). // выяснить, равна ли длина данного слова 6 или более символам bool GT6(const string &s) { return s.size() >= 6; } Функция GT 6 () использовалась как аргумент алгоритма count_if при подсчете количества слов, для которых функция GT6 () возвращала значение true. vector<string>::size_type wc = count_i f(words.begin(), words.end(), GT6); Объекты функции могут оказаться гибче функций Предыдущая реализация имела серьезную проблему: число шесть в определении функции GT6 () было задано жестко. Алгоритм count_if использует функцию, пе- редавая ей один параметр и получая из нее значение типа bool. В идеале, функции следовало бы передать стороку и искомый размер. Таким образом можно было бы использовать тот же код для подсчета строк разных размеров. Определив GT6 как класс, обладающий оператором вызова функции, можно до- биться необходимой гибкости. Присвоим этому классу имя GT_cls, чтобы оно от- личалось от имени функции. // выясняет, является ли длина данного слова больше, чем указано // в связанном объекте
562 Часть III. Абстракция, классы и данные class GT_cls { public: GT_cls(size_t val = 0): bound(val) { } bool operator()(const string &s) { return s.size() >= bound; } private: std::string::size_type bound; } ; Конструктор этого класса получает целочисленное значение и сохраняет его в переменной-члене bound. Если никакого значения не передано, конструктор ис- пользует переменную bound с нулевым значением. В классе определен также опера- тор обращения, который получает строку и возвращает тип bool. Этот оператор сравнивает длину переданной строки со значением, хранимым в его переменной- члене bound. Применение объекта функции класса GT_c 1 в Можно, конечно, применить тот же способ подсчета, что и прежде, но на сей раз используется объект класса GT_cls, а не функция GT6 (). cout << count_if(words.begin(), words.end(), GT_cls(6)) << " words 6 characters or longer" « endl; Здесь функции count_if () передан временный объект класса GT_cls, а не функция GT6O. Этот временный объект инициализирован значением 6, которое конструктор класса GT_cls сохранит в своей переменной-члене bound. Теперь при каждом вызове функции count_if О ее параметром будет оператор обращения к функции класса GT_cls. Этот оператор обращения выясняет размер переданной в качестве аргумента строки и сравнивает его со значением переменной-члена bound. Используя объект функции, программу можно легко переделать так, чтобы про- верять другие значения. Достаточно изменить аргумент в конструкторе объекта, пе- редаваемого функции count_i f (). Например, можно подсчитать количество слов размером пять или больше символов, переделав программу следующим образом. cout << count_if(words.begin(), words.end(), GT_cls(5)) « " words 5 characters or longer" << endl; Можно даже подсчитать количество слов размером от одного символа до десяти. i = 0; i != 11; ++i) count_if(words.begin(), " words " << i " characters or longer" words.end() GT(i)) endl; Попытка создать такую программу при помощи функций, а не объекта функции, потребовала бы наличия десяти разных функций, каждая из которых проверяла бы свое значение. Упражнения раздела 14.8.1 Упражнение 14.33. Используя библиотечные алгоритмы и класс gt_c1s, напишите программу поиска первого элемента последовательности, значение которого больше указанного. Упражнение 14.34. Напишите класс объекта функции, подобный классу gt_c1s, который про- веряет равенство двух значений. Используйте этот объект и библиотечные алгоритмы для созда- ния программы, которая заменяет все экземпляры данного значения в последовательности.
Глава 14. Перегрузка операторов и преобразования 563 Упражнение 14.35. Напишите класс объекта функции, подобный классу gt_c1s, который про- веряет, соответствует ли длина переданной строки значению связанного объекта. Используйте этот объект в программе раздела 11.2.3 (стр. 427) для сообщения о количестве слов во введенном тексте, размеры которых составляют от 1 до 10 символов включительно. Упражнение 14.36. Переделайте предыдущую программу так, чтобы она сообщала количество слов, размер которых составляет от 1 до 9 символов, а также 10 или более символов. 14.8.2. Библиотечные объекты функций В стандартной библиотеке определен набор арифметических, реляционных и логи- ческих классов объектов функций, которые перечислены в табл. 14.3. В библиотеке оп- ределен также набор адаптеров функций, которые позволяют специализировать или дополнять классы объектов функций, определенных в библиотеке, либо самостоятель- но. Библиотечные классы объектов функции определены в заголовке functional. Таблица 14.3. Библиотечные объекты функций Классы арифметических объектов функции plus<Type> minus<Type> multiplies<Type> divides<Type> modulus<Type> negate<Type> применяет + применяет - применяет * применяет / применяет % применяет - Классы реляционных объектов функции equal to<Type> применяет == no t _е qua l_t о < Тур е > greatег<Туре> greater_equal<Type> less<Type> less_equal<Type> применяет ! = применяет > применяет >= применяет < применяет <= Классы логических объектов функции logical_and<Type> применяет && logical_or<Type> logical_not<Type> применяет | применяет ! Каждый класс предоставляет определенный оператор Каждый из библиотечных классов объектов функций предоставляет оператор, т.е. в каждом классе определен оператор обращения, который осуществляет одно- именную операцию. Например, шаблон класса plus предоставляет оператор суммы. Оператор обращения шаблона plus применяет к паре операндов оператор +.
564 Часть III. Абстракция, классы и данные В разных классах объектов функций определены операторы обращения, ко- торые выполняют различные операции. Подобно тому, как в классе plus опре- делен оператор обращения, реализующий сумму (оператор +), оператор обраще- ния класса modulus реализует бинарный оператор %, а класс equal_to приме- няет оператор == и т.д. Существует два шаблона класса объектов унарных функций (unary function- object): унарный минус (negate<Type>) и логическое NOT (logical_not<Type>). Остальные библиотечные шаблоны представляют объекты бинарных функций (binary function-object), которые соответствуют бинарным операторам. Операторы обраще- ния, определенные для бинарных операторов, ожидают два параметра данного типа; унарные типы объектов функций определяют оператор обращения, получающий один аргумент. Тип шаблона определяет тип операндов Каждый из типов объектов функции является шаблоном класса, которому пере- дается определенный тип. Как уже было продемонстрировано на примере последо- вательных контейнеров типа vector, шаблон класса — это класс, который может быть применен для нескольких типов. Тип шаблона для класса объекта функции оп- ределяет тип параметра оператора обращения. Например, выражение plus<string> применит оператор суммы класса string к объектам класса string. Операндами выражения plus<int> будут целые числа, выражение plus<Sales_item> применит оператор + к объектам класса Sales_ item и т.д. plus<int> intAdd; // объект функции, способный сложить // два значения типа int negate<int> intNegate; // объект функции, способный изменить знак !/ значения типа int // использование оператора intAdd::operator(int, int) для II сложения чисел 10 и 20 int sum = intAdd(10, 20); // sum =30 II использование оператора intNegate::operator(int) для создания II числа -10 как второго параметра // выражения intAdd::operator(int, int) sum = intAdd(10, intNegate(10)); // sum = 0 Применение библиотечного объекта функции с алгоритмами Объекты функций зачастую используются для переопределения заданного по умолчанию оператора, применяемого в алгоритме. Например, для сортировки со- держимого контейнера в порядке возрастания алгоритм sort по умолчанию исполь- зует оператор operators Чтобы отсортировать содержимое контейнера в порядке убывания, алгоритму можно передать объект функции greater. Этот класс создаст оператор обращения, который вызовет оператор > указанного класса. Если вектор svec имеет тип vector< strings», следующий код отсортирует его содержимое по убыванию. // передает временный объект функции, который применяет // оператор > к двум строкам sort (svec.begin() , svec.endO, greater<string>() ) ;
Глава 14. Перегрузка операторов и преобразования 565 Как обычно, для обозначения подлежащей сортировке последовательности, функции sort () передано два итератора. Третий аргумент используется для переда- чи функции предиката (раздел 11.2.3, стр. 429), используемого для сравнения элемен- тов. Этот аргумент представляет собой временный объект типа greater<string>, который является объектом функции, применяющей оператор > к двум операндам типа string. 14.8.3. Адаптеры функций для объектов функций Стандартная библиотека предоставляет набор адаптеров функций (function adaptor), позволяющих специализировать и дополнять объекты унарных или бинар- ных функций. Адаптеры функций разделены на две следующие категории. 1. Компоновщики. Компоновщик (binder) — это адаптер функции, который преоб- разует объект бинарной функции в объект унарной функции, связывая один из операндов с переданным значением. 2. Инверторы. Инвертор (negator) — это адаптер функции, который меняет на об- ратное значение, возвращаемое объектом функции предиката. В библиотеке определены два адаптера компоновщика: bindlst и bind2nd. Каждый компоновщик получает объект функции и значение. Как и следовало ожи- дать, адаптер bindlst связывает переданное значение с первым аргументом объек- та бинарной функции, а адаптер bind2nd — со вторым. Например, чтобы подсчитать внутри контейнера количество элементов, значение которых меньше или равно 10, функции count_if () можно было бы передать следующие аргументы. count_i f(vec.begin() , vec.end() , bind2nd(less_equal<int>(), 10)); Третий аргумент функции count_if () использует адаптер функции bind2nd. Этот адаптер возвращает объект функции, который применяет оператор <=, в каче- стве правого операнда которого используется число 10. В результате алгоритм count_if подсчитает количество элементов в исходном диапазоне, значение кото- рых меньше или равно 10. Библиотека предоставляет также два инвертора: notl и not2. Здесь тоже, как и следовало ожидать, адаптер notl инвертирует значение объекта унарной функции предиката, а адаптер not2 — бинарной. Чтобы поменять на обратное значение объекта функции less_equal, можно применить следующий код. count_i f(vec.begin(), vec.end(), notl(bind2nd(less_equal<int>(), 10))); Сначала второй операнд объекта функции less_equal связывается с числом 10, что преобразует бинарный оператор в унарный. Затем, для инвертирования резуль- тата, используется адаптер notl. В результате каждый элемент будет проверен в выражении <= 10. Наконец, полученный результат будет изменен на обратный. Фактически в результате инверсии будет подсчитано количество тех элементов, ко- торые не меньше или равны 10.
566 Часть III. Абстракция, классы и данные Упражнения раздела 14.8.3 Упражнение 14.37. Используя библиотечные объекты и адаптеры функций, определите объекты для следующих целей. (а) Поиск всех значений больше 1024. (Ь) Поиск всех строк, не равных pooh. (с) Умножение всех значений на 2. Упражнение 14.38. В последнем примере обращения к функции count_if О, для инверсии результата применения адаптера bind2nd в выражении bind2nd(iess_equai<int> (), ю), был использован адаптер noti. Почему был использован адаптер noti, а не not2. Упражнение 14.39. Примените вместо класса gt_c1s библиотечные объекты функций для поис- ка слов определенной длины. 14.9. Преобразования и типы классов В разделе 12.4.4 (стр. 490) было продемонстрировано, что неявный конструктор с одним аргументом способен осуществить неявное преобразование. Компилятор использует его в случае, когда необходимо преобразование типа объекта, переданного в качестве аргумента. Такие конструкторы называют преобразованием (conversion) или приведением к типу класса. Кроме определения преобразований в тип класса, можно определить преобразо- вание из типа класса. То есть вполне можно создать оператор, который преобразует объект данного класса в объект другого. Подобно другим функциям преобразования, компилятор может применить ее автоматически. Прежде чем научиться создавать такие функции преобразования, давайте выясним, зачем они нужны. 14.9.1. Зачем нужны функции преобразования Предположим, что необходимо создать класс, назовем его SmallInt, для безопасной работы с маленькими целыми числами. Этот класс позволит определять объекты, кото- рые смогут содержать тот же диапазон значений, что и 8-битовый тип uns igned char, т.е. от 0 до 255. Этот класс отреагирует на попытку использования числа вне этого диапазона, что значительно безопасней встроенного типа unsigned char. Создаваемый класс должен поддерживать все операции, которыми обладает класс unsigned char. В частности, необходимо определить пять арифметических операторов (+, -, *, / и %), соответствующие им составные операторы, четыре опера- тора отношения (<, <=, > и >=) и операторы равенства (== и !=). Таким образом, предстоит определить 16 операторов. Выражения смешанного типа Кроме того, необходимо обеспечить применимость этих операторов в выражени- ях смешанного типа. Например, кроме возможности суммирования двух объектов класса SmallInt, необходимо обеспечить сумму объекта класса SmallInt с объек- том любого из арифметических типов. Каждый из этих операторов следует опреде- лить в трех версиях.
Глава 14. Перегрузка операторов и преобразования 567 int operators- (int, const Smalllnt&) ; int operator+(const Smalllnt&, int); Smalllnt operator+(const Smalllnt&, const Smalllnt&); Поскольку функции преобразования в тип int существуют для всех арифмети- ческих типов, этих трех функций хватит для всех случаев смешанного применения объектов класса Smalllnt. Однако в этом проекте лишь имитируется поведение арифметических функций встроенного целочисленного типа. Здесь не предполага- ется точно выполнять все смешанные операции для типов с плавающей запятой, а также не предусматривается правильное взаимодействие с такими типами, как long, unsigned int или unsigned long. Дело в том, что данный проект предполагает преобразование всех арифметических типов (даже больших, чем int) в тип int. Преобразования уменьшают количество необходимых операторов Даже игнорируя проблему операндов с плавающей запятой и больших целочис- ленных типов, на проекте придется реализовать 48 операторов! К счастью, язык С++ предоставляет механизм, благодаря которому в классе можно определить собствен- ные функции преобразования, применимые к объектам данного класса. Для класса Smalllnt можно определить функцию преобразования из типа Smalllnt в тип int. Определив эту функцию преобразования, остальные операторы (арифметичес- кие, реляционные и равенства) можно не определять. Возможность преобразования в тип int, позволяет использовать объект класса Smalllnt везде, где применим объект типа int. Если возможно преобразование в тип int, вполне допустимо и следующее пре- образование. Smalllnt si (3); si + 3.14159; // преобразовать объект si в объект типа int, // а затем в объект типа double Оно происходит следующим образом. 1. Преобразование объекта si в объект типа int. 2. Преобразование полученного объекта типа int в объект типа double и его сум- ма с литеральной константой типа double (3.14159). В результате получается значение типа double. 14.9.2. Операторы преобразования Оператор преобразования (conversion operator) — это специальный вид функции- члена класса. Он задает способ преобразования значения типа класса в значение не- которого другого типа. Оператор преобразования объявляют в теле класса с исполь- зованием ключевого слова operator, за которым следует указание типа, получае- мого в результате преобразования. class Smalllnt { public: Smalllnt(int i = 0) : val(i) {if (i < 0 II i > 255) throw std::out_of_range("Bad ) operator int() { return val; } Smalllnt initializer");
568 Часть III. Абстракция, классы и данные }; Общий синтаксис функции преобразования имеет следующий вид. operator тип() ; Здесь тип — это имя встроенного типа, класса или типа, определенного при по- мощи ключевого слова typedef. Функции преобразования могут быть определены для любого типа (кроме void), значение которого может быть возвращено функци- ей. В частности, преобразование в тип массива или функции недопустимо. Однако преобразование в тип указателя на данные или функцию, а также ссылочные типы вполне возможны. Функция преобразования должна быть функцией-членом. Для нее не нужно указывать тип возвращаемого значения, а список параметров должен быть пуст. Все следующие объявления ошибочны. // ошибка: не член класса operator int(SmallInt &); }; // ошибка: тип возвращаемого значения // ошибка: список параметров Хотя для функции преобразования не указывают тип возвращаемого значения, каждая из них должна явно возвратить значение именованного типа. Например, функция operator int () возвращает объект типа int, а если определить функ- цию operator Sales_item (), она возвратит объект класса Sales_item и т.д. Обычно операции преобразования не должны изменять объект, который они преоб- разуют. Следовательно, операторы преобразования имеет смысл определять как константные функции-члены. Применение преобразования классов Существующую функцию преобразования компилятор автоматически (раз- дел 5.12.1, стр. 205) применяет в тех же местах, где он использовал бы встроенное преобразование. В выражениях. SmallInt si; double dval; si >= dval // si преобразуется в int, а затем в double В условиях. if (si) // si преобразуется в int, а затем в bool При передаче функции аргументов и возвращении ей значений, int calc(int); SmallInt si; int i = calc(si); // преобразует si в int и вызывает calc()
Глава 14. Перегрузка операторов и преобразования 569 Как операнд перегруженного оператора. // преобразует si в int, а затем применяет для него оператор « cout « si << endl; При явном приведении. int ival; Smalllnt si = 3.541; // вынуждает компилятор привести si к int ival = static_cast<int>(si) + 3; Преобразования типов классов и стандартные преобразования При использовании функции преобразования, преобразуемый тип необязательно должен точно соответствовать необходимому типу. При необходимости, чтобы по- лучить желаемый тип, преобразование типов классов может сопровождаться стан- дартным преобразованием (раздел 5.12.3, стр. 207). Рассмотрим пример сравнения объектов типа Smalllnt и double. Smalllnt si; double dval; si >= dval // si преобразуется в int а затем в double Сначала объект si преобразуется из типа Smalllnt в тип int, а затем значение типа int преобразуется в тип double. Применена может быть только одна функция преобразования типов Функция преобразования типов не может сопровождаться другой функцией преобразова- Зд/^1 ния. Если необходимо несколько преобразований, код считается ошибочным. Предположим, например, что существует другой класс, по имени Integral, объ- ект которого может быть преобразован в объект класса Smalllnt, но не имеет функции преобразования в тип int. // класс, содержащий беззнаковые целочисленные значения class Integral { public: Integral(int i = 0) : val(i) { } operator Smalllnt() const { return val % 256; } private: std::size_t val; Объект класса Integral можно использовать там, где необходим объект класса Smalllnt, но не переменная типа int. int calc(int); Integral intVal; Smalllnt si(intVal); // ok: преобразовать intVal в Smalllnt и // скопировать в si // ok: преобразует si в int и вызывает calc() int i = calc(si); // ok: преобразует si в int и вызывав int j = calc(intVal); // ошибка: нет функции преобразования II из Integral в int При создании объекта si используется конструктор копий класса Smalllnt. Сначала объект intVal преобразуется в объект класса Smalllnt. Для этого ис- пользуется оператор преобразования класса Integral, создающий временное зна-
570 Часть III. Абстракция, классы и данные чение типа Smalllnt. Впоследствии синтезируемый конструктор копий класса Smalllnt использует это значение для инициализации объекта si. Первое обращение к функции calc () тоже пройдет нормально: аргумент si бу- дет автоматически преобразован в тип int и передан функции. Второе обращение ошибочно, поскольку нет прямого способа преобразования объект класса Integral в переменную типа int. Для этого потребуется два преоб- разования: сначала из типа Integral в тип Smalllnt, а затем из типа Smalllnt в тип int. Однако язык C++ позволяет осуществлять только одно преобразование, поэтому такое обращение будет ошибкой. Стандартные преобразования могут предшествовать преобразованию типа При использовании конструктора, выполняющего неявное преобразование (раз- дел 12.4.4, стр. 491), тип его параметра необязательно должен точно соответствовать требуемому типу. Например, следующий код вызывает конструктор Smalllnt (int), определенный в классе Smalllnt, для преобразования объекта sobj типа short в объект типа Smal lint. void calc(Smalllnt); short sobj; // sobj допускает преобразование из short в int II этот объект типа int преобразуется в объект типа Smalllnt при // помощи конструктора Smalllnt(int) calc(sobj); При необходимости, перед вызовом конструктора, осуществляющего преобразо- вание типов, к аргументу может быть применена последовательность стандартных преобразований. Чтобы вызвать функцию calc (), применяется стандартное преоб- разование, позволяющее преобразовать объект dob j1 из типа double в тип int. За- тем происходит вызов конструктора Smalllnt (int), преобразующего результат предыдущего преобразования в тип Smalllnt. Упражнения раздела 14.9.2 Упражнение 14.40. Напишите операторы, которые могут преобразовывать объект класса Saies_item в объекты класса string и double. Какие значения должны были бы возвра- щать эти операторы? Есть ли смысл в этих преобразованиях? Объясните ответ. Упражнение 14.41. Объясните различие между двумя следующими операторами преобразования. class Integral { public: }; Не являются ли эти преобразования чересчур специализированными? Если да, то как их можно сделать более общими? Упражнение 14.42. Определите для класса CheckoutRecord, из упражнений раздела 14.2 (стр. 545), оператор преобразования в тип bool. ' Вероятно, имелся в виду объект sobj типа short. — Примеч. ред.
Глава 14. Перегрузка операторов и преобразования 571 Упражнение 14.43. Объясните, что выполняет оператор преобразования в тип bool. Является ли это преобразование единственно возможным для класса checkoutRecord? Объясните, имеет ли смысл применение этой операции преобразования. 14.9.3. Соответствие аргументов и преобразования В остальной части этой главы рассматривается несколько дополнительных тем. При первом чтении их можно пропустить. Преобразования типов могут быть весьма полезны при реализации и применении классов. Определив для класса SmallInt функции преобразования в тип int, мож- но существенно упростить его реализацию и облегчить использование. Функция преобразования в тип int разрешит пользователям класса SmallInt применять для его объектов все арифметические операторы и операторы отношения. Кроме то- го, пользователи смогут без проблем записывать выражения, в которых объекты класса SmallInt используются совместно с объектами других арифметических ти- пов. Задача разработчика класса существенно упростится, поскольку определив од- ну функцию преобразования можно избежать необходимости определять 48 (или даже больше) перегруженных операторов. Однако функции преобразования типов могут стать неисчерпаемым источником ошибок времени компиляции. Проблемы возникают тогда, когда существует не- сколько способов преобразования одного типа в другой. Когда есть несколько впол- не применимых функций преобразования типов, компилятор должен решить, какую из них следует использовать для данного выражения. В этом разделе будет описано, как правильно подобрать аргумент, соответствующий параметру функции преобра- зования типов. Но сначала рассмотрим, как подбираются аргументы, соответствую- щие параметрам обычных, неперегруженных функций. Корректное применение функций преобразования типов может существенно упростить код и повысить удобство для пользователей. Но когда функций преобразования слиш- ком много, это может привести к необъяснимым ошибкам во время компиляции. Соответствие аргументов множественных операторов преобразования Чтобы продемонстрировать, как функции преобразования типов значений взаи- модействуют с соответствующими функциями, добавим в рассматриваемый класс SmallInt две дополнительные функции преобразования. Добавим также второй конструктор, получающий значение типа double, и второй оператор преобразова- ния, преобразующий объект класса SmallInt в переменную типа double. // не самое лучшее определение класса: II множество конструкторов и операторов преобразования в и из II встроенных типов может привести к проблемам неоднозначности class Smalllnt {
572 Часть III. Абстракция, классы и данные public: // преобразования в тип Smalllnt из int и double Smalllnt(int = 0) ; Smalllnt(double); // преобразования в тип int и double из Smalllnt I/ как правило, крайне неблагоразумно определять функции II преобразования в несколько арифметических типов operator int() const { return val; } operator double() const { return val; } Как правило, предоставление функций преобразования для нескольких встроенных ти- пов является очень плохой идеей. Здесь это сделано преднамеренно, чтобы продемон- стрировать возможные проблемы. Давайте рассмотрим простой случай, когда происходит вызов неперегруженной функции. void compute(int); void fp_compute(double); void extended_compute(long double); Smalllnt si; compute(si); // Smalllnt::operator int () const fp_compute(si); // Smalllnt::operator double 0 const extended_compute(si); // ошибка: неоднозначность При обращении к функции compute () применим любой оператор преобразования. 1. Оператор operator int () точно соответствует типу параметра. 2. Для соответствия типу параметра, оператор operator double () требует стан- дартного преобразования из типа double в тип int. Точное соответствие лучше, чем требующее стандартного преобразования. Сле- довательно, первая последовательность преобразований лучше. Таким образом, для преобразования аргумента будет выбрана функция Smal 1 Int: : operator int (). Второе обращение аналогично первому, при вызове функции fp_compute () может быть использовано любое из преобразований. Однако преобразование в тип double точнее соответствует типу аргумента и не требует никаких дополнительных стандартных преобразований. Последнее обращение к функции extended_compute () неоднозначно. К типу long double применима любая из функций преобразования, но для каждой из них предварительно потребуется осуществить стандартное преобразование. Следова- тельно, ни одна из функций преобразования не лучше другой. Поэтому в данном случае обращение неоднозначно. Соответствие аргументов и преобразования при помощи конструкторов Подобно двум операторам преобразования, вполне могут существовать два кон- структора, применимые для преобразования значений в необходимый тип.
Глава 14. Перегрузка операторов и преобразования 573 Рассмотрим функцию manip (), которой передают аргумент типа Smalllnt. void manip(const Smalllnt &); double d; int i; long 1; manip(d); // ok: применение для преобразования II аргумента конструктора Smalllnt(double) manip(i); // ok: применение для преобразования // аргумента конструктора Smalllnt(int) manip(1); // ошибка: неоднозначность При первом обращении для преобразования переменной d в тип Smalllnt, мо- жет быть использован любой из конструкторов класса Smalllnt. Конструктору для типа int требуется стандартное преобразование переменной d, в то время как кон- структор для типа double точно соответствует типу аргумента. Поскольку точное соответствие лучше стандартного преобразования, будет использован конструктор Smalllnt(double). Во втором обращении все наоборот. Точное соответствие обеспечивает конструк- тор Smalllnt (int) — ему не нужно никаких дополнительных преобразований. В дан- ном случае конструктор Smalllnt (double) потребовал бы предварительного пре- образования переменной i в тип double. Таким образом, в данном обращении при преобразовании аргумента будет использован конструктор для типа int. Третье обращение неоднозначно. Точно типу long не соответствует ни один из конструкторов. Каждый из них потребовал бы предварительного преобразования аргумента. 1. Конструктору Smalllnt (double) необходимо стандартное преобразование (long в double). 2. Конструктору Smalllnt (int) необходимо стандартное преобразование (long в int). Разницы между этими двумя последовательностями преобразований нет, поэто- му данное обращение неоднозначно. Когда применяются два преобразования, определенные конструкторами, при выборе 1 наилучшего соответствия используется приоритет стандартного преобразования (если он есть), необходимого для аргумента конструктора. Неоднозначность в результате определения функции преобразования в двух классах Когда в двух классах определены функции преобразования друг в друга, весьма вероятна неоднозначность. class Integral; class Smalllnt { public: Smalllnt(Integral); // преобразование из Integral в Smalllnt } ; class Integral { public: operator Smalllnt() const; // преобразование из Smalllnt II в Integra1
574 Часть III. Абстракция, классы и данные } ; void compute(SmallInt); Integral int_val; compute(int_val); // ошибка: неоднозначность Аргумент int_val может быть преобразован в тип SmallInt двумя разными спо- собами. Компилятор может использовать конструктор класса SmallInt, получаю- щий объект класса Integral, или оператор преобразования класса Integral, пре- образующий объект класса Integral в объект класса SmallInt. Поскольку обе эти функции одинаково хороши, компилятор не сможет отдать предпочтения ни одной из них, в результате произойдет ошибка. В данном случае, во избежание неоднозначности, следует исключить приведе- ние — ведь оно подразумевает использование либо оператора преобразования, либо конструктора. Вместо этого необходимо явно вызвать оператор преобразования или конструктор. compute(int_val.operator Smalllnt()); // ok: используется оператор // преобразования compute(Smalllnt(int_val)); // ok: используется конструктор II класса Smalllnt Кроме того, казалось бы, неоднозначных преобразований можно избежать очень просто. Например, конструктор класса Smalllnt копирует свой аргумент типа Integral. Если изменить конструктор так, чтобы он получал ссылку на констант- ный объект класса Integral, обращение к функции compute (int_val) переста- нет быть неоднозначным! class Smalllnt { public: Smalllnt(const }; Integral&); Дело в том, что применение конструктора класса Smalllnt предполагает полу- чение ссылки на объект int_val, а при использовании оператора преобразования класса Integral этого дополнительного этапа нет. Такого, казалось бы, незначи- тельного отличия вполне достаточно для выбора в пользу использования оператора преобразования. Наилучший способ избежать неоднозначности или неожиданности — это не созда- вать пары классов, где каждый осуществляет неявное преобразование в другой. Внимание! Не злоупотребляете функциями преобразования. Как и в случае с перегруженными операторами, разумное использование функций преобразования помогает существенно упростить работу разработчика класса и сде- лать полученный класс удобным в применении. Однако здесь есть две потенциальные ловушки: определение слишком большого количества функций преобразования мо- жет привести к неоднозначности кода, а некоторые преобразования могут оказаться скорее вредными, чем полезными. Во избежание неоднозначности, имеет смысл удостовериться в том, что существует как максимум только один способ преобразования одного типа в другой. Для этого следует ограничить количество операторов преобразования. В частности, должен су- ществовать только один способ преобразования во встроенный тип.
Глава 14. Перегрузка операторов и преобразования 575 Когда отсутствует единственный и вполне очевидный способ преобразования объекта класса в необходимый тип, функции преобразования могут только ввести в заблужде- ние. В таких случаях функции преобразования могут быть излишними и даже вред- ными для класса. Предположим, например, что существует класс Date, представляющий данные о дате. Вполне очевидно, что имеет смысл предоставить способ преобразования объекта клас- са Date в объект типа int. По какое значение должна возвращать функция преобра- зования? Она могла бы вернуть дату по юлианскому стилю, которая является поряд- ковым номером текущего дня, где нулевому значению соответствует 1 января. Однако должен ли год предшествовать дню, или он должен следовать за ним' 1 б есть следует ли дат}' 31 января 1986 года представлять как 1986031 или как 311986? В качестве альтернативы, оператор преобразования мог бы возвращать целое число, соответст- вующее количеству дней начиная с некоторой эпохальной даты. Счетчик мог бы счи- тать дни начиная с 1 января 1971 года или некой другой отправной точки. Проблема заключается в том, что при любом выборе использование объектов Date будет приводить к неоднозначности, поскольку не существует единого способа сопос- тавить объект типа Date и значение типа int. В таких случаях функцию преобразо- вания лучше не создавать. Вместо этого следует определить в классе один или не- сколько обычных членов, позволяющих извлекать эту информацию в различных форматах. 14.9.4. Поиск перегруженной функции и аргументы класса Как уже упоминалось, компилятор автоматически применяет функцию преобра- зования или конструктор класса, когда необходимо преобразовать аргумент, пере- данный в функцию. Следовательно, при вызове функции происходит поиск подхо- дящего способа преобразования объекта класса. Поиск перегруженой функции (раз- дел 7.8.2, стр. 295) осуществляется в три этапа. 1. Выяснение набора функций-кандидатов: имена этих функций совпадают с име- нем вызываемой функции. 2. Выбор подходящих функций: типы и количество параметров этих функций- кандидатов совпадают с указанными для вызываемой функции. При выборе подходящих функций компилятор также выясняет, какие операции преобразо- вания (если они есть) понадобятся для преобразования каждого из аргументов. 3. Выбор функции наилучшего соответствия. При выборе наилучшего соответст- вия учитываются все операторы преобразования типов, необходимые для преоб- разования типов аргументов в типы параметров и их приоритет. В набор воз- можных преобразований аргументов в необходимый тип, включаются и функ- ции преобразования класса. Стандартные преобразования после функции преобразования Выбор функции наилучшего соответствия может зависеть от количества преоб- разований типов, необходимых для ее работы.
576 Часть III. Абстракция, классы и данные Если в наборе перегруженных функций подходящими являются две функции, причем ис- кч '9 1 пользующие туже функцию преобразования, при определении той из них, которая будет применена, учитывается приоритет стандартной последовательности преобразований, необходимой для ее работы. В противном случае, если используются разные функции преобразования, выбор наилуч- шего соответствия осуществляется независимо от приоритета необходимых им стандарт- ных преобразований. На стр. 571 был продемонстрирован результат преобразования типов при обра- щении к неперегруженным функциям. Теперь рассмотрим аналогичное обращение, но уже к перегруженой функции. void compute(int); void compute(double); void compute(long double); Предположим, что здесь используется первоначальный класс Smalllnt, в кото- ром определен только один оператор преобразования в тип int. Поэтому при пере- даче функции compute () объекта класса Smalllnt, соответствующей будет вы- брана та версия функции compute (), которой передается аргумент типа int. Допустимы все три версии функции compute (). Версия compute (int) допустима потому, что класс Smalllnt обладает функ- цией преобразования в тип int. Это преобразование точно соответствует типу параметра. Версии compute (double) и compute (long double) тоже вполне допусти- мы, поскольку преобразование в тип int модно сопроводить соответствующим стандартным преобразованием в тип double или long double. Поскольку при использовании некоторых преобразований типов в принципе подходят все три функции, выбор той из них, которая будет применена на самом де- ле, осуществляется с учетом приоритета необходимых стандартных преобразований. Поскольку точное соответствие лучше осуществляемого с применением стандартно- го преобразования, наиболее подходящей будет выбрана функция compute (int). Стандартная последовательность преобразований, следующая после преобразования ти- 1 па класса, используется как критерий выбора только тогда, когда две последовательно- сти преобразований используют одинаковую операцию преобразования. Несколько функций преобразования и поиск перегруженной функции Рассмотрим одну из причин, по которым добавление функции преобразования для типа double является не наилучшей идеей. Если использовать модернизиро- ванную версию класса Smalllnt, в который определены функции преобразования для типов int и double, при вызове функции compute () для значения типа Smalllnt возникнет неоднозначность. class Smalllnt { public: // преобразование в int или double из Smalllnt // конечно, определять функции преобразования для нескольких / / арифметических типов неблагоразумно
Глава 14. Перегрузка операторов и преобразования 577 operator int() const { return val; } operator double() const { return val; } private std::size_t val; }; void compute(int) ; void compute(double); void compute(long double); Smalllnt si; compute (si); // ошибка: неоднозначность В данном случае для преобразования объекта si и вызова версии функции compute (), получающей аргумент типа int, можно использовать оператор operator int (), а можно использовать для преобразования оператор operator double () и версию функции compute (double). Компилятор даже не будет пытаться выяснить разницу между двумя функциями преобразования типов. В частности, даже если одно из обращений требует стандарт- ного преобразования после преобразования типов класса, а другое обеспечивает точное соответствие, компилятор все равно пометит обращение как ошибочное. Для устранения неоднозначности применяется явный вызов конструктора Программист, перед которым возникла проблема неоднозначности преобразова- ния, может использовать приведение (cast), чтобы указать явно, какую именно из операций преобразования следует применить. void compute(int); void compute(double); Smalllnt si; compute(static_cast<int> (si)); // ok: преобразовать и // вызвать compute (int) Теперь это обращение вполне допустимо, поскольку здесь явно указано, какая из операций преобразования применяется к аргументу. Типом аргумента приведения должен быть int. Этот тип точно соответствует параметру первой версии функции compute (), которой передается аргумент типа int. Стандартные преобразования и конструкторы Рассмотрим вопрос о поиске перегруженной версии, когда существует несколько конструкторов преобразования. class Smalllnt { public: Smalllnt(int = 0); }; class Integral { public: Integral(int = 0) ; }; void manip(const Integrals); void manip(const SmalllntS); manip(10); / // ошибка: неоднозначность Проблема заключается в том, что оба класса, Integral и Smalllnt, предостав- ляют конструкторы, получающие аргумент типа int. Для функции manip () при-
578 Часть III. Абстракция, классы и данные меним конструктор любой из версий. Следовательно, обращение неоднозначно: здесь можно преобразовать тип int в тип Integral и вызывать первую версию функции manip (), но можно преобразовать тип int в тип Smalllnt и вызывать вторую вер- сию функции manip (). Это обращение будет неоднозначно, даже если в одном из классов определен кон- структор, требующий стандартного преобразования для аргумента. Например, если в классе Smalllnt определен конструктор, получающий аргумент типа short, а не int, обращение manip (10) потребует стандартного преобразования из типа int в тип short перед использованием этого конструктора. Тот факт, что одно обраще- ние требует стандартного преобразования, а другое нет, совершенно не важен при выборе перегруженной версии. Компилятор не сможет отдать предпочтение ни од- ному из конструкторов, и обращение останется неоднозначным. Явный вызов конструктора для устранения неоднозначности Вызывающая функция способна устранять неоднозначность, явно создав значе- ние необходимого типа. manip(Smalllnt(10)); // ок: вызов manip(Smalllnt) manip(Integral(10)) ; // ok: вызов manip(Integral) Необходимость в использовании конструктора или приведения для преобразования ар- гумента при обращении к перегруженной функции — это признак плохого проекта. Упражнения раздела 14.9.4 Упражнение 14.44. Опишите возможную последовательность преобразований типов для каждой из следующих инициализаций. Каков результат каждой инициализации? class LongDouble { operator double(); operator float(); }; LongDouble IdObj; (a) int exl = IdObj; (b) float ex2 = IdObj; Упражнение 14.45. Какая из версий функции calc (), если она есть, будет выбрана в качестве наилучшей для следующего обращения? Опишите последовательности преобразований, необхо- димые для вызова каждой из функций, и объясните, почему была выбрана именно эта функция. class LongDouble { public LongDouble(double) ; } ; void calc(int); void calc(LongDouble); double dval; calc(dval); // которая из функций?
Глава 14. Перегрузка операторов и преобразования 579 14.9.5. Перегрузка, преобразования и операторы Перегруженные операторы — это перегруженые функции. Для выбора оператора, встроенного или класса, используемого в данном выражении, применяется тот же подход. Рассмотрим следующий код. ClassX sc; int iobj = sc + 3; Здесь есть четыре возможности. Применить перегруженный оператор суммы, который соответствует типам ClassX и int. Применить функции преобразования, чтобы преобразовать объект sc и/или преобразовывать объект типа int в типы, для которых определен оператор +. В этом случае выражение использует функции преобразования и применит со- ответствующий оператор суммы. Выражение окажется неоднозначным, поскольку определены и оператор преоб- разования, и перегруженная версия оператора +. Выражение недопустимо, потому что не определены ни оператор преобразова- ния, ни перегруженный оператор +. Выбор перегруженной версии операторов Не забывайте, что функции-члены класса и функции не члены класса способны изменить набор доступных функций-кандидатов. Выбор перегруженной версии (раздел 7.8.2, стр. 295) оператора осуществляется, как правило, в три этапа. 1. Выбор функций-кандидатов. 2. Выбор возможно подходящих функций, включая потенциальные последова- тельности преобразований для каждого аргумента. 3. Выбор функции с наилучшим соответствием. Функции-кандидаты операторов Как обычно, в набор функций-кандидатов входят все функции, имя которых совпадает с именем вызванной функции и которые видимы в точке вызова. В случае оператора, используемого в выражении, в набор функций-кандидатов войдут все встроенные версии оператора и все обычные (не члены класса) вер- сии этого оператора. Кроме того, если левый операнд имеет тип класса, в набор функций-кандидатов войдут перегруженные версии оператора, если они опре- делены в данном классе. Как правило, набор кандидатов включает либо только функции-члены, либо функции не 5 $ члены класса, но не те и другие. При выборе используемого оператора, кандидатами мо- гут быть версии оператора как являющиеся, так и не являющиеся членами класса.
580 Часть III. Абстракция, классы и данные При выборе версии именованной функции (в отличие от выбора оператора), учитывается область видимости рассматриваемых имен. Если обращение происхо- дит для объекта класса (либо при помощи ссылки или указателя на такой объект), рассматриваются только функции-члены этого класса. Функции-члены класса и одноименные функции не члены класса друг друга не перегружают. При исполь- зовании перегруженного оператора, обращение не учитывает области видимости функции используемого оператора. Следовательно, рассматриваться будут версии как являющиеся, так и не являющиеся членами класса. Внимание! Преобразования и операторы Корректная разработка перегруженных операторов, конструкторов преобразования и функций преобразования для класса требует большой осторожности. В частности, ес- ли в классе определены и операторы преобразования и перегруженные операторы, вполне возможны неоднозначные ситуации. Здесь могут пригодиться следующие эм- пирические правила. 1. Никогда не создавайте взаимных преобразований типов. То есть, если класс Foo имеет конструктор, получающий объект класса Ваг, пе создавайте в клас- се Ваг оператор преобразования для типа Foo. 2. Избегайте преобразований во встроенные арифметические типы. Но если пре- образование в арифметический тип необходимо, придется учесть следующее. • Не создавайте перегруженных версий тех операторов, которые получают аргу- менты арифметических типов. Если пользователи используют эти операторы, функция преобразования преобразует объект данного типа, а затем применит встроенный оператор. • Не создавайте функций преобразования больше чем в один арифметический тип. Позвольте осуществлять преобразования в другие арифметические типы стандартным функциям преобразования. Самое простое правило: избегайте создания функций преобразования и ограничьте неявные конструкторы теми, которые “безусловно необходимы’. Функции преобразования и встроенные операторы могут привести к неоднозначности Давайте еще раз модернизируем класс Smalllnt. Теперь, кроме оператора пре- образования в тип int и конструктора, получающего аргумент типа int, добавим в класс перегруженный оператор суммы. class Smalllnt { public: Smalllnt(int = 0); // преобразование из int в Smalllnt // преобразование из int в Smalllnt operator int() const { return val; } // арифметические операторы friend Smalllnt operators- (const SmalllntSc, const Smalllnt&) ; private:
Глава 14. Перегрузка операторов и преобразования 581 Теперь этот класс можно использовать для суммы двух объектов класса Smalllnt, но при пытке выполнения арифметических действий в смешанном режиме могут возникнуть проблемы неоднозначности. Smalllnt si, s2; Smalllnt s3 = si + s2; int i = s3 + 0; // ok: применение перегруженного // оператора operator+ // ошибка: неоднозначность В первом операторе суммы используется перегруженная версия оператора + (получающая два объекта класса Smalllnt). Во втором возникает неоднозначность. Проблема заключается в том, что значение 0 можно преобразовать в объект класса Smalllnt, а затем использовать версию оператора + класса Smalllnt, но можно также преобразовывать тип объекта S3 в int и использовать встроенный оператор суммы типа int. Предоставление функций преобразования для арифметических типов и перегруженных операторов класса для тех же типов может привести к неоднозначности. Допустимые операторы функций и преобразования Чтобы лучше понять поведение этих двух обращений, давайте рассмотрим функ- ции, подходящие для каждого из них. При первом обращении допустимыми будут два оператора суммы: оператор operators- (const Smalllnt&, const Smalllnt&); встроенный оператор operator+ (int, int). Первый оператор суммы не требует никаких преобразований ни для одного из аргументов: объекты si и s2 точно соответствуют типам параметров. Использова- ние встроенного оператора суммы потребовало бы преобразования обоих аргумен- тов. Следовательно, перегруженный оператор лучшее соответствует обоим аргумен- там. Во втором операторе суммы происходит ошибка. int i = s3 + 0; // ошибка: неоднозначность Здесь допустимы те же две функции. Но в данном случае перегруженной версии оператора + точно соответствует первый аргумент, а встроенная версия точно соот- ветствует второму аргументу. Первая подходящая функция лучше соответствует ле- вому операнду, а вторая — правому. Обращение помечено как неоднозначное пото- му, что ни одна из функций не подходит лучше другой. Упражнения раздела 14.9.5 Упражнение 14.46. Какой из операторов operator+, если он есть, будет выбран в качестве наилучшей подходящей функции для операции суммы в функции main () ? Перечислите функции- кандидаты, подходящие функции и функции преобразования типов для аргументов каждой подхо- дящей функции. class Complex { Complex(double);
582 Часть III. Абстракция, классы и данные } ; class LongDouble { friend LongDouble operator*(LongDouble&, int); public: LongDouble(int); operator double(); LongDouble operator*(const complex &); } ; LongDouble operator*(const LongDouble &, double); LongDouble ld(16.08); double res = Id + 15.05; // который operator+ ? Резюме В главе 5, “Выражения”, был описан богатый набор операторов, определенных в языке C++ для встроенных типов. В этой главе также рассматривались стандартные функции пре- образования, которые автоматически преобразуют операнды одного типа в другой. Для объектов собственных типов (т.е. классов и перечислений) можно определить не ме- нее богатый набор версий перегруженных операторов. Перегруженный оператор должен иметь по крайней мере один операнд типа перечисления или класса. Перегруженный опера- тор имеет то же количество операндов и тот же приоритет, что и соответствующий оператор встроенного типа. Перегруженные операторы могут быть определены как члены класса или как обычные функции (не члены класса). Операторы присвоения, индексирования, обращения и стрелки должны быть членами класса. Когда оператор определяют как член класса, он представляет собой обычную функцию-член. В частности, операторы-члены класса имеют неявный пара- метр в виде указателя this, который связан с первым операндом (единственным операндом унарных операторов или левым операндом бинарных операторов). Объекты классов, для которых перегружен оператор вызова функции operator (), назы- вают объектами функций. Такие объекты зачастую применяют в качестве функции предика- та, которые следует употреблять в комбинации со стандартными алгоритмами. В классах можно определять функции преобразования, которые будут автоматически применяться в случаях, когда объект одного типа используется там, где необходим объект другого типа. Конструкторы, которые получают один параметр и не объявлены явными, с ис- пользованием ключевого слова explicit, (раздел 12.4.4, стр. 491), могут быть использованы для преобразования типа данного класса в другой. Перегруженные функции преобразования позволяют задать операторы преобразования из типа класса в другие типы. Операторы пре- образования должны быть членами того класса, который они преобразуют. Они не имеют ни параметров, ни возвращаемого значения. Операторы преобразования возвращают значение типа оператора. Например, operator int () возвращает тип int. Как перегруженные операторы, так и функции преобразования класса способны упро- стить класс и сделать его применение более удобным. Однако не следует создавать операторы и функции преобразования, назначение и способ применения которых не очевидны для поль- зователей. Кроме того, желательно избегать наличия нескольких способов преобразования одного типа в другой.
Глава 14. Перегрузка операторов и преобразования 583 Т ермины Адаптер функции (function adaptor). Библиотечный тип, который предоставляет новый интерфейс для объекта функции. Бинарный объект функции (binary function object). Класс, который имеет оператор вызо- ва функции и предоставляет один из бинарных операторов, например, один из арифметиче- ских операторов или оператор отношения. Инвертор (negator). Адаптер, который меняет значение, возвращенное указанным объек- том функции, на противоположное. Например, адаптер not2 (equal_to<int> () ) создает объект функции, который эквивалентен not_equal_to<int>. Интеллектуальный указатель (smart pointer). Класс, который имитирует поведение ука- зателя, но обладает дополнительными функциональными возможностями, такими как счет- чик ссылок, управление памятью или более жесткие проверки. В таких классах обычно опре- деляют перегруженные версии операторов обращения к значению (operator*) и доступа к члену класса (operator- >). Объект функции (function object). Объект класса, в котором определен перегруженный оператор обращения. Объекты функций применяются там, где обычно ожидаются функции. Оператор преобразования (conversion operator). Оператор преобразования — это функ- ция-член, которая осуществляет преобразование из типа класса в другой тип. Операторы пре- образования должны быть членами их класса. Такие функции не получают параметров и не имеют типа возвращаемого значения. Они возвращают значение типа оператора преобразова- ния. То есть оператор operator int() возвращает тип int, operator Sales_item() возвращает тип Sales_item и т.д. Преобразование типа класса (class-type conversion). Преобразования в тип класса или из типа класса. Неявные преобразования из типа параметра в тип класса осуществляют конст- рукторы, которые получают один параметр. Преобразования из типа класса в указанный тип осуществляют операторы преобразования. Компоновщик (binder). Адаптер, который связывает операнд с определенным объектом функции. Например, адаптер bind2nd (minus<int > () , 2) создает унарный объект функ- ции, который вычитает из операнда число 2. Унарный объект функции (unary function object). Класс, который имеет оператор вызова функции и предоставляет один из унарных операторов. Например, унарный минус или логи- ческий оператор NOT.

ЧАСТЬ IV Объектно-ориентированное И ОБЩЕЕ ПРОГРАММИРОВАНИЕ В ЭТОЙ ЧАСТИ... Глава 15. Объектно-ориентированное программирование Глава 16. Шаблоны и общее программирование Часть IV, “Объектно-ориентированное и общее программирование”, развивает темы, затронутые в части III, “Абстракция, классы и данные”, а именно объектно- ориентированное и общее программирование на языке C++. В главе 15, “Объектно-ориентированное программирование”, рассматривается наследование и динамическое связывание. Абстракция данных, наследование и ди- намическое связывание являются основой объектно-ориентированного программи- рования. В главе 16, “Шаблоны и общее программирование”, рассматриваются шаблоны классов и функций. Шаблоны позволяют создавать общие классы и функции, кото- рые не зависят от типа. Создание собственных объектно-ориентированных или общих классов подразу- мевает довольно глубокое знание языка C++. К счастью, объектно-ориентированные и общие типы можно использовать без понимания деталей их создания. Фактически стандартная библиотека использует средства, которые подробно описаны в главах 15, “Объектно-ориентированное программирование”, и 16, “Шаблоны и общее про- граммирование”, а библиотечные типы и алгоритмы можно использовать без озна- комления с подробностями их реализации. Таким образом, часть IV, “Объектно- ориентированное и общее программирование”, посвящена дополнительным воз- можностям. Создание шаблонов и объектно-ориентированных классов предполагает хорошее понимание основ языка C++ и создания простых классов.

ГЛАВА 15 Объектно-ориентированное ПРОГРАММИРОВАНИЕ В ЭТОЙ ГЛАВЕ... 15.1. Краткий обзор OOP 588 15.2. Определение базовых и производных классов 590 15.3. Преобразования и наследование 607 15.4. Конструкторы и функции управления копированием 611 15.5. Область видимости класса при наследовании 621 15.6. Чистые виртуальные функции 627 15.7. Контейнеры и наследование 628 15.8. Управляющие классы и наследование 629 15.9. Продолжение приложения TextQuery 639 Резюме 652 Термины 653 Объектно-ориентированное программирование основано на трех фундаменталь- ных концепциях: абстракция данных, наследование и динамическое связывание. Классы в языке C++ используются для абстракции данных и наследования, когда один класс наследует возможности другого: производный класс наследует перемен- ные и функции-члены базового класса (классов). Динамическое связывание позво- ляет выяснять во время выполнения, использовать ли функцию, определенную в ба- зовом классе, или функцию, определенную в производном классе. Наследование и динамическое связывание рационализируют программы двумя способами: они упрощают создание новых классов, которые подобны но не идентич- ны другим классам, а также облегчают написание программы, позволяя игнориро- вать незначительные различия в подобных классах. При создании большинства приложений используются одинаковые принципы, которые различаются лишь способами их реализации. Например, рассматриваемый для примера книжный магазин мог бы применять различные системы тарификации для разных книг. Некоторые книги можно было бы продавать лишь по фиксирован- ной цене, а для других применить гибкую систему скидок. Можно было бы предос- тавлять скидку тем покупателям, которые покупают несколько экземпляров книги.
588 Часть IV. Объектно-ориентированное и общее программирование Скидку можно было бы также предоставить на несколько первых экземпляров, а для остальных оставить полную цену. Объектно-ориентированное программирование (OOP — Object-Oriented Program- ming) — это наилучший способ создания приложений такого типа. Используя насле- дование можно определять классы, моделирующие книги различных типов, а при помощи динамического связывания создавать приложения, которые эти классы ис- пользуют, но могут игнорировать различия, зависящие от типа. Концепции наследования и динамического связывания относительно просты, но имеют принципиальное значение для способов создания приложений и возможно- стей, которыми должны обладать поддерживающие их языки программирования. Прежде чем рассматривать реализацию OOP на языке C++, давайте рассмотрим об- щие принципы, составляющие основу данного способа программирования. 15.1. Краткий обзор OOP В основе OOP лежит полиморфизм (polymorphism). Слово “полиморфизм” про- исходит от греческих слов “поли” — много и “морф” — форма, т.е. дословно — много- образие форм. Связанные с наследованием классы называют полиморфными типами (polymorphic type), поскольку в зависимости от ситуации могут быть попеременно использованы формы как производного, так и базового класса. Как будет продемон- стрировано вскоре, полиморфизм в языке C++ применим только к ссылкам или ука- зателям на объекты классов, связанных с наследованием. Наследование Наследование (inheritance) позволяет создавать классы, моделирующие отноше- ния между типами, некоторые элементы которых можно использовать совместно, а другие являются специфическими. Члены, определенные в базовом классе (base class), наследуются классами, производными от него. Производный класс (derived class) может без изменения использовать члены базового класса, которые не зависят от специфических особенностей производного класса. Но чтобы учесть особенности производного класса, функции-члены базового класса можно переопределить, т.е. специализировать функцию (specialize function). И наконец, кроме унаследованных из базового, в производном классе можно определить дополнительные члены класса. Набор классов, связанных наследственными отношениями, зачастую называют относящимися к иерархии наследования (inheritance hierarchy). В иерархии сущест- вует один класс, называемый корневым (root), от которого непосредственно или кос- венно происходят все остальные классы. В рассматриваемом примере приложения книжного магазина будет создан базовый класс (по имени Item_base (Экземп- ляр базовый)), представляющий книги без скидки. Как производный от класса ltem_base, будет создан второй класс (по имени Bulk_item (Оптовый_экземп- ляр)), представляющий книги, продаваемые с оптовой скидкой. В этих классах должны быть определены, как минимум, следующие функции. Функция по имени book (книга), которая возвращает ISBN. Функция по имени net_price (реальная_цена), которая возвращает цену при покупке определенного количества экземпляров книги.
Глава 15. Объектно-ориентированное программирование 589 Классы, производные от класса Item_base, наследуют функцию book () без из- менений: производным классам нет никакой необходимости переопределять функ- цию, возвращающую ISBN. С другой стороны, в каждом производном классе при- дется определить его собственную версию функции net_price (), чтобы реализо- вать соответствующую стратегию тарификации скидок. Язык C++ позволяет базовому классу указать, какие из его функций предполага- ется переопределить в классах, производных от него. Функции, которые следует пе- реопределить в производных классах, объявляют виртуальными (virtual). Те функ- ции базового класса, которые предполагается использовать в наследующих классах, виртуальными не объявляют. Предположим, что обсуждаемые классы следует дополнить тремя константными функциями-членами. Невиртуальная функция std::string book(), которая возвращает ISBN. Она будет определена в классе Item_base и унаследована классом Bulk_item. Два версии виртуальной функции double net_price (size_t), возвращаю- щей общую выручку за указанное количество экземпляров определенной книги. В классах Item_base и Bulk_item определены их собственные версии этой функции. Динамическое связывание Динамическое связывание (dynamic binding) позволяет создавать программы, ко- торые используют объекты любых классов иерархии наследования, не заботясь об их конкретных типах. Программы, использующие эти классы, не должны различать функции, определенные в базовом или производном классе. Например, рассматриваемое приложение книжного магазина должно позволять клиенту покупать несколько книг за один раз. Когда клиент осуществляет покупки, приложение вычисляет общую сумму к оплате. Кроме окончательной суммы, про- грамма должна отобразить сумму по каждой книге в отдельной строке, а также об- щее количество и цену. Для реализации этой части приложения следует определить функцию print_ total () (печать суммы). Функция print_total (), получив экземпляр и коли- чество, должна отпечатать ISBN и общую сумму за покупку данного количества эк- земпляров этой книги. Результат ее работы должен выглядеть следующим образом. ISBN: 0-201-54848-8 number sold: 3 total price: 98 ISBN: 0-201-82470-1 number sold: 5 total price: 202.5 Функция print_total () могла бы выглядеть следующим образом. // вычислить и отобразить цену за указанное количество экземпляров, // с применением всех скидок void print_total(ostream &os, const Item_base &item, size_t n) { os << "ISBN: " << item.book() // вызов Item_base::book « "\tnumber sold: " << n << "\ttotal price: " // виртуальное обращение: какая из версий net:_price() // будет применена во время выполнения }
590 Часть IV. Объектно-ориентированное и общее программирование Работа функции очень проста: она отображает результаты вызова функций book () и net_price () для переданного параметра item. В этой функции есть две интересные особенности. Во-первых, несмотря на то, что второй параметр функции является ссылкой на объект класса ltem_base, ей может быть передан объект как класса ltem_base, так и класса Bulk_item. Во-вторых, поскольку параметр является ссылкой, а функция net_price () яв- ляется виртуальной, во время выполнения придется выяснять, какая из версий функции net_price () будет применена. Конкретная версия функции net_jprice (), которая будет применена, зависит от типа аргумента, переданного при вызове функ- ции print_total (). Когда аргументом функции net_price () является объект класса Bulk_item, используется та версия функции net_price (), которая опре- делена в классе Bulk_item (т.е. подразумевающая скидку). Если аргументом явля- ется объект класса ltem_base, обращение произойдет к версии, определенной в классе ltem_base‘. В языке C++ динамическое связывание происходит тогда, когда обращение к виртуальной h '*5 j функции осуществляется при помощи ссылки (или указателя) на базовый класс. В основе динамического связывания лежит тот факт, что ссылка (или указатель) может относиться & к объекту как базового, так и производного класса. Поиск версии при вызове виртуальной функции для ссылки (или указателя) осуществляется во время выполнения: т.е. используется та из них, которая принадлежит классу фактически переданного объекта. 15.2. Определение базовых и производных классов В основном определение базовых и производных классов осуществляется анало- гично другим классам, однако некоторые отличия все же имеются. При определении классов в иерархии наследования есть также несколько дополнительных возможно- стей. Эти особенности и рассматриваются в данном разделе. В последующих разде- лах будут продемонстрировано, как с помощью этих возможностей можно создавать классы и программы, в которых они применяются. 15.2.1. Определение базового класса Подобно любому другому классу, членами базового класса (base class) могут быть данные и функции, которые составляют его интерфейс и реализацию. В случае дан- ного (очень упрощенного) приложения тарификации книжного магазина, в классе ltem_base следует определить функции book() и net_price О, а также пере- менные для хранения ISBN и стандартной цены книги. // Экземпляр, продаваемый по цене без скидки // различные системы скидок будут определены в производных классах class Item__base { ' Возникает резонный вопрос: почему при передаче объекта, класс которого отличается от указанного для параметра функции, не происходит преобразование типа? Дело в том, что в состав объекта производного класса фактически входит объект базового. — Примеч. ред.
Глава 15. Объектно-ориентированное программирование 591 public: Item_base(const std::string &book = double sales_price = 0.0): isbn(book), price(sales_price) { } std::string book() const { return isbn; } // возвращает общий цену за определенное количество проданных // экземпляров, а различные системы скидок определяют и // применяют производные классы virtual double net_price(std::size_t n) const { return n * price; } virtual ~Item_base() { } private: std::string isbn; // идентификатор экземпляра protected: double price; // стандартная цена (без скидки) }; Выглядит этот класс практически так же, как и другие, которые были рассмотре- ны ранее. Здесь определен конструктор и функции, описание которых приведено вы- ше. В конструкторе используются аргументы по умолчанию (раздел 7.4.1, стр. 278), ко- торые позволяют использовать его без аргументов, а также с одними или двумя аргу- ментами. Эти аргументы он использует для инициализации переменных-членов. Новыми элементами здесь являются маркер доступа protected (защищенные) и ключевое слово virtual, используемое в объявлении деструктора и функции net_price (). Подробно виртуальные деструкторы описаны в разделе 15.4.4 (стр. 618), а сейчас лишь заметим, что деструктор корневого класса иерархии наследования оп- ределяется обычно как виртуальный. Функции-члены базового класса В классе ltem_base определены две функции, одной из которых предшествует ключевое слово virtual. Ключевое слово virtual применяется для обеспечения динамического связывания. По умолчанию функции-члены не виртуальны. Поиск не- виртуальных функций, при обращении, осуществляется во время компиляции. Чтобы объявить функцию виртуальной, перед типом ее возвращаемого значения следует ука- зать ключевое слово virtual. Любая нестатическая функция-член, отличная от кон- структора, может быть объявлена виртуальной. Ключевое слово virtual применимо только в объявлении функции-члена внутри класса. В определении функции, распо- ложенной вне тела класса, ключевое слово virtual неприменимо. Более подробно виртуальные функции описаны в разделе 15.2.4 (стр. 597). Как правило, виртуальной в базовом классе объявляют каждую функцию, которую в производном классе следует переопределить. Управление доступом и наследование В базовом классе маркеры доступа public и private имеют стандартное значе- ние: пользовательский код может обращаться к открытым (public) членам класса, а к закрытым (private) — нет. Закрытые члены доступны лишь членам данного класса и дружественным для него. Производный класс имеет тот же уровень доступа
592 Часть IV. Объектно-ориентированное и общее программирование к открытым и закрытым членам своего базового класса, что и любая другая часть программы, т.е. он может обращаться к его открытым членам, а к закрытым — нет. Иногда необходимо организовать доступ к членам базового класса только для классов, производных от него, а для всех остальных запретить. Для таких членов ис- пользуется маркер доступа protected (защищенный). К защищенному члену класса можно обращаться из объекта класса, производного от него, а для пользова- телей и других объектов он будет недоступен. Рассматриваемый класс ltem_base ожидает, что в классах, производных от него, функция net_price () будет переопределена. Для этого производному классу по- надобится доступ к переменной-члену price. Доступ к переменной-члену isbn производные классы получат точно так же, как и обычные пользователи: при помо- щи функции book (). Следовательно, переменную-член i sbn можно оставить за- крытой (private) и недоступной для классов, производных от Item_base. Упражнения раздела 15.2.1 Упражнение 15.1. Что такое виртуальный член класса? Упражнение 15.2. Примените маркер доступа protected. Чем он отличается от маркера дос- тупа private? Упражнение 15.3. Определите собственную версию класса item_base. Упражнение 15.4. Предположим, что библиотека предоставляет абонентам разнообразные носи- тели информации — книги, компакт-диски (CD), цифровые видеодиски (DVD) и т.д. Для каждого из предоставляемых видов носителей информации определены собственные правила выдачи и прие- ма и отдельные сроки пользования. Ниже приведен базовый класс, который можно было бы ис- пользовать в этом приложении. Укажите, какие из функций предоставления носителей вероятнее всего следует объявить виртуальными, а какие (если они есть) оставить обычными. (Примечание: здесь подразумевается, что класс LibMember представляет клиента библиотеки, и класс Date — календарную дату.) class Library { public: bool check_out(const LibMember&); bool check_in(const LibMember&); bool is_late(const Date& today); double apply_fine(); ostreamk print(ostream& = cout); Date due_date() const; Date date_borrowed() const; string title() const; const LibMember& member() const; }; 15.2.2. Защищенные члены Степень доступа protected можно рассматривать как нечто среднее между private и public. Подобно закрытым (private), защищенные (protected) члены недоступны для пользователей класса. Подобно открытым (public), защищенные члены доступны для классов, произ- водных от данного класса.
Глава 15. Объектно-ориентированное программирование 593 Кроме того, защищенные члены обладают еще одной немаловажной особенностью. Объект производного класса может обратиться к защищенным членам своего ба- зового класса только внутри самого объекта производного класса. Производный класс не имеет доступа к защищенным членам объектов базового класса. Предположим, например, что в классе Bulk_item определена функция-член, получающая ссылку на объект класса Bulk_item и ссылку на объект класса ltem_base. Эта функция сможет обращаться к защищенным членам собственного объекта, а также защищенным членам объекта класса Bulk_item, переданного в ка- честве параметра. Однако она не имеет доступа к защищенным членам переданного в качестве параметра объекта класса ltem_base. void Bulk_item:: meinfcn (const Bulk_item &d, const Item_base &b) { // попытка использовать защищенный член класса double ret = price; // ok: использует this->price ret = d.price; // ok: использует price из объекта Bulk_item ret = b.price; // ошибка: нет доступа к price из Item_base } Обращение к переменной-члену d. price прошло без проблем, поскольку эта переменная price принадлежит объекту класса Bulk_item. Обращение к пере- менной-члену b. price некорректно, поскольку объект класса Bulk_item не имеет доступа к переменным объекта класса Item_base. Фундаментальная концепция. Проектирование класса и защищенные члены Без наследования класс имел бы два вида пользователей: члены самого класса и его пользователи. Это различие между пользователями отражено в разделении уровней доступа к закрытым private) и открытым (public) членам класса. Пользователи могут обращаться только к открытому интерфейсу, а члены самого класса и класса, дружественного для него, могут обращаться как к открытым, так и к закрытым членам. При наследовании возникает третий вид пользователя: классы, являющиеся произ- водными от данного класса. Производный класс зачастую (но не всегда) нуждается в доступе к реализации базового класса (обычно закрытой). Чтобы обеспечить такой доступ, но все же предотвратить доступ к реализации всех остальных участков кода, используется дополнительный маркер, protected. Данные и функции-члены, объяв- ленные в разделе protected, остаются недоступными для остального кода програм- мы, но не для членов производного класса. Все, что объявлено внутри раздела private базового класса, останется доступно только самому классу и классам, друже- ственным для него. Закрытые члены для производных классов недоступны. При проектировании класса, который предполагается сделать базовым, критерии объ- явления его членов открытыми остаются неизменны: открытыми должны быть функ- ции интерфейса, а данные — обязательно закрытыми. При разработке производного класса необходимо решить, какие из элементов реализации следует объявить защи- щенными, а какие закрытыми. Член класса должен быть закрытым, если необходимо предотвратить последующий доступ к нему из производных классов. Член класса должен быть защищенным, если его должен будет использовать производный класс. Другими словами, предоставляемый производному классу интерфейс является ком- бинацией открытых и защищенных членов.
594 Часть IV. Объектно-ориентированное и общее программирование 15.2.3. Производные классы При определении производного класса используется список наследования класса (class derivation list), позволяющий указать базовый класс (или классы). В списке наследования класса указывают один или несколько базовых классов. Он имеет сле- дующий вид. class имякласса: маркер_доступа базовый_класс Здесь маркер_доступа — это маркер public, protected или private, а базовый_класс — имя определенного ранее базового класса. Как будет продемон- стрировано далее, список наследования может содержать имена нескольких базовых классов. Наследование от одного базового класса является наиболее распространен- ным случаем. Применение нескольких базовых классов рассматривается в разде- ле 17.3 (стр. 764). Более подробная информация об используемом в списке наследования маркере доступа приведена в разделе 15.2.5 (стр. 602), а пока имеет смысл упомянуть, что мар- кер доступа определяет доступ к унаследованным членам. Когда необходимо унасле- довать интерфейс базового класса, наследование должно быть объявлено как public. Производный класс наследует все члены базового класса и может определять свои собственные, дополнительные члены. Объект каждого производного класса со- стоит из двух частей: членов, унаследованных из базового класса, и членов, опреде- ленных самостоятельно. Как правило, в производном классе определены (или пере- определены) только те аспекты, которые отличают его от остальных классов или до- полняют поведение базового класса. Определение производного класса В рассматриваемом приложении книжного магазина класс Bulk_item являет- ся производным от класса Item_base, поэтому класс Bulk_item наследует его переменные-члены book, isbn и price. Класс Bulk_item должен переопреде- лить его функцию-член net_price () и определить переменные-члены, необхо- димые для ее работы. // скидка предоставляется в случае, когда приобретается // определенное количество экземпляров той же книги // скидка выражена в качестве доли, уменьшающей стандартную цену class Bulk_item : public Item_base { public: // переопределить базовую версию и реализовать политику // скидок при оптовых закупках std::size_t min_qty; // минимальная покупка для скидки double discount; // доля применяемой скидки }; Каждый объект класса Bulk_item содержит четыре переменные-члена: isbn и price, унаследованные от класса ltem_base, а также собственные min_qty (минимальное количество) и discount (скидка). Эти две последние переменные- члена содержат минимальное количество приобретаемых экземпляров, необходимое для применения скидки, и собственно скидку. В классе Bulk_item должен быть также определен конструктор, рассматриваемый в разделе 15.4 (стр. 611).
Глава 15. Объектно-ориентированное программирование 595 Производные классы и виртуальные функции Как правило, унаследованные виртуальные функции в производных классах пе- реопределяются, хотя это и не обязательно. Если виртуальная функция в производ- ном классе не переопределена, используется ее первоначальная версия, определен- ная в базовом классе. Производный класс должен содержать объявления для каждого унаследованного члена, который предполагается переопределить. В рассматриваемом классе Bulk_ item функцию net_price () предполагается переопределить, а функцию book () — использовать в унаследованной версии. За одним исключением, объявление (раздел 7.4, стр. 277) виртуальной функции в производном классе должно точно соответствовать ее объявлению в базовом. Исклю- чение составляют виртуальные функции, которые возвращают ссылку (или указатель) на класс, который сам является базовым. Виртуальная функция в производном классе может возвращать ссылку (или указатель) на класс, который является открытым (public) и производным от класса, возвращаемого функцией базового класса. Например, в классе ltem_base могла бы быть определена виртуальная функция, которая возвращает тип ltem_base*. В этом случае экземпляр, определенный в классе Bulk_item, мог бы быть определен как возвращающий тип Item_base* или Bulk_item*. Пример подобного вида виртуальных функций рассматривается в разделе 15.9 (стр. 639). Если функция объявлена виртуальной в базовом классе, она остается виртуальной и в < < I производных класса. При переопределении виртуальной функции в производном классе / вполне можно использовать ключевое слово virtual, но это не обязательно и ничего —& не меняет. В состав объектов производных классов входят объекты базовых классов Объект производного класса состоит из несколькими частей: нестатических чле- нов, определенных в самом производном классе, а также объекта, состоящего из не- статических членов его базового класса. Объект класса Bulk_item можно рассмат- ривать как состоящий из двух частей, как представлено на рис. 15.1. Объект класса Bulkjtem Переменные-члены класса ltem_base Переменные-члены класса Bulkjtem Isbn price Min_qty discount Рис. 15.1. Концептуальная структу- ра объекта класса Bulk_i tem Поскольку компилятор необязательно должен располагать части базового и производного объекта рядом, на рис. 15.1 представлена лишь концептуальная, а не физическая модель.
596 Часть IV. Объектно-ориентированное и общее программирование Функции производного класса способны использовать переменные базового класса Подобно любой другой функции-члену, функция производного класса может быть определена как внутри класса, так и вне его, как это сделано далее для функции net_price(). // если приобретено достаточное количество экземпляров, // использовать цену со скидкой double Bulk_item::net_price(size_t ent) const { if (ent >= min_qty) return ent * (1 - discount) * price; else return ent * price; } Эта функция вычисляет цену со скидкой: если приобретенное количество экзем- пляров превышает значение переменной min_qty, к цене (price) применяется скидка (discount). Поскольку каждый объект производного класса обладает частью базового, в классе мож- но обращаться к его открытым и защищенным членам так, как будто они являются члена- ми самого производного класса. Чтобы использовать класс как базовый, его следует определить Прежде чем использовать класс как базовый, его следует определить. Если объявить, но не определить класс ltem_base, его нельзя будет использовать как базовый. class Item_base; // объявлен, но не определен // ошибка: класс Item_base должен быть определен class Bulk_item : public Item_base { ... }; Причина этого ограничения вполне очевидна: каждый объект производного клас- са содержит и может обращаться к членам базового. Чтобы использовать эти члены, производный класс должен “знать”, что они из себя представляют. Следствием этого правила является тот факт, что получить класс как производный от себя самого не- возможно. Использование производного класса как базового Базовый класс сам вполне может быть производным от другого класса. class class class Base { /* ... */ }; DI: public Base { /* D2: public Dl { /* . Каждый класс наследует все члены своего базового класса. Все производные классы наследуют члены базового класса, который в свою очередь наследует члены его базового класса и т.д. Таким образом, объект производного класса содержит объект каждого из его непосредственно базовых (immediate-base) и косвенно базовых (indirect-base) классов.
Глава 15. Объектно-ориентированное программирование 597 Объявление производных классов Если производный класс необходимо объявить (но еще не определить), в объявлении следует указать имя класса, но не список наследования. Например, следующее предвари- тельное объявление класса Bulk_item приведет к ошибке во время компиляции. // ошибка: предварительное объявление не должно содержать II списка наследования class Bulk_item : public Item_base; Правильные предварительные объявления выглядят следующим образом. // предварительные объявления производного и непроизводного класса class Bulk_item; class Item_base; Упражнения раздела 15.2.3 Упражнение 15.5. Какие из следующих объявлений (если они есть) некорректны? class Base { ... }; (a) class Derived : public Derived { ... }; (b) class Derived : Base { ... } ; (c) class Derived : private Base { ... }; (d) class Derived : public Base; (e) class Derived inherits Base { ... }; Упражнение 15.6. Напишите собственную версию класса Buik_item. Упражнение 15.7. Можно было определить класс, реализующий стратегию частичных скидок. Этот класс предоставил бы скидку, например, на первые 100 книг. Когда количество проданных экземпляров превысит этот предел, ко всем остальным книгам применяется стандартная цена. Определите класс, который реализует эту стратегию. 15.2.4. Виртуальные и другие функции-члены По умолчанию вызов функции в языке C++ не использует динамическое связы- вание. Для применения динамического связывания необходимо, чтобы были выпол- нены два условия. Во-первых, динамическое связывание применимо только для тех функций-членов, которые объявлены виртуальными. По умолчанию функции- члены виртуальными не являются, а невиртуальные функции динамическому свя- зыванию не подлежат. Во-вторых, обращение должно происходить по ссылке (или указателю) на тип базового класса. Чтобы понять смысл этого требования, следует проанализировать, что происходит при использовании ссылки или указателя на объ- ект, класс которого принадлежит иерархии наследования. Преобразование из производного класса в базовый Поскольку частью каждого объекта производного класса является объект базово- го, ссылку на тип базового класса можно связать с частью базового класса в объекте производного. Кроме того, указатель на тип базового класса можно использовать как указатель на объект производного. // функция, параметром которой является ссылка на класс Item_base double print_total(const Item_base&, size_t); Item_base item; // объект базового класса
598 Часть IV. Объектно-ориентированное и общее программирование // ок: использование указателя или ссылки на класс Item_base // для обращения к объекту класса Item_base print_total(item, 10); // передача ссылки на класс Item_base Item_base *р - Scitem; // р указывает на объект класса Item_base Bulk_item bulk; // объект производного класса // ок: указатель или ссылка на класс Item_base может быть связана // с объектом класса Bulk_item print_total(bulk, 10); // передача ссылки на часть Item_base // объекта bulk р = &bulk; // р указывает на часть Item_base // объекта bulk В этом коде для указания на объекты базового и производного класса использу- ется тот же указатель на базовый класс. Здесь также происходит вызов функции, ожидающей ссылку на объект базового класса, которой передается объект базового, а затем производного класса. Оба обращения отлично срабатывают, поскольку частью каждого объекта производного класса является объект базового. Поскольку для обращения к объекту производного класса можно использовать указатель или ссылку на базовый класс, не вполне очевидно, с объектом какого именно типа связан указатель или ссылка: это может быть объект как базового, так и производного класса. Но независимо от фактического типа, компилятор обработает объект корректно, как будто это объект базового типа. Обработка объекта производ- ного класса как базового вполне безопасна, поскольку каждый объект производного класса содержит объект базового. Кроме того, производный класс наследует функ- ции базового, а следовательно, любая операция, допустимая для объекта базового класса, вполне корректна и для объекта производного. Критически важными при использовании ссылок и указателей на типы базового класса ^4 В являются концепции статического типа (static type), т.е. типа ссылки или указателя, '€*п*у/ распознаваемого во время компиляции, и динамического типа (dynamic type), т.е. типа объекта, привязка указателя или ссылки на который осуществляются только во время выполнения. Выбор версии вызванной виртуальной функции может происходить во время выполнения Привязка ссылки или указателя базового типа на объект производного никак не влияет на объект базового класса. Сам объект неизменен и остается объектом произ- водного класса. Но тот факт, что реальный тип объекта может отличаться от стати- ческого типа ссылки или указателя на этот объект, лежит в основе концепции дина- мического связывания языка C++. Когда вызов виртуальной функции осуществляется при помощи ссылки или ука- зателя, компилятор создает такой код, который способен решить во время выполне- ния, какая именно из версий функции будет применена. То есть применена будет та версия вызванной функции, которая соответствует динамическому типу. Давайте вернемся к примеру функции print_total (). // вычислить и отобразить цену за указанное количество экземпляров, // с применением всех скидок void print_total(ostream &os, const Item_base &item, size_t n) { os << "ISBN: " << item.book() // calls Item_base::book << "\tnumber sold: " « n << "\ttotal price: "
Глава 15. Объектно-ориентированное программирование 599 // виртуальное обращение: какая из версий net price() // будет применена во время выполнения « item.net_price(n) << endl; Поскольку параметр item является ссылкой, а функция net_price () объявлена виртуальной, ее конкретная версия, используемая при вызове item.net_price (n), зависит во время выполнения от фактического типа объекта, связанного с аргумен- том параметра item. Item_base base; Bulk_item derived; // print_total() осуществляет виртуальное обращение к net priсе() print_total(cout, base, 10); // вызов Item_base::net_price() print_total(cout, derived, 10); // вызов Bulk_item::net_price() При первом вызове параметр item связывается во время выполнения с объектом класса Item_base. В результате обращение к функции net_price() внутри функции print_total () приводит к вызову той ее версии, которая определена в классе Item_base. При втором вызове параметр item связан с объектом класса Bulk_item. В этом обращении при вызове функции print_total () будет приме- нена та версия функции net_price (), которая определена в классе Bulk_item. Фундаментальная концепция. Полиморфизм в языке С++ Краеугольпым камнем поддержки полиморфизма в языке C++ является тот факт, что статические и динамические типы ссылок и указателей MOiyr отличаться. Когда при помощи ссылки или указателя на базовый класс происходит вызов функ- ции, определенной в базовом классе, точный тип объекта, для которого будет выпол- няться функция, неизвестен. Это может быть объект базового класса, а может быть и производного класса. Если вызываемая функция невиртуальна, независимо от фактического типа объекта, выполнена будет та версия функции, которая определена в базовом классе. Если функция виртуальна, решение о фактически выполняемой версии функции отклады- вается до времени выполнения. Она определяется на основании типа объекта, с кото- рым связана ссылка или указатель. Но с точки зрения создаваемого кода об этом вполне можно не заботиться. Пока классы разрабатываются и используются правильно, функции будут работать кор- ректно, поэтому вовсе не важно, имеет ли фактический объект тип базового или производного класса. С другой стороны, объект не полиморфен — его тип известен и не изменяется. Дина- мический тип объекта (в отличие от ссылки или указателя) всегда совпадает со стати- ческим, а выполняемой функцией, виртуальной или невиртуальной, будет та, которая определена в классе данного объекта. Поиск версии виртуальной функции осуществляется во время выполнения только тогда, 1 когда обращение осуществляется при помощи ссылки или указателя. Только в этом слу- чае возможна ситуация, когда динамический тип объекта неизвестен до времени вы- полнения.
600 Часть IV. Объектно-ориентированное и общее программирование Выбор версии невиртуальной функции происходит во время компиляции Независимо от фактического типа аргумента, переданного функции print_ total (), обращение к функции book () будет интерпретировано во время компи- ляции какItem_base::book(). Даже если в классе Buik_item определена его собственная версия функции book (), при обращении будет вызвана его версия из базового класса. Выбор версии невиртуальной функции всегда происходит во время компиляции на основании типа объекта, ссылки или указателя, для которого осуществляется вы- зов функции. Типом параметра item является ссылка на константу класса Item_ base. Таким образом, обращение к невиртуальной функции для этого объекта при- ведет к вызову ее версии из класса Item_base, независимо от типа фактического объекта, на который указывает параметр item во время выполнения. Переопределение механизма виртуальных функций В некоторых случаях необходимо переопределить механизм вызова виртуальных функций и принудительно использовать определенную версию виртуальной функ- ции. Это можно сделать при помощи оператора области видимости. Item_base *baseP = &derived; // обращение к версии из базового класса, независимо от // динамического типа baseP double d = baseP->Item_base::net_price(42); В этом коде происходит принудительное обращение к той версии функции net_price (), которая определена в классе Item_base. В этом случае выбор вер- сии происходит во время компиляции. Чтобы переопределить механизм вызова виртуальных функций внутри функций- членов, код должен использовать оператор области видимости. Зачем переопределять механизм вызова виртуальных функций? Как правило, это происходит в случае, когда в производном классе необходимо обратиться к версии функции из базового класса. В таких случаях версия функции из базового класса могла бы осуществлять действия, общие для всех классов иерархии. Напомним, что в каждый производный класс следует добавлять только те функции, которые специ- фичны именно для него. Определим, например, иерархию Camera с виртуальной функцией di splay (). Функция display () класса Camera отображает информацию, общую для всех камер. Производный класс Perspectivecamera должен отображать информацию, общую для всех камер и уникальную для камер типа Perspectivecamera. Чтобы не дубли- ровать внутри класса Perspectivecamera реализацию функции display () класса Camera, для отображения общей информации можно явно вызывать ее версию из класса Camera. В этом случае можно точно указать, какая из версий вызвана, и поэто- му нет никакой необходимости в переопределении механизма виртуальных функций.
Глава 15. Объектно-ориентированное программирование 601 Когда виртуальная функция производного класса обращается к версии функции базово- го класса, она должна сделать это явно, используя оператор области видимости. В про- тивном случае, во время выполнения такое обращение будет интерпретировано как об- ращение к себе самой, что приведет к бесконечной рекурсии. Виртуальные функции и аргументы по умолчанию Подобно любой другой функции, виртуальная функция может иметь аргументы по умолчанию. Как обычно, значение аргумента по умолчанию, если оно есть, при- меняется во время компиляции. Если в обращении отсутствует аргумент, который имеет значение по умолчанию, при вызове будет использовано именно это значение, независимо от динамического типа объекта. При вызове виртуальной функции с ис- пользованием ссылки или указателя типа базового класса, используется значение аргумента по умолчанию, указанное в объявлении виртуальной функции базового класса. При вызове виртуальной функции с использованием ссылки или указателя типа производного класса, используется значение аргумента, по умолчанию указан- ное в объявлении виртуальной функции производного класса. Применение разных значений для аргументов по умолчанию в версиях одной и той же виртуальной функции базовых и производных классов, обязательно приведет к возникновению проблем. Например, проблемы возникнут в случае, когда вызов виртуальный функции происходит при использовании ссылки или указателя типа базового класса, но выполняется версия, определенная в производном классе. В та- ких случаях аргумент по умолчанию определенный для базовой версии виртуальной функции, будет передан производной версии, которая была определена с другим ар- гументом по умолчанию. Упражнения раздела 15.2.4 Упражнение 15.8. Объясните каждый случай применения функции print () в следующих классах, struct base { string name() { return basename; } virtual void print(ostream &os) { os << basename; } private: string basename; }; struct derived { void print() { print(ostream &os); os << " " << mem; } private: int mem; }; Если в этом коде имеются ошибки, устраните их. Упражнение 15.9. С учетом классов из предыдущего упражнения и следующих объектов укажите, какие из версий функций будут применены во время выполнения, base bobj; base *bpl = &base; base &brl = bobj; derived dobj; base *bp2 = &doboj; base &br2 - dobj; (a) bobj.print(); (b) dobj.print(); (c) bpl->name(); (d) bp2->name(); (e) brl.print(); (f) br2.print();
602 Часть IV. Объектно-ориентированное и общее программирование 15.2.5. Открытое, закрытое и защищенное наследование Доступ к членам, определенным внутри производного класса, осуществляется так же, как и к членам любого другого класса (раздел 12.1.2, стр. 460). В производном классе мо- жет располагаться любое количество маркеров доступа, задающих уровень доступа к членам класса, объявленным после этой метки. Доступ к членам, унаследованным классом, осуществляется в соответствии с комбинацией уровня доступа в базовом классе и маркера доступа, указанного в списке наследования производного класса. Каждый класс предоставляет доступ к членам, которые он определяет. Производный класс может впоследствии “ужесточить”, но не “ослабить” правила доступа к наследуе- мым членам. В самом базовом классе имеет смысл предоставить минимальный уровень досту- па к его собственным членам. Если в базовом классе член объявлен закрытым (private), обращаться к нему могут только члены самого базового класса и клас- сов, дружественных для него. Производный класс не имеет доступа к закрытым чле- нам своего базового класса и не может сделать их доступными для своих пользова- телей. Если член базового класса объявлен открытым (public) или защищенным (protected), уровень доступа к нему в производном классе определяет маркер дос- тупа, указанный в списке наследования. При открытом наследовании (public inheritance) члены базового класса сохра- няют свой уровень доступа: открытые члены базового класса остаются открыты- ми в производных, а защищенные в базовом — защищенными в производном. При защищенном наследовании (protected inheritance) открытые и защищенные члены базового класса становятся в производном классе защищенными. При закрытом наследовании (private inheritance) все члены базового класса ста- новятся закрытыми в производном. В качестве примера рассмотрим следующую иерархию. class Base { public: void basement () ; // открытый член класса protected: int i; // защищенный член класса } ; struct Public_derived : public Base { int use_base() { return i; } // ok: производные классы могут // обращаться к i } ; struct Private_derived : private Base { int use_base() { return i; } // ok: производные классы могут // обращаться к i }; Все классы, производные от класса Base, имеют одинаковый уровень доступа к его членам, независимо от маркера доступа в своих списках наследования. Маркер доступа в списке наследования влияет на уровень доступа пользователей производ- ного класса к членам, унаследованным от класса Base.
Глава 15. Объектно-ориентированное программирование 603 Base Ь; Public_derived dl; Private_derived d2; b.basemem(); // ok: basemem является открытым dl.basemem(); // ok: basemem является открытым в производном классе d2.basemem(); // ошибка: basemem является закрытым в // производном классе Структуры Public_derived и Private_derived наследуют функцию basemem (). При открытом наследовании этот член сохраняет свой уровень доступа, поэтому для объекта dl вполне можно вызвать функцию basemem (). В структуре Private_derived члены класса Base закрыты, поэтому пользователи объекта структуры Private_derived не могут вызывать функцию basemem (). Маркер доступа наследования задает также уровень доступа из классов, которые являются производными косвенно. struct Derived_from Private : public Private_derived { // ошибка: Base::i закрытая в Private_derived int use_base() { return i; } }; struct Derived_from_Public : public Public_derived { // ok: Base::i остается защищенной в Public_derived int use_base() { return i; } }; Классы (структуры), производные от структуры Public_derived, могут обращать- ся к переменной i класса Base, поскольку она остается защищенным членом струк- туры Public_derived. Классы, производные от структуры Private_derived, та- кого доступа не имеют. Здесь все члены структуры Private_base, унаследованные от класса Base, являются закрытыми. Интерфейс и реализация наследования При открытом наследовании производный класс наследует интерфейс своего ба- зового класса, т.е. он имеет тот же интерфейс, что и базовый класс. В хорошо прора- ботанных иерархиях классов, объекты открыто производного класса применимы везде, где ожидается объект базового класса. Классы, производные как закрытые или защищенные, не наследуют интерфейс базового класса. Такое наследование зачастую называют наследованием реализации (implementation inheritance). Производный класс использует унаследованный класс в своей реализации, но не предоставляет его как часть собственного интерфейса. Как будет продемонстрировано в разделе 15.3 (стр. 607), способ наследования (интерфейса или реализации) весьма важен для пользователей производного класса. Наиболее распространенной является открытая форма наследования. Фундаментальная концепция. Наследование или композиция Проектирование иерархии наследования — это достаточно сложная тема, которая выходит за рамки данного вводного курса. Однако имеет смысл упомянуть об одном достаточно важном факторе проектирования, с которым должен быть знаком каждый программист.
604 Часть IV. Объектно-ориентированное и общее программирование При определении класса как открыто производного от другого, производный и базо- вый классы реализуют взаимоотношения типа “является " (is а). В рассматриваемом примере с книжным магазином, базовый класс представляет концепцию книги, прода- ваемой по предусмотренной цене, а класс Bulk_item — конкретную книгу, продавае- мую по розничной цене с определенной стратегией тарификации. Еще одним популярным способом взаимоотношений классов является принцип “имеет ” (has а). Классы приложения книжного магазина имеют цену и ISBN. Типы, связанные отношениями “имеет”, подразумевают принадлежность. Таким образом, клас- сы приложения книжного магазина состоят из членов, представляющих цену и ISBN. Освобождение отдельных членов класса При закрытом или защищенном наследовании уровень доступа к членам базово- го класса может быть ужесточен в производном классе. class Base { public: std::size_t size() const { return n; } protected: Производный класс может восстановить уровень доступа унаследованного члена. Уро- /33/^ 1 вень доступа нельзя сделать менее жестким, чем уровень, первоначально указанный внутри базового класса. Функция size () этой иерархии является открытой в классе Base и закрытой в классе Derived. Чтобы сделать функцию size() открытой в классе Derived, в раздел public класса Derived можно добавить объявление using. Изменив опре- деление класса Derived следующим образом, функцию size () можно сделать дос- тупной для пользователей, а переменную п — доступной для классов, впоследствии производных от класса Derived. class Derived : private Base { public: // зафиксировать уровни доступа для членов, относящихся II к размеру объекта using Base::size; protected: using Base::n; } ; Подобно применению объявления using (раздел 3.1, стр. 102) для доступа к име- нам из пространства имен std, его можно использовать и для доступа к именам из ба- зового класса. Форма практически та же, за исключением того, что с левой стороны от оператора области видимости располагается имя класса, а не пространства имен. Уровни доступа по умолчанию В разделе 2.8 (стр. 88) упоминалось, что классы, определенные с использованием ключевых слов struct и class, имеют разные уровни доступа по умолчанию. Ана- логично, в зависимости от ключевого слова, использованного при определении про-
Глава 15. Объектно-ориентированное программирование 605 изводного класса, отличаются уровни доступа, принятые по умолчанию при насле- довании. Производный класс, определенный с использованием ключевого слова class, по умолчанию имеет закрытое (private) наследование. Производный класс, определенный с использованием ключевого слова struct, по умолчанию имеет от- крытое (public) наследование. class Base { /* ... */ }; struct Dl : Base { /* ... */ }; class D2 : Base { /* ... */ }; // по умолчанию открытое II наследование II по умолчанию закрытое // наследование Весьма распространено заблуждение, что между классами, определенными с ис- пользованием ключевого слова struct, и классами, определенными с помощью ключевого слова class, есть принципиальные различия. На самом деле они отли- чаются лишь заданными по умолчанию уровнем доступа к членам и уровнем доступа при наследовании. Никаких других различий между ними нет. class D3 : public Base { public: / * * / } ; // эквивалентное определение D3 struct D3 : Base { // по умолчанию открытое наследование /*...*/ // изначально доступ к членам открытый }; // equivalent definition of D4 class D4 : Base { // по умолчанию закрытое наследование /*...*/ // изначально доступ к членам закрытый }; Упражнения раздела 15.2.5 Упражнение 15.10. В упражнениях раздела 15.2.1 (стр. 592) был создан базовый класс, реализую- щий правила для читателей библиотеки. Предположим, что библиотека предоставляет следующие ви- ды носителей информации, для каждого из которых применяются отдельные правила выдачи и прие- ма и собственные сроки пользования. Организуйте эти элементы в иерархию наследования. book audio book record children's puppet sega video game video cdrom book nintendo video game rental book sony playstation video game Упражнение 15.11. Выберите одну из следующих абстракций, содержащую семейство типов (или придумайте собственную). Организуйте типы в иерархию наследования. (а) Форматы графических файлов (например gif, tiff, jpeg, bmp). (b) Геометрические примитивы (например box (прямоугольник), circle (круг), sphere (сфе- ра), cone (конус)). (с) Типы языка C++ (например class (класс), function (функция), member function (функция-член)). Упражнение 15.12. Укажите виртуальные функции, а также открытые и защищенные члены, кото- рые, вероятно, понадобятся для класса, выбранного в предыдущем упражнении.
606 Часть IV. Объектно-ориентированное и общее программирование Закрытое наследование по умолчанию при помощи ключевого слова class, на практике применяется относительно редко. Поэтому закрытое наследование имеет смысл указывать явно, при помощи маркера private, а не полагаться на поведе- ние по умолчанию. Это позволит избежать возможных недоразумений. 15.2.6 . Наследование и дружественные отношения Подобно любому другому классу, базовый и производный классы могут иметь дружественные классы и функции (раздел 12.5, стр. 493). Дружественные классы могут обращаться к закрытым и защищенным данным. Дружественные отношения не наследуются. Классы, дружественные для базового класса, не имеют никаких привилегий доступа к членам его производных классов. Если базовому классу предоставлен дружественный доступ, пользоваться им может только он. Классы, производные от него, не имеют никаких привилегий доступа к членам того класса, кото- рый предоставил дружественный доступ. Каждый класс сам устанавливает дружественный доступ к собственным членам. class Base { friend class Frnd; protected: int i ; }; // Frnd не имеет доступа к членам Dl class Dl : public Base { protected: int j ; }; class Frnd { public: int mem(Base b) { return b.i; } int mem(Dl d) { return d.i; } }; // ok: Frnd дружественен Base // ошибка: дружественные // отношения не наследуются II D2 не имеет доступа к членам Base class D2 : public Frnd { public: int mem(Base b) { return b.i; } // ошибка: дружественные // отношения не наследуются }; Если производному классу необходимо предоставить доступ к его членам тем классам или функциям, которые являются дружественными для его базового класса, сделать это необходимо непосредственно в производном классе. Классы, дружест- венные для базового класса, не имеют никаких привилегий доступа к типам, произ- водным от данного базового класса. Аналогично, если базовый и производный клас- сы нуждаются в доступе к другому классу, этот класс должен явно предоставить доступ как базовому, так и производному классу. 15.2.7 . Наследование и статические члены Если в базовом классе определен статический (static) член (раздел 12.6, стр. 496), для всей иерархии существует только один его экземпляр. Независимо от количества классов, производных от базового класса, существовать будет только один экземпляр каждого статического члена.
Глава 15. Объектно-ориентированное программирование 607 Статические члены подчиняются обычным правилам управления доступом: если член класса объявлен в базовом классе закрытым, производные классы не получат к нему доступа. Когда статический член класса доступен, к нему можно обращаться как из базового, так и из производного класса. Как обычно, для доступа можно исполь- зовать либо оператор области видимости, либо оператор точки, либо оператор стрелки. struct Base { static void statmem(); // открытая по умолчанию }; struct Derived : Base { void f(const Derived&); }; void Derived::f(const Derived &derived_obj) { Base::statmem(); // ok: statmem() определена в Base Derived::statmem(); // ok: Derived наследует statmemO // ok: объект производного класса применим для доступа к // статическому члену базового derived_obj.statmem(); // доступ в объекте класса Derived statmem(); // доступ в объекте данного класса Упражнения раздела 15.2.7 Упражнение 15.13. Перечислите для следующих классов все способы, которыми функция-член структуры ci могла бы обратиться к статическим членам структуры concreteBase. Перечис- лите все способы, которыми объект типа С2 мог бы обращаться к тем же членам. struct ConcreteBase { static std::size_t object_count(); protected: static std::size_t obj_count; }; struct Cl : public ConcreteBase { /* ... struct C2 : public ConcreteBase { /* . . . 15.3. Преобразования и наследование Понимание того, как происходит преобразование типов между базовыми и производ- ными классами, очень важно для освоения принципов объектно-ориентированного про- граммирования на языке C++. Как уже упоминалось, частью каждого объекта производного класса является объект базового, а следовательно, к объекту производного класса применимы опера- ции, рассчитанные на объект базового класса. Поскольку объект производного клас- са является также объектом базового, преобразование ссылки на производный класс в ссылку на базовый класс осуществляется автоматически. То есть ссылку на объект производного класса можно использовать там, где необходима ссылка на объект ба- зового (аналогично для указателей). Объекты базового класса могут существовать и как независимые объекты, и как часть объекта производного класса. Следовательно, объект базового класса может быть, а может и не быть частью объекта производного класса. В результате не нужно
608 Часть IV. Объектно-ориентированное и общее программирование никакого (автоматического) преобразования ссылки (или указателя) на базовый класс в ссылку (или указатель) на производный. Ситуация с преобразованием объектов (в отличие от ссылок или указателей) не- сколько сложней. Хотя для инициализации или присвоения объекта базового класса вполне можно использовать объект производного, нет никакого прямого способа преобразования объекта производного класса в объект базового. 15.3.1. Преобразование производного класса в базовый Если существует объект производного класса, его адрес можно использовать для присвоения или инициализации указателя на базовый класс. Аналогично, ссылку или объект производного класса можно использовать для инициализации ссылки на базовый класс. На самом деле, такой функции преобразования для объектов не су- ществует. Компилятор не будет автоматически преобразовывать объект производно- го класса в объект базового. Однако объект производного класса зачастую вполне возможно использовать для присвоения или инициализации объекта базового класса. Различие между инициа- лизацией (присвоением) объекта и автоматическим преобразованием, которое впол- не допустимо для ссылки или указателя, заключается в нюансах и заслуживает от- дельного пояснения. Преобразование ссылок и объектов — это разные вещи Как уже упоминалось, объект производного класса можно передать по ссылке функции, которая ожидает ссылку на базовый класс. Можно было бы предположить, что объект оказался преобразован. Однако на самом деле этого не происходит. При передаче объекта функции, ожидающей ссылку, на самом деле передается ссылка, связанная с объектом. Хотя может показаться, что передается объект, фактически аргументом является ссылка на него. Сам объект не копируется и преобразование его не изменяет. Он так и остается объектом производного класса. При передаче объекта производного класса функции, ожидающей объект базово- го класса (в отличие от ссылки),, ситуация совершенно иная. В этом случае тип па- раметра фиксирован: и во время компиляции, и во время выполнения это будет объ- ект базового класса. Если вызов такой функции происходит с объектом производно- го класса, в параметр будет скопирована та часть объекта производного класса, которая представляет собой объект базового класса. Очень важно понимать разницу между преобразованием объекта производного класса в ссылку на базовый класс и использованием объекта производного класса для инициализации или присвоения объекту базового класса. Использование объекта производного класса при инициализации или присвоении объекту базового При инициализации или присвоении значения объекту базового класса, фактиче- ски происходит вызов функции: при инициализации — вызов конструктора, а при присвоении —вызов оператора присвоения.
Глава 15. Объектно-ориентированное программирование 609 При использовании объекта производного класса для инициализации или присвоения объекту базового класса, существует два пути. Первый (хоть и мало- вероятный) подразумевает явное определение в базовом классе того, что должно происходить при копировании или присвоении объекта производного класса объекту базового. Для этого следует определить соответствующий конструктор и оператор присвоения. class Derived; class Base { public: Base(const Derived&); Base &operator=(const } ; // создать новый Base из Derived Derived&); // присвоение Derived В данном случае определение этих функций-членов позволит задать действия, осуществляемые при инициализации и присвоении объекта класса Derived объ- екту класса Base. Однако явное определение способов инициализации и присвоения объекту базо- вого класса объекта производного, встречается довольно редко. Значительно чаще в базовых классах определяют (явно или неявно) их собственный конструктор копий и оператор присвоения (глава 13, “Управление копированием”). Эти функции-члены получают параметр, который является ссылкой (константной) на базовый класс. По- скольку имеется способ преобразования ссылки на производный класс в ссылку на базовый, эти функции-члены управления копированием применимы для инициали- зации и присвоения объекту базового класса объекта производного. Item_base item; // объект базового класса Bulk_item bulk; // объект производного класса // ок: применение /! конструктора Item_base::Item_base(const Item_base&) Item_base item(bulk); // bulk усечен до его части Item_base II ok: вызов Item_base::operator=(const Item_base&) item = bulk; // bulk усечен до его части Item_base Когда происходит вызов конструктора копий или оператора присвоения класса Item_base для объекта класса Bulk_item, выполняются следующие действия. Объект Bulk_item преобразуется в ссылку на класс ltem_base, т.е. с объектом класса Bulk_item будет связана ссылка на класс Item_base. Полученная ссылка передается как аргумент оператору присвоения или конст- руктору копий. Операторы используют часть Item_base объекта класса Bulk_item для ини- циализации или присвоения значений соответствующим переменным-членам того объекта класса ltem_base, для которого вызван конструктор или оператор присвоения. По завершении работы оператора получается объект класса Item_base. Он со- держит копию части Item_base того объекта класса Bulk_item, который был использован при инициализации или присвоении, а часть аргумента, специфиче- ская для класса Bulk item, будет проигнорирована. В этих случаях говорят, что при инициализации или присвоении объекту item объекта bulk часть Bulk_item оказывается усечена. Объект класса Item_base со-
610 Часть IV. Объектно-ориентированное и общее программирование держит только те члены, которые определены в базовом классе ltem_base. Он не содержит члены, определенные в его производных классах, для них в объекте класса Item_base просто нет места. Доступ при преобразовании производный-базовый Подобно унаследованной функции-члену, преобразование из производного клас- са в базовый может быть либо доступно, либо нет, в зависимости от маркера доступа, указанного при наследовании производного класса. Чтобы узнать, будет ли преобразование в базовый класс доступным, следует выяс- нить, доступны ли открытые (public) члены базового класса. Если это так, преоб- разование доступно, а в противном случае — нет. При открытом наследовании и пользовательский код, и функции-члены произ- водных классов впоследствии смогут использовать преобразование производный- базовый. Если производный класс получен в результате закрытого или защищен- ного наследования, пользовательский код не сможет преобразовать объект произ- водного класса в объект базового. Объекты производных классов, полученных в результате закрытого наследования, не могут быть преобразованы в объекты базо- вого класса, а объекты производных классов, полученных в результате защищен- ного наследования, — могут. Независимо от маркера доступа наследования, открытый член базового класса дос- тупен в объекте производного. Следовательно, преобразование производный-базовый всегда доступно для членов производного класса и классов, дружественных для него. 15.3.2. Преобразование из базового класса в производный Автоматического преобразования из базового класса в производный не существу- ет. Объект базового класса нельзя использовать там, где ожидается объект произ- водного класса. Item_base base; Bulk_item* bulkP = &base; Bulk_item& bulkRef - base; Bulk_item bulk = base; // ошибка: невозможно преобразовать // базовый в производный II ошибка: невозможно преобразовать // базовый в производный II ошибка: невозможно преобразовать // базовый в производный Невозможность автоматического преобразования базового класса в производный объясняется тем, что базовый класс не содержит членов производного класса. Если бы объект базового класса можно было присвоить объекту производного, появилась бы возможность получить доступ к тем его членам, которых просто не существует. Но самым удивительным является то, что запрет преобразования базовый- производный распространяется даже на тот случай, когда указатель или ссылка на базовый класс фактически связаны с объектом производного класса. Bulk_item bulk; Item_base *itemP = &bulk; Bulk_item *bulkP = itemP; // ok: динамический тип Bulk_item II ошибка: невозможно преобразовать II базовый в производный
Глава 15. Объектно-ориентированное программирование 611 Во время компиляции компилятор никак не может “узнать”, будет ли фактически данное преобразование безопасно во время выполнения. Чтобы выяснить допусти- мость преобразований, компилятор просматривает лишь статические типы указате- лей и ссылок. В тех случаях, когда абсолютно точно известно, что преобразование объекта базового класса в объект производного совершенно безопасно, для изменения обыч- ного поведения компилятора можно использовать оператор static_cast (раз- дел 5.12.4, стр. 209). В качестве альтернативы для преобразования можно было бы применить оператор dynamic_cast (раздел 18.2.1, стр. 804), осуществляющий про- верку во время выполнения. 15.4. Конструкторы и функции управления копированием Тот факт, что в состав каждого объекта производного класса входят нестатиче- ские члены, определенные в производном классе, и один или несколько объектов базового (базовых) класса, весьма существенно влияет на способ создания, копи- рования, присвоения и удаления объектов производного класса. При создании, копировании, присвоении и удалении объектов производного класса, происходит также создание, копирование, присвоение и удаление внутренних объектов базо- вого класса. Конструкторы и функции-члены управления копированием не наследуются, в каждом классе следует определить его собственный конструктор (конструкторы) и функции-члены управления копированием. Подобно любым другим классам, ес- ли в классе не определены его собственные версии конструктора и функций- членов управления копированием, будут использованы их стандартные синтези- руемые версии. 15.4.1. Конструкторы и функции управления копированием базового класса Конструкторы и функции управления копированием базовых классов, не яв- ляющихся в свою очередь производными классами, на наследование существенно не влияют. Конструктор класса Item_base выглядит аналогично многим другим при- веденным ранее. Item_base(const std::string &book - double sales_price = 0.0): isbn(book), price(sales_price) { } Воздействие на наследование конструкторов базового класса заключается лишь в том, что появляется новый вид пользователя, который следует учитывать при созда- нии конструкторов. Подобно любому другому члену класса, конструкторы могут быть защищенными или закрытым. Некоторые классы нуждаются в специальных конструкторах, которые предназначены для использования только в объектах клас- сов, производных от них. Такие конструкторы должны быть защищенными.
612 Часть IV. Объектно-ориентированное и общее программирование 15.4.2. Конструкторы производного класса На конструкторы производных классов влияет тот факт, что они наследуются из другого класса. Кроме инициализации собственных переменных-членов, каждый конструктор производного класса инициализирует входящий в его состав объект ба- зового класса. Синтезируемый стандартный конструктор производного класса Синтезируемый стандартный конструктор производного класса (раздел 12.4.3, стр. 488) отличается от конструктора обычного класса только одним: кроме инициа- лизации переменных-членов производного класса, он должен выполнить также ини- циализацию той своей части, которая является объектом базового класса. Эта часть инициализируется стандартным конструктором базового класса. У рассматриваемого класса Bulk_item синтезируемый стандартный конструк- тор выполняется следующим образом. 1. Вызов стандартного конструктора класса Item_base инициализирует перемен- ную-член isbn пустой строкой, а переменную-член price — нулевым значением. 2. Инициализация переменных-членов класса Bulk_item осуществляется в соот- ветствии с обычными правилами инициализации переменных, т.е. переменные- члены qty и discount останутся неинициализированными. Определение стандартного конструктора Поскольку переменные-члены класса Bulk_item имеют встроенный тип, необ- ходимо определить собственный стандартный конструктор. class Bulk_item : public Item_base { public: Bulk_item(): min_qty(0), discount(0.0) { } // другие члены, как и прежде }; Для инициализации переменных-членов min_qty и discount, этот конструктор использует список инициализации (раздел 7.7.3, стр. 289). Конструктор неявно вы- зывает стандартный конструктор класса ltem_base, инициализирующий его часть базового класса. В результате выполнения этого конструктора, часть Item_base будет инициали- зирована стандартным конструктором Item_base(), после выполнения которого переменная-член isbn будет содержать пустую строку, а переменная-член price — нуль. После завершения работы конструктора Item_base (), начнется инициали- зация переменных-членов части Bulk_item и будет выполнено (пустое) тело конструктора. Передача аргументов конструктору базового класса Кроме стандартного конструктора, класс ltem_base предоставляет пользователям конструктор, позволяющий инициализировать переменные-члены isbn и price не- обходимыми значениями. Эту возможность желательно сохранить и при инициали- зации объектов класса Bulk item. То есть пользователи должны быть способны
Глава 15. Объектно-ориентированное программирование 613 указать исходные значения для всех переменных-членов объекта класса Bulk_item, включая процентную ставку и количество. Список инициализации для конструктора производного класса позволяет ини- циализировать переменные-члены только производного класса, он не может непо- средственно инициализировать свои унаследованные переменные-члены. Вместо этого конструктор производного класса инициализирует их косвенно, включив базо- вый класс в список инициализации конструктора. class Bulk_item : public Item_base { public: Bulk_item(const std::string& book, double sales_price, std::size_t qty = 0, double disc_rate = 0.0): Item_base(book, sales_price), min_qty(qty), discount(disc_rate) { } // другие члены, как и прежде }; Для инициализации внутреннего объекта базового класса этот конструктор ис- пользует конструктор Item_base () с двумя параметрами. Этому конструктору он передает собственные аргументы book и sales_price. Данный конструктор мож- но использовать следующим образом. // аргументами являются isbn, цена, минимальное количество и скидка Bulk_item bulk("0-201-82470-1", 50, 5, .19); Объект bulk создается при запуске конструктора ltem_base (), который ини- циализирует переменные-члены isbn и price, используя аргументы, передан- ные в списке инициализации конструктора Bulk_item (). После завершения ра- боты конструктора ltem_base(), переменные-члены объекта класса Bulk_item оказываются инициализированными. И наконец, выполняется (пустое) тело конст- руктора Bulk_item. ' Список инициализации конструктора содержит исходные значения для переменных- Ц членов базового класса. Порядок их инициализации он не определяет. Сначала инициа- / лизируются переменные-члены базового класса, а затем производного, причем в порядке их объявления. Использование аргументов по умолчанию в конструкторе производного класса Безусловно, эти два конструктора Bulk_item() можно было бы написать как один конструктор с аргументами по умолчанию, class Bulk_item : public Item_base { public: Bulk_item(const std::string& book, double sales_price, std::size_t qty = 0, double disc_rate = 0.0): Item_base(book, sales_price), min_qty(qty), discount(disc_rate) { } // другие члены, как и прежде }; Здесь каждый параметр снабжен значением по умолчанию, поэтому данный конструктор можно использоваться как с четырьмя аргументами, так и без едино- го аргумента.
614 Часть IV. Объектно-ориентированное и общее программирование Инициализирован может быть объект только непосредственного базового класса Класс может инициализировать внутренний объект лишь своего непосредствен- ного базового класса. Непосредственным базовым (immediate base) называют тот класс, имя которого указано в списке наследования. Если класс С происходит от класса В, который в свою очередь является производным из класса А, класс В счита- ется непосредственным базовым для класса С. Даже при том, что каждый объект класса С содержит внутренний объект класса А, конструкторы класса С не смогут непосредственно инициализировать часть А. Вместо этого класс С вынужден ини- циализировать объект класса В, а конструктор класса В в свою очередь должен ини- циализировать объект класса А. Причина этого ограничения кроется в том, что спо- соб создания и инициализации объектов класса В определяет автор класса В. Подоб- но любым другим пользователям класса В, автор класса С не имеет никакого права изменять эту спецификацию. Так, рассматриваемый книжный магазин мог бы иметь несколько систем скидок. Кроме оптовой скидки, он мог бы предоставлять скидку при приобретении некото- рого количества первых экземпляров, а остальные книги продавать за полную стои- мость. Или он мог бы предоставлять скидку при приобретении количества книг, превышающего некоторый предел. Каждая из этих систем скидок предполагает некоторое количество экземпляров и объем скидки. Для облегчения реализации этих отличающихся друг от друга систем скидок, создадим новый класс по имени Disc_item, предназначенный для хране- ния количества экземпляров и объема скидки. В этом классе не будет функции net_price О, но он послужит в качестве базового для таких классов, как Bulk_ i t em, позволяя реализовать разные системы скидок. Фундаментальная концепция. Рефакторинг Добавление класса Disc_item в иерархию Item_base является примером рефакто- ринга (refactoring). Рефакторинг подразумевает переделку иерархии классов с переда- чей некоторых функций и/или данных из одного класса в другой. Обычно рефакто- ринг имеет место в случаях, когда классы необходимо перестроить так, чтобы доба- вить новые функциональные возможности или изменить их в соответствии с новыми требованиями приложения. Рефакторинг — довольно обычная процедура в объектно-ориентированных приложе- ниях. Примечателен тот факт, что даже при изменении иерархии наследования код, который использует классы Bulk_item и Item base, изменять не придется. Но по- сле рефакторинга классов или их изменения любым другим способом, весь исполь- зующий эти классы код придется перекомпилировать. Чтобы реализовать этот проект, сначала необходимо определить класс Disc_item. // класс, содержащий объем скидки и количество экземпляров // используя эти данные, производные классы реализуют // ценовую стратегию class Disc_item : public Item_base { public: std::pair<size_t, double> discount_policy() const
Глава 15. Объектно-ориентированное программирование 615 { return std::make_pair(quantity, discount); } // другие члены, как и прежде double net_price(std::size_t) const = 0; Disc_item(const std::string& book = Item_base(book, sales_price), quantity(qty), discount(disc_rate) { } protected: std::size__t quantity; // минимальная покупка для скидки double discount; // доля применяемой скидки }; В этом классе, производном от класса Item_base, определены собственные пе- ременные-члены discount и quantity (количество). Его единственной функци- ей-членом является конструктор, который инициализирует базовую часть (класс Item_base) и собственные переменные-члены, определенные в классе Disc_item. Теперь можно переделать класс Bulk_item так, чтобы он происходил от класса Disc_item, а не непосредственно от класса Item_base. // скидка предоставляется в случае, когда приобретается // определенное количество экземпляров той же книги II скидка выражена в качестве доли, уменьшающей стандартную цену class Bulk_item : public Disc_item { public: std::pair<size_t, double> discount_policy() const { return std::make_pair(quantity, discount); } // другие члены, как и прежде Bulk_item* clone() const { return new Bulk_item(*this); } Bulk_item(const std::string& book = double sales_price = 0.0, std::size_t qty = 0, double disc_rate = 0.0): Disc_item(book, sales_price, qty, disc_rate) { } // переопределить базовую версию и реализовать политику II скидок при оптовых закупках double net price(std::size t) const; }; Теперь класс Bulk_item имеет прямой базовый класс, Disc_item, и косвенный базовый класс, Item_base. Каждый объект класса Bulk_item состоит из трех вло- женных частей: объекта Bulk_item (пустого), внутри которого находится объект Disc_item, внутри которого в свою очередь находится объект Item_base. Несмотря на то, что класс Bulk_item никаких собственных переменных-членов не имеет, ему необходим конструктор, который получает значения, используемые при инициализации унаследованных переменных-членов. Конструктор производного класса может инициализировать объект лишь непо- средственного базового класса. Применение имени Item_base в списке инициали- зации конструктора Bulk item () было бы ошибкой. Фундаментальная концепция. Соблюдение интерфейса базового класса Причиной, по которой конструктор может инициализировать объект лишь своего не- посредственного базового класса, заключается в том, что каждый класс определяет свои собственный интерфейс. При создании класса Disc_item. его конструкторы оп- ределяют способы инициализации объекта класса Disc_item. Как только интерфейс
616 Часть IV. Объектно-ориентированное и общее программирование класса определен, все взаимодейству ющие с ним объекты должны соблюдать его, даже когда он является частью объекта производного класса. По тем же причинам конструкторы производного класса не могут и не должны ини- циализировать или присваивать значения переменным-членам базового класса. Если это открытые или защищенные переменные-члены, конструктор производного класса вполне мог бы присвоить им значения. Но это нарушило бы интерфейс базового клас- са. Производные классы должны соблюдать интерфейс и инициализировать объект базового класса, используя конструкторы, а не присвоение значений переменным- членам в теле конструктора. Упражнения раздела 15.4.2 Упражнение 15.14. Переопределите классы Buik_item и item_base так, чтобы в каждом из них было достаточно определить только один конструктор. Упражнение 15.15. Укажите конструкторы базовых и производных классов иерархии классов библиотеки, описанной в первом упражнении на стр. 605. Упражнение 15.16. Рассмотрим следующее определение базового класса. }; Объясните, почему каждый из следующих конструкторов некорректен. (a) struct Cl : public Base { Cl(int val): id(val) { } }; (b) struct C2 : public Cl { C2(int val): Base(val), Cl(val){ } }; (c) struct C3 : public Cl { C3(int val): Base(val) { } }; (d) struct C4 : public Base { C4(int val) : Base(id + val){ } }; (e) struct C5 : public Base { C5() { } }; 15.4.3. Управление копированием и наследование Подобно любому другому классу, производный класс вполне может использовать синтезируемые функции-члены управления копированием, описанные в главе 13, “Управление копированием”. Наряду с членами производной части, синтезируемые версии функций копирования, присвоения и удаления учитывают и часть базового класса объекта. При копировании, присвоении и удалении базовой части объекта ис- пользуются конструктор копий, оператор присвоения и деструктор базового класса. Должен ли класс определять функции-члены управления копированием, полно- стью зависит от собственных членов класса. Базовый класс мог бы определить соб- ственные функции управления копированием, а в производном классе использовать синтезируемые версии или наоборот.
Глава 15. Объектно-ориентированное программирование 617 В классах, переменные-члены которых имеют лишь встроенный тип или тип класса (но не указатели), как правило, вполне можно использовать синтезируе- мые версии. Для копирования, присвоения и удаления таких членов, никаких специальных функций управления не нужно. Тем классам, членами которых яв- ляются указатели, зачастую необходимы их собственные функции управления копированием. Рассматриваемый класс Item_base и классы, производные от него, могут ис- пользовать синтезируемые версии функций управления копированием. При копи- ровании объекта класса Bulk_item происходит вызов (синтезируемого) конструк- тора копий класса ltem_base, копирующего переменные-члены isbn и price. Для копирования переменной-члена isbn используется конструктор копий класса string, а переменная-член price копируется непосредственно. По завершении ко- пирования базовой части, начинается копирование производной. Обе переменные- члена класса Bulk_item имеют тип double, поэтому они копируются непосредст- венно. Оператор присвоения и деструктор работают аналогично. Определение конструктора копий производного класса Если в производном классе явно определен его собственный конструктор копий или опе- \ ратор присвоения, стандартные функции оказываются полностью переопределенными. W Конструктор копий и оператор присвоения для унаследованных классов отвечают за ко- пирование и присвоение компонентов их базового класса, а также членов самого класса непосредственно. Если в производном классе определен собственный конструктор копий, он дол- жен, как правило, явно использовать конструктор копий базового класса, чтобы инициализировать базовую часть объекта. class Base { /* ... */ }; class Derived: public Base { public: // Base::Base(const Base&) не вызывается автоматически Derived(const Derived& d): Base(d) /* инициализация других членов */ { /* ... */ } }; Список инициализации конструктора Base (d) преобразует (раздел 15.3, стр. 607) объект d производного класса в ссылку на ее базовую часть и вызывает конструктор копий базового класса. Вследствие отсутствия списка инициализации базового класса, для инициализации базовой части объекта будет запущен стандарт- ный конструктор класса Base. // вероятно некорректное определение конструктора копий Derived() Derived(const Derived& d) /* инициализация производного класса ★/ { / * * / } Если бы при инициализации объекта класса Derived копировались соответст- вующие переменные-члены объекта d, вновь созданный объект оказался бы не- сколько странным: его часть Base содержала бы значения по умолчанию, а перемен- ные-члены части Derived — копии значений другого объекта.
618 Часть IV. Объектно-ориентированное и общее программирование Оператор присвоения производного класса Как обычно, оператор присвоения подобен конструктору копий: если в произ- водном классе определен его собственный оператор присвоения, этот оператор дол- жен явно присваивать базовую часть. // Base::operator=(const Base&) не вызывается автоматически Derived &Derived::operator=(const Derived &rhs) { if (this != &rhs) { Base::operator=(rhs); // присвоение базовой части // выполнить все действия, необходимые для освобождения II прежнего значения производной части II присвоить переменные-члены производной части } Как обычно, оператор присвоения должен иметь средство предотвращения само- присвоения. Если левый и правый операнды отличаются, происходит вызов опера- тора присвоения класса Base, осуществляющего присвоение части базового класса. Оператор присвоения может быть определен в классе, но может быть и синтезируе- мым. Это не имеет значения, поскольку он может быть вызван непосредственно. Оператор базового класса удаляет прежние значения в базовой части левого операн- да и присваивает им новые значения из правого операнда. Как только этот оператор заканчивает работу, остается осуществить необходимые действия по присвоению значений переменным-членам производного класса. Деструктор производного класса Деструктор работает совершенно не так, как оператор присвоения и конструктор копий: деструктор производного класса никогда не отвечает за удаление перемен- ных-членов своих базовых объектов. Компилятор всегда неявно вызывает деструк- тор для базовой части производного объекта. Каждый деструктор делает только то, что необходимо для освобождения переменных-членов только своего класса. class Derived: public Base { public: // Base::-Base вызывается автоматически -Derived() { /* удаление значений переменных-членов производного класса */ } }; Освобождение объектов происходит в порядке, противоположном их созданию: сначала выполняется деструктор производного класса, а затем происходит вызов де- структора базового класса, т.е. назад по иерархии наследования. 15.4.4. Виртуальные деструкторы Тот факт, что вызов деструкторов для базовых частей осуществляется автомати- чески, имеет важнейшие последствия при проектировании базовых классов. При удалении указателя, который содержит адрес области динамически распре- деляемой памяти, занятой объектом, сначала следует выполнить деструктор, кото- рый удалит объект, прежде чем будет освобождена занимаемая им память. Когда речь идет об объектах в иерархии наследования, статический тип указателя вполне
Глава 15. Объектно-ориентированное программирование 619 может отличаться от динамического типа удаляемого объекта. То есть может быть удален указатель на объект базового класса, который фактически указывает на объ- ект производного. При удалении указателя на базовый класс будет вызван деструктор базового класса, который освободит его переменные-члены. Если в действительности объект имеет тип производного класса, результат окажется непредсказуемым. Чтобы гаран- тировать вызов деструктора соответствующего класса, деструктор в базовом классе следует сделать виртуальным. class Item_base { public: // не делает ничего, но виртуальный деструктор нужен // когда указатель базового класса содержит адрес объекта II производного, он все равно будет удален virtual ~Item_base() { } }; Если деструктор объявлен виртуальным, выбор его конкретной версии, при удале- нии указателя, будет зависеть от конкретного типа объекта, на который он указывает. Item_base *itemP = new Item_base; // статический и динамический // типы одинаковы delete itemP; // ok: вызов деструктора для Item_base itemP - new Bulk_item; // ok: статический и динамический II типы разные delete itemP; // ok: вызов деструктора для Bulk_item Подобно другим виртуальным функциям, виртуальный характер деструктора на- следуется. Следовательно, если в корневом классе иерархии деструктор объявлен виртуальным, во всех производных классах деструкторы также будут виртуальны- ми. В этом случае деструктор производного класса будет виртуальным независимо от того, определен ли в классе собственный деструктор явно или использован стан- дартный синтезируемый деструктор. Деструкторы базовых классов — это важнейшее исключение из Правила трех (Rule of Three) (раздел 13.3, стр. 515). Это правило гласит, что если класс нуждается в собственном деструкторе, он почти наверняка нуждается и в других собственных функциях-членах управления копированием. Базовый класс почти всегда нуждается в деструкторе, поэтому имеет смысл объявить его виртуальным. Тот факт, что в ба- зовом классе имеется пустой деструктор, предназначенный лишь для того, чтобы объявить его виртуальным, вовсе не означает необходимость в операторе присвое- ния и конструкторе копий. В корневом классе иерархии наследования следует определить виртуальный дест- руктор, даже если он ничего не должен делать. Конструкторы и операторы присвоения не должны быть виртуальными Из всех функций-членов управления копированием, только деструктор должен быть объявлен как виртуальный. Конструкторы не могут быть объявлены виртуаль- ными. Они выполняются прежде, чем объект будет создан полностью. То есть во время работы конструктора динамический тип объекта еще не завершен.
620 Часть IV. Объектно-ориентированное и общее программирование Хотя в базовом классе функцию-член operator= вполне можно объявить вир- туальной, это никак не повлияет на операторы присвоения, используемые в произ- водных классах. Каждый класс имеет собственный оператор присвоения. Оператор присвоения в производном классе имеет параметр, тип которого совпадает с типом самого класса. Этот тип параметра оператора присвоения будет отличаться от типа аналогичного параметра в любом другом классе иерархии. Объявление оператора присвоения виртуальным, вероятно, покажется сомни- тельным, поскольку виртуальная функция в производном классе должна иметь параметр того же типа, что и в базовом. Типом параметра оператора присвоения ба- зового класса является ссылка на тип собственного класса. Когда этот оператор вир- туален, каждый класс унаследует виртуальную функцию-член operator=, ожи- дающую передачи объекта базового класса. Но этот оператор отличается от операто- ра присвоения производного класса. Объявление оператора присвоения виртуальным — бесполезная и сомнительная затея. Упражнения раздела 15.4.4 Упражнение 15.17. Опишите условия, при которых класс должен иметь виртуальный деструктор. Упражнение 15.18. Какие операции должен выполнять виртуальный деструктор? Упражнение 15.19. Что неправильно в определении этого класса? class Abstractobject { public: virtual void doit(); // другие члены, за исключением функций управления // копированием }; Упражнение 15.20. Вернемся к упражнению раздела 13.3 (стр. 509), где был написан класс, функции-члены управления копированием которого отображали сообщение о своем выполнении. Добавьте аналогичные сообщения в конструкторы классов Buik_item и item_base. Создайте функции-члены управления копированием, осуществляющие те же действия, что и синтезируемые версии, но отображающие сообщения. Создайте программу, использующую объекты и функции классов, которые используют класс item_base. Попробуйте сделать прогноз, какие сообщения будут отображены при создании и удалении объектов, а затем проверьте его на практике. Про- должайте эксперимент до тех пор, пока не сможете правильно сделать прогноз о поведении функ- ций-членов управления копированием при выполнении данного фрагмента кода. 15.4.5. Виртуальность конструкторов и деструкторов Создание объекта производного класса начинается с запуска конструктора базо- вого класса, позволяющего инициализировать базовую часть объекта. Во время ра- боты конструктора базового класса производная часть объекта остается неинициа- лизированной. Фактически объект еще является объектом производного класса. При удалении объекта производного класса, сначала удаляется его производная часть, а затем базовые части в порядке, обратном их созданию.
Глава 15. Объектно-ориентированное программирование 621 В обоих случаях, при работе конструктора и деструктора, объект находится в не- завершенном состоянии. Чтобы приспособиться к этой незавершенности, компиля- тор рассматривает объект так, как будто его тип изменяется в процессе создания и удаления. Внутри конструктора и деструктора базового класса объект производного класса рассматривается как объект базового. Изменение типа объекта в процессе создания и удаления влияет на привязку виртуальных функций. ПРИ вызове виртуальной функции внутри конструктора или деструктора сработает та из ее версий, которая определена для класса текущего конструктора или деструктора. Связывание применяется к виртуальным функциям при их непосредственном вызове конструктором (или деструктором) либо при косвенном вызове функцией, вызванной конструктором (или деструктором). Чтобы лучше понять такое поведение, давайте рассмотрим, что произойдет, если версия виртуальной функции производного класса будет вызвана из конструктора (или деструктора) базового класса. Производная версия виртуальной функции, веро- ятнее всего, обратится к переменным-членам объекта производного класса. В конце концов, если бы версия функции производного класса не должна была бы использо- вать переменные-члены объекта производного класса, производный класс, вероятнее всего, использовал бы ее версию, определенную в базовом классе. Однако во время вы- полнения конструктора (или деструктора) базового класса переменные-члены произ- водной части объекта еще не инициализированы. Если бы такой доступ был разрешен, вероятнее всего, произошло бы аварийное завершение выполнения программы. 15.5. Область видимости класса при наследовании Каждый класс обладает собственной областью видимости (scope) (раздел 12.3, стр. 473), внутри которой определены имена его членов. При наследовании область видимости производного класса вложена внутрь области видимости его базовых классов. Если имя не найдено внутри области видимости производного класса, по- иск его определения продолжается в областях видимости базовых классов. Именно это иерархическое вложение областей видимости класса при наследова- нии позволяет ему непосредственно обращаться к членам своего базового класса, как будто они являются членами производного класса. Bulk_item bulk; cout << bulk.book(); В данном случае поиск определения имени book осуществляется следующим образом. 1. bulk — это объект класса Bulk_item. Поиск определения имени book произво- дится внутри класса Bulk_item. Имя не найдено. 2. Поскольку класс Bulk_item является производным от класса Item_base, по- иск продолжается в классе Item_base. Имя book обнаружено в классе Item_ base. Определение найдено успешно.
622 Часть IV. Объектно-ориентированное и общее программирование 15.5.1. Поиск имен осуществляется во время компиляции Статический тип объекта, ссылки или указателя определяет действия, которые может выполнять объект. Даже когда статический и динамический типы отличаются (это бывает в случае, когда используются ссылка или указатель на базовый класс), набор применимых членов определяет статический тип. Например, в класс Disc_ item можно было бы добавить функцию-член, которая возвращает пару (тип pair), содержащую минимальное (или максимальное) количество и цену со скидкой. class Disc_item : public Item_base { public: std::pair<size_t, double> discount_policy() const { return std::make_pair(quantity, discount); } // другие члены, как и прежде Обратиться к функции discount_policy () можно только при помощи объек- та, указателя или ссылки класса Disc_item или класса, производного от него. Bulk_item bulk; Bulk_item *bulkP = &bulk; // ok: статический и динамический Item base * itemP I/ типы совпадают &bulk; // ok: статический и динамический // типы разные bulkP->discount_policy(); // ok: bulkP имеет тип Bulk_item* itemP->discount_policy(); // ошибка: itemP имеет тип Item_base* Обращение через itemP — это ошибка, поскольку указатель (ссылка или объект) на базовый класс позволяет обратиться только к базовой части объекта, а в базовом классе нет функции-члена discount_policy (). Упражнения раздела 15.5.1 Упражнение 15.21. Переопределите иерархию item_base так, чтобы включить в нее класс Disc_item. Упражнение 15.22. Переопределите класс Buik_item и класс, реализованный в упражнениях раздела 15.2.3 так, чтобы они наследовали класс Disc_item, представляющий ограничивающую стратегию скидок. 15.5.2. Конфликт имен и наследование Хотя к члену базового класса вполне можно обратиться непосредственно, как будто это член производного класса, он сохраняет свою принадлежность к базовому классу. О том, какой из классов фактически содержит данный член, обычно можно не заботиться. Главное, чтобы в базовом и производном классах не использовались совпадающие имена. Г""^ Член производного класса, имя которого совпадает с именем члена базового класса, 1 скрывает член базового класса и предотвращает прямой доступ к нему. struct Base { Base(): mem(0) { }
Глава 15. Объектно-ориентированное программирование 623 struct Derived : Base { Derived(int i): mem(i) { } // инициализирует Derived::mem int get_mem() { return mem; } // возвращает Derived::mem int get_base_mem() { return Base::mem; } protected: int mem; // скрывает mem в базовом классе }; При обращении к переменной mem внутри функции get_mem () будет использо- вана та переменная mem, которая определена в классе Derived. Derived d(42); cout << d.get_mem() endl ; // отобразит 42 Результатом выполнения этого кода будет 4 2. Применение оператора области видимости для доступа к скрытым членам Чтобы обратиться к скрытому члену базового класса, можно использовать опера- тор области видимости. struct Derived : Base { int get_base_mem() { return Base::mem; } }; Оператор области видимости вынуждает компилятор искать имя mem начиная с класса Base. По возможности, при проектировании производных классов желательно избегать конфликтов с именами членов базового класса. Упражнения раздела 15.5.2 Упражнение 15.23. Предположим, что существуют следующие базовый и производный классы, struct Base { struct Derived : public Base { foo(string); bool bar(Base *pb); void foobar(); }; Укажите ошибки в каждом из следующих примеров, а также способы их устранения. (a) Derived d; d.foo(1024); (b) void Derived::foobar() { bar = 1024; } (c) bool Derived::bar(Base *pb) { return foo_bar -- pb->foo_bar; }
624 Часть IV. Объектно-ориентированное и общее программирование 15.5.3. Область видимости и функции-члены Функция-член производного класса, имя которой совпадает с именем функции- члена базового класса, ведет себя в подобной ситуации аналогично переменной- члену, т.е. скрывает член базового класса внутри области видимости производного. Сокрытие функции-члена базового класса происходит даже тогда, когда прототипы функций отличаются. struct Base { int memfcn(); } ; struct Derived : Base { int memfcn(int); // скрывает memfcnO в базовом классе }; Derived d; Base b; b.memfcn(); // вызов Base::memfen() d.memfcn(10) ; // вызов Derived::memfen() d.memfcn(); // ошибка: версия memfcn() без аргументов скрыта d.Base::memfcn(); // ok: вызов Base::memfen() Объявление функции memfen () в классе Derived скрывает ее объявление в классе Base. Не удивительно, что при первом обращении к ней с использованием объекта b класса Base, срабатывает ее версия из базового класса. Аналогично, при втором обращении с использованием объекта d срабатывает версия из класса Derived. Весьма интересно третье обращение. d.memfcn(); // ошибка: производный класс не имеет версии // функции memfсп() без аргументов Чтобы выполнить это обращение, компилятор ищет имя memfen, которое и нахо- дит в классе Derived. Обнаружив имя, компилятор прекращает дальнейшие поис- ки. Однако количество переданных аргументов и их тип (точнее, их отсутствие) не совпадает с указанным в определении функции memfen () в классе Derived (один аргумент типа int). В результате происходит ошибка. Напомним, что функции, объявленные в локальной области видимости, не перегру- жают функции, определенные в глобальной области видимости (раздел 7.8.1, стр. 293). ( ) Аналогично, функции-члены, определенные в производном классе, не перегружают функции-члены, определенные в базовом классе. При вызове функции с использовани- ем объекта производного класса, аргументы должны соответствовать версии той функ- ции, которая определена в производном классе. Функции базового класса рассматри- ваются только тогда, когда в производном классе такой функции нет вообще. Перегруженные функции Подобно любой другой функции, функция-член (виртуальная или нет) может быть перегружена. В производном классе можно переопределить любое количество унаследованных версий функции. Если в производном классе переопределена любая из перегруженных версий функции- В члена, при помощи объекта производного класса будет доступна только эта версия (версии) функции, переопределенная в производном классе. Если в объекте производного класса необходимо сделать доступными все версии перегруженной функции, следует переопределить либо все версии, либо ни одной.
Глава 15. Объектно-ориентированное программирование 625 Иногда в классе следует переопределить только некоторые из версий в наборе перегруженных функций, а другие унаследовать. В таких случаях было бы крайне утомительно переопределять каждую из необходимых для специализации версий функции базового класса. Чтобы не переопределять в производном классе каждую версию функции базово- го класса, которую можно унаследовать, в производном классе следует воспользо- ваться объявлением using (раздел 15.2.5, стр. 604) для перегруженного члена клас- са. Объявление using задает только имя, но не список параметров. Таким образом, объявление using для имени функции-члена базового класса добавляет все ее пере- груженные экземпляры в область видимости производного класса. Перенос всех имен в область видимости производного класса позволяет переопределять только те версии функции, которые должны отличаться от базовых, а для остальных исполь- зовать унаследованные определения. 15.5.4. Виртуальные функции и область видимости Напомним, что для динамического связывания виртуальный член класса следует вызвать через ссылку или указатель на базовый класс. В этом случае компилятор ищет функцию в базовом классе. Обнаружив необходимое имя, компилятор прове- ряет, соответствуют ли переданные аргументы ее параметрам. Теперь вполне очевидно, почему виртуальные функции должны иметь одинако- вый прототип и в базовом, и в производном классе. Если бы функция-член базового класса получала аргументы, отличные от одноименной функции-члена производно- го класса, ее невозможно было бы вызвать, используя ссылку или указатель на базо- вый класс. Рассмотрим следующую (гипотетическую) коллекцию классов. class Base { public: virtual int fcn(); }; class Dl : public Base { public: // скрывает fcn() в базовом; данная fcn() не виртуальна int fen(int); // список параметров fcn() отличается от Base !/ D1 наследует определение Base::fen() }; class D2 : public Dl { public: int fen (int); // невиртуальная функция скрывает Dl:: fen (int) int fcn(); // переопределяет виртуальную fcn() из Base }; Версия функции f cn () в объекте Dl не переопределяет виртуальную функцию fen () из класса Base. Вместо этого она скрывает функцию fen () базового клас- са. На самом деле, объект D1 имеет две функции по имени fen: унаследованную из базового класса Base и собственную, невиртуальную функцию-член fen (), полу- чающую параметр типа int. Однако объект D1 (либо ссылка или указатель на объект D1) не может вызвать виртуальную функцию-член класса Base, поскольку эта функция скрыта определением функции fen (int).
626 Часть IV. Объектно-ориентированное и общее программирование Вызов скрытой виртуальной функции при помощи базового класса Когда вызов функции происходит при помощи ссылки или указателя базового класса, компилятор ищет эту функцию в базовом классе, а производные игнорирует. Base bobj; Dl dlobj; D2 dobj; Base *bpl - &bobj, *bp2 = &dlobj, *bp3 = &d2obj; bpl->fcn(); // ok: во время выполнения, вызов виртуальной функции // рассматривается как Base::fen() bp2->fcn(); // ok: во время выполнения, вызов виртуальной функции // рассматривается как Base::fen() bp3->fcn(); // ok: во время выполнения, вызов виртуальной функции // рассматривается как D2::fcn() Все три указателя являются указателями на базовый класс, поэтому поиск опре- деления функции fen () во всех трех обращениях осуществляется в классе Base. Поскольку оно там есть, все обращения допустимы. Затем, поскольку функция f сп () была виртуальной, компилятор создает такой код, который распознает обра- щение во время выполнения на основании фактического типа объекта, с которым связана ссылка или указатель. В случае указателя Ьр2, основным будет объект D1. Данный класс не переопределил виртуальную функцию fcn() в версии без аргу- ментов. Обращение с использованием указателя Ьр2 во время выполнения будет рассматриваться как вызов версии, определенной в классе Base. Фундаментальная концепция. Поиск имени и наследование Концепция поиска имени функции в иерархии наследования при вызове крайне важ- на. Этот процесс проходит в четыре этапа. 1. Сначала определяется статический тип объекта, ссылки или указателя, ис- пользуемого для вызова функции. 2. Поиск функции осуществляется в данном классе. Если она не найдена, поиск продолжается в непосредственном базовом классе и далее по цепи классов, по- ка либо функция не будет найдена, либо окажется достигнут последний класс иерархии. Если имя не найдено ни в данном классе, ни в его базовых классах, обращение считается ошибочным. 3. Как только имя оказывается найдено, осуществляется обычная проверка соот- ветствия типов (раздел 7.1.2, стр. 254), позволяющая выявить допустимость данного обращения для найденного определения. 4. Если обращение допустимо, компилятор создает код. Если функция вир гуаль- на и обращение осуществляется с использованием ссылки или указателя, ком- пилятор создает код, способный на основании динамического типа объекта вы- яснить, какую именно из версий следует запустить. В противном случае ком- пилятор создает код, вызывающий функцию непосредственно. Упражнения раздела 15.5.4 Упражнение 15.24. Почему выражение p->net_price (ю); приводит к вызову версии функции net_price(), определенной в классе Buik_item, а выражение item.net_ price (ю); — версии, определенной в классе item_base?
Глава 15. Объектно-ориентированное программирование 627 Bulk_item bulk; Item_base item(bulk); Item_base *p = &bulk; Упражнение 15.25. Предположим, что класс Derived происходит от класса Base, в котором каждая из следующих функций объявлена виртуальной. Допустим также, что в классе Derived предполагается определить собственные версии виртуальных функций. Укажите, какие из объяв- лений в классе Derived приведут к ошибке и почему. (a) Base* Base::copy(Base*); Base* Derived::copy(Derived*); (b) Base* Base::copy(Base*); Derived* Derived::copy(Base*); (c) ostream& Base::print(int, ostream&=cout); ostream& Derived::print(int, ostream&); (d) void Base::eval() const; void Derived::eval(); 15.6. Чистые виртуальные функции Созданный ранее класс Disc_item (стр. 614) демонстрирует весьма интересную проблему: он наследует функцию net_price О из класса Item_base, но не пере- определяет ее. Функция net_price() не переопределена потому, что для класса Disc_item она не имеет никакого смысла. Класс Disc_item не придерживается ни одной из стратегий скидок в приложении. Этот класс существует исключительно как вспомогательный для других классов, наследующих его. Здесь не предполагается, что пользователи будут создавать объекты класса Disc_item. Объекты класса Disc_item должны существовать исключительно в составе объектов других классов, производных от класса Disc_item. Однако, как установлено, ничто не мешает пользователю создать обычный объект класса Disc_item. Это оставляет открытым вопрос о том, что произойдет, если пользова- тель, создав объект класса Disc_item, вызовет его функцию net_price()? Как уже известно из предыдущего раздела, будет вызвана функция net_price (), унас- ледованная из класса ltem_base, которая возвращает цену без скидки. Сложно предположить, какого поведения ожидает пользователь от функции net_price () класса Disc_item. Реальная проблема заключается в том, чтобы за- претить создавать такие объекты вообще. Чтобы реализовать это намерение и одно- значно указать, что функция net_price () в классе Disc_item никакого смысла не имеет, ее следует объявить чистой виртуальной функцией (pure virtual function). Чтобы объявить функцию чистой виртуальной, после списка ее параметров следует добавить = 0. class Disc_item : public Item_base { public: double net_price(std::size_t) const = 0; } Объявление функции чистой виртуальной означает, что она предоставляет ин- терфейс для последующих классов, где ее следует переопределить, но в данном клас- се ее вызывать никогда не будут. Это очень важно, поскольку в результате пользова- тели не смогут создать объект класса Disc_item.
628 Часть IV. Объектно-ориентированное и общее программирование Попытка создания объекта абстрактного базового класса (или абстрактного класса) приводит к ошибке во время компиляции. // в классе Disc_ item объявлены чистые виртуальные функции Disc_item discounted; // ошибка: невозможно создать объект // класса Disc_item Bulk_item bulk; // ok: объект класса Disc_item внутри // объекта класса Bulk_item Класс, который содержит (или наследует) одну или несколько чистых виртуальных функ- 1 ций, называют абстрактным классом (abstract base class). Объект абстрактного класса не ^у/ может быть создан самостоятельно, а только как часть объекта класса, производного от --Яг абстрактного. Упражнения раздела 15.6 Упражнение 15.26. Создайте собственную версию класса Disc_item и сделайте его абстрактным. Упражнение 15.27. Попытайтесь создать объект класса Disc_item и убедитесь, что компиля- тор сообщил об ошибке. 15.7. Контейнеры и наследование Для хранения объектов, связанных наследственными отношениями, вполне мо- жет понадобится использовать контейнеры (или встроенные массивы). Однако тот факт, что объекты не полиморфны (раздел 15.3.1, стр. 608) существенно влияет на способ использования контейнеров с объектами классов из иерархии наследования. Рассматриваемое приложение книжного магазина, например, вероятно, нуждается в элементе, имитирующем покупательскую корзинку (basket), в которой содержатся книги, приобретаемые клиентом. Информация о приобретаемых книгах будет зано- ситься в контейнер типа multiset (раздел 10.5, стр. 403). Чтобы определить контей- нер multiset, необходимо указать тип объектов, которые он будет хранить. При по- мещении объекта в контейнер, его содержимое копируется (раздел 9.3.3, стр. 346). Определим контейнер multiset для хранения объектов базового класса. multiset<Item_base> basket; Item_base base; Bulk_item bulk; basket.insert(base); // ok: добавить в basket копию объекта base basket.insert(bulk); // ok: но bulk будет усечен до базовой части Впоследствии, при добавлении объектов, имеющих тип производного класса, в контейнере сохранится только базовая часть объекта. Напомним, что при копиро- вании объекта производного класса в объект базового, объект производного класса усекается (раздел 15.3.1, стр. 608). Элементами контейнера являются объекты класса Item_base. Несмотря на то, что элемент был создан как копия объекта класса Bulk_item, при вызове его функ- ции net_price() цена будет получена без скидки. Помещенный в контейнер multiset объект перестанет быть объектом производного класса.
Глава 15. Объектно-ориентированное программирование 629 Поскольку при присвоении объекту базового класса, объект производного класса усе- кается, контейнеры не очень удобны для хранения объектов разных классов, связанных наследственными отношениями. Определив контейнер как предназначенный для хранения объектов производного класса, устранить эту проблему все равно не удастся. В данном случае, в контейнер не удастся поместить объекты класса Item_base, поскольку нет стандартного преобра- зования из базового класса в производный. Можно, конечно, явно преобразовать объ- ект базового класса в объект производного, а полученный в результате объект добавить в контейнер. Однако теперь катастрофа произойдет при попытке использования эле- мента, ведь в данном случае элемент будет рассматриваться как объект производного класса, а его переменные-члены производной части окажутся не инициализированы. Единственный реальный вариант использования контейнера — это хранение в нем указателей на объекты. Этот подход вполне работоспособен, однако ценой переклады- вания проблем манипулирования объектами и указателями на пользователей класса. Пользователь должен гарантировать, что объекты не будут удалены до тех пор, пока указали на них хранятся контейнере. Если объекты размещаются в динамически рас- пределяемой памяти, пользователь должен гарантировать, что занимаемые ими облас- ти будут правильно освобождены при удалении контейнера. Лучшее и наиболее попу- лярное решение этой проблемы продемонстрировано в следующем разделе. Упражнения раздела 15.7 Упражнение 15.28. Создайте вектор (контейнер типа vector) для хранения объектов класса item_base, а затем скопируйте в него несколько объектов класса Bulk_item. Организуйте перебор элементов вектора и вызов их функции net_price (). Упражнение 15.29. Повторите программу, но на сей раз сохраните в векторе указатели на объек- ты класса item_base. Сравните полученные результаты. Упражнение 15.30. Объясните расхождение в ценах, полученных с помощью разработанных ра- нее программ. Если никакого расхождения нет, объясните, почему. 15.8. Управляющие классы и наследование Ирония объектно-ориентированного программирования на языке C++: сами объ- екты можно использовать не всегда. Вместо них приходится использовать указатели и ссылки на объекты. Рассмотрим следующий фрагмент кода. void get__prices (Item_base object, const Item_base *pointer, const Item_base Preference) { // какая из версий функции net_price() будет использована, // выясняется во время выполнения cout << pointer->net_price(1) << endl; cout << reference.net_price(1) << endl; // всегда применяется Item_base::net_price() cout << object.net_price(1) << endl; )
630 Часть IV. Объектно-ориентированное и общее программирование Распознавание обращения при помощи указателя (pointer) и ссылки (reference) во время выполнения осуществляется на основании динамического типа объекта, с которым они связаны. К сожалению, применение указателей и ссылок возлагает на пользователей клас- са дополнительные задачи. Одна из таких задач рассматривалась в предыдущем раз- деле, на примере взаимодействия объектов наследуемых классов и контейнеров. Общепринятая практика в языке C++ подразумевает применение т.н. оболочки (cover), или управляющего класса (handle class). Управляющий класс хранит и мани- пулирует указателем на базовый класс. Тип объекта, адрес которого хранит указа- тель, может изменяться; он может указывать на объект как базового, так и производ- ного класса. При помощи этого управляющего класса пользователи могут обращать- ся к функциям иерархии наследования. Поскольку для выполнения этих функций управляющий класс использует свой указатель, поведение виртуальных членов во время выполнения будет изменяться в зависимости от вида объекта, с которым управляющий класс фактически связан в настоящий момент. Таким образом, поль- зователь управляющего класса не только получает динамическое поведение, но и может не заботиться о манипулировании указателем. Управляющий класс, являющийся оболочкой иерархии наследования, имеет два важных свойства. Подобно любым другим классам, которые содержат указатель (раздел 13.5, стр. 522), здесь следует принять решение о поведении функций управления ко- пированием. Управляющий класс, являющийся оболочкой иерархии наследо- вания, как правило, обладает поведением интеллектуального указателя (раз- дел 13.5.1, стр. 525) или значения (раздел 13.5.2, стр. 530). Управляющий класс определяет, скроет ли интерфейс управляющего класса иерархию наследования или же предоставит ее. Если иерархия не скрыта, пользователи должны иметь информацию об использовании объектов в основ- ной иерархии. Единственно правильного выбора здесь нет, решение зависит от подробностей реализации конкретной иерархии, конструкции классов и способов их взаимодейст- вия с другими классами. В двух следующих разделах будет реализовано два разных вида управляющих классов, которые решают задачи проекта разными способами. 15.8.1. Управляющий класс, подобный указателю В качестве первого примера создадим подобный указателю управляющий класс по имени Sales_item, предоставляющий иерархию Item_base. Пользователи класса Sales_item используют его как указатель: объект класса Sales_item сле- дует связать с объектом типа Item_base, а затем, используя операторы * и - >, вы- звать функции класса Item_base. // связать управляющий класс с объектом класса Bulk_item Sales_item item(Bulk_item("0-201-8247 0-1" , 35, 3, .20)); item->net_price(); // обращение к виртуальной функции net_price() Пользователи, однако, не должны будут манипулировать объектом, на который указывает управляющий класс; эту часть работы возьмет на себя класс Sales_item.
Глава 15. Объектно-ориентированное программирование 631 Когда пользователь вызывает функцию при помощи объекта класса Sales_item, он получает полиморфное поведение. Определение управляющего класса Снабдим разрабатываемый класс тремя конструкторами: стандартным конструк- тором, конструктором копий и конструктором, получающим объект класса Item_base. Третий конструктор копирует объект класса Item_base и гарантирует существование этой копии, пока существует объект класса Sales_item. При копи- ровании или присвоении объекта класса Sales_item, копируется лишь указатель, а не объект. Подобно другим подобным указателям, для контроля количества исполь- зуемых копий управляющий класс используют счетчик пользователей. Для хранения указателя и счетчика пользователей, описанные до сих пор классы использовали вспомогательный класс. В этом классе применим другой подход, про- иллюстрированный на рис. 15.2. Класс Sales_item имеет две переменные-члена, и обе они являются указателями: указатель на объект класса ltem_base и указатель на счетчик пользователей. Указатель типа ltem_base может хранить адрес объекта класса ltem_base или класса, производного от него. Обладая указателем на единый счетчик пользователей, несколько объектов класса Sales_item могут использовать его совместно. Item_base item; Salesjtem obj(item); Sales_item copy(obj); Puc. 15.2. Стратегия использования счетчика пользователей в классе Sales item Кроме функций управления счетчиком пользователей, в классе Sales_item оп- ределены операторы стрелки и обращения к значению. // управляющий класс со счетчиком пользователей для иерархии Item_base class Sales_item { public: // стандартный конструктор: управляющий класс не связан Sales_item(): р(0), use(new std::size_t(1)) { } // привязка управляющего класса к копии объекта Item_base Sales_item(const Item_base&); // функции управления копированием манипулируют счетчиком // пользователей и указателями Sales_item(const Sales_item &i): p(i.p), use(i.use) { ++*use; } ~Sales_item() { decr_use(); } Sales_item& operator=(const Sales_item&); // операторы доступа к членам класса
632 Часть IV. Объектно-ориентированное и общее программирование const Item_base *operator->() const { if (p) return p; else throw std::logic_error("unbound Sales_item"); } const Item_base &operator*() const { if (p) return *p; else throw std::logic_error("unbound Sales_item"); } private: Item_base *p; // указатель на совместно используемый I/ элемент std::size_t *use; // указатель на совместно используемый II счетчик пользователей II применяется деструктором и оператором присвоения для // освобождения указателей void decr_use() { if (--*use == 0) {delete p; delete use;} } } ; Управление копированием в классе счетчика пользователей Функции-члены управления копированием манипулируют счетчиком пользова- телей и соответствующим указателем класса ltem_base. Копирование объекта класса Sales_item подразумевает копирование двух указателей и приращение счетчика пользователей. Деструктор осуществляет декремент счетчика пользовате- лей и удаляет указатели, если счетчик содержит нуль. Поскольку оператор присвое- ния должен будет выполнить те же действия, реализуем их в закрытой вспомога- тельной функции decr_use (). Оператор присвоения немного сложней конструктора копий. // оператор присвоения класса счетчика // указателя на совместно используемый пользователей; применение счетчик пользователей Оператор присвоения действует подобно конструктору копий, который увеличи- вает значение счетчика пользователей правого операнда и копирует указатель. Он также действует подобно деструктору, который сначала осуществляет декремент счетчика пользователей левого операнда, а затем удаляет указатели, если значение счетчика пользователей равно нулю. Как и все операторы присвоения, его необходимо защитить от самоприсвоения. От- рабатывая самоприсвоение, этот оператор сначала осуществляет приращением счетчи- ка пользователей в правом операнде. Если левый и правый операнды являются тем же объектом, счетчик пользователей будет содержать по крайней мере значение 2, когда будет вызвана функция decr_use (). Она осуществляет декремент и проверку счет- чика пользователей левого операнда. Если счетчик пользователей содержит нулевое значение, функция decr_use () освободит объект класса Item_base и объект use данного объекта. Остается лишь скопировать указатели из правого операнда в левый. Как обычно, оператор присвоения возвращает ссылку на левый операнд. Кроме функций-членов управления копированием, в классе Sales_item опре- делены функции-операторы operator* и operator->. При помощи этих операто- ров пользователи будут обращаться к членам класса Item_base. Поскольку эти
Глава 15. Объектно-ориентированное программирование 633 операторы возвращают указатель и ссылку, соответственно, вызываемые с их помо- щью функции подвергнутся динамическому связыванию. Здесь определена только константная версия этих операторов, поскольку все откры- тые члены лежащего в основе иерархии класса Item_base являются константными. Конструкторы управляющего класса Рассматриваемый управляющий класс имеет два конструктора: стандартный конструктор, который создает несвязанный объект класса Sales_item, и конструк- тор, получающий объект, с которым предстоит связать управляющий класс. Первый конструктор довольно прост: указателю на класс ltem_base присваива- ется значение 0, свидетельствующее о том, что данный управляющий класс не свя- зан ни с одним из объектов. Конструктор создает новый счетчик пользователей и инициализирует его значением 1. Второй конструктор несколько сложнее. Пользователям управляющего класса следует предоставить возможность создать их собственные объекты, с которым они смогут связать управляющий класс. Конструктор создает новый объект соответст- вующего типа и копирует параметр во вновь созданный объект. Таким образом, класс Sales_item будет обладать объектом и сможет гарантировать, что он не ока- жется удален до тех пор, пока не будет удален последний объект класса Sales_ item, связанный с объектом. 15.8.2. Клонирование неизвестного типа Чтобы реализовать конструктор, получающий объект класса Item_base, необ- ходимо сначала решить следующую проблему: неизвестен фактический тип объекта, передаваемого конструктору. Это может быть объект класса ltem_base или класса, производного от него. Управляющий класс зачастую должен создавать новую копию существующего объекта, не имея заранее информации о его типе. Хорошим приме- ром является конструктор класса Sales item. Общепринятый подход решения этой проблемы подразумевает создание виртуальной функции (по имени clone), которая и осуществляет копирование. Для поддержки управляющего класса, в каждый из классов иерархии, начиная с базового класса, необходимо добавить виртуальную функцию clone (). class Item_base { public: virtual Item_base* clone() const { return new Item_base(*this); Теперь виртуальную функцию следует переопределить в каждом классе. По- скольку функция предназначена для создания новой копии объекта класса, в качест- ве типа возвращаемого значения следует указать текущий класс. class Bulk_item : public Disc_item { public: Bulk_item* clone() const { return new Bulk_item(*this); } }
634 Часть IV. Объектно-ориентированное и общее программирование Как упоминалось на стр. 595, существует одно исключение из правила, которое требует, чтобы тип возвращаемого значения виртуальной функции в производном классе точно соответствовал ее типу в базовом классе. Это исключение относится именно к данному случаю. Если в базовом классе виртуальная функция возвращает ссылку или указатель на тип класса, ее версия в производном классе может возвра- щать объект класса (указатель или ссылку на класс), открыто производного от клас- са, возвращаемого версией базового класса. Определение конструкторов управляющих классов С учетом существования функции clone (), конструктор Sales_item () можно переписать следующим образом. Sales_item::Sales_item(const Item_base &item): p(item.clone()), use(new std::size_t(1)) { } Подобно стандартному конструктору, этот конструктор создает и инициализиру- ет счетчик пользователей. Для создания (виртуальной) копии объекта, он вызывает в списке параметров его функцию clone (). Если аргументом является объект клас- са Item_base, выполняется функция clone () класса Item_base, а если объект класса Bulk_item — функция clone () класса Bulk_item. Упражнения раздела 15.8.2 Упражнение 15.31. Спроектируйте и реализуйте функцию clone О для класса частичных ски- док, реализованного в упражнениях раздела 15.2.3 (стр. 597). Упражнение 15.32. На практике, при первом запуске и применении к реальным данным, про- граммы довольно редко сразу же срабатывают правильно. Поэтому имеет смысл еще на этапе разработки продумать стратегию отладки классов. Реализуйте для иерархии класса item_base виртуальную функцию debug (), которая отображает значения переменных-членов соответст- вующих классов. Упражнение 15.33. С учетом версии иерархии item_base, содержащей абстрактный класс Disc_item, укажите, должен ли класс Disc_item реализовать функцию clone (). Объясни- те ответ. Упражнение 15.34. Модифицируйте функцию отладки так, чтобы позволить пользователю вклю- чить или выключить отладку. Реализуйте это двумя способами: (а) определив для функции debug () соответствующий параметр; (Ь) определив переменную-член класса, которая позволяет отдельным объектам включать и от- ключать отображение отладочной информации. 15.8.3. Использование управляющего класса Используя класс Sales_item, написать приложение книжного магазина вовсе не сложно. Создаваемый код не должен манипулировать указателями на объекты класса Item_base, однако при обращениях с использованием объектов класса Sales_item функции реализуют виртуальное поведение. Например, объекты класса Item_base можно использовать для решения проблемы, описанной в разделе 15.7 (стр. 628). Класс Sales_item можно использовать для отсле- живания сделанных клиентом приобретений, сохраняя их в контейнере multiset. Когда клиент завершает выбор книг, ему предоставляется общая сумма покупки.
Глава 15. Объектно-ориентированное программирование 635 Сравнение двух объектов класса Sales_i tem Однако прежде чем создавать функцию, отображающую общую сумму покупки, необходимо обеспечить способ сравнения объектов класса Sales_itern. Чтобы ис- пользовать объекты класса Sales_itern в качестве ключей ассоциативного контей- нера, необходим способ их сравнения (раздел 10.3.1, стр. 388). По умолчанию ассо- циативные контейнеры используют оператор “меньше” типа ключа. Однако по при- чинам, описанным при обсуждении исходного класса Sales_item в разделе 14.3.2 (стр. 430), определение оператора operators для управляющего класса Sales_ item окажется плохой идеей. Дело в том, что при использовании объекта класса Sales_item в качестве ключа, рассматриваться при сравнении будет только ISBN, а при определении равенства следует учитывать все переменные-члены. К счастью, ассоциативные контейнеры позволяют определить функцию (или объект функции (раздел 14.8, стр. 560)), которая будет использована в качестве функции сравнения. Сделаем это аналогично способу, которым специальная функ- ция была передана алгоритму stable_sort в разделе 11.2.3 (стр. 430). Чтобы пре- доставить функцию сравнения, используемую вместо оператора <, функции stable_ sort () достаточно было передать дополнительный аргумент. Переопределение функции сравнения ассоциативного контейнера немного сложней, поскольку, как будет продемонстрировано вскоре, функцию сравнения следует предоставить при определении объекта контейнера. Давайте начнем с самой простой части — определения функции, используемой для сравнения объектов класса Sales_item. // сравнить элементы контейнера multiset в объекте класса Basket inline bool compare(const Sales_item &lhs, const Sales_item &rhs) { return lhs->book() < rhs->book(); } Данная функция compare () имеет тот же самый интерфейс, что и оператор “меньше”. Функция возвращает тип bool и получает две константные ссылки на класс Sales_item. Она сравнивает ISBN параметров. Функция использует опера- тор -> класса Sales_item, который возвращает указатель на объект класса Item_ base. Этот указатель используется для доступа и запуска функции-члена book (), которая возвращает ISBN. Использование компаратора с ассоциативным контейнером Если обдумать способ применения функции сравнения, можно прийти к выводу, что она должна быть частью контейнера. Функция сравнения используется во всех операциях добавления или поиска элемента в контейнере. В принципе, каждая из функций этих операций могла бы получать необязательный дополнительный аргу- мент, который передаст функция сравнения. Однако такой подход чреват ошибками: если в двух операциях будут использованы разные функции сравнения, содержимое контейнера может оказаться несогласованным. Результат подобной ошибки в реаль- ных условиях непредсказуем. Для корректной работы, ассоциативный контейнер должен использовать ту же функцию сравнения при каждой операции. Однако было бы наивно полагать, что
636 Часть IV. Объектно-ориентированное и общее программирование пользователи запомнят, какую именно функцию сравнения они использовали ка- ждый раз, особенно когда нет никакого способа удостовериться в одинаковости применяемых функций сравнения при каждом обращении. Следовательно, ис- пользуемую функцию сравнения должен помнить сам контейнер. Сохранив ком- паратор (comparator)2 в объекте контейнера, можно гарантировать, что в каждой требующей сравнения элементов операции будет использована одна и та же функ- ция сравнения. По тем же самым причинам, тип элемента и тип компаратора должны быть из- вестны контейнеру. В принципе, контейнер может “вычислить” тип компаратора, поскольку известно, что он является указателем на функцию, которая возвращает тип bool и получает ссылки на два объекта типа ключа контейнера (тип key_type). К сожалению, этот вычисленный тип был бы чересчур ограничивающим. С другой стороны, компаратор может быть как объектом функции, так и обычной функцией. Даже если компаратор должен быть функцией, вычисленный тип все еще остается слишком ограничивающим. В конце концов, функция сравнения могла бы возвра- щать тип int или любой другой тип, который применим в условии. Аналогично, тип параметра может не соответствовать точно типу key_type. Допустим аргумент лю- бого типа, который может быть преобразован в тип key_type. Таким образом, чтобы использовать функцию сравнения класса Sales_item, тип компаратора необходимо указать при определении контейнера multiset. В дан- ном случае этим типом является функция, которая возвращает тип bool и получает две ссылки на константы класса Sales_item. Сначала следует определить тип, который будет синонимом этому типу (раз- дел 7.9, стр. 302). // тип функции сравнения, используемой для упорядочивания // содержимого контейнера multiset typedef bool (*Comp)(const Sales_item&, const Sales_item&); Этот оператор определяет тип Comp как синоним типу указателя на функцию, которая соответствует функции сравнения, предназначенной для сравнения объек- тов класса Sales_item. Затем необходимо определить контейнер multiset, который будет содержать объекты класса Sales_item. При этом в качестве функции сравнения следует ука- зать тип Comp. Конструктор каждого ассоциативного контейнера позволяет указать имя функции сравнения. Можно создать пустой контейнер multiset, который бу- дет использовать функцию compare () следующим образом. std::multiset<Sales_item, Comp> items(compare); Данное определение гласит, что items — это предназначенный для хранения объектов класса Sales_item контейнер multiset, который использует для их сравнения объект типа Comp. Поскольку никаких элементов не предоставлено, кон- тейнер multiset пуст, но функция сравнения (по имени compare) для него уже указана. При добавлении и поиске элементов в контейнере items, для их упорядо- чивания будет использована функция compare (). 2 То есть функцию сравнения. — Примеч. ред.
Глава 15. Объектно-ориентированное программирование 637 Контейнеры и управляющие классы Теперь, когда известен способ обеспечения функции сравнения, можно опреде- лить класс по имени Basket, предназначенный для отслеживания продаваемых эк- земпляров и вычисления суммы покупки. class Basket { // тип функции сравнения, используемой для упорядочивания // содержимого контейнера multiset typedef bool (*Comp)(const Sales_item&, const Sales_item&); public: // упростить ввод типа контейнера typedef std::multiset<Sales_item, Comp> set_type; // определения типов, моделирующие соответствующие // типы контейнера typedef set_type::size_type size_type; typedef set_type::const_iterator const_iter; Basket(): items(&compare) { } // инициализация компаратора void add_item(const Sales_item &item) { items.insert(item); } size_type size(const Sales_item &i) const { return items.count(i); } double total() const; // окончательная сумма за все II экземпляры в корзинке private: std::multiset<Sales_item, Comp> items; }; Этот класс содержит приобретения клиента в контейнере multiset для объек- тов класса Sales_item. Использование контейнера multiset позволить клиенту купить несколько экземпляров той же книги. В классе определен один стандартный конструктор Basket (). Собственный стандартный конструктор нужен для того, чтобы передать функцию compare () конструктору контейнера multiset (), который создает элемент items. Функции, определенные в классе Basket, довольно просты: функция add_item () получает ссылку на объект класса Sales_item и помещает его копию в элемент контейнера multiset; функция item_count () возвращает количество записей в корзине, обладающих данным ISBN. Кроме этих функций, в классе Basket опре- делены три типа, облегчающие использование его контейнера multiset. Использование управляющего класса для запуска виртуальных функций Единственным сложным элементом класса Basket является функция total (), которая возвращает общую сумму за все экземпляры в корзине. double Basket::total() const { double sum =0.0; // содержит общую сумму / * найти каждый набор элементов с одинаковым isbn и вычислить * окончательную цену за это количество элементов * iter указывает на первый экземпляр каждой книги в наборе * upper_bound указывает на следующий экземпляр, с другим isbn * / for (const_iter iter = items.begin(); iter != items.end(); iter = items.upper_bound(*iter))
638 Часть IV. Объектно-ориентированное и общее программирование { // известно, что в корзинке есть по крайней мере один // элемент с этим ключом // виртуальное обращение к функции net_price() применяет // соответствующие скидки, если они есть sum += (*iter)->net_price(items.count(*iter)); } return sum; } В функции total () есть два интересных момента: обращение к функции net_ price () и структура цикла for. Рассмотрим каждый из них. При вызове, функции net_price () необходимо сообщить количество экземп- ляров приобретаемой книги. Функция net_price () использует этот аргумент для выяснения того, “заслуживает” ли данное приобретение скидки. Это требование подразумевает, что содержимое контейнера multiset следует обрабатывать по час- тям, т.е. сначала все записи по одной книге, затем по второй и т.д. К счастью, контей- нер multiset прекрасно подходит для решения этой проблемы. Цикл for начинается с определения и инициализации итератора iter, относя- щегося к первому элементу контейнера multiset. Чтобы выяснить количество элементов контейнера multiset, обладающих данным ключом (например, одина- ковым isbn), здесь используется функция count () контейнера multiset (раз- дел 10.3.6, стр. 394). Полученное значение используется как аргумент при обраще- нии к функции net_price (). Весьма интересен раздел “инкремента” в выражении цикла for. В отличие от обычного цикла, который читает каждый элемент и перемещает итератор iter на следующий ключ, здесь пропускаются все элементы, которые соответствуют ключу, полученному при вызове функции upper_bound () (раздел 10.5.2, стр. 404). В ре- зультате обращения к функции upper_bound () возвращается итератор на элемент, расположенный после последнего элемента с ключом, совпадающим с ключом ите- ратора iter. Полученный итератор обозначает конец текущего набора и начало на- бора следующей книги. Если в результате проверки нового значения итератора iter оказывается, что оно равно значению, возвращаемому функцией items . end (), вы- полнение цикла for завершается. В противном случае код переходит к обработке следующей книги. В теле цикла for происходит вызов функции net_price (). Это обращение вы- глядит довольно сложным. sum += (*iter)->net_price(items.count(*iter)); В результате обращения к значению итератора iter получается объект класса Sales_item, к которому применяется перегруженный в классе Sales_item опера- тор стрелки. Этот оператор возвращает базовый объект Item_base, с которым свя- зан управляющий класс. От имени этого объекта происходит вызов функции net_price (), которой передается количество (count) элементов с одинаковым ISBN. Функция net_price () виртуальна, поэтому сработает та из версий функции тарификации, которая соответствует классу используемого объекта.
Глава 15. Объектно-ориентированное программирование 639 Упражнения раздела 15.8.3 Упражнение 15.35. Создайте собственные версии функции compare () и класса Basket. Ис- пользуйте их для контроля продаж. Упражнение 15.36. Какому типу соответствует Basket:: const_iter? Упражнение 15.37. Зачем был определен тип comp в разделе private класса Basket? Упражнение 15.38. Почему в классе Basket находится два раздела private? 15.9. Продолжение приложения TextQuery В качестве последнего примера наследования рассмотрим модернизированное приложение TextQuery из раздела 10.6. Разработанный для него класс позволял находить местоположение указанного слова в текстовом файле. Усовершенствуем эту систему, обеспечив возможность выполнения более сложных запросов. Запросы будут отрабатываться на примере следующей статьи. Alice Emma has long flowing red hair. Her Daddy says when the wind blows through her hair, it looks almost alive, like a fiery bird in flight. A beautiful fiery bird, he tells her, magical but untamed. "Daddy, shush, there is no such thing," she tells him, at the same time wanting him to tell her more. Shyly, she asks, "I mean, Daddy, is there?" Система должна обеспечивать следующее. 1. Запросы на поиск отдельных слов. Все строки, в которых присутствует искомое слово, должны отображаться в порядке возрастания. Executed Query for: Daddy match occurs 3 times: (line 2) Her Daddy says when the wind blows (line 7) "Daddy, shush, there is no such thing," (line 10) Shyly, she asks, "I mean. Daddy, is there?" 2. Инверсные запросы с использованием оператора - (Not). Отображаются все строки, которые не соответствуют запросу. Executed Query for: -(Alice) match occurs 9 times: (line 2) Her Daddy says when the wind blows (line 3) through her hair, it looks almost alive, (line 4) like a fiery bird in flight. 3. Запросы с использованием оператора | (Or). Отображаются все строки, в кото- рых присутствует любое из двух указанных слов. Executing Query for: (hair | Alice) match occurs 2 times: (line 1) Alice Emma has long flowing red hair. (line 3) through her hair, it looks almost alive,
640 Часть IV. Объектно-ориентированное и общее программирование 4. Запросы с использованием оператора & (And). Отображаются все строки, в кото- рых присутствуют оба указанных слова. Executed query: (hair & Alice) match occurs 1 time: (line 1) Alice Emma has long flowing red hair. Кроме того, необходимо, чтобы эти элементы запроса можно было объединить следующим образом. fiery & bird | wind Разрабатываемая система не будет достаточно сложной, чтобы прочитать такие выражения, они будут встроены внутрь программы. Следовательно, составные вы- ражения подобного типа будут обрабатываться согласно обычным правилам при- оритета языка C++. Этому запросу соответствует строка, в которой присутствуют слова fiery и bird или слово wind. Ему не будет соответствовать строка, в кото- рой слова fiery или bird встречаются по отдельности. Executing Query for: ((fiery & bird) | wind) match occurs 3 times: (line 2) Her Daddy says when the wind blows (line 4) like a fiery bird in flight. (line 5) A beautiful fiery bird, he tells her, В отображаемом результате, для указания способа интерпретации запроса, ис- пользуются круглые скобки. Подобно первоначальной реализацией, система не должна отображать одинаковые строки несколько раз. 15.9.1. Объектно-ориентированное решение Для представления запросов на поиск слов, вполне логично было бы использо- вать класс TextQuery (раздел 10.6.2, стр. 409). Другие классы запросов можно было бы получить как производные от этого класса. Однако такой подход неверен. Концептуально, инверсный запрос не является разновидностью запроса на поиск слова. Инверсный запрос — это скорее запрос типа “имеет” (запрос на поиск слова или любой другой тип запроса), результат которого интерпретируется негативно. Исходя из этого можно придти к выводу, что разные виды запросов следует офор- мить как независимые классы, которые совместно используют общий базовый класс. WordQuery // Shakespeare NotQuery // -Shakespeare OrQuery // Shakespeare I Marlowe AndQuery // William & Shakespeare Вместо того, чтобы наследовать их от класса TextQuery, используем его для хранения файла и создания связанной с ним карты word_map. Для создания выра- жения будут использованы классы запросов, которые в конечном счете осуществят обращение к файлу в объекте класса TextQuery. Класс абстрактного интерфейса Таким образом, выявлено четыре вида классов запроса. Концептуально, это клас- сы одного уровня. Каждый из них совместно использует тот же абстрактный ин- терфейс (abstract interface), который предполагает необходимость определения
Глава 15. Объектно-ориентированное программирование 641 абстрактного класса (раздел 15.6, стр. 627), представляющего функции, выполняемые запросом. Присвоим этому абстрактному классу имя Query_base (Запросбазо- вый), указывающее на его роль корневого класса в иерархии запроса. Классы WordQuery и NotQuery будут производными непосредственно от абст- рактного базового. Классы AndQuery и OrQuery совместно используют одно свойст- во, которого нет в других классах системы: каждый из них имеет два операнда. Чтобы смоделировать этот факт, добавим в иерархию еще один абстрактный класс, по имени BinaryQuery (БинарныйЗапрос), который будет представлять запросы с двумя опе- рандами. Классы AndQuery и OrQuery происходят от класса BinaryQuery, который в свою очередь происходит от класса Query_base. Эти решения позволяют спроек- тировать иерархию классов, представленную на рис. 15.3. Рис. 153. Иерархия наследования Query_base Функции Класс Query_base задуман как базовый для классов запросов разных видов, фактической работы он делает немного. Класс TextQuery будет неоднократно ис- пользован для хранения файла, создания карты и поиска отдельных слов. Классам запросов необходимы лишь две функции. 1. Функция eval (), возвращающая набор (контейнер set) номеров найденных строк. Для обработки запроса эта функция получает объект класса TextQuery. 2. Функция display (), получающая ссылку на объект класса ostream и отобра- жающая информацию о запросе, который данный объект выполняет для данного потока. В классе Query_base каждая из этих функций будет объявлена как чистая вир- туальная (раздел 15.6, стр. 627). Каждый из производных классов должен будет оп- ределить собственную версию этих функций. 15.9.2. Управляющий класс, подобный значению Рассматриваемая программа будет отрабатывать запросы, а не создавать их. Но чтобы запустить программу на выполнение, необходимо определить способ создания запроса. Проще всего сделать это непосредственно в коде, при помощи выражения C++. Например, чтобы создать описанный ранее составной запрос, можно использо- вать следующий код. Query q = Query("fiery") & Query("bird") I Query("wind");
642 Часть IV. Объектно-ориентированное и общее программирование Это довольно сложное описание неявно предлагает, что код пользовательского уровня не будет использовать унаследованные классы непосредственно. Вместо это- го будет создан управляющий класс по имени Query (Запрос), который и скроет ие- рархию. Пользовательский код будет выполняться от имени управляющего класса; т.е. будет лишь косвенно манипулировать объектами класса Query_base. Подобно управляющему классу Sales_item, управляющий класс Query будет содержать указатель на объект класса из иерархии наследования. Класс Query со- держит также указатель на счетчик пользователей, используемый для манипулиро- вания объектом, на который указывает управляющий класс. В данном случае управляющий класс полностью скроет основную иерархию на- следования. Пользователи создают и манипулируют объектами класса Query_base только косвенно, при помощи функций объектов класса Query. В классе Query оп- ределим три перегруженных оператора и конструктор Query (), который создает новый объект Query_base в динамически распределяемой памяти. Каждый опера- тор будет связывать созданный объект класса Query_base с управляющим классом Query: оператор & создаст объект класса Query, связанный с новым объектом клас- са AndQuery; оператор | объект класса Query, связанный с новым объектом класса OrQuery; а оператор ~ создаст объект класса Query, связанный с новым объектом класса NotQuery. Конструктор Query (), получающий аргумент типа string, соз- дает новый объект класса WordQuery. Класс Query будет предоставлять те же функции, что и класс Query_base: т.е. функцию eval () для обработки соответствующего запроса и функцию display () для отображения запроса. Для отображения соответствующего запроса понадобится перегруженный оператор вывода. Проект TextQuery: резюме Таблица 15.1. Проект TextQuery: резюме TextQuery Класс, который читает указанный файл и создает соответствующую карту поиска. Этот Query_base Query класс предоставляет функцию query_text (), которая получает строковый аргумент и возвращает набор номеров строк, в которых присутствует аргумент Абстрактный класс, базовый для классов запроса Управляющий класс счетчика пользователей, который указывает на объект класса, про- изводного ОТ класса Query_base WordQuery Класс, производный от класса Query_base, который ищет указанное слово NotQuery Класс, производный от класса Query_base, который возвращает набор номеров строк, в которых указанный операнд Query отсутствует BinaryQuery Абстрактный класс, производный от класса Query_base, который представляет за- просы с двумя операндами типа Query OrQuery Класс, производный от класса BinaryQuery, который возвращает набор номеров строк, в которых присутствует хотя бы один из операндов AndQuery Класс, производный от класса BinaryQuery, который возвращает набор номеров строк, в которых присутствуют оба операнда qi & q2 Возвращает объект класса Query, связанный с новым объектом класса AndQuery, который содержит объекты qi и q2
Глава 15. Объектно-ориентированное программирование 643 Окончание табл. 15.1 TextQuery ql | q2 ~ q Query q(s) Класс, который читает указанный файл и создает соответствующую карту поиска. Этот класс предоставляет функцию query_text (), которая получает строковый аргумент и возвращает набор номеров строк, в которых присутствует аргумент Возвращает объект класса Query, связанный с новым объектом класса or Query, который содержит объекты qi и q2 Возвращает объект класса Query, связанный с новым объектом класса NotQuery, который содержит объект q Связывает объект q класса Query с новым объектом класса WordQuery, который содержит объект s класса string Зачастую, особенно для новичков недостаточно знакомых с основами объектно- ориентированных систем, процесс проектирования является самой трудной задачей. Однако после удачного завершения проектирования, реализация не составляет проблем. Очень важно понять, что большая часть работы этого приложения заключается в создании объектов, представляющих запрос пользователя. Query q = Query("fiery") & Query("bird") I Query("wind"); Как проиллюстрировано на рис. 15.4, приведенное выше выражение создает де- сять объектов: пять объектов класса Query__base и связанные с ними управляю- щие классы. Тремя из пяти объектов класса Query_base являются WordQuery, Or Query и AndQuery. Каждый из прямоугольников WordQuery Объекты, созданные выражением Query("fiery") & Query("bird") | Query("wind"); Рис. 15.4. Объекты, созданные выражением запроса Как только создано дерево объектов, обработка (или отображение) данного за- проса сводится к простому процессу (осуществляемому компилятором), который, следуя по линиям, опрашивает каждый объект дерева, чтобы выполнить (или ото- бразить) необходимые действия. Например, если происходит вызов функции eval () объекта q (т.е. корневого класса дерева), функция eval () опросит объект класса OrQuery, на который он указывает. Обработка этого объект класса Or Query
644 Часть IV. Объектно-ориентированное и общее программирование приведет к вызову функции eval () для двух его операндов, что в свою очередь приведет к вызову функции eval () для объектов класса AndQuery и WordQuery, которые осуществляют поиск слова wind, и т.д. Упражнения раздела 15.9.2 Упражнение 15.39. При условии, что si, s2, s3 и s4 являются строками, укажите, какие объек- ты создаются в следующих случаях применения класса Query. (a) Query(sl) I Query(s2) & ~ Query(s3); (b) Query(si) I (Query(s2) & ~ Query(s3)) (c) (Query(sl) & (Query(s2)) I (Query(s3) & Query(s4))); 15.9.3. Класс Query_base Завершив проектирование, начнем реализацию с определения класса Query_base. // закрытый, абстрактный класс, являющийся базовым для конкретных // классов запроса protected: typedef TextQuery::line_no line_no; virtual -Query_base() { } private: // функция eval () возвращает набор соответствующих // запросу строк virtual std::set<line_no> eval(const TextQuery&) const = 0; // функция display() отображает запрос virtual std::ostream& display(std::ostream& = std::cout) const = 0; }; В этом классе определены два члена интерфейса — функции eval О и display (). Обе они объявлены чистыми виртуальными функциями (раздел 15.6, стр. 627), что делает этот класс абстрактным. В приложении не будут создаваться объекты класса Query_base. Пользователи и производные классы смогут использовать класс Query_base только при помощи управляющего класса Query. Следовательно, интерфейс класса Query_base можно сделать закрытым. Виртуальный деструктор (раздел 15.4.4, стр. 618) и определение типа объявлены защищенными (protected), чтобы произ- водные классы могли обращаться к ним. Деструктор (неявно) используется деструк- торами производных классов, а следовательно, должен быть доступен для них. Управляющий класс Query объявлен дружественным. Члены этого класса обра- щаются к виртуальным функциям класса Query_base, а следовательно, должны иметь доступ к им. 15.9.4. Управляющий класс Query Управляющий класс Query подобен классу Sales_item, он будет содержать указатель на класс Query_base и указатель на счетчик пользователей. Как и у клас- са Sales_item, функции-члены управления копированием класса Query будут ма- нипулировать счетчиком пользователей и указателем класса Query_base.
Глава 15. Объектно-ориентированное программирование 645 В отличие от класса Sales_item, класс Query будет предоставлять только ин- терфейс иерархии Query_base. Ни к одному из членов класса Query_base (или классов, производных от него) пользователи не будут обращаться непосредственно. Эта особенность проекта обусловлена двумя различиями между классами Query и Sales_item. Во-первых, класс Query не будет определять перегруженные версии операторов стрелки и обращения к значению. Класс Query_base не имеет никаких открытых членов. Если бы в управляющем классе Query были определены операто- ры обращения к значению или стрелки, они оказались бы бесполезны! Любая по- пытка применения этих операторов для доступа к члену класса Query_base потер- пела бы неудачу. Вместо этого класс Query должен определить собственные версии функций eval () и display () интерфейса класса Query_base. Второе различие является результатом назначения объектов создаваемой иерар- хии. В данном случае объекты, производные от класса Query_base, создаются только при помощи функций управляющего класса Query. Это является причиной возникновения отличий между конструкторами, необходимыми классу Query и классу Sales_item. Класс Query С учетом созданного ранее проекта, сам класс Query довольно прост. // управляющий класс, манипулирующий иерархией // наследования Query_base class Query { // этим функциям необходим доступ к конструктору Query_base* friend Query operator-(const Query S) ; friend Query operator I (const Query&, const QueryS); friend Query operators(const Query&, const QueryS); public: Query(const std::strings); // создает новый WordQuery II функции управления копированием, манипулирующие // указателями и счетчиками пользователей Query(const Query Sc): q(c.q), use(c.use) { ++*use; } -Query() { decr_use(); } QueryS operator=(const QueryS); // функции интерфейса: вызывают соответствующие функции // класса Query_base std::set<TextQuery::line_no> eval(const TextQuery St) const { return q->eval(t); } std::ostream Sdisplay(std::ostream Sos) const { return q->display(os); } private: Query(Query_base *query): q(query), use(new std::size_t(1)) { } Query_base *q; std::size_t *use; void decr_use() { if (—*use -- 0) { delete q; delete use; } } Сначала указаны дружественные функции, которые создают объекты запросов. Вско- ре будет описано, почему эти операторы должны быть объявлены дружественными. В разделе интерфейса (public) класса Query объявлен (но не определен) кон- структор, получающий строку. Этот конструктор создает объект класса WordQuery, поэтому его нельзя определить до тех пор, пока не определен класс WordQuery.
646 Часть IV. Объектно-ориентированное и общее программирование Следующие три члена управляющего класса осуществляют управление копиро- ванием, как и соответствующие члены класса Sales_item. Последние два открытых члена предоставляют интерфейс для класса Query_ base. В любом случае, чтобы вызвать соответствующую функцию класса Query_base, функция класса Query использует свой указатель на класс Query_base. Эти функ- ции объявлены виртуальными. Фактически используемая версия определяется во время выполнения и зависит от типа объекта, на который указывает указатель q. Закрытая реализация класса Query содержит конструктор, получающий указа- тель на объект класса Query_base. Этот конструктор сохраняет полученный указа- тель в объекте q, создает новый счетчик пользователей и инициализирует его значе- нием 1. Данный конструктор объявлен закрытым потому, что его применение для создания объектов класса Query_base обычным пользовательским кодом не пред- полагается. Этот конструктор необходим лишь тем функциям, которые создают объек- ты класса Query. Поскольку конструктор закрытый, эти функции следует объявить дружественны ми. Перегруженные операторы класса Query Операторы |, & и ~ создают, соответственно, объекты класса OrQuery, AndQuery и NotQuery. inline Query operator&(const Query &lhs, const Query &rhs) { return new AndQuery(Ihs, rhs); } inline Query operatori(const Query &lhs, const Query &rhs) { return new OrQuery(Ihs, rhs); } return new NotQuery(oper); J Каждый из этих операторов динамически создает новый объект класса, произ- водного от класса Query_base. Чтобы создать объект класса Query из указателя в объекте класса Query_base, созданного оператором, оператор return (неявно) ис- пользует тот конструктор класса Query, который получает указатель на класс Query_ base. Например, оператор return в операторе ~ эквивалентен следующему коду. // создать новый объект класса NotQuery II преобразовать полученный в результате указатель на // класс NotQuery в указатель на класс Query_base Query_base *tmp = new NotQuery(expr); return Query(tmp); // применение конструктора класса Query для II получения указателя на класс Query_base Здесь нет специальной функции для создания объекта класса WordQuery. Вместо нее использован получающий стороку конструктор класса Query. Этот конструктор создает объект класса WordQuery, предназначенный для поиска переданной строки.
Глава 15. Объектно-ориентированное программирование 647 Оператор вывода класса Query Необходимо предоставить пользователям возможность отображать запрос ис- пользуя обычный оператор вывода. Функцию отображения тоже следует сделать виртуальной, поскольку объект класса Query должен отображать тот объект класса Query_base, на который он указывает. Но существует одна проблема: виртуальны- ми могут быть только функции-члены, а оператор вывода не может быть членом класса Query_base (раздел 14.2.1, стр. 544). Чтобы добиться необходимого виртуального поведения, в классе Query base следует определить виртуальную функцию-член display (), которую и использует оператор вывода класса Query. inline std::ostream& operator<<(std::ostream &os, const Query &q) return q.display(os); } В результате следующий код использует оператор вывода класса Query. Query andq = Query(sought1) & Query(sought2); cout << "\nExecuted query: " << andq << endl; Этот оператор обращается к функции q. display (os), где q относится к объекту класса Query, который указывает на данный объект класса AndQuery, а объект os связан с объектом cout. Следующий код применит версию функции display () из класса WordQuery. Query name(sought); cout << "XnExecuted query for: " << name << endl; В общем случае подобное обращение вызовет экземпляр функции di splay О, связанный с объектом, который объект класса Query использует в данный момент. Query query = some_query; cout << query << endl; 15.9.5. Производные классы Теперь можно реализовать конкретные классы запроса. Наиболее интересной ча- стью этих классов является их представление. Наиболее простым является класс WordQuery. Его задача состоит в сохранении искомого слова. Другие классы используют один или два операнда класса Query. Оператор клас- са Not Que г у инвертирует результат запроса, а операторы классов AndQuery и OrQuery имеют по два операнда, которые фактически сохраняются в общем для них базовом классе BinaryQuery. Операнд (операнды) каждого из этих классов может быть объектом любого из конкретных классов, производных от класса Query_base: оператор объекта класса NotQuery может быть применен к объекту класса WordQuery, AndQuery, OrQuery или другому объекту класса NotQuery. Для обеспечения такой гибкости, операнды следует сохранять как указатели класса Query_base, которые могут содержать ад- реса любых объектов конкретных классов, производных от класса Query base. Теперь, когда конструкция этих классов известна, их можно реализовать.
648 Часть IV. Объектно-ориентированное и общее программирование Класс WordQuery- Класс WordQuery, производный от класса Query_base, предназначен для поис- ка указанного слова в содержащем текст контейнере типа тар. class WordQuery: public Query_base { friend class Query; // Query использует конструктор WordQuery() WordQuery(const std::string &s): query_word(s) { } // конкретный класс: WordQuery определяет все унаследованные // чистые виртуальные функции std::set<line_no> eval(const TextQuery &t) const { return t.run_query(query_word); } std::ostream& display (std::ostream &os) const { return os << query_word; } std::string query_word; // искомое слово }; Подобно классу Query_base, класс WordQuery не имеет никаких открытых членов; класс Query должен быть дружественным для класса WordQuery, чтобы предоставить ему доступ к конструктору WordQuery (). Каждый из конкретных классов запроса должен переопределить унаследованные чистые виртуальные функции. Функции класса WordQuery достаточно просты и оп- ределены в теле класса. Функция-член eval () вызывает функцию query_text () класса TextQuery и передает ей в качестве параметра строку, которая использова- лась при создании данного объекта класса WordQuery. Чтобы отобразить объект класса WordQuery, следует вывести строку query_word. Класс Not Query- Класс NotQuery инвертирует результат константного запроса. class NotQuery: public Query_base { friend Query operator-(const Query &); NotQuery(Query q): query(q) { } // конкретный класс: NotQuery определяет все унаследованные I/ чистые виртуальные функции std::set<line_no> eval(const TextQuery&) const; std::ostream& display(std::ostream &os) const { return os << "~(" << query << } const Query query; }; Перегруженный оператор ~ объявлен дружественным для класса Query, чтобы позволить этому оператору создавать новый объект класса NotQuery. Чтобы ото- бразить объект класса NotQuery, следует вывести символ сопровождаемый ос- новным запросом. Чтобы сделать приоритет очевидным для читателя, заключим за- прос в скобки. Применение оператора вывода в функции display (), приводит в конечном счете к вир- туальному Обращению К Объекту класса Query_base. // применение оператора вывода класса Query приводит // к вызову функции Query::display() // эта функция осуществляет виртуальное обращение // к функции Query_base::display() { return os << "~(" << query << ")"
Глава 15. Объектно-ориентированное программирование 649 Функция-член eval () достаточно сложна, поэтому реализуем ее вне тела класса. Более подробно функция eval () рассматривается в разделе 15.9.6 (стр. 651). Класс BinaryQuery- Класс BinaryQuery — это абстрактный класс, который содержит данные, необходи- мые двум классам запроса, AndQuery и OrQuery, которые используют по два операнда. class BinaryQuery: public Query_base { protected: BinaryQuery(Query left, Query right, std::string op): Ihs(left), rhs(right), oper(op) { } // абстрактный класс: BinaryQuery функцию eval() не определяет std::©streams display(std::ostream Sos) const { return os « "(" « Ihs « " " << oper << " " << rhs << ")"; } const Query Ihs, rhs; // правый и левый операнды const std::string oper; // имя оператора Данными класса BinaryQuery являются два операнда запроса и символ опера- тора, используемый при отображении запроса. Все эти данные, включая символ, объявлены константными, поскольку после создания содержимое запроса не должно изменяться. Конструктор получает два операнда и символ оператора, которые и со- храняет в соответствующих переменных-членах. Чтобы отобразить объект класса Binaryoperator, следует вывести выражение в скобках, состоящее из левого операнда, оператора и правого операнда. Как и в слу- чае класса NotQuery, для отображения необходим перегруженный оператор <<, ис- пользуемый для вывода значений параметров left и right. В конечном счете он осуществляет виртуальное обращение к функции display () основного объекта класса Query_base. Класс BinaryQuery не переопределяет функцию eval (), а следовательно, наследу- 3 I ет ее чистой виртуальной. Следовательно, класс BinaryQuery остается абстрактным / W и его объекты создавать нельзя. Классы AndQuery и OrQuery Классы AndQuery и OrQuery почти идентичны. class AndQuery: public BinaryQuery { friend Query operators(const QueryS, const QueryS); AndQuery(Query left, Query right): BinaryQuery(left, right, "S") { } // конкретный класс: AndQuery наследует функцию display (), // а остальные чистые виртуальные функции переопределяет std::set<line_no> eval(const TextQuery&) const; }; class OrQuery: public BinaryQuery { friend Query operatori(const Query&, const QueryS); OrQuery(Query left, Query right): BinaryQuery(left, right, "I") { } // конкретный класс: OrQuery наследует функцию display(), // а остальные чистые виртуальные функции переопределяет std::set<line_no> eval(const TextQueryS) const; );
650 Часть IV. Объектно-ориентированное и общее программирование Эти классы объявляют соответствующий оператор дружественным и определяют конструктор, создающий их базовую часть класса BinaryQuery с соответствующим оператором. Они наследуют определение функции display () от класса BinaryQuery, но каждый из них определяет собственную версию функции eval (). Упражнения раздела 15.9.5 Упражнение 15.40. Исходя из выражения, представленного на рис. 15.4, (а) перечислите конструкторы, задействованные при обработке этого выражения; (Ь) перечислите обращения к функции display () и перегруженному оператору <<, задейство- ванные при выполнении выражения cout « q; (с) перечислите обращения к функции eval (), задействованные при выполнении выражения q. eval. 15.9.6. Виртуальные функции eval () Основой иерархии класса запроса являются виртуальные функции eval (). Ка- ждая из них вызывает функцию eval () своего операнда (операндов), а затем при- меняет собственную логику вычислений: функция eval () класса AndQuery воз- вращает объединение результатов вычисления двух его операндов, а функция eval () класса OrQuery возвращает их пересечение. Функция eval () класса NotQuery немного сложней: она должна возвращать номера строк, не входящих в набор операнда. Функция OrQuery: : eval () Функция eval () класса OrQuery объединяет наборы номеров строк, возвра- щенных его операндами. То есть ее результатом является объединение результатов двух операндов. // возвращает объединение наборов результатов своих операндов set<TextQuery::line_no> OrQuery::eval(const TextQuery& file) const { // виртуальное обращение с использованием управляющего // класса Query, позволяющее получить результирующие / / наборы операндов set<line_no> right = rhs.eval(file), ret_lines = Ihs.eval(file); // хранилище результатов // добавить строки из right, которых еще нет в ret_lines ret_lines.insert(right.begin(), right.end()); return ret_lines; } Функция eval () начинается с вызова функций eval () каждого из операндов запроса. Эти обращения к функции Query: :eval () в свою очередь осуществляют виртуальное обращение к функции eval () основного объекта класса Query_base. Каждое из этих обращений возвращает набор номеров строк, в которых присутству- ет его операнд. Затем происходит вызов функции insert () набора ret_lines, ко- торой передается два итератора, обозначающие набор, возвращенный в результате обработки правого операнда. Поскольку ret_lines — это набор, данное обращение
Глава 15. Объектно-ориентированное программирование 651 добавит в набор ret_lines те элементы операнда right, которых нет в операнде left. После обращения к функции insert () набор ret_lines содержит номера всех строк, которые были в наборах left и right. Функция завершается возвраще- нием набора ret_lines. Функция AndQuery: : eval () Версия функции eval () класса AndQuery использует один из библиотечных алгоритмов, который выполняет подобную операцию. Библиотечные алгоритмы описаны в приложении А, “Библиотека”. // возвращает пересечение наборов результатов своих операндов set<TextQuery::line_no> AndQuery::eval(const TextQuery& file) const { // виртуальное обращение с использованием управляющего // класса Query, позволяющее получить результирующие / / наборы операндов set<line_no> left = Ihs.eval(file), right = rhs.eval(file); set<line_no> ret_lines; // хранилище результатов / / заносит пересечение двух диапазонов в результирующий // итератор / / результирующий итератор в этом обращении добавляет элементы // в набор ret_lines set_intersection(left.begin(), left.end(), right.begin(), right.end(), inserter(ret_lines, ret_lines.begin() ) ) ; return ret_lines; } Эта версия функции eval () использует для поиска строк, совпадающих в обоих результатах запроса, алгоритм set_inter sect ion. Этот алгоритм получает пять итераторов: первые четыре обозначают два исходных диапазона, а последний — ре- зультирующий. Алгоритм записывает каждый элемент, который находится в обоих исходных диапазонах в результирующий диапазон. Получателем этого обращения является итератор вставки (раздел 11.3.1, стр. 432), который добавляет новые эле- менты в набор ret_lines. Функция NotQuery; : eval () Функция eval () класса NotQuery ищет в тексте все строки, внутри которых операнд отсутствует. Для обеспечения работы этой функции, необходим класс TextQuery, который позволяет добавлять элементы, возвращать размер файла и выяснять, существуют ли строки с такими номерами. // возвращает номера строк, отсутствующие в наборе результатов // операнда set<TextQuery::line_no> NotQuery::eval(const TextQuery& file) const { // виртуальное обращение с использованием управляющего // класса Query, позволяющее получить исходные данные set<TextQuery::line_no> has_val = query.eval(file); set<line_no> ret_lines; // проверить каждую строку исходного файла на совпадение с // находящимися в has_val
652 Часть IV. Объектно-ориентированное и общее программирование // если это не так, добавить ее номер в ret_lines for (TextQuery::line_nо n - 0; n != file.size(); ++n) if (has_val.find(n) == has_val.end()) ret_lines.insert(n); return ret_lines; Как и другие функции eval (), данная начинается с вызова функции eval () операнда объекта. Это обращение возвращает набор номеров строк, в которых опе- ранд присутствует. Однако вернуть необходимо набор номеров строк, в которых операнд отсутствует. Для этого необходимо перебрать все строки в исходном файле. Для организации управления циклом for, используется функция-член size (), ко- торую следует добавить в класс TextQuery. Этот цикл добавляет номер каждый строки, отсутствующей в наборе has_val, в набор ret_lines. По завершении об- работки всех номеров строк, осуществляется возвращение набора ret_lines. Упражнения раздела 15.9.6 Упражнение 15.41. Реализуйте классы Query и Query_base, а также добавьте в класс TextQuery из главы 10, “Ассоциативные контейнеры”, необходимую функцию size О. Про- верьте приложение, выполнив и отобразив запрос, подобный приведенному на рис. 15.4. Упражнение 15.42. Разработайте и реализуйте одно из следующих дополнений. (а) Организуйте обработку слов на основании их наличия внутри того же предложения, а не внутри той же строки. (Ь) Организуйте систему хранения истории, при помощи которой пользователь сможет вернуться к определенному количеству предыдущих запросов и, возможно, изменить их или объединить с другим. (с) Вместо отображения количества совпадений и всех найденных строк, позвольте пользователю указать диапазон отображаемых строк, результат промежуточного запроса и окончательный результат запроса. Резюме Идея наследования и динамического связывания проста и вместе с тем весьма продуктив- на. Наследование позволяет создавать новые классы, которые совместно используют возмож- ности их базового класса (классов), но при необходимости могут их переопределить. Дина- мическое связывание позволяет компилятору во время выполнения выбрать версию приме- няемой функции на основании динамического типа объекта. Комбинация наследования и динамического связывания позволяет создавать программы, которые либо не зависят от типа объекта либо имеют поведение, зависящие от типа объекта. В языке C++ динамическое связывание применимо только к тем функциям, которые объ- явлены виртуальными и вызываются при помощи ссылок или указателей. Для организации взаимодействия с иерархией наследования в программах C++ обычно используют управ- ляющие классы. Эти классы создают и манипулируют указателями на объекты классов из ие- рархии наследования, обеспечивая таким образом динамическое поведение при защите поль- зовательского кода от необходимости иметь дело с указателями. Объекты производных классов состоят из части (частей) базового класса и части произ- водного. При создании, копировании и присвоении объектов производных классов, сначала происходит создание, копирование и присвоение базовой части объекта, а затем производной. Поскольку частью объекта производного класса является объект базового, ссылку или указа-
Глава 15. Объектно-ориентированное программирование 653 тель на объект производного класса вполне можно преобразовать в ссылку или указатель на его базовый класс. В базовых классах обычно определяют виртуальный деструктор, даже если класс не имеет в нем никакой потребности. Термины Абстрактный базовый класс (abstract base class). Класс, который имеет или наследует од- ну или несколько чистых виртуальных функций. Создать объект абстрактного класса невоз- можно. Абстрактные классы предназначены лишь для определения интерфейса. Определив и реализовав в соответствии со своими особенностями чистые виртуальные функции, указан- ные в базовом классе, производные классы завершат тип (т.е. незавершенный тип становится завершенным). Базовый класс (base class). Класс, от которого происходит другой класс. Члены базового класса становятся членами производного класса. Виртуальная функция (virtual function). Функция-член, которая обеспечивает поведение, зависящее от типа. Во время выполнения, выбор конкретной версии функции при обращении к виртуальной функции с помощью ссылки или указателя, осуществляется на основании ти- па объекта, с которым связана ссылка или указатель. Динамический тип (dynamic type). Тип времени выполнения. Указатели и ссылки на тип базового класса могут быть связаны с объектами производного класса. В таких случаях стати- ческим типом будет ссылка (или указатель) на базовый класс, а динамическим — ссылка (или указатель) на производный. Динамическое связывание (dynamic binding). Отсрочка выбора выполняемой функция до времени выполнения. В языке C++ динамическим связыванием называют выбор во время выполнения используемой версии виртуальной функции на основании фактического типа объекта, который связан со ссылкой или указателем. Закрытое наследование (private inheritance). Форма наследования реализации, при кото- рой открытые и защищенные члены закрытого базового класса становятся закрытыми в про- изводном. Защищенное наследование (protected inheritance). При защищенном наследовании за- щищенные и открытые члены базового класса становятся защищенными в производном. Иерархия наследования (inheritance hierarchy). Термин, используемый для описания от- ношений между классами, связанными наследованием и совместно использующими общий базовый класс. Косвенный базовый класс (indirect base class). Базовый класс, который не является непо- средственным базовым. Класс, от которого происходит непосредственный базовый класс, прямо или косвенно является косвенным базовым классом производного класса. Маркер доступа protected (protected access label). К членам класса, определенным после маркера protected, могут обращаться члены самого класса, дружественного класса и производного класса (но не дружественных для производного). Защищенные члены не дос- тупны для обычных пользователей класса. Непосредственный базовый класс (immediate base class). Базовый класс, от которого не- посредственно происходит производный класс. Имя непосредственного базового класса ука- зывают в списке наследования. Непосредственный базовый класс сам может быть производ- ным классом. Объектно-ориентированное программирование (object-oriented programming). Термин, применяемый для описания программ, которые используют абстракцию данных, наследова- ние и динамическое связывание. Открытое наследование (public inheritance). Открытый (public) интерфейс базового класса является частью открытого интерфейса производного класса.
654 Часть IV. Объектно-ориентированное и общее программирование Полиморфизм (polymorphism). Термин, произошедший от греческих слов, означающих многообразие форм. В объектно-ориентированном программировании под полиморфизмом понимают способность изменять поведение в зависимости от динамического типа ссылки или указателя. Производный класс (derived class). Класс, который происходит от другого класса. Члены базового класса являются также членами производного класса. В производном классе можно переопределить члены его базового класса, а также определить новые. Область видимости производного класса вложена в области видимости его базового класса (классов), поэтому производный класс вполне может непосредственно обращаться к членам базового. Члены производного класса, имена которых совпадают с именами членов базового класса, скрывают их. В частности, функции-члены в производном классе не перегружают функции-члены базо- вого. К скрытому члену базового класса можно обратиться при помощи оператора области видимости. Прямой базовый класс (direct base class). То же, что и непосредственный базовый класс. Рефакторинг (refactoring). Способ перепроектирования программ, позволяющий собрать взаимосвязанные части в единую абстракцию при замене первоначального кода новой абст- ракцией. В объектно-ориентированных программах рефакторинг (перераспределение) зачас- тую имеет место при перепроектировании классов в иерархии наследования или в результате изменения требований. Во избежание дублирования кода, рефакторинг классов перемещает данные или функции-члены вверх по иерархии наследования в более общий класс3. Список наследования класса (class derivation list). Используется в определении класса как указание на то, что он является производным. Список наследования может содержать уровень доступа и имя базового класса. Если маркер доступа не указан, тип наследования за- висит от ключевого слова, использованного при определении производного класса. Если про- изводный класс определен с использованием ключевого слова struct, базовый класс по умолчанию наследуется как public. Если класс определен с использованием ключевого сло- ва class, базовый класс наследуется как private. Статический тип (static type). Тип времени компиляции. Статический тип объекта совпа- дает с динамическим. Динамический тип объекта, к которому относится ссылка или указа- тель, может отличаться от статического типа ссылки или указателя. Управляющий класс (handle class). Класс, который предоставляет интерфейс для другого класса. Как правило, используется для создания и манипулирования указателями на объекты классов из иерархии наследования. Усечение (sliced). Термин, используемый для описания событий, которые происходят при инициализации или присвоении объекта производного класса объекту базового. Произ- водная часть объекта усекается, а оставшаяся базовая часть присваивается объекту базового класса. Чистая виртуальная функция (pure virtual). Виртуальная функция, объявленная в заго- ловке класса с использованием = 0 в конце списка параметров функции. Чистая виртуальная функция необязательно должна (но вполне может) быть определена классом. Класс с чистой виртуальной функцией является абстрактным. Если производный класс не определяет собст- венную версию унаследованной чистой виртуальной функции, он также становится абст- рактным. 3 Это еще называется переносом вверх (percolating up). — Примеч. ред.
ГЛАВА 16 Шаблоны и общее ПРОГРАММИРОВАНИЕ В ЭТОЙ ГЛАВЕ... 16.1. Определение шаблона 656 16.2. Создание экземпляра 667 16.3. Модели компиляции шаблона 676 16.4. Члены шаблона класса 679 16.5. Общий управляющий класс 698 16.6. Специализация шаблона 703 16.7. Перегрузка и шаблоны функций 711 Резюме 714 Термины 715 Общее программирование подразумевает создание кода, не зависящего от специ- фических типов. Когда используется общая программа, ей необходимо предоставить тип (типы) или значение (значения), с которым данный экземпляр программы будет работать. Примерами общего программирования являются библиотечные контейне- ры, итераторы и алгоритмы, описанные в части II, “Контейнеры и алгоритмы”. Для каждого контейнера, например vector, существует единое определение, но пользо- ватель вполне может определять разные вектора, которые отличаются лишь типом хранимых в них элементов. Шаблоны (template) — это основа общего программирования. Шаблоны вполне можно использовать, не понимая, как они устроены (обычно так и происходит). В этой главе будет продемонстрировано, как можно определить собственные шаблоны классов и функций. Общее программирование, подобно объектно-ориентированному программиро- ванию, основано на полиморфизме. При объектно-ориентированном программиро- вании, к классам, связанным наследственными отношениями во время выполнения, применяется полиморфизм. Вполне можно написать такой код, который, используя эти классы, будет игнорировать различия между базовым и производным классами. Пока используются ссылки или указатели на базовый класс, тот же код можно при- менять к объектам как базового класса, так и производного.
656 Часть IV. Объектно-ориентированное и общее программирование Общее программирование позволяет создавать классы и функции, которые во время компиляции являются полиморфными и не связанны с типами. Для манипу- лирования объектами нескольких типов применяются единый класс или функция. Хорошими примерами общего программирования являются контейнеры стандарт- ной библиотеки, итераторы и алгоритмы. Каждый из видов контейнера, итератора и алгоритма определен в библиотеке способом, не зависящим от типа. Библиотечные классы и функции можно использовать для любого типа. Например, вектор для хра- нения объектов класса Sales_item вполне можно создать несмотря даже на то, что разработчики шаблона vector понятия не имели о существовании такого класса. В языке C++ шаблоны являются основой для общего программирования. Шаб- лон — это проект или формула для создания класса или функции. Например, в стандартной библиотеке определен единый шаблон класса, который задает способ создания векторов. Этот шаблон используется для создания любого количества классов векторов для специфического типа, например вектора vector<int> или vector<string>. Применение общих типов и функций описано в части II, “Контейнеры и алгоритмы”, а эта глава посвящена способам создания собствен- ных шаблонов. 16.1. Определение шаблона Давайте предположим, что необходимо написать функцию, которая сравнивает два значения и указывает, является ли первое из них меньшим, равным или боль- шим, чем второе. Фактически, придется создать несколько таких функций, каждая из которых сможет сравнивать значения определенного типа. На первом этапе мож- но было бы определить несколько перегруженных функций. // возвращает 0, если значения равны, -1, если vl меньше, и 1, / / если меньше v2 int compare(const string &vl, const string &v2) { int compare(const double &vl, { if (vl < v2) return -1; if (v2 < vl) return 1; return 0; } const double &v2) Эти функции почти идентичны: они отличаются только типом параметров. Тела у обоих функций одинаковы. Повторение тела функции для каждого сравниваемого типа не только утоми- тельно, но и повышает вероятность возникновения ошибок. Однако важней всего то, что в этом случае необходимо заранее знать все типы, которые придется сравнивать. Этот подход не сработает в случае, когда функцию предполагается использовать для типов, неизвестных на данный момент.
Глава 16. Шаблоны и общее программирование 657 16.1.1. Определение шаблона функции Чтобы не создавать новую функцию для каждого типа, можно определить еди- ный шаблон функции (function template). Шаблон функции — это независимая от ти- па функция, которая используется как формула при создании версии функции, спе- цифической для определенного типа. Например, можно создать шаблон функции по имени compare, который проинструктирует компилятор о том, как создать специ- фическую версию функции сравнения для указанного типа. Давайте рассмотрим шаблон функции compare (). // реализация функции сравнения, подобной встроенной // функции st гетр() // возвращает 0, если значения равны, 1, если vl больше, и -1 / / если vl меньше template ctypename Т> int compare(const T &vl, const T &v2) { Определение шаблона начинается с ключевого слова template, за которым сле- дует заключенный в угловые скобки (<>) список параметров шаблона (template parameter list), элементы которого разделены запятыми. На Список параметров шаблона не может быть пуст. Список параметров шаблона Список параметров шаблона очфнь похож на список параметров функции. Спи- сок параметров функции задает имена и типы локальных переменных, но оставляет их неинициализированными. Инициализацию параметров во время выполнения обеспечивают аргументы. Аналогично, параметры шаблона представляют типы или значения, которые можно использовать при определении класса или функции. Например, рассматри- ваемая функция compare () объявляет, что ее единственный параметр имеет тип Т. Внутри шаблона compare имя Т можно использовать там, где должно быть название типа данных. Фактический тип Т будет определен компилятором на основании спо- соба применения функции. Параметр шаблона (template parameter) может быть параметром типа (type parameter), представляющим тип данных, или параметром не типа (nontype parameter), который представляет константное выражение и называется еще пара- метром значения. Параметр значения объявляют со спецификатором типа. Более подробная информация о параметрах значения приведена в разделе 16.1.5 (стр. 664). Параметр типа определяют после ключевого слова class или typename. Напри- мер, class Т — это параметр типа по имени Т. В данном случае между ключевыми словами class и typename нет никакой разницы.
658 Часть IV. Объектно-ориентированное и общее программирование Использование шаблона функции При использовании шаблона функции, компилятор связывает каждый аргу- мент шаблона (template argument) с соответствующим параметром шаблона. Как только компилятор выясняет фактические аргументы шаблона, он создает экземп- ляр (instantiate) шаблона функции. По существу, компилятор использует соответст- вующий тип вместо каждого из параметров типа и соответствующее значение вместо каждого параметра значения. Выявив и использовав аргументы вместо соответст- вующих параметров шаблона, компилятор создает необходимую версию функции. Таким образом, компилятор сам создаст необходимые версии функции для каждого используемого типа. Рассмотрим следующее обращение. int main() { // Т - тип int; // компилятор создает экземпляр // функции int compare(const int&, const int&) cout << compare(1, 0) « endl; // T - тип string; // компилятор создает экземпляр // функции int compare(const string&, const string&) string si = "hi", s2 = "world"; cout « compare(si, s2) << endl; return 0; } Здесь компилятор создает два экземпляра разных версий функции compare (). В одной из них параметр Т заменен типом int, а во второй — типом string. Встраиваемый шаблон функции Шаблон функции может быть объявлен встраиваемым (inline) точно так же, как и обычная функция. Спецификатор inline размещается после списка парамет- ров шаблона, но перед типом возвращаемого значения. Его не указывают перед клю- чевым словом template. // ok: спецификатор inline следует за списком параметров шаблона template <typename Т> inline Т min(const Т&, const Т&); // ошибка: неправильное размещение спецификатора inline inline template <typename T> T min(const Т&, const Т&); Упражнения раздела 16.1.1 Упражнение 16.1. Создайте шаблон, который возвращает абсолютное значение его параметра. Используйте шаблон для значений по крайней мере трех разных типов. Обратите внимание: пока даже не обсуждается, как компилятор создает экземпляры шаблона, речь об этом пойдет в разде- ле 16.3 (стр. 676), а пока заметим, что определение каждого шаблона и все случаи его использо- вания должны располагаться в том же файле. Упражнение 16.2. Создайте шаблон функции, получающей ссылку на объект класса ostream и значение, которое она записывает в поток. Используйте функцию по крайней мере для четырех разных типов. Проверьте программу на примере записи в объект cout, файл и объект класса stringstream.
Глава 16. Шаблоны и общее программирование 659 Упражнение 16.3. При вызове функции compare О для сравнения двух строк, ей передается два объекта класса string, инициализированные строковыми литералами. Что произойдет в следующем случае? compare("hi", "world"); 16.1.2. Определение шаблона класса Подобно шаблону функции, вполне можно создать шаблон класса. Для иллюстрации шаблонов класса, реализуем собственную версию класса Queue, оп- ределенного в стандартной библиотеке (раздел 9.7, стр. 376). В пользовательских про- граммах следует использовать стандартный класс Queue, а не тот, который будет раз- работан здесь. Разрабатываемый класс Queue должен быть способен содержать объекты разных типов, поэтому создадим его как шаблон класса (class template). Реализуемый класс Queue будет поддерживать некоторые из функций интерфейса стандартного класса Queue. Функция push () добавляет элемент в конец очереди (queue). Функция pop () удаляет элемент из начала очереди. Функция f ront () возвращает ссылку на элемент в начале очереди. Функция empty () указывает, имеются ли в очереди элементы. Реализация класса Queue осуществляется в разделе 16.4 (стр. 679), а сейчас можно начать определение его интерфейса. template <class Туре> class Queue { public: Queue(); // Type &front(); // const Type &front() const; void push(const Type &); // void pop(); // bool empty() const; // private: стандартный конструктор возвращает элемент из начала очереди добавляет элемент в конец очереди удаляет элемент из начала очереди возвращает значение true, если очередь пуста Шаблон класса — это шаблон, поэтому он должен начинаться с ключевого слова template, за которым следует список параметров шаблона. Разрабатываемый шаб- лон Queue получает один параметр типа по имени Туре. За исключением списка параметров, определение шаблона класса выглядит по- добно любому другому классу. В шаблоне класса можно определять данные- члены, функции-члены и типы; для управления доступом к этим членам использу- ются маркеры доступа; здесь можно определять конструкторы, деструкторы и т.д. В определении класса и его членов вполне можно использовать параметры шабло- на для обозначения их типов или значений, которые будут подставлены во время применения класса.
660 Часть IV. Объектно-ориентированное и общее программирование Например, разрабатываемый шаблон Queue имеет один параметр типа. Этот па- раметр можно использовать везде, где применяется имя типа. Для указания типа возвращаемого значения перегруженной функции front () и типа параметра функ- ции push (), в этом определении шаблона используется имя Туре. Использование шаблона класса В отличие от шаблона функции, при использовании шаблона класса необходимо явно указать аргументы параметров шаблона. Queue<int> qi ; Queue< vector<double Queue<string> qs; очередь, очередь, очередь, содержащая целые числа содержащая векторы double содержащая строки Компилятор использует аргументы для создания экземпляра специфической для указанного типа версии класса. По существу, компилятор переделывает1 класс Queue заменяя параметр Туре фактическим типом, указанным пользователем. В дан- ном случае компилятор создает три экземпляра класса: версию класса Queue, ис- пользующую вместо параметра Туре тип int, версию класса Queue, где вместо па- раметра Туре использован тип vector<double> и третью версию, где параметр Туре заменен типом string. Упражнения раздела 16.1.2 Упражнение 16.4. Что такое шаблон функции? Что такое шаблон класса? Упражнение 16.5. Создайте шаблон функции, которая возвращает большее из двух значений. Упражнение 16.6. Подобно рассматриваемой упрощенной версии класса Queue, напишите шаб- лон класса List, имитирующий упрощенную версию стандартного класса List. 16.1.3. Параметры шаблона Подобно параметрам функции, выбранное программистом имя для параметра шаблона не имеет никакого значения. В рассматриваемом примере шаблона функ- ции compare () параметр типа имел имя Т, однако вполне могло быть использовано любое другое имя. // эквивалентное определение шаблона template <class Glorp> В этом коде определен тот же шаблон compare, что и прежде. Единственный смысл, который можно придать параметру шаблона, — должен ли он являться параметром типа или параметром значения? Если это параметр типа, он представляет неизвестный пока еще тип. Если это параметр значения, он представ- ляет неизвестное пока еще значение. ' Скорее, использует шаблон для создания класса. — Примеч. ред.
Глава 16. Шаблоны и общее программирование 661 Когда необходимо использовать тип или значение, которое представляет пара- метр шаблона, применяется то же имя, что и у соответствующего параметра шабло- на. Например, все обращения к имени Glorp в шаблоне функции compare () будут рассматриваться как обращения к тому же типу, что и у экземпляра функции. Область видимости параметра шаблона Имя параметра шаблона может быть использовано лишь после его объявления как параметра шаблона и до конца объявления или определения шаблона. Параметры шаблона подчиняются обычным правилам сокрытия имен. Если имя параметра шаблона совпадет с именем объекта, функции или типа объявленного в глобальной области видимости, оно скроет глобальное имя typedef double Т; template <class Т> Т calc(const Т &а, const Т &Ь) { // tmp имеет тип параметра Т шаблона, // а не одноименного глобального типа Т tmp = а; return tmp; } Глобальный тип Т, который определен как double, окажется скрыт параметром типа Т. Таким образом, переменная tmp будет иметь тип не double, а любой ука- занный для параметра Т шаблона. Ограничения на использование имен параметров шаблона Имя, присвоенное параметру шаблона, не может быть многократно использовано внутри шаблона. template <class Т> Т calc(const Т &а, const Т &Ь) typedef double Т; // ошибка: попытка повторного объявления // имени, совпадающего с именем параметра Т // шаблона Т tmp - а; return tmp; } Это ограничение означает также то, что имя параметра шаблона применимо только один раз внутри того же списка параметров шаблона. // ошибка: недопустимое многократное использование имени V template <class V, class V> V calc(const V&, const V&); Однако подобно именам параметров функций, для параметров разных шаблонов вполне можно многократно использовать то же имя. // ок: многократное использование имени параметра в разных шаблонах template <class Т> Т calc(const Т&, const Т&); template <class Т> int compare(const T&, const T&); Объявление шаблона Подобно любой другой функции или классу, шаблон можно объявить, но вре- менно не определять. Объявление должно указать, что данное имя принадлежит шаблону функции или класса.
662 Часть IV. Объектно-ориентированное и общее программирование // объявить шаблон compare, но не определять его template <class Т> int compare(const T&, const T&); Имена параметров шаблона необязательно должны совпадать в объявлении и оп- ределении того же шаблона. // все три случая использования имени calc относятся к тому же // шаблону функции II предварительные объявления шаблона template <class Т> Т calc(const Т&, const Т&); template cclass U> U calc(const U&, const U&) ; // фактическое определение шаблона template <class Type> Type calc(const Type& a, const Type& b) { /* ... */ } Каждому параметру типа шаблона должно предшествовать либо ключевое слово class, либо ключевое слово typename; каждому параметру значения должно предшествовать имя типа. Отсутствие ключевого слова или спецификатора типа яв- ляется ошибкой. // ошибка: U должно предшествовать либо typename, либо class template <typename Т, U> Т calc(const Т&, const U&); Упражнения раздела 16.1.3 Упражнение 16.7. Объясните каждое из следующих определений шаблонов функций и укажите, нет ли среди них недопустимых. Исправьте все обнаруженные ошибки. (а) (Ь) (с) (d) (е) template <class Т, U, typename V> void fl(T, U, V); template cclass T> T f2(int &T); inline template cclass T> T foo(T, unsigned int*); template cclass T> f4(T, T); typedef char Ctype; template ctypename Ctype> Ctype f5(Ctype a); Упражнение 16.8. Объясните, какие из следующих объявлений являются ошибочными и почему (если они есть). (а) (Ь) template template template template Type> Type bar(Type, Type); Type> Type bar(Type, Type); T1, class T2> void bar(Tl, T2); Cl, typename C2> void bar(Cl, C2); Упражнение 16.9. Создайте шаблон, который работает подобно библиотечному алгоритму find. Шаблон должен получать один параметр типа, который передает тип пары итераторов, являющих- ся параметрами функции. Используйте полученную функцию для поиска указанного значение в векторе vectorcint> И списке listcstring>. 16.1.4. Параметры типа шаблона Параметр типа состоит из ключевого слова class или typename, за которым следует идентификатор. В списке параметров шаблона эти ключевые слова имеют одинаковый смысл: они означают, что следующее далее имя представляет тип. Параметр типа используется внутри шаблона там, где следует указать специфи- катор встроенного типа или класса. В частности, они применяется для задания типов возвращаемого значения и типов параметров функций, при объявлении переменных и приведении типов внутри тела функции.
Глава 16. Шаблоны и общее программирование 663 // ок: для возвращаемого значения и обоих параметров используется // одинаковый тип template <class Т> Т calc(const Т& a, const Т& Ь) { // ок: тип переменной tmp совпадает с типом параметров II и возвращаемого значения Т tmp = а; return tmp; } Различия между ключевыми словами typename и class В списке параметров шаблона функции, ключевые слова typename и class имеют одинаковый смысл и являются взаимозаменяемыми. Оба ключевых слова можно применить в том же списке параметров шаблона. // ок: никакой разницы между ключевыми словами typename и class нет template <typename Т, class U> calc(const T&, const U&); Для обозначения параметра типа шаблона интуитивно понятней использовать ключевое слово typename, а не ключевое слово class; в конце концов, для факти- ческого типа параметра вполне может быть использован встроенный тип, а не только класс. Кроме того, ключевое слово typename более точно указывает на то, что сле- дующее за ним имя принадлежит типу. Однако ключевое слово typename было до- бавлено в язык C++ как часть стандарта C++, поэтому в устаревших программах, ве- роятнее всего, осталось исключительно ключевое слово class. Обозначение типов внутри определения шаблона Кроме определения данных и функций-членов, в классе могут быть определены типы данных. Например, в библиотечных классах контейнеров определено несколь- ко собственных типов, например тип size_type, который позволяет использовать контейнеры машинно-независимым способом. Когда такие типы необходимо ис- пользовать внутри шаблона функции, компилятору следует сообщить, что исполь- зуемое имя относится к типу. Это необходимо сделать явно, поскольку компилятор (и читатель программы) не сможет выяснить самостоятельно, определяет ли данный параметр тип или значение. Рассмотрим, например, следующую функцию. template <class Parm, class U> Parm fen(Parm* array, U value) Parm::size_type * p; // если Parm::size_type - тип, // это объявление // если Parm::size_type - объект, II это умножение } Вполне понятно, что size_type должен быть членом класса объекта Parm, од- нако неизвестно, принадлежит ли имя size_type типу или переменной-члену. По умолчанию компилятор подразумевает, что такие имена принадлежат переменным- членам, а не типам. Если компилятор должен рассматривать имя size_type как тип, это необходи- мо указать явно.
664 Часть IV. Объектно-ориентированное и общее программирование template <class Parm, class U> Parm fen(Parm* array, U value) { typename Parm::size_type * } // ok: p будет указателем Предварив имя члена класса ключевым словом typename, можно указать ком- пилятору явно, что его следует рассматривать как тип. Часть typename Parm: : size_type указывает, что член size_type класса объекта Parm является именем типа. Безусловно, это объявление накладывает некоторые обязательства на типы, используемые при создании экземпляра функции f сп (): они должны иметь член по имени size_type, который является типом. Еспи есть сомнения, необходимо ли применять ключевое слово typename для ука- зания того, что данное имя принадлежит типу, имеет смысл его применить. Вреда от y-A этого никакого не будет: если ключевое слово typename не нужно, оно не изменит смысл выражения. Упражнения раздела 16.1.4 Упражнение 16.10. В чем различие (если оно есть) между параметрами типа, в объявлении кото- рых использованы ключевые слова typename и class? Упражнение 16.11. Когда следует использовать ключевое слово typename? Упражнение 16.12. Напишите шаблон функции, которая получает два значения, представляющие собой итераторы неизвестного типа. Найдите значение, которое наиболее часто встречается в по- следовательности. Упражнение 16.13. Напишите функцию, которая получает ссылку на контейнер и отображает его элементы. Используйте для организации цикла, отображающего элементы контейнера, его члены size_type И size (). Упражнение 16.14. Перепишите функцию из предыдущего упражнения так, чтобы для управления циклом были использованы итераторы, возвращаемые функциями begin () и end (). 16.1.5. Параметры значения шаблона Параметрам шаблона тип не нужен. В этом разделе описано применение в шаблонах функций параметров значений. Параметры значений для шаблонов класса рассматриваются в разделе 16.4.2 (стр. 686), после описания реализации шаблонов класса. При вызове функции параметры значений заменяются значениями. Тип исполь- зуемого значения задается в списке параметров шаблона. Например, в следующем шаблоне функции array_init () объявлен один параметр типа и один параметр значения. Сама функция получает один параметр, который является ссылкой на массив (раздел 7.2.4, стр. 266). // инициализировать элементы массива нулевым значением template <class Т, size_t N> void array_init(T (&parm)[N]) { for (size_t i = 0; i 1= N; + + i) { parm[i] = 0;
Глава 16. Шаблоны и общее программирование 665 Внутри определения шаблона, параметр значения является постоянным значени- ем. Параметр значения применяется в случае, когда необходимо константное выра- жение, например, как показано ниже, для определения размера массива. При вызове функции array_init () компилятор заменяет параметры значения аргументами-массивами, int х[42]; double у[10]; array_init(х); // создает экземпляр array_init(int(&)[42] array_init(у); // создает экземпляр array_init(double(&)[10] Для каждого вида массива, использованного в обращении к функции аггау_ init (), компилятор создаст собственную версию функции array_init (). В приве- денной выше программе компилятор создает два экземпляра функции array_init (): первый для переданного в качестве параметра массива int [42], а второй для double [10]. Эквивалентность типов и параметры значений Выражения, которые возвращают одинаковые значения для параметров значений шаблона, считаются эквивалентными аргументами шаблона. Следующие обращения к функции array_init () приводят к созданию одинаковых экземпляров, аггау_ initcint, 42>. int х[42]; const int sz = int у[sz + 2]; array_init(x); array_init(y); // создает экземпляр array_init(int(&)[42]) // создание эквивалентного экземпляра Упражнения раздела 16.1.5 Упражнение 16.5. Напишите шаблон функции, которая определяет размер массива. Упражнение 16.6. Перепишите функцию printvalues () со стр. 266 в качестве шаблона функции, применяемой для отображения содержимого массивов разных размеров. 16.1.6. Создание общих программ При создании шаблона, код не может ориентироваться на определенный тип, од- нако он может сделать некоторые предположения о типах, которые будут использо- ваны. Например, чисто технически функция compare () допустима для любого ти- па, однако на практике экземпляры некоторых из ее версий могут оказаться недо- пустимы. Будет ли созданная программа допустимой, зависит от того, поддерживает ли данный тип операторы, используемые в функции. Данная функция compare () ис- пользует следующие операторы. if (vl if (v2 return return -1; return 1; // < для двух объектов типа Т II < для двух объектов типа Т II return int; не зависит от типа Т Первые два оператора содержат код, который неявно зависит от типа параметра. Оператор if использует при проверке параметров оператор <. Тип этих параметров
666 Часть IV. Объектно-ориентированное и общее программирование остается неизвестным до тех пор, пока компилятор не встретит обращение к функции compare () и не свяжет параметр Т с фактическим типом. То, какой именно из опе- раторов < будет использован на самом деле, полностью зависит от типа аргумента. Если вызов функции compare () произойдет для объекта, который не поддержи- вает оператор <, произойдет ошибка. Sales_item iteml, item2; // ошибка: у Sales_item нет оператора < cout << compare(iteml, item2) << endl; Этот код недопустим. В классе Sales_item оператор < не определен, поэтому программа не будет откомпилирована. Операторы, применяемые внутри шаблона функции, налагают ограничения на типы, ко- торые могут быть использованы при создании экземпляра функции. Поэтому разработ- чик должен гарантировать, что используемые в качестве аргументов функции типы фак- тически поддерживают все операторы, применяемые в шаблоне, а также то, что в дан- ном контексте они ведут себя корректно. Создание кода, независимого от типа Искусство создания хорошего кода, независимого от типа, выходит за рамки это- го вводного курса. Однако приведем некоторые общие пожелания. Ъг При создании кода шаблона, имеет смысл предъявлять по возможности меньше требований к типам аргументов. Акомендуем Продемонстрируем два наиболее важных принципа создания общего кода на примере функции compare (). Параметры шаблона должны быть константными ссылками. При проверке в теле шаблона следует использовать только оператор сравнения <. Объявление параметров константными ссылками позволит использовать те ти- пы, которые не допускают копирования. Большинство встроенных типов (исключая типы 10) и все рассмотренные ранее библиотечные типы допускают копирование. Однако вполне могут существовать классы, которые копирования не допускают. Сделав параметры константными ссылками, функцию compare () можно сделать применимой и для них. Кроме того, если функция compare () будет применена для больших объектов, такая конструкция позволит избежать копирования и сэкономит время при выполнении. Некоторые читатели могут подумать, что для сравнения было бы более целесооб- разно использовать оба оператора < и >. // ожидаемое сравнение if (vl < v2) return -1; if (vl > v2) return 1; return 0; Однако следующий код позволяет снизить требования к типам, с которыми при- меняется функция compare (). Эти типы обязаны поддерживать лишь оператор < и не обязаны поддерживать оператор >.
Глава 16. Шаблоны и общее программирование 667 ожидаемое return 0; сравнение return -1; return 1; // эквивалент vl > v2 Упражнения раздела 16.1.6 Упражнение 16.17. В разделе “Фундаментальная концепция” на стр. 118 упоминалось, что программисты C++ предпочитают использовать для сравнения оператор ! =, а не <. Объясните, почему. Упражнение 16.18. В данном разделе упоминалось, что для снижения требований к используе- мым типам, проверка в функции compare () осуществлялась так, чтобы исключить необходи- мость в обоих операторах < и >. С другой стороны, существующая тенденция подразумевает, что типы будут иметь два оператора == и ! =. Объясните, почему это кажущееся расхождение факти- чески соответствует хорошему стилю программирования. Внимание! Ошибки времени компиляции и времени компоновки Процесс создания шаблона есть три этапа, на протяжении которых компилятор может сообщить об ошибке. Первый — когда компилируется само определение шаблона. На этом этапе компилятор, как правило, не может найти большую часть ошибок. Здесь обнаруживаются в основном синтаксические ошибки, такие как пропущенная точка с запятой или неправильно написанное имя переменной. Следующий этап, на котором может быть получено сообщение об ошибке, — это создание экземпляра. Только на этом этапе могут проявиться ошибки, связанные с используемым типом. Могут ли быть такие ошибки выявлены во время компонов- ки, зависит от того, как именно компилятор создает экземпляр (это будет описано на стр. 67 6). Следует уяснить, что во время компиляции определения шаблона, практически ниче- го не известно о том, насколько допустимой получится программа. Даже после успеш- ной компиляции каждого файла, который использует шаблон, компилятор вполне может выдать сообщение об ошибке. Нет ничего странного в том, что ошибки будут обнаружены только при создании экземпляра во время компоновки. 16.2. Создание экземпляра Шаблон — это только проект, а не класс и не функция. Компилятор использует шаблон для создания конкретной версии класса или функции, предназначенной для указанного типа. Процесс создания экземпляра шаблона для определенного типа называют созданием экземпляра (instantiation). Этот термин описывает про- цесс, в результате которого по шаблону создается новый экземпляр (instance) класса или функции. Экземпляр шаблона создается в процессе его применения. Экземпляр шаблона класса создается при обращении к нему с передачей фактического типа (класса), а создание экземпляра шаблона функции происходит при обращении к ней или ис- пользовании, а также при инициализации или присвоении указателя на функцию.
668 Часть IV. Объектно-ориентированное и общее программирование Создание экземпляра класса В результате выполнения приведенного ниже кода компилятор автоматически создает класс по имени Queue<int>. Queue<int> Создавая класс Queue<int>, компилятор как будто переписывает шаблон Queue, заменяя каждый параметр Туре типом int. Класс, полученный в результате созда- ния экземпляра шаблона, имеет следующий вид. // эквивалентная версия класса Queue для типа int template <class Туре> class Queue<int> { public: Queue(); // заменено на Queue<int>* int &front(); const int &front() const; void push(const int &); void pop(); bool empty() const; private: } ; // тип возвращаемого значения // заменен на int II тип возвращаемого значения II заменен на int / / тип параметра заменен на int II код, не зависящий от типа // код, не зависящий от типа Чтобы создать класс Queue для объектов класса string, применяется следую- щий код. Queue<string> qs; В данном случае каждое слово Туре будет заменено на string. Класс, полученный в результате создания экземпляра шаблона, никак не зависит от дру- гих классов, созданных по тому же шаблону. Например, экземпляр класса Queue для типа int не имеет никакого отношения к классу Queue для другого типа и не предос- тавляет доступа к его членам. Шаблону класса необходимы аргументы При использовании шаблона класса, его аргументы следует определять явно. Queue qs; // ошибка: какой из экземпляров шаблона создать? Сам шаблон класса тип не определяет; определение типа происходит только при создании экземпляра шаблона. При создании экземпляра шаблона, для каждого его параметра следует предоставить соответствующий аргумент. Аргументы шаблона указывают в списке, разделяемом запятыми и заключенном в угловые скобки (< >). Queue<int> qi; // ok: определение очереди для хранения / / целых чисел Queue<string> qs; // ok: определение очереди для хранения строк Тип, определенный шаблоном класса, всегда включает аргумент (аргументы). Например, Queue — это не тип; тип — это Queue<int > или Queue<string>. Создание экземпляра шаблона функции При использовании шаблона функции, компилятор обычно сам передает аргу- менты в шаблон.
Глава 16. Шаблоны и общее программирование 669 int main() compare(1, 0); compare(3.14, 2.7); return 0; ) // ok: связывает параметр шаблона II с типом int II ок: связывает параметр шаблона II с типом double Этот код создает две версии функции compare (): ту, где параметр Т заменен ти- пом int, и ту, где он заменен типом double. По существу, компилятор создает сле- дующие две версии функции compare (). &vl, int compare(const const int &v2) if (vl if (v2 return return -1; return 1; compare(const double &vl, const double &v2) if (vl if (v2 return return -1; return 1; 16.2.1. Дедукция аргумента шаблона To, какую именно из версий функции создавать, компилятор выясняет на осно- вании типа каждого из аргументов. Если соответствующий параметр был объявлен с указанием типа (когда речь идет о параметре типа), компилятор использует для типа параметра тип аргумента. В случае шаблона compare, оба аргумента имеют одина- ковый тип: оба они объявлены с использованием параметра типа Т. При первом вызове, compare (1, 0), аргументы имеют тип int, а во втором, compare (3.14, 2.7), они имеют тип double. Процесс выяснения типов и значе- ний аргументов шаблона на основании типов аргументов функции называется де- дукцией аргумента шаблона (template argument deduction). Аргументы множественных параметров должны быть однозначны Параметр типа шаблона может быть использован в качестве типа параметра функции несколько раз. В таких случаях дедукция типа аргумента шаблона должна возвращать одинаковый тип для каждого соответствующего аргумента функции. Если полученные в результате дедукции типы не совпали, обращение считается ошибочным. template <typename Т> int compare(const T& vl, const if (vl < v2) return -1; if (v2 < vl) return 1; return 0; } int main() T& v2) short si; // ошибка: невозможно создать экземпляр compare(short, int)
670 Часть IV. Объектно-ориентированное и общее программирование // должно быть: compare(short, short) или compare (int int) compare(si, 1024); return 0; Это обращение ошибочно потому, что аргументы функции compare () имеют разный тип. В результате дедукции первого аргумента шаблона получается тип short, а второго — int. Поскольку эти типы не совпадают, происходит ошибка. Если разработчик шаблона compare хочет обеспечить возможность стандартного преобразования аргументов, функцию следует определить с двумя параметрами типа. // типы аргументов могут отличаться, но они должны быть совместимы template <typename A, typename В> int compare(const A& vl, const B& v2) { Теперь пользователь может использовать аргументы разных типов. short si; compare(si, 1024); // ок: создание экземпляра compare(short, int) Однако при этом необходим оператор <, который сможет сравнить значения этих типов. Ограничения на преобразования аргументов параметра типа Рассмотрим следующие обращения к функции compare (). short si, s2; int il, i2; compare(il, i2); // ok: создание экземпляра compare(int, int) compare(sl, s2); // ok: создание экземпляра compare(short, short) При первом вызове создается экземпляр функции compare (), где с параметром Т связан объект типа int. При втором обращении создается новый экземпляр функ- ции compare (), где с параметром Т связан объект типа short. Функция compare (int, int) является обычной нешаблонной функцией. Этой функции соответствует второе обращение. Аргументы типа short вполне мо- гут быть преобразованы (раздел 5.12.2, стр. 205) в тип int. Поскольку compare яв- ляется шаблоном, создается новый экземпляр функции compare (), с параметром типа которого связан объект типа short. Как правило, преобразование аргументов в тип, соответствующий уже сущест- вующему экземпляру, не происходит. Вместо этого просто создается новый экземп- ляр функции. Существует лишь два вида преобразований, которые компилятор пред- почтет выполнять вместо создания нового экземпляра. Преобразования констант: функция, которая получает ссылку или указатель на константу, может быть вызвана со ссылкой или указателем на неконстантный объ- ект. Новый экземпляр функции при этом не создается. Если функция получает нессылочный тип, ключевое слово const игнорируется как в типе параметра, так и аргумента. То есть при передаче константного или неконстантного объекта функ- ции, предназначенной для нессылочного типа, используется тот же экземпляр.
Глава 16. Шаблоны и общее программирование 671 Преобразование массива или функции в указатель: если параметр шаблона не является ссылочным типом, к аргументам типа массива или функции будет при- менено стандартное преобразование указателя. Аргумент типа массива будет рассматриваться как указатель на его первый элемент, а аргумент типа функ- ции — как указатель на тип функции. В качестве примера рассмотрим обращение к функциям fobj О и fref (). Функция fobj () копирует свои параметры, а параметрами функции fref () явля- ются ссылки. template <typename Т> Т fobj (Т, Т); // аргументы копируются template <typename Т> Т fref(const Т&, const Т&); // аргументы ссылки string si("a value"); const string s2("another value"); fobj(si, s2); // ok: вызов f(string, string), const игнорируется fref(si, s2); // ok: неконстантный объект si преобразуется в // константную ссылку int а[10], b [42] ; fobj (а, Ь) ; // ок: вызов f(int*, int*) fref(a, b); // ошибка: типы массивов не совпадают; аргументы в // указатели не преобразуются В первом случае как аргументы передаются строка и константная строка. Даже при том, что эти типы не соответствуют точно друг другу, оба обращения допусти- мы. При обращении к функции fobj () аргументы копируются, поэтому то, являет- ся ли исходный объект константным или нет, не имеет значения. В обращении к функции fref () типом параметра является ссылка на константу. Поскольку преоб- разование в константу для ссылочного параметра вполне приемлемо, такое обраще- ние также допустимо. В следующем случае в качестве аргументов передаются массивы, размер которых не совпадает. Тот факт, что массивы не совпадают, при обращении к функции fobj О не имеет значения. Оба массива преобразуются в указатели. Типом пара- метра шаблона функции fobj () является int*. Однако обращение к функции fref () оказывается недопустимо. Когда параметр является ссылкой (раздел 7.2.4, стр. 266), массив в указатель не преобразуется. Типы а и Ь не совпадают, поэтому обращение является ошибкой. Стандартные преобразования применимы для нешаблонных аргументов Ограничение на преобразование типов относится только к тем аргументам, которые пе- редаются параметрам шаблона. Стандартные преобразования (раздел 7.1.2, стр. 254) позволят использовать для определенных параметров обычные типы. Приведенный ниже шаблон функции sum () имеет два параметра. template cclass Туре> Type sum(const Type &opl, int ор2) return opl + op2;
672 Часть IV. Объектно-ориентированное и общее программирование Первый параметр шаблона, opl, является параметром типа. Его фактический тип не известен, пока функция не будет применена. Тип второго параметра, ор2, извес- тен: это тип int. Поскольку тип ор2 не изменен, к переданным ему при вызове функции sum () аргументам вполне применимы стандартные преобразования. double d = 3.14; string si("hiya"), s2(" world"); sum(1024, d); // ok: создание экземпляра sum(int, int), // преобразует d в int sum(1.4, d); // ok: создание экземпляра sum(double, int), // преобразует d в int sum(sl, s2); // ошибка: s2 не может быть преобразован в int В первых двух обращениях, тип второго аргумента, double, не совпадает с ти- пом соответствующего параметра функции. Однако оба обращения вполне допус- тимы, поскольку существует стандартное преобразование из типа double в тип int. Поскольку тип второго параметра не зависит от параметра шаблона, компи- лятор неявно преобразует его. При первом вызове будет создан экземпляр функ- ции sum (int, int), а при втором — sum (double, int). Третье обращение является ошибкой, поскольку нет стандартного преобразова- ния из типа string в тип int. Таким образом, применение аргумента типа string, не совпадающего с типом параметра int, как правило, недопустимо. Дедукция аргументов шаблона и указатели на функцию Шаблон функции можно использовать для инициализации или присвоения ука- зателя на функцию (раздел 7.9, стр. 302). В этом случае компилятор использует тип указателя при создании экземпляра шаблона с соответствующим аргументом (аргументами). Предположим, что в рассматриваемом примере существует указатель на функ- цию, которая возвращает значение типа int и получает два параметра, каждый из которых являются ссылкой на константу типа int. Этот указатель можно использо- вать как указатель на созданный экземпляр функции compare (). template <typename Т> int compare (const T&, const T&); // pfl указывает на созданный // экземпляр int compare(const int&, const int&) int (*pfl)(const int&, const int&) = compare; Таким образом, тип pfl — это указатель на функцию, возвращающую значение типа int и получающую два параметра типа const int&. Тип параметров указате- ля pfl определяет тип аргумента шаблона Т. Типом аргумента шаблона Т в данном случае является int. Таким образом, указатель pfl позволяет обратиться к экземп- ляру функции, у которой параметр Т заменен типом int. При обращении к адресу созданного экземпляра шаблона функции, контекст должен J быть таким, чтобы он допускал однозначную дедукцию типа или значения для каждого ^у/ параметра шаблона. Если аргументы шаблона не могут быть выявлены исходя из типа указателя на функцию, происходит ошибка. Предположим, например, что существует две функ- ции по имени f unc (). Каждая из них получает аргумент в виде указателя на функ-
Глава 16. Шаблоны и общее программирование 673 цию. Первая версия функции f unc () получает указатель на функцию, два парамет- ра которой являются ссылками на тип const string и которая возвращает тип string. Вторая версия функции func () получает указатель на функцию, полу- чающую две ссылки на тип const int и возвращающую значение типа int. Функ- цию compare () нельзя использовать как аргумент функции func (). // перегруженные версии функции func(); каждая получает свой // тип указателя на функцию void func(int(*)(const string&, const string&)); void func(int(*)(const int&, const int&)); func(compare); // ошибка: который из экземпляров функции compare () ? Проблема заключается в том, что при поиске типа параметра функции func () невозможно определить уникальный тип аргумента шаблона. В результате обраще- ния к функции func () может быть создан любой из следующих экземпляров. compare(const string&, const strings) compare(const int&, const int&) Поскольку невозможно найти уникальный экземпляр для аргумента функции func (), это обращение во время компиляции (или во время компановки) приведет к ошибке. Упражнения раздела 16.2.1 Упражнение 16.19. Что такое создание экземпляра? Упражнение 16.20. Что происходит при дедукции аргумента шаблона? Упражнение 16.21. Укажите два вида преобразования типов, допустимых для аргументов функ- ций, участвующих в дедукции аргумента шаблона. Упражнение 16.22. Рассмотрим следующие шаблоны. template <class Туре> Type calc(const Type* array, int size); template <class Type> Type fen(Type pl,Type p2; Какие из следующих обращений ошибочны (если они есть)? Почему? double dobj; float fobj; char cobj; int ai[5] - { 511, 16, 8, 63, 34 }; (a) calc(cobj, 'c'); (b) calc(dobj, fobj); (c) fen(ai, cobj); 16.2.2. Явные аргументы шаблона функции Иногда невозможно выяснить типы аргументов шаблона. Как правило, эта про- блема возникает в случае, когда тип возвращаемого значения функции не совпадает ни с одним из типов в списке параметров. В таких ситуациях следует отказаться от дедукции аргументов шаблона и явно указать типы или значения, которые нужно использовать для параметров шаблона.
674 Часть IV. Объектно-ориентированное и общее программирование Явное указание аргумента шаблона Рассмотрим следующую проблему. Допустим, необходимо определить шаблон функции по имени sum, который получает два аргумента разных типов. Тип возвра- щаемого значения должен быть достаточно большим, чтобы содержать сумму значе- ний любых двух типов, переданных в любом порядке. Как это можно сделать? Как определить тип возвращаемого значения функции sum () ? // типом возвращаемого значения будет Т или U? template <class Т, class U> ??? sum(T, U); В данном случае ответ парадоксален: ни один из них. Дело в том, что в опреде- ленных условиях применение любого из этих параметров приведет к ошибке. // ни Т ни U не годится для типа возвращаемого значения sum(3, 4L) ; // второй тип больше; необходимо U sum(T, U) sum(3L, 4); // первый тип больше; необходимо Т sum(T, U) Один из способов решения этой проблемы подразумевает приведение (раз- дел 5.12.4, стр. 209) меньшего из типов к типу, который следует использовать для ре- зультата. // ок: теперь и Т и U применим как тип возвращаемого значения int i; short s; sum(static_cast<int>(s), i); // ok: создание // экземпляра int sum(int, int) Применение параметра типа для типа возвращаемого значения Альтернативный способ определения типа возвращаемого значения подразуме- вает наличие третьего параметра шаблона, который должен быть явно определен вы- зывающей функцией. // дедукция Т1 невозможна, он отсутствует в списке // параметров функции template <class Т1, class Т2, class Т3> Т1 sum(T2, ТЗ); В эту версию добавлен параметр шаблона, задающий тип возвращаемого значе- ния. Однако здесь есть небольшая проблема: у функции нет аргумента, чей тип мог бы быть использован при выяснении типа параметра Т1. Вместо него вызывающая функция должна явно предоставлять аргумент для этого параметра при каждом об- ращении к шаблону sum. Передача явного аргумента шаблону при обращении осуществляется тем же спо- собом, который применяется при создании экземпляра шаблона класса. Явные аргу- менты шаблона указывают в списке, разделяемом запятыми и заключенном в угло- вые скобки (<>). Список явных типов шаблона располагается после имени функции, но перед списком аргументов. // ok: Т1 задан явно; Т2 и ТЗ получены из типов аргументов long val3 = sum<long>(i, Ing); // ok: вызов long sumfint, long) В этом обращении тип параметра Т1 указан явно. Компилятор выяснит типы па- раметров Т2 и ТЗ на основании типов аргументов, переданных при обращении. Явный аргумент (аргументы) шаблона предназначен для соответствующего па- раметра (параметров) шаблона в порядке слева направо, т.е. первый аргумент шаб- лона соответствует первому параметру шаблона, второй аргумент — второму пара-
Глава 16. Шаблоны и общее программирование 675 метру и т.д. Явный аргумент шаблона может быть опущен только в конце списка па- раметров (с правого края), т.е. подразумевается, что он может быть получен из пара- метров функции. Предположим, что функция sum () имеет следующий вид. // плохой подход: пользователи вынуждены явно указать все три // параметра шаблона template <class Т1, class Т2, class Т3> ТЗ alternative_sum(Т2, Т1); Здесь придется явно задать аргументы для всех трех параметров. // ошибка: нельзя выяснить исходные параметры шаблона long val3 = alternative_sum<long>(i, Ing); // ok: все три параметра указаны явно long val2 = alternative_sum<long, int, long>(i, Ing); Явные аргументы и указатели на шаблоны функций Еще один случай, где явные аргументы шаблона весьма полезны, — при возник- новении неоднозначности в программе (см. стр. 673). Но, используя явный аргумент шаблона, эту неоднозначность вполне можно устранить. template <typename Т> int compare(const T&, const T&); // перегруженная версия функции func(); каждая получает разный // тип указателя на функцию void func(int(*)(const string&, const string&)); void func(int(*)(const int&, const int&)); func(compare<int>); // ok: явное указание версии функции compare() Как и прежде, при обращении к перегруженной функции func (), ей необходимо передать созданный экземпляр функции compare (). Анализируя списки парамет- ров разных версий функции func (), невозможно выяснить, какой из экземпляров функции compare () следует передать. В этом обращении могли бы быть использо- ваны два разных экземпляра функции compare (). Однако явный аргумент шаблона указывает, какой именно из них будет применен при вызове функции func (). Упражнения раздела 16.2.2 Упражнение 16.23. Библиотечная функция тах() получает один параметр типа. Можно ли вы- звать ее для типов int и double? Если да, то как? Если нет, то почему? Упражнение 16.24. В разделе 16.2.1 (стр. 669) упоминалось, что аргументы, переданные версии функции compare () с одним параметром типа шаблона, должны точно совпадать. Если функцию необходимо вызвать для совместимых типов, например int и short, то для непосредственного указания типа параметра (int или short) можно использовать явный аргумент шаблона. Напи- шите программу, которая использует версию функции compare () с одним параметром шабло- на. Вызовите функцию compare () с использованием явного аргумента шаблона, который по- зволит передавать аргументы типа int и short. Упражнение 16.25. Используйте явный аргумент шаблона при вызове функции compare о, ко- торой передается два строковых литерала. Упражнение 16.26. Рассмотрим следующее определение шаблона sum. template <class Т1, class Т2, class Т3> Т1 sum(T2, ТЗ); Объясните каждое из следующих обращений. Укажите, какие из них являются ошибочными (если они есть). Объясните каждую ошибку.
676 Часть IV. Объектно-ориентированное и общее программирование double dobjl, dobj2; float fobjl, fobj2; char cobjl, cobj2; (a) (b) (c) (d) sum(dobjl, dobj2); sum<double, double, double>(fobj1, sum<int>(cobj1, cobj2); sum<double, ,double>(fobj2, dobj2) fobj 2); 16.3. Модели компиляции шаблона Когда компилятор встречает определение шаблона, он не создает код немедлен- но. Компилятор создает специфический для типа экземпляр шаблона только в слу- чае применения шаблона, например, при вызове шаблонной функции или создании объекта класса, определенного в шаблоне. Когда происходит вызов функции, компилятору обычно необходимо только ее объявление. Аналогично, когда происходит создание объекта класса, доступно должно быть определение класса, а определения функций-членов не обязательны. Таким образом, определения классов и объявления функций помещают в файлы за- головка, а определения обычных функций и функций-членов класса — в файлы ис- ходного кода. Шаблоны не одинаковы: чтобы создать экземпляр, компилятор должен иметь доступ к исходному коду, в котором шаблон определен. Когда происходит вызов шаблонной функции или функции-члена шаблона класса, компилятору необходимо определение функции. То есть необходим код, который обычно помещают в файл исходного кода. Стандарт C++ определяет две модели компиляции кода шаблона. Структура про- граммы в каждой из них остается в основном той же: определения классов и объяв- ления функции находятся в файлах заголовка, а определения функций и членов класса — в файлах исходного кода. Модели отличаются способом предоставления доступа компилятору к определениям в файлах исходного кода. Все современные компиляторы поддерживают первую модель, известную как инклюзивная модель (“inclusion” model), и только некоторые из них поддерживают вторую, раздельную модель компиляции (“separate compilation” model). Прежде чем компилировать код, который использует собственный шаблон класса или ч & '’a I функции, следует ознакомиться с документацией на применяемый компилятор и убедить- ся, что он способен создавать экземпляры шаблона. Инклюзивная модель компиляции В инклюзивной модели компиляции (“inclusion” model), компилятору должны быть доступны определения всех используемых шаблонов. Как правило, чтобы сделать определения доступными, в файл заголовка, содержащий объявление шаблона функции или класса, добавляют директиву #include. Эта директива подключает файл (файлы) исходного кода, который содержит соответствующие определения. // файл заголовка utlities.h ttifndef UTLITIES_H // защита заголовка (раздел 2.9.2, стр. 93) ttdefine UTLITIES_H template <class T> int compare(const T&, const T&);
Глава 16. Шаблоны и общее программирование 677 // другие объявления #include "utilities.сс" // предоставляет определения // функции compare() и т.д. #endi f // реализация файла utlities.cc template <class Т> int compare(const T &vl, const T &v2) { if (vl < v2) return -1; if (v2 < vl) return 1; return 0; } // другие объявления Этот подход обеспечивает разделение на файлы заголовков и файлы реализации, одновременно гарантируя, что при компиляции кода, в котором используются шаб- лоны, компилятору будут доступны оба файла. Некоторые (особенно старые) компиляторы, которые используют инклюзивную модель, могут создавать несколько экземпляров. Если два или более отдельно от- компилированных файла исходного кода используют тот же шаблон, эти компиля- торы создадут экземпляры шаблона для каждого файла. Как правило, этот подход подразумевает, что экземпляр данного шаблона будет создан несколько раз. Во вре- мя компоновки или предварительной компоновки компилятор выбирает один из созданных экземпляров, а остальные отбрасывает. В таких случаях, когда экземпляр того же шаблона создается для множества файлов, эффективность компиляции мо- жет существенно ухудшиться. На современных компьютерах для большинства при- ложений это снижение производительности во время компиляции несущественно и не создает проблем. Однако при создании достаточно больших систем, снижение производительности во время компиляции может оказаться весьма существенным. Такие компиляторы зачастую предоставляют механизмы, которые предотвраща- ют снижение эффективности компиляции за счет неявного создания нескольких эк- земпляров того же шаблона. Способ оптимизации процесса компиляции зависит от конкретного компилятора. Если процесс компиляции использующих шаблоны про- грамм занимает слишком много времени, обратитесь к документации на компилятор и выясните, какие из предоставляемых им средств позволяют избежать создания из- быточных экземпляров. Раздельная модель компиляции В раздельной модели компиляции (separate compilation model), компилятор сам от- слеживает соответствующие определения шаблона. Однако компилятор следует уведомить о том, что он должен запомнить данное определение шаблона. Для этого используется ключевое слово export. Ключевое слово export означает, что данное определение может быть необхо- димо для создания экземпляра в других файлах. Шаблон может быть определен в программе как экспортируемый только один раз. Теперь, когда потребуется создать экземпляр этого шаблона, компилятор сможет обнаружить его определение. В объ- явлении шаблона ключевое слово export не нужно. Как правило, экспортируемым шаблон функции указывают в его определении. Для этого перед ключевым словом template размещают ключевое слово export.
678 Часть IV. Объектно-ориентированное и общее программирование // определение шаблона располагается отдельно от откомпилированного // файла исходного кода export template <typename Туре> Type sum(Type tl, Type t2) { /* ... */ } Как обычно, объявление этого шаблона функции следует разместить в файле за- головка. Объявление необязательно должно содержать ключевое слово export. Процесс использования ключевого слова export в шаблоне класса немного сложней. Как обычно, объявление класса должно располагаться в файле заголовка. Тело класса в заголовке не должно использовать ключевое слово export. Если ис- пользовать его в заголовке, этот заголовок будет применим только в одном файле исходного кода программы. Вместо этого ключевое слово export следует использовать в файле класса реа- лизации. // заголовок шаблона класса располагается в совместно используемом // файле заголовка template <class Туре> class Queue { ... } ; // файл реализации Queue.сс объявляет шаблон Queue как // экспортируемый export template <class Туре> class Queue; ttinclude "Queue.h" // определения членов шаблона Queue Члены экспортируемого класса автоматически объявлены как экспортируемые. Кроме того, отдельные члены шаблона класса также можно объявить экспортируе- мыми. В данном случае ключевое слово export не указано на шаблоне класса непо- средственно. Оно указано в определении только одного члена класса, который необ- ходимо экспортировать. Определение экспортируемых функций-членов не должно быть видимым при их использовании. Определения всех неэкспортируемых членов класса обрабатываются как в инклюзивной модели: определение следует поместить внутрь заголовка, в котором определен шаблон класса. Упражнения раздела 16.3 Упражнение 16.27. Выясните, какую модель компиляции реализует используемый компилятор. Создайте и присвойте имя шаблону функции, которая используется для поиска значения медианы вектора, содержащего объекты неизвестного типа. (Примечание: медиана (median) — это значе- ние, которое больше одной половины значений элементов и меньше другой.) Используйте для программы обычную структуру: определение функции должно располагаться в отдельном файле, а объявление — в заголовке, который следует подключить в файл кода, определяющего и исполь- зующего шаблон функции. Упражнение 16.28. Где следует помещать определения функций-членов и статических перемен- ных-членов шаблонов класса, если используемый компилятор поддерживает раздельную модель компиляции? Объясните, почему. Упражнение 16.29. Где следует поместить такие определения членов шаблона класса, если ис- пользуемый компилятор реализует инклюзивную модель? Объясните, почему. Внимание! Поиск имен в шаблонах класса Компиляция шаблонов — это на удивление трудная задача. К счастью, эту задачу взя- ли на себя разработчики компилятора. Однако частично эти сложности затрагивают и пользователей шаблонов: шаблоны содержат два вида имен.
Глава 16. Шаблоны и общее программирование 679 1. Имена, не зависящие от параметра шаблона. 2. Имена, зависящие от параметра шаблона. Именно разработчик шаблона гарантирует, что все имена, не зависящие от параметров шаблона, будут определены в той же области видимости, что и сам шаблон. Пользователь шаблона должен обеспечить объявления для всех функций, типов и операторов, которые связаны с типами, используемыми для создания экземпляра шаблона. Это означает, что пользователь должен сделать эти объявления видимым на момент, когда создается экземпляр шаблона класса или функции. Выполнение этих требований невозможно без хорошо организованной структуры программы, в которой заголовки используются соответствующим образом. Автор шаблона должен предоставить заголовок, который содержит объявления всех имен, используемых в шаблоне класса или в определениях его членов. Прежде чем создать экземпляр шаблона для определенного типа или использовать член класса, созданного по этому шаблону, пользователь должен подключить заголовок для типа шаблона и заголовок, в котором определен используемый тип. 16.4. Члены шаблона класса До сих пор речь шла лишь об объявлении членов интерфейса шаблона класса Queue. В этом разделе рассматривается реализация класса. Стандартная библиотека реализует шаблон Queue как адаптер (раздел 9.7, стр. 376) для Зам I ДРУГ0Г0 контейнера. Чтобы продемонстрировать подходы программирования, подразуме- / вающие применение низкоуровневой структуры данных, реализуем класс Queue на базе связанного списка. На практике, при реализации подобного шаблона имеет смысл ис- пользовать библиотечный контейнер. Стратегия реализации шаблона Queue Схема реализации, представленная на рис. 16.1, подразумевает использование двух классов. 1. Класс Queue Item (ЭлементОчереди) представляет элементы связанного списка Queue. Он имеет две переменные-члена: item и next. • Переменная-член item (элемент) содержит значение элемента очереди; ее тип зависит от конкретного экземпляра шаблона Queue. • Переменная-член next (следующий) является указателем на следующий объект класса Queue Item в очереди. 2. Каждый элемент очереди сохраняется в объекте класса Queue Item. 3. Класс Queue (Очередь) предоставит функции интерфейсы, описанные раз- деле 16.1.2 (стр. 659). Класс Queue также будет иметь две переменные-члена: head (голова) и tail (хвост). Они являются указателями на объекты клас- са Queue Item. Подобно стандартным контейнерам, класс Queue будет копировать переданные ему значения.
680 Часть IV. Объектно-ориентированное и общее программирование Queue Queueitem Queueitem Queueitem Рис. 16.1. Реализация шаблона Queue Класс Queueltem Начнем реализацию с класса Queueitem. template <class Туре> class Queueitem { // закрытый класс: раздела public нет Queueitem(const Type &t): item(t), next(0) { } Type item; // значение, хранимое в данном элементе Queueitem *next; // указатель на следующий элемент очереди }; Как ни странно, но этот класс уже закончен: он содержит два элемента данных, которые инициализирует его конструктор. Подобно шаблону Queue, Queueitem является шаблоном класса. Чтобы выяснить тип переменной-члена item, класс ис- пользует параметр шаблона. Значение каждого элемента очереди будет сохранено в переменной-члене item. При каждом создании экземпляра класса Queue, будет создана соответствующая версия класса Queueitem. Например, при создании класса Queue<int> соответст- венно будет создан класс QueueItem< int >. Класс Queueitem является закрытым, поскольку он не имеет открытого интер- фейса. Предполагается, что этот класс будет использован только для реализации класса Queue, а не для общего пользования. Следовательно, никаких открытых чле- нов ему не нужно. Класс Queue следует сделать дружественным классу Queue Item, чтобы его члены могли обращаться к членам класса Queue Item. Механизм реализа- ции будет описан в разделе 16.4.4 (стр. 690). Внутри области видимости шаблона класса, к классу можно обращаться используя его имя без уточнения. Класс Queue Рассмотрим класс Queue более подробно. template <class Туре> class Queue { public: // пустая очередь Queue(): head(0), tail(0) { } // функции управления копированием, манипулирующие указателями // на элементы очереди Queue(const Queue &Q): head(0), tail(0) { copy_elems(Q); } Queues operator=(const Queues); -Queue() { destroy(); } // возвращает элемент из начала очереди
Глава 16. Шаблоны и общее программирование 681 // операция без проверки: Туре& front() const Type &front() const void push(const Type &) ; void pop(); bool empty() const { return head == 0; private: QueueItem<Type> *head; QueueItem<Type> *tail; начало пустой очереди неопределено { return head->item; } { return head->item; } // добавляет элемент в конец очереди // удаляет элемент из начала очереди // возвращает значение true, если // очередь пуста / указатель на первый элемент очереди / указатель на последний элемент / очереди // вспомогательные функции, используемые конструктором копий, II оператором присвоения и деструктором void destroy(); // удалить все элементы void copy_elems(const Queued); // скопировать элементы // из параметра Кроме членов класса, относящихся к интерфейсу, здесь есть три функции-члена управления копированием, описанные в главе 13, “Управление копированием”, и ис- пользуемые ими вспомогательные функции. Закрытые вспомогательные функции destroy () и copy_elems() выполняют действия по освобождению элементов очереди и копированию элементов из другой очереди. Функции-члены управления копированием необходимы для манипулирования переменными-членами head и tail, которые являются указателями на первый и последний элементы очереди. Эти элементы являются значениями типа Queue It em<Type>. Класс реализует несколько функций-членов. Стандартный конструктор, обнуляющий указатели head и tail. Это означает, что очередь в настоящее время пуста. Конструктор копий инициализирует переменные-члены head и tail, а также вызывает вспомогательную функцию copy_elems (), копирующую элементы из инициализирующего объекта. Функция front () возвращает значение из начала очереди. Никакой проверки эта функция не осуществляет: как и в случае с аналогичной функцией стандарт- ного шаблона Queue, пользователи не могут вызвать функцию front () для пустой очереди. Функция empty () возвращает результат сравнения указателя head с нулем. Если значением указателя head является нуль, очередь считается пустой, в ином случае — нет. Обращение к типу шаблона в области видимости шаблона Определение этого класса не слишком отличается от определений других клас- сов, которые уже рассматривались. В данном разделе речь пойдет об использовании параметра типа шаблона в обращениях к типам Queue и Queue Item. Когда используется имя шаблона класса, обычно необходимо указывать парамет- ры шаблона. Но у этого правила есть одно исключение: внутри области видимости самого класса имя шаблона класса можно использовать без уточнения. Например, в объявлениях стандартного конструктора и конструктора копий имя Queue является
682 Часть IV. Объектно-ориентированное и общее программирование сокращенной формой записи от Queue<Type>. По существу, компилятор расценивает обращение к имени класса как обращение к той же его версии. Таким образом, опре- деление конструктора копий в действительности эквивалентно следующему коду. QueuecType>(const QueuecType> &Q): head(O), tail(O) { copy_elems(Q); } Для параметра (параметров) других шаблонов, используемых внутри класса, компилятор подобных логических выводов не делает. Поэтому при объявлении ука- зателей на вспомогательный класс Queue Item, следует указывать параметр типа. QueueItem<Type> *head; QueueItemcType> *tail; // указатель на первый элемент очереди II указатель на последний элемент // очереди Эти объявления свидетельствуют о том, что для данного экземпляра класса Queue создаются указатели head и tail на объект типа Queueitem, созданный для того же параметра шаблона. То есть типом указателей head и tail внутри экземп- ляра очереди Queue<int > будет Queueltem<int>*. Пропуск параметра шаблона в определении переменных-членов head и tail считается ошибкой. Queueltem *head; // ошибка: какая из версий Queueltem? Queueitem *tail; // ошибка: какая из версий Queueitem? Упражнения раздела 16.4 Упражнение 16.30. Укажите, какие из следующих объявлений шаблона класса (или пары объяв- лений) некорректны (если они есть). (a) template cclass Туре> class Cl; template cclass Type, int size> class Cl; (b) template cclass T, U, class V> class C2; (c) template cclass Cl, typename C2> class C3 { }; (d) template ctypename myT, class myT> class C4 { }; (e) template cclass Type, int *ptr> class C5; template cclass T, int *pi> class C5; Упражнение 16.31. Следующее определение шаблона List некорректно. Как его исправить? template cclass elemType> class Listitem; template cclass elemType> class List { public: ListcelemType>(); ListcelemType>(const ListcelemType> &); ListcelemType>& operator=(const ListcelemType> &); "List(); void insert(Listitem *ptr, elemType value); Listitem * find(elemType value); private: 16.4.1 . Функции-члены шаблона класса Определение функции-члена шаблона класса имеет следующую форму. Оно должно начинаться с ключевого слова template, за которым следует спи- сок параметров шаблона для класса.
Глава 16. Шаблоны и общее программирование 683 В нем должен быть указан класс, которому принадлежит данная функция-член. Имя класса должно включать параметры его шаблона. Исходя из этих правил, можно сделать вывод, что функция-член класса Queue, определенная вне класса, должна начинаться следующим образом. template cclass Т> тип-возвращаемого-значения Queue<T>::имя-функции-члена Функция destroy () Чтобы продемонстрировать определение функции-члена шаблона класса вне ее класса, давайте рассмотрим функцию destroy (). template cclass Туре> void Queue<Type>::destroy() { } Это определение можно прочить слева направо следующим образом. Определение шаблона функции с одним параметром типа по имени Туре, которая возвращает void и находится в области видимости шаблона класса Queue < Туре >. Использованию функции шаблона Queue <Туре > предшествует оператор об- ласти видимости (: : ), указывающий класс, к которому принадлежит данная функ- ция-член. После имени функции-члена следует ее определение. Тело функции destroy () выглядит подобно определению любой другой нешаблонной функции. Она долж- на перебрать список элементов очереди и, вызвав функцию pop (), удалить каж- дый из них. Функция pop ( ) Функция-член pop () удаляет значение, расположенное в голове очереди. template cclass Туре> void QueuecType>::рор() // операция без проверки: удаление из пустой очереди невозможно QueueItemcType>* р - head; // хранение указателя на головной // элемент очереди позволит удалить / / его head = head->next; // теперь head указывает на // следующий элемент delete р; // удалить прежний головной элемент } Функция pop () подразумевает, что пользователь не будет вызывать ее для пус- той очереди. Задачей функции pop () является удаление головного элемента очере- ди. Однако, прежде чем удалять элемент, указатель head необходимо перевести на следующий элемент очереди, а затем удалить тот, на который он указывал ранее. Единственной сложностью здесь является сохранение указателя на тот элемент, ко- торый следует удалить после перевода указателя head на следующий элемент.
684 Часть IV. Объектно-ориентированное и общее программирование Функция push () Функция-член push () помещает новый элемент в хвост очереди. template cclass Туре> void Queue<Type>::push(const Type &val) { // создает новый объект класса Queueltem QueueItem<Type> *pt = new QueueItem<Type>(val); // помещает элемент в существующую очередь if (empty()) head = tail = pt; // сейчас очередь имеет только // один элемент else { tail->next = pt; // добавить новый элемент в конец // очереди tail = pt; } Работа функции начинается с создания нового объект класса Queue Item, кото- рый инициализируется переданным значением. На самом деле это выражение под- разумевает выполнение очень большого количества действий. 1. Конструктор Queue Item () копирует свой аргумент в переменную-член item объекта класса Queue Item. Подобно стандартному контейнеру, разрабатывае- мый класс Queue хранит копии переданных элементов. 2. Если типом переменной-члена item является класс, для его инициализации ис- пользуется конструктор копий этого класса. 3. Конструктор Queue Item () инициализирует также указатель next значением О, а значит, он не указывает ни на один из объектов класса Queue 11 em. Поскольку элемент добавляется в конец очереди, значение 0 для указателя next — это именно то, что нужно. Создав и инициализировав новый элемент, его следует поместить в очередь. Если очередь пуста, указатели head и tail должны содержать адрес этого нового эле- мента. Если очередь уже имеет другие элементы, текущий указатель tail следует переместить на этот новый элемент. Поскольку прежний элемент больше не являет- ся последним, указатель tail должен содержать адрес вновь созданного элемента. Функция сору ( ) Кроме оператора присвоения, реализацию которого остается читателю в качестве упражнения, необходима еще одна функция, copy_elems (). Эта функция предна- значена для использования оператором присвоения и конструктором копий. Она должна копировать значение параметра в элемент очереди. template <class Туре> void Queue<Type>::сору_elems(const Queue &orig) { // копировать исходный элемент в очередь // цикл прекращается при pt == 0, что происходит при // достижении orig.tail for (QueueItem<Type> *pt - orig.head; pt; pt = pt->next) push(pt->item); // копирование элемента }
Глава 16. Шаблоны и общее программирование 685 Копирование элементов осуществляет цикл for, который начинается с установ- ки указателя pt равным указателю head параметра. Цикл for продолжается до тех пор, пока указатель pt не станет равен 0, а это произойдет тогда, когда будет достиг- нут последний элемент очереди orig. Для каждого элемента в очереди orig приме- няется функция push (), копирующая значение в элемент очереди и переводящая указатель pt на следующий элемент очереди orig. Создание экземпляров функций-членов шаблона класса Функции-члены шаблонов класса сами являются шаблонами функций. Подобно любому другому шаблону функции, функция-член шаблона класса используется для создания экземпляра этого члена класса. В отличие от обычных шаблонов функций, при создании функций-членов экземпляра шаблона класса компилятор не осущест- вляет дедукции аргумента шаблона. Вместо этого параметры шаблона функции- члена шаблона класса определяют по типу объекта, для которого осуществляется обращение. Например, когда происходит вызов функции-члена push () объекта клас- са Queue<int>, создается экземпляр следующей версии функции push (). void Queue<int>::push(const int &val) To, что параметры функции-члена шаблона определяются аргументами шаблона объекта, обеспечивает большую гибкость при вызове функции-члена шаблона клас- са, чем сопоставимое обращения к шаблону функции. Для аргументов параметров функции, которые были определены с использованием параметра шаблона, допус- тимы стандартные преобразования. Queue<int> qi; // создание экземпляра класса Queue<int> short s = 42; int i = 42; // ok: s преобразуется в int и передается функции push () qi.push(s); // создание экземпляра Queue<int>::push(const int&) qi.push(i); // применение Queue<int>::push(const int&) f(s); // создание экземпляра f(const short&) f(i); // создание экземпляра f(const int&) Когда создаются экземпляры классов и членов классов Экземпляры функций-членов шаблона класса создаются только для тех функ- ций, которые используются программой. Если функция-член не используется, ее эк- земпляр не создается. Такое поведение означает, что используемые для создания эк- земпляра шаблона типы должны удовлетворять требованиям только тех функций, которые фактически используются. Вернемся, например, к конструктору последова- тельного контейнера (раздел 9.1.1, стр. 336), параметром которого является только размер. Этот конструктор использует стандартный конструктор типа элемента. Если для используемого типа стандартный конструктор не определен, контейнер для хра- нения объектов этого типа создать все же возможно. Однако использовать конструк- тор, получающий только размер, нельзя. Создание объекта типа шаблона соответствует созданию экземпляра шаблона класса. Одновременно с объектом создаются также экземпляры всех членов его класса, а для их инициализации используется конструктор. // создает экземпляр класса Queue<int> // и функции Queue<int>::Queue()
686 Часть IV. Объектно-ориентированное и общее программирование Queue<string> qs; qs .push("hello"); // создает экземпляр Queue<int>::push() Первый оператор создает экземпляр класса Queue<string> и вызывает его стандартный конструктор. Следующий оператор создает экземпляр функции-члена push (). Создание экземпляра функции-члена push () в свою очередь приводит к созда- нию вспомогательного класса QueueItem<string> и вызову его конструктора, template <class Туре> void Queue<Type>::push(const Type &val) { // создает новый объект класса Queueitem QueueItem<Type> *pt = new QueueItem<Type>(val); // помещает элемент в существующую очередь if (empty()) head = tail = pt; // сейчас очередь имеет только // один элемент else { tail->next = pt; // добавить новый элемент в конец // очереди tail = pt; } } Переменные-члены класса Queue Item являются указателями. Создание указа- теля на шаблон класса не приводит к созданию экземпляра класса; это происходит только тогда, когда такой указатель используется. Таким образом, при создании объ- екта класса Queue экземпляр шаблона Queue Item не создается. Это происходит только тогда, когда используется функция-член класса Queue, например front (), push () или pop (). Упражнения раздела 16.4.1 Упражнение 16.32. Реализуйте оператор присвоения для класса Queue. Упражнение 16.33. Объясните, как указатель next недавно созданной очереди получает значе- ние при выполнении функции copy_elems (). Упражнение 16.34. Напишите определение функции-члена для класса List, определенного в упражнении раздела 16.1.2 (стр. 660). Упражнение 16.35. Напишите общую версию класса CheckedPtr, описанного в разделе 14.7 (стр. 556). 16.4.2. Аргументы шаблона для параметров значения Теперь, узнав больше о реализации шаблонов класса, можно рассмотреть их па- раметры значения. Для этого определим новую версию класса Screen, впервые пред- ставленного в главе 12, “Классы”. В данном случае будет создан шаблон Screen, параметрами которого будут высота и ширина окна. template <int hi, int wid> class Screen { public: // параметры значений шаблона, используемые для инициализации // переменных-членов
Глава 16. Шаблоны и общее программирование 687 Этот шаблон имеет два параметра, причем оба они являются параметрами значения. Когда пользователи создают объекты шаблона Screen, для каждого из этих параметров они должны предоставить константное выражение. Эти парамет- ры класс использует в стандартном конструкторе для установки размера экрана по умолчанию. Как и для всех шаблонов класса, при использовании шаблона Screen значения параметров следует указать явно. Screen<24, 80> hp2621; // экран на 24 строки и 80 символов Объект hp2621 использует экземпляр шаблона screen<24, 80>. Аргументом шаблона для параметра hi является значение 24, а для параметра wid — 80. В обоих случаях аргумент шаблона является константным выражением. На момент компиляции аргументами параметров значения шаблона должны быть кон- стантные выражения. Упражнения раздела 16.4.2 Упражнение 16.36. Объясните, какие из экземпляров будут созданы каждым помеченным опера- тором (если будут). template <class Т> class Stack { }; void f1(Stack<char>); // (a) class Exercise { Stack<double> &rsd; // (b) Stack<int> si; // (c) ); int main() { Упражнение 16.37. Укажите, какие из следующих экземпляров шаблона (если они есть) допусти- мы. Объясните, почему некоторые из экземпляров недопустимы. template <class Т, int size> class Array { /* ... */ }; template <int hi, int wid> class Screen { /* ... */ }; (a) const int hi = 40, wi - 80; Screen<hi, wi+32> sObj ; (b) const int arr_size = 1024; Array<string, arr_size> al; (c) unsigned int asize = 255; Arraycint, asize> a2; (e) const double db = 3.1415; Array<double, db> a3;
688 Часть IV. Объектно-ориентированное и общее программирование 16.4.3. Дружественные отношения в шаблонах класса В шаблоне класса может присутствовать три вида объявления дружественных отношений. Каждый из них позволяет объявить дружественным один или несколько объектов. 1. Объявление дружественным обычного нешаблонного класса или функции, пре- доставляет доступ классу или функции, указанной по имени. 2. Объявление дружественным шаблона класса или шаблона функции, предостав- ляет доступ всем их экземплярам. 3. Объявление дружественным лишь определенного экземпляра шаблона класса или функции. Объявление дружественным обычного класса или функции Дружественным шаблону класса может быть объявлен обычный класс или функция. template <class Туре> class Bar { // предоставить доступ обычному, нешаблонному классу и функции friend class FooBar; friend void fcn(); } ; Здесь объявлено, что функции-члены класса FooBar и функция fcn() смогут обращаться к закрытым и защищенным переменным-членам любого экземпляра класса Ваг. Объявление дружественным всего шаблона класса или функции Дружественным может быть шаблон класса или функции. template cclass Туре> class Bar { // предоставить доступ шаблонам Fool и templ_fcnl, параметрами // которых могут быть любые типы template cclass Т> friend class Fool; template cclass T> friend void templ_fcnl(const T&); } ; Здесь дружественными объявлены все возможные экземпляры класса и функции, которые могут быть созданы при передаче шаблону параметра допустимого типа. Этими параметрами являются параметры типа шаблонов класса Fool и функции templ_fcnl (). В обоих этих случаях дружественными экземпляру шаблона класса Ваг будет неограниченное количество классов и функций, созданных на основании соответствующих шаблонов. То есть любой экземпляр шаблона Fool сможет обра- титься к закрытым членам любого экземпляра шаблона Ваг. Аналогично, любой эк- земпляр функции templ_fcnl () сможет обратиться к закрытым членам любого экземпляра шаблона Ваг. Здесь устанавливаются дружественные отношения по принципу “один ко мно- гим” между экземплярами шаблона Ваг и дружественными ему экземплярами шаб- лонов Fool и templ_f cnl. Для каждого экземпляра шаблона Ваг, любой экземп- ляр шаблона Fool или templ_fcnl будет дружественным.
Глава 16. Шаблоны и общее программирование 689 Объявление дружественным определенного экземпляра шаблона класса или функции Доступ можно предоставить только определенному экземпляру шаблона, но не всем экземплярам. template cclass Т> class Foo2; template cclass T> void templ_fcn2(const T&); template cclass Type> class Bar { // предоставить доступ только одному экземпляру шаблона, / / параметром которого является char* friend class Foo2cchar*>; friend void templ_fcn2cchar*>(char* const &) ; }; Несмотря на то, что Foo2 является шаблоном класса, дружественные отношения распространяются только на тот экземпляр шаблона Foo2, параметром которого яв- ляется тип char*. Аналогично, во втором случае дружественным объявлен только тот экземпляр функции templ_f сп2 (), параметром которой является тип char*. К любому экземпляру класса Ваг смогут обращаться только те экземпляры шаблона класса Foo2 и функции templ_f сп2 (), параметром которых является тип char*. Однако гораздо чаще дружественные отношения объявляются в следующей форме. template <class Т> class Foo3; template <class T> void templ_fcn3(const T&); template cclass Type> class Bar { // каждый экземпляр шаблона Bar предоставляет доступ версиям II класса Foo3 и функции templ_fcn3() с одинаковым типом / / параметра friend class Foo3<Type>; friend void templ_fcn3<Type>(const Types); }; Здесь дружественными объявлены только те экземпляры шаблона Ваг и экземп- ляры шаблонов Foo3 или templ_f спЗ (), у которых совпадает используемый аргу- мент шаблона. Каждый экземпляр класса Ваг имеет по одному связанному с ним эк- земпляру класса Foo3 и функции templ_f спЗ (). Bar<int> bi; // дружественными являются Foo3<int> // и templ_fcn3<int> Bar<string> bs; // дружественными являются Foo3<string> // и templ_fcn3<string> Дружественными экземпляру шаблона Ваг будут только те версии класса Foo3 или функции templ_fcn3 (), аргумент шаблона которых совпадает с аргументом шаблона Ваг. Таким образом, шаблонный класс Foo3<int> может обращаться к за- крытым членам класса Bare int >, но не класса Bar<string> или любого другого экземпляра шаблона Ваг. Зависимости объявлений Когда предоставляется доступ ко всем экземплярам данного шаблона, он не дол- жен содержать в области видимости объявления этого шаблона класса или функции. По существу, компилятор рассматривает указание класса или функции дружествен- ной как ее объявление.
690 Часть IV. Объектно-ориентированное и общее программирование Когда дружественные отношения необходимо ограничить определенным экземп- ляром класса или функции, их следует объявить прежде, чем они будут указаны дружественными. template <class Т> class А; template <class Т> class В { public: friend class A<T>; // ok: известно, что А является шаблоном friend class С; // ok: С - обычный, нешаблонный класс template <class S> friend class D; // ok: D является шаблоном friend class E<T>; // ошибка: E не был объявлен как шаблон friend class F<int>; // ошибка: F не был объявлен как шаблон } ; Если не сообщить компилятору заранее, что дружественный класс является шаблоном, компилятор придет к выводу, что это обычный нешаблонный класс или функция. 16.4.4. Объявление дружественных отношений между шаблонами Queue и Queue Item Класс Queue Item не предназначен для самостоятельного использования в про- грамме: все его члены являются закрытыми. Чтобы класс Queue смог использовать класс Queue Item, его следует сделать дружественным классу Queue. Как сделать дружественным шаблон класса Как уже упоминалось, при объявлении шаблона класса дружественным, его раз- работчик должен решить, насколько широко должны распространяться дружествен- ные отношения. В случае класса Queue Item необходимо принять решение, должен ли он быть дружественным для всех экземпляров шаблона Queue или только для некоторых из них. Не стоит делать класс Queue Item дружественным для любого экземпляра шаб- лона Queue. Бессмысленно предоставлять экземпляру класса Queue для типа string, доступ к члену экземпляра класса Queue Item, созданного для типа double. Экземпляр Queue<string> должен быть дружественным только для того экземпляра Queue Item, который предназначен для строк. То есть между классами Queue и Queue Item должны быть отношения по принципу “один к одному”. // объявление шаблона Queue, необходимо для объявления // дружественных отношений класса Queueitem template cclass Туре> class Queue; template cclass Type> class Queueitem { friend class Queue<Type>; }; Это объявление устанавливает необходимые отношения “один к одному”; т.е. дружественным будет только тот класс Queue, экземпляр которого создан с тем же типом, что и класс Queue Item.
Глава 16. Шаблоны и общее программирование 691 Оператор вывода класса Queue Одной из функций, которую имеет смысл добавить в интерфейс шаблона Queue, яв- ляется оператор, способный отобразить содержимое объекта класса Queue. Для этого используем перегруженный экземпляр оператора вывода. Этот оператор будет переби- рать элементы очереди и отображать значение каждого из них внутри угловых скобок. Поскольку отображать придется содержимое очередей любого типа, оператор вывода также необходимо сделать шаблоном. template <class Туре> ostream& operator<<(ostream &os, const { os << "< "; QueueItemcType> *p; for (p = q.head; p; p = p->next) os << p->item << " "; os <<">"; return os; } Queue<Type> &q) Если предназначенная для целых чисел очередь содержит значения 3, 5, 8 и 13, результат работы оператора должен выглядеть следующим образом. < 3 5 8 13 > Если очередь пуста, тело цикла for не будет выполнено. В результате будет ото- бражена пустая пара скобок. Как сделать дружественным шаблон функции Оператор вывода должен быть дружественным для классов Queue и Queue Item. Он использует переменную-член head класса Queue и переменные-члены next и item класса Queue Item. Эти классы объявляют дружественным тот экземпляр оператора вывода, тип которого совпадает с их типом. // объявление шаблона функции должно предшествовать ее // объявлению дружественной в классе Queueltem template <class Т> std::ostream& operatorcc(std::ostream&, const QueuecT>&); template <class Type> class Queueitem { friend class QueuecType>; // необходим доступ к item и next friend std::ostream& operator<< <Type> (std::ostream&, const Queue<Type>&); }; template cclass Type> class Queue { // необходим доступ к head friend std::ostream& operatorcc cType> (std::ostream&, }; const QueuecType>&); Каждое объявление функции дружественной предоставляет ей доступ к соответ- ствующему экземпляру оператора operatorcc. То есть оператор вывода, который отображает содержимое объекта класса Queuecint>, будет дружественным для класса Queuecint> (и класса Queueltemcint>). Но он не будет дружественным для класса Queue любого другого типа.
692 Часть IV. Объектно-ориентированное и общее программирование Зависимости типов и оператор вывода Для фактического отображения каждого элемента, класс Queue применяет опе- ратор operator<< к его переменной-члену item. os << p->item << " "; Применение оператора operator<< к переменной-члену с использованием син- таксиса р- >item, приводит к вызову того оператора <<, который определен для ти- па элемента item. Этот код демонстрирует зависимость между типами класса Queue и хранимыми в нем элементами. Тип каждого хранимого в очереди объекта, для которого приме- няется оператор вывода класса Queue, фактически должен иметь собственный опе- ратор вывода. Язык C++ не имеет механизма, позволяющего отменить эту зависи- мость или изменить ее так, чтобы использовался оператор, определенный непосред- ственно в классе Queue. Создать очередь для класса, не имеющего оператора вывода, вполне возможно, однако попытка его отображения приведет к ошибке во время компиляции или во время компоновки. Упражнения раздела 16.4.4 Упражнение 16.38. Напишите шаблон для класса screen, в котором для указания высоты и ши- рины экрана используются параметры значения. Упражнение 16.39. Реализуйте операторы ввода и вывода для шаблона класса screen. Упражнение 16.40. Что в классе screen следует объявить дружественным (если следует), для обеспечения работы операторов ввода и вывода? Объясните, почему? Упражнение 16.41. Объявление дружественных отношений в классе Queue для оператора operator<< выглядит следующим образом, friend std::ostream& operator<< <Type> (std::ostream&, const Queue<Type>&); Что произойдет, если параметр const Queue<Type>& заменить на const Queues? Упражнение 16.42. Напишите оператор ввода, который читает значения из объекта класса istream и помещает их в очередь. 16.4.5. Шаблоны-члены Любой класс (шаблонный или нет) может иметь члены, которые в свою очередь являются шаблоном класса или функции. Такие члены называют шаблонами-членами (member template). Шаблоны-члены не могут быть виртуальными. Одним из примеров шаблона-члена является функция-член assign () стандарт- ных контейнеров (раздел 9.3.8, стр. 356). Чтобы выяснить типы итераторов, являю- щихся ее параметрами, функция assign () использует параметры шаблона. Еще од- ним примером шаблона-члена является конструктор контейнера, получающий два итератора (раздел 9.1.1, стр. 335). Этот конструктор и функция-член assign () позво- ляют создавать контейнеры из последовательностей элементов разных, но совмести- мых типов и/или контейнеров разных типов. Теперь, реализуя собственный класс Queue, можно лучше понять конструкцию этих членов стандартных контейнеров.
Глава 16. Шаблоны и общее программирование 693 Рассмотрим конструктор копий класса Queue: он получает один параметр, кото- рый является ссылкой на тип Queue<Type>. Если бы очередь необходимо было соз- дать скопировав элементы из вектора, это было бы невозможно, поскольку нет ника- кого способа преобразования вектора в очередь. Аналогично, не получилось бы ско- пировать элементы очереди Queue<short > в очередь Queue<int>. Несмотря на то, что вполне реально преобразовать значение типа short в значение типа int, не су- ществует способа преобразования очереди Queue<short > в очередь Queue<int>. Та же логика применима к оператору присвоения класса Queue, который также по- лучает параметр типа Queue <Туре >&. Проблема заключается в том, что конструктор копий и оператор присвоения фиксируют тип как элемента, так и контейнера. Вполне логично было бы определить такой конструктор и функцию-член assign(), которые позволят изменить тип контейнера и элемента. Когда тип параметра необходимо изменить, следует опреде- лить шаблон функции. В данном случае конструктор и функцию-член assign () необходимо определить как получающие два итератора, которые обозначают диапа- зон в некоторой другой последовательности. Эти функции будут иметь один пара- метр типа шаблона, который предоставит тип итератора. В стандартном классе Queue эти члены не определены, поскольку он не обеспечивает ни присвоения, ни создания очереди из другого контейнера. Здесь это сделано исклю- чительно в демонстрационных целях. Определение шаблона-члена Объявление шаблона-члена выглядит подобно объявлению любого другого шаблона. template <class Туре> class Queue { public: // создание очереди с использованием двух итераторов // той же последовательности template <class It> Queue(It beg, It end): head(O), tail(O) { copy_elems(beg, end); } // замена текущей очереди содержимым, указанным // двумя итераторами template <class Iter> void assign(Iter, Iter); // остальная часть класса Queue как прежде private: // версия функции копирования, используемая при присвоении // элементов, диапазон которых указан итераторами template <class Iter> void copy_elems(Iter, Iter); }; Объявление шаблона-члена начинается его собственным списком параметров. Каждый из них, конструктор и функция assign (), имеют один параметр типа шаб- лона. Эти функции используют данный параметр типа для указания типа собствен- ных параметров, которые являются итераторами, обозначающими диапазон подле- жащих копированию элементов.
694 Часть IV. Объектно-ориентированное и общее программирование Определение шаблона-члена вне класса Подобно обычным членам (не шаблонам), шаблон-член может быть определен как внутри, так и вне самого класса или определения шаблона класса. Конструктор определен внутри тела класса. Его задачей является копирование элементов диапа- зона, заданного итераторами, переданными в качестве аргументов. Для фактическо- го копирования здесь применяется версия функции copy_elems () итератора. В определение шаблона-члена, расположенное вне области видимости шаблона класса, необходимо включить оба списка параметров шаблона. template cclass Т> template cclass Iter> void QueuecT>::assign(Iter beg, Iter end) { destroy(); // удалить текущие элементы // данной очереди copy_elems(beg, end); // скопировать элементы из II исходного диапазона } В определение шаблона-члена шаблона класса следует включить параметры как самого шаблона класса, так и его собственные параметры шаблона. Сначала располагается список параметров шаблона класса, а затем список параметров соб- ственно шаблона-члена. Определение шаблона функции assign () начинается следующим образом. template cclass Т> template cclass Iter> Первый список параметров шаблона, templatecclass Т>, принадлежит шаб- лону класса. Второй список параметров шаблона, templatecclass Iter>, при- надлежит шаблону-члену. Действия функции assign () очень просты: сначала происходит вызов функции de s t roy (), которая удаляет элементы очереди, находящиеся в ней на настоящий момент. Затем функция-член assign () вызывает новую вспомогательную функ- цию copy_elems (), которая копирует элементы из исходного диапазона. Эта функ- ция также является шаблоном-членом. template cclass Туре> template cclass It> void QueuecType>::copy_elems(It beg, It end) { while (beg != end) { push(*beg); ++beg; } } Версия функции copy_elems () для итераторов перебирает исходный диапазон, обозначенный парой итераторов, и вызывает для каждого из элементов функцию push (), которая фактически добавляет элемент в очередь. Поскольку функция assign () удаляет элементы текущего контейнера, переданные ей итераторы должны относиться к элементам в другом контейнере. Шаблон-член assign () стандартного контейнерами конструкторы итератора имеет те же ограничения.
Глава 16. Шаблоны и общее программирование 695 Шаблоны-члены подчиняются обычным правилам управления доступом Шаблоны-члены подчиняются тем же правилам доступа, что и любой другой член класса. Если шаблон-член объявлен закрытым, обратиться к нему смогут лишь функ- ции-члены самого класса или класса, дружественного ему. Поскольку шаблон-член функции assign () является открытым, он доступен всей программе, а шаблон-член функции copy_elems () — напротив, является закрытым, поэтому обратиться к нему могут только члены самого класса Queue и классов, дружественных ему. Шаблоны-члены и создание экземпляров Подобно любому другому члену класса, экземпляр шаблона-члена создается только тогда, когда он используется в программе. Создание экземпляра шаблона- члена в шаблоне класса немного сложнее, чем создание экземпляра обычной функ- ции-члена шаблона класса. Шаблоны-члены имеют два вида параметров шаблона: определенные для класса и определенные для самого шаблона-члена. Параметры шаблона класса зависят от типа объекта, для которого вызвана функция. Параметры шаблона, определенные для членов класса, действуют подобно параметрам обычных функций шаблона. Тип этих параметров выясняется в ходе обычной дедукции аргу- мента шаблона (раздел 16.2.1, стр. 669). Чтобы продемонстрировать действия, происходящие при создании экземпляра, давайте рассмотрим их применение для копирования и присвоения элементов из массива типа short или вектора vector<int>. short а[4] - О, 3, // создает экземпляр б, 9 }; Queue<int>::Queue(short *, short ★) 4) ; / / копирует элементы из а в qi vector<int> vi(a, а + 4); // создает экземпляр Queue<int>::assign(vector<int>::iterator, // vector<int>::iterator) qi . assign (vi . begin () , vi.endO); Поскольку создается объект типа Queue<int >, компилятор использует для этого ориентированный на итераторы конструктор класса Queue<int>. Тип собственного параметра шаблона конструктора выясняется компилятором на основании типа ар- гументов а и а + 4. Их типом является указатель на тип short. Таким образом, определение очереди qi приводит к созданию следующего экземпляра. void Queue<int>::Queue(short *, short *); В результате выполнения этого конструктора элементы типа short будут скопи- рованы из массива по имени а в очередь qi. Обращение к функции assign () создает такой экземпляр очереди qi, типом ко- торого является Queue<int>. Таким образом, это обращение создает экземпляр очереди Queue<int> по имени assign. Эта функция в свою очередь является шаб- лоном. Подобно любому другому шаблону функции, компилятор выясняет тип ар- гументов шаблона для функции assign () на основании аргументов обращения. Полученный в результате тип, vector<int>: : iterator, свидетельствует о том, что при обращении будет создан следующий экземпляр.
696 Часть IV. Объектно-ориентированное и общее программирование 16.4.6. Законченный класс Queue Рассмотрим окончательное определение класса Queue. // объявление шаблона Queue, необходимо для объявления / / дружественных отношений класса Queueitem template cclass Туре> class Queue; // объявление шаблона функции должно предшествовать ее // объявлению дружественной в Queueitem template cclass Т> std::ostream& operatorcc(std::ostream&, const QueuecT>&); template cclass Type> class Queueitem { friend class QueuecType>; // необходим доступ к item и next friend std::ostreamk operatorcc cType> (std::ostream&, const QueuecType>&); // закрытый класс: раздела public нет Queueitem(const Type &t): item(t), next(O) { } Type item; // значение, хранимое в данном элементе Queueitem *next; // указатель на следующий элемент очереди }; template cclass Туре> class Queue { // необходим доступ к head friend std::ostream& operatorcc cType> (std::ostreamS, const QueuecType>&); public: // пустая очередь Queue(): head(0), tail(O) { } // создание очереди с использованием двух итераторов // той же последовательности template cclass It> Queue(It beg, It end): head(O), tail(O) { copy_elems(beg, end); } // функции управления копированием, манипулирующие указателями // на элементы очереди Queue(const Queue &Q): head(0), tail(O) { copy_elems(Q); } Queued operator=(const Queue&); -Queue() { destroy(); } // замена текущей очереди содержимым, указанным II двумя итераторами template cclass Iter> void assign(Iter, Iter); // остальная часть класса Queue как прежде // операция без проверки: начало пустой очереди неопределено Туре& front() { return head->item; } const Type &front() const { return head->item; } void push(const Type &); // добавляет элемент в конец очереди void pop(); // удаляет элемент из начала очереди bool empty() const { // возвращает значение true, если II очередь пуста return head == 0; QueueItemcType> *head; // указатель на первый элемент очереди QueueItemcType> *tail; // указатель на последний элемент // очереди // вспомогательные функции, используемые конструктором копий, // оператором присвоения и деструктором void destroy(); // удалить все элементы void copy_elems(const Queue&); // скопировать элементы // из параметра
Глава 16. Шаблоны и общее программирование 697 private: // версия функции копирования, используемая при присвоении // элементов, диапазон которых указан итераторами template cclass lter> void copy_elems(Iter, Iter); // Инклюзивная модель компиляции: необходимо подключить II также определение функции-члена #include "Queue.сс" О членах, которые не определены непосредственно в классе, речь идет в предше- ствующих разделах этой главы. Упражнения раздела 16.4.6 Упражнение 16.43. Добавьте в созданный ранее класс List функцию assign () и конструк- тор, которые получают пару итераторов. Упражнение 16.44. Чтобы научиться создавать шаблоны класса, реализуйте собственный класс Queue. Один из способов упрощения собственной реализации заключается в использовании од- ного из контейнеров, уже существующих в библиотеке. Таким образом, можно избежать необхо- димости организовывать размещение в памяти и освобождение элементов очереди. При реализа- ции очереди используйте для фактического хранения элементов список std:: list. 16.4.7. Статические члены шаблонов класса Как и в любом другом классе, в шаблоне класса вполне возможно объявлять ста- тические члены (раздел 12.6, стр. 496),. template cclass Т> class Foo { public: static std::size_t count() // другие члены интерфейса private: static std::size_t ctr; // другие члены реализации } ; return Здесь определен шаблон класса по имени Foo, в разделе public которого объяв- лена статическая функция-член count (), а в разделе private — статическая пере- менная-член ctr. Каждый экземпляр класса Foo имеет собственный статический член. // все объекты совместно использует те же // члены Foo<int>::ctr и Foo<int>::count Foo<int> fi, fi2, fi3; // имеет статические члены Foo<string>::ctr и Foo<string>::count Foo<string> fs; Поскольку каждый экземпляр шаблона представляет отдельный тип, один ста- тический член совместно используется всеми объектами данного экземпляра. Следовательно, все объекты класса Foo<int> совместно используют одну стати- ческую переменную-член ctr, а объекты класса Foo<string> — другую пере- менную-член ctr.
698 Часть IV. Объектно-ориентированное и общее программирование Использование статических членов в шаблоне класса Обычно к статическому члену шаблона класса можно обратиться используя объ- ект класса или оператор области видимости, позволяющий получить доступ к члену класса непосредственно. Безусловно, при попытке доступа к статическому члену с использованием класса, необходимо создать его фактический экземпляр. Foo<int> fi, fi2; size_t ct = Foo<int ct = f i.count(); ct = fi2.count(); Ct = Foo::count(); :count(); // создает экземпляр класса Foocint II создает экземпляр U функции Foo<int>::count() // ok: применение Foo<int>::count() II ok: применение Foo<int>::count() // ошибка: какой из экземпляров !/ шаблона создан? Подобно любой другой функции-члену, экземпляр статической функции-члена создается только при использовании в программе. Определение статического члена Подобно любой другой переменной-члену, статическую переменную-член можно определить вне класса. В случае статической переменной-члена шаблона класса, оп- ределение должно содержать указание на то, что она принадлежит шаблону класса, template <class Т> size_t Foo<T>::ctr =0; // определить и инициализировать ctr Статическую переменную-член определяют подобно любой другой переменной- члену шаблона класса, который определен вне класса. Определение начинается с ключевого слова template, за которым следует список параметров шаблона класса и имя класса. В данном случае имени статической переменной-члена предшествует часть Foo<T>: :, которая означает, что она принадлежит шаблону класса Foo. 16.5. Общий управляющий класс Представленный в этом разделе пример затрагивает довольно сложную тему языка C++. Чтобы усвоить ее, следует хорошо разбираться и в наследовании, и в шаблонах. Если еще не вполне понятны вопросы, связанные с наследованием и шаблонами, дан- ную тему имеет смысл пока отложить. С другой стороны, этот пример позволит удосто- вериться, хорошо ли читатель понял предыдущие темы. В главе 15, “Объектно-ориентированное программирование”, были определены два управляющих класса: Sales_item (раздел 15.8, стр. 629) и Query (раздел 15.9, стр. 639). Эти классы управляли указателями на объекты в иерархии наследования. Пользователи управляющих классов не должны были заботиться об управлении указателями на эти объекты. Пользовательский код работал только с управляющим классом. Управляющий класс создавал в динамически распределяемой памяти и удалял из нее объекты классов, связанных наследственными отношениями, а всю “реальную” работу делегировал соответствующим классам в основной иерархии на- следования. Эти управляющие классы были подобными, но не одинаковыми. Они были по- добны тем, что каждый из них содержал использующие счетчик пользователей оп- ределения функций управления копированием, предназначенных для манипули-
Глава 16. Шаблоны и общее программирование 699 рования указателем на объект соответствующего класса в иерархии наследования. Однако они отличаются интерфейсом, предоставляемым пользователям иерархии наследования. Реализация счетчика пользователей в обоих классах совпадала. Решение подоб- ной проблемы весьма полезно для демонстрации идей общего программирования: вполне можно создать шаблон класса, который будет управлять указателем и мани- пулировать счетчиком пользователей. Используя этот шаблон, обеспечивающий общие действия по работе со счетчиком пользователей, можно существенно упро- стить никак не связанные классы Sales_item и Query. Управляющие классы мо- гут различаться тем, что они способны предоставлять или скрывать основную ие- рархию наследования. В этом разделе будет реализован общий управляющий класс (generic handle class), предназначенный для обеспечения функций управления счетчиком пользователей и основных объектов. Затем класс Sales_item будет переделан так, чтобы он мог ис- пользовать общий управляющий класс, а не определять свои собственные функции манипулирования счетчиком пользователей. 16.5.1. Определение управляющего класса Создаваемый класс Handle будет вести себя подобно указателю: его копирова- ние не приводит к копированию основного объекта. После копирования оба объекта класса Handle будут относиться к тому же основному объекту. Чтобы создать объ- ект класса Handle, пользователь должен будет передать его конструктору адрес объекта класса, управляемого объектом класса Handle, или класса, производного от него. С этого момента объект класса Handle будет “обладать” данным объектом. В частности, класс Handle возьмет на себя ответственность за удаление этого объекта, если с ним больше не связан ни один из объектов класса Handle. Исходя из этого проекта, реализуем общий класс Handle следующим образом. / * общий управляющий класс: обеспечивает поведение, подобное * указателю. Попытка доступа при помощи несвязанного объекта * класса Handle обнаруживается и приводит к передаче исключения * time_error. Объект, на который указывает объект класса Handle, * удаляется только тогда, когда удаляется последний объект * класса Handle. Пользователь должен создать новый объект типа ★ Т и связать его с объектом класса Handle. Пользователь * не должен удалять объект, связанный с объектом класса Handle. * / template <class Т> class Handle { public: // несвязанный управляющий класс Handle(Т *р = 0): ptr(p), use(new size_t(l)) { } // перегруженные операторы, обеспечивающие поведение, // подобное указателю Т& operator*(); Т* operator-:» () ; const Т& operator*() const; const T* operator->() const; // управление копированием: поведение, присущее указателю, // но последний Handle удаляет объект Handle(const Handle& h): ptr(h.ptr), use(h.use) { ++*use; } Handle& operator=(const Handle&);
700 Часть IV. Объектно-ориентированное и общее программирование -Handle() { rem_ref(); } private: Т* ptr; // совместно используемый объект size_t *use; // количество объектов класса Handle, // указывающих на *ptr void rem_ref() { if (—*use == 0) { delete ptr; delete use; } } }; Этот класс, подобно другим управляющим классам, реализует оператор при- своения. template cclass Т> inline Handle<T>& Handle<T>::operator=(const Handle &rhs) { ++*rhs.use; // защита от самоприсвоения rem_ref(); // декремент счётчика пользователей и удаление // указателя, если это необходимо } В классе осталось определить операторы обращения к значению и члену класса. Эти операторы будут использованы для доступа к основному объекту. Они пред- принимают необходимые меры безопасности, проверяя наличие фактической связи объекта класса Handle с основным объектом. В случае ее отсутствия, попытка дос- тупа окончится передачей исключения. Неконстантная версия этих операторов выглядит следующим образом. template cclass Т> inline Т& Handle<T>::operator*() ("dereference of unbound Handle"); } template cclass T> inline T* HandlecT>::operator->() { ("access through unbound Handle"); } Константная версия очень похожа на неконстантную и оставлена читателю в ка- честве упражнения. Упражнения раздела 16.5.1 Упражнение 16.45. Реализуйте собственную версию класса Handle. Упражнение 16.46. Объясните, что происходит при копировании объекта класса Handle. Упражнение 16.47. Какие ограничения (если есть) класс Handle налагает на типы, используе- мые для создания его экземпляра. Упражнение 16.48. Объясните, что произойдет, если пользователь свяжет объект класса Handle с локальным объектом. Объясните, что произойдет, если пользователь удалит объект, с которым связан объект класса Handle.
Глава 16. Шаблоны и общее программирование 701 16.5.2. Применение управляющего класса Предполагается, что этот класс будет использован другими классами в их внут- ренней реализации. Чтобы лучше понять, как работает класс Handle, рассмотрим сначала упрощенный пример, который демонстрирует поведение класса Handle, связанного с вновь созданным объектом типа int. // новая область видимости // пользователь должен создать, но не должен удалять объект, // с которым связан объект класса Handle Handle<int> hp(new int(42)); { // новая область видимости Handle<int> hp2 = hp; // копирует указатель; // инкремент счетчика пользователей cout « *hp << " " << *hp2 << endl; // отображает 42 42 *hp2 = 10; // изменяет значение совместно используемого // основного объекта класса int } // hp2 выходит из области видимости; // декремент счетчика пользователей cout « *hp << endl; // отображает 10 // hp выходит из области видимости; // его деструктор удаляет объект типа int Объект типа int создает пользователь класса Handle, однако удалит его дест- руктор класса Handle. Здесь объект типа int удаляется в конце внешнего блока, когда последний объект класса Handle выходит из области видимости. Для доступа к основному объекту применяется оператор * класса Handle. Этот оператор воз- вращает ссылку на основной объект типа int. Использование класса Handle со счетчиком пользователей указателя В качестве примера использования класса Handle в реализации другого класса, переделаем класс Sales_item (раздел 15.8.1, стр. 630). Интерфейс в этой версии класса останется тем же, но члены управления копированием будут устранены, а указатель Item_base заменен на класс Handledtem_base>. class Sales_item { public: // стандартный конструктор: управляющий класс не связан Sales_item(): h() { } // скопировать элемент и связать управляющий класс с копией Sales_item(const Item_base &item): h(item.clone()) { } // нет функций управления копированием: используются // синтезируемые версии // операторы доступа к членам класса: их задача возложена на // класс Handle const Item_base& operator*() const { return *h; } const Item_base* operator->() const { return h.operator->(); } private: Handle<Item_base> h; // управляющий класс co счетчиком // пользователей }; Хотя интерфейс класса не изменился, его реализация значительно отличается от первоначальной.
702 Часть IV. Объектно-ориентированное и общее программирование В обоих классах определен стандартный конструктор и конструктор, получаю- щий константную ссылку на объект класса Item_base. В обоих классах перегруженные операторы * и - > определены как констант- ные члены. Основанная на классе Handle версия класса Sales_item имеет одну перемен- ную-член. Эта переменная-член представляет собой объект класса Handle, связан- ный с копией объекта класса Item_base, переданного конструктору. Поскольку данная версия класса Sales_item не имеет никаких членов-указателей, нет ника- кой необходимости и в функциях-членах управления копированием. Эта версия класса Sales_item может безопасно использовать синтезируемые функции-члены управления копированием. Все действия по управлению счетчиком пользователей и объектом Item_base осуществляются внутри объекта класса Handle. Поскольку интерфейс остался неизменен, нет никакой необходимости изменять код, который использует класс Sales_item. Например, программа, написанная в разделе 15.8.3 (стр. 634), сможет использовать его без изменений. double Basket::total() const { double sum =0.0; // содержит общую сумму / * найти каждый набор элементов с одинаковым isbn и вычислить * окончательную цену за это количество элементов * iter указывает на первый экземпляр каждой книги в наборе * upper_bound указывает на следующий экземпляр, с другим isbn * / for (const_iter iter = items.begin(); iter != items.end(); iter = items.upper_bound(*iter)) что в корзинке есть по крайней мере один // известно, что в корзинке есть по крайней мере один // элемент с этим ключом print_total(cout, *(iter->h), items.count(*iter)); // виртуальное обращение к функции netprice () применяет // соответствующие скидки, если они есть sum += (*iter)->net price(items.count(*iter)); ветствующие скидки, если они есть (*iter)->net_price(items.count(*iter)); return sum; Подробного рассмотрения заслуживает выражение, в котором происходит вызов функции net_price (). sum += (*iter)->net_price(items.count(*iter)); Для доступа и запуска функции net_price () это выражение использует опера- тор - >. Однако давайте разберемся, как этот оператор работает. Часть (*iter) возвращает объект h управляющего класса со счетчиком пользо- вателей. Часть (*iter)->, где используется перегруженный оператор стрелки управ- ляющего класса. Компилятор обрабатывает часть h. operator - > (), которая в свою очередь воз- вращает указатель на класс Item_base, содержащий объект класса Handle. Компилятор обращается к значению указателя ltem_base и выполняет функ- цию net_price () для объекта, на которую он указывает.
Глава 16. Шаблоны и общее программирование 703 Упражнения раздела 16.5.2 Упражнение 16.49. Реализуйте версию управляющего класса Saies_item, использующего общий класс Handle для управления указателем на класс item_base. Упражнение 16.50. Переделайте функцию суммы выручки. Перечислите все изменения, которые необходимо внести, чтобы заставить код работать. Упражнение 16.51. Перепишите класс Query из раздела 15.9.4 (стр. 645) так, чтобы в нем ис- пользовался общий класс Handle. Обратите внимание, класс Handle необходимо будет сде- лать дружественным для класса Query_base, чтобы позволить ему обратиться к деструктору класса Query_base. Перечислите и объясните все остальные изменения, которые придется внести в программу для обеспечения ее работоспособности. 16.6. Специализация шаблона В остальной части этой главы рассматривается ряд дополнительных тем. При первом чтении этот раздел можно пропустить. Не всегда можно написать один шаблон, который наилучшим образом подходит для всех возможных типов аргументов шаблона, для которых может быть создан его экземпляр. В некоторых случаях общий шаблон просто не подходит для типа. Об- щая конструкция может привести к ошибке при компиляции или к неправильным действиям. С другой стороны, иногда можно воспользоваться уникальными воз- можностями определенного типа для создания более эффективной функции, чем та, которой снабжен экземпляр общего шаблона. Хорошим примером этой проблемы являются класс Queue и функция compare (): с символьными строками в стиле С они работают неправильно. Давайте вернемся к шаблону функции compare (). template <typename Т> int compare(const T &vl, const T &v2) { if (vl < v2) return -1; if (v2 < vl) return 1; return 0; } При вызове экземпляра этого шаблона для двух аргументов типа const char*, функция сравнит значения указателей. Она вернет информацию о взаимоположе- нии областей в памяти, адреса которых содержат указатели, но не о содержимом массивов, расположенных в них. Чтобы заставить функцию compare () работать с символьными строками, необ- ходимо предоставить специализированное определение, которое может сравнивать строки в стиле С. Тот факт, что версия специализирована, должен быть скрыт от пользователей шаблона. Вызов специализированной функции и применение спе- циализированного класса должны быть такими же, как и при использовании экзем- пляра версии общего шаблона.
704 Часть IV. Объектно-ориентированное и общее программирование 16.6.1. Специализация шаблона функции Специализация шаблона (template spacialization) — это отдельное определение, в котором изменен фактический тип (типы) или значение (значения) одного или не- скольких параметров шаблона. Специализация имеет следующую форму. Ключевое слово template, сопровождаемое пустой парой угловых скобок (< >). Далее следует имя шаблона и пара угловых скобок, в которых указан параметр (параметры) шаблона, определяющий специализацию. Список параметров функции. Тело функции. Следующий код определяет специализированную версию функции compare (), типом параметра шаблона которой является const char*. // специальная версия функции compare () , способная сравнивать // символьные строки в стиле С template о int comparecconst char*>(const char* const &vl, const char* const &v2) return strcmp(vl, v2); } Объявление специализированной версии шаблона должно соответствовать об- щей версии того же шаблона. В данном случае шаблон имеет один параметр типа и два параметра функции. Параметры функции являются константными ссылками на параметры типа. Здесь параметр типа исправлен на const char*, а следовательно, параметрами функции будут константные ссылки на тип const char*. Теперь при вызове функции compare () с передачей двух символьных указате- лей, компилятор использует ее специализированную версию. Для аргументов любых других типов, включая обычный char*, он использует общую версию. const char *cpl = "world", *ср2 = "hi"; int il, i2; compare(cpl, cp2); // вызов специализированной версии compare(il, i2); // вызов общей версии для типа int Объявление специализированного шаблона Подобно любой другой функции, специализированную версию шаблона функции можно сначала объявить, а потом определить. Объявление специализированного шаблона выглядит подобно определению, но без тела функции. // явное объявление специализированного шаблона функции. templateo int compare<const char*>(const char* const&, const char* const&); Это объявление состоит из пустого списка параметров шаблона (templateo), сопровождаемого типом возвращаемого значения, именем функции и (необязатель- но) явным аргументом (аргументами) шаблона, указанным внутри пары угловых скобок,-а также списком параметров функции. Специальная версия шаблона всегда должна содержать пустой спецификатор параметра шаблона, templateo и список
Глава 16. Шаблоны и общее программирование 705 параметров функции. Если аргументы шаблона могут быть получены из списка па- раметров функции, нет никакой необходимости явно указывать аргументы шаблона. // ошибка: неправильное объявление специализированной версии / / пропущен t empl ateo int compare<ct>nst char*>(const char* const&, const char* const&); // ошибка: пропущен список параметров функции templateo int compare<const char*>; // ok: аргумент шаблона const char* получен из типа параметра templateo int compare(const char* const&, const char* const&); Перегрузка функций или специализация шаблона Пропуск пустого списка параметров шаблона, templateo, при специализации может создать удивительный эффект. Когда синтаксис специализации отсутствует, получается объявление перегруженной нешаблонной версии функции. // определение общего шаблона template <class Т> int compare(const T& tl, const T& t2) { /* ... */ } // ok: объявление обычной функции int compare(const char* const&, const char* const&); Здесь определен вовсе не специализированный шаблон функции compare (), а объявлена обычная функция, обладающая типом возвращаемого значения и списком параметров, который может соответствовать списку экземпляра шаблона. Более подробно взаимодействие перегруженной версии и шаблона будет рас- смотрено в следующем разделе, а пока лишь заметим, что при определении обычной нешаблонной функции к аргументам применимы стандартные преобразования. К ти- пам аргумента специализированного шаблона преобразования не применяются. При обращении к специализированной версии шаблона, типы переданных при вызове ар- гументов должны точно соответствовать типам параметров специализированной версии функции. Если они не совпадают, компилятор использует определение шаб- лона и создаст его экземпляр именно для данных аргументов. Двойные определения не всегда могут быть обнаружены Если программа состоит из более чем одного файла, объявление специализиро- ванной версии шаблона должно быть видимым в каждом файле, в котором он ис- пользуется. Экземпляр шаблона функции не может быть создан при помощи общего определения в одних файлах, и при помощи специализированного определения для того же набора аргументов шаблона в других. ХЖ Подобно объявлениям других функций, объявления специализированных шаблонов , следует включить в файл заголовка. Этот файл заголовка следует впоследствии под- ^комен^уем ключить в каждый файл исходного кода, в котором используется специализирован- ная версия.
706 Часть IV. Объектно-ориентированное и общее программирование К специализированным версиям применимы обычные правила области видимости Прежде чем объявлять или определять специализированные версии, в области видимости следует объявить общий шаблон, который подлежит специализации. Аналогично, объявление специализированной версии в области видимости должно предшествовать применению этой версии шаблона. // определение общей версии шаблона compare() template <class Т> int compare(const T& tl, const T& t2) { /* ... */ } int main() { // используется определение общего шаблона int i = compare("hello", "world"); // неверный подход: явная специализация после обращения templateo int comparecconst char*>(const char* const& si, const char* const& s2) Эта программа содержит ошибку, поскольку обращение, которому соответство- вал бы специализированный шаблон, сделано прежде, чем специализированная вер- сия была объявлена. Когда компилятор встречает это обращение, он еще “не знает”, что дальше следует определение специализированной версии. В результате компи- лятор создаст экземпляр функции, используя определение общего шаблона. Г Программа не может содержать явно специализированную версию и экземпляр того же ] шаблона с тем же набором аргументов шаблона. Ошибкой является также создание специализированной версии шаблона уже по- сле обращения к его экземпляру. Упражнения раздела 16.6.1 Упражнение 16.52. Определите шаблон функции count (), подсчитывающей, сколько раз в век- торе встречается некоторое значение. Упражнение 16.53. Напишите программу, использующую созданную в предыдущем упражнении функцию count (), сначала для вектора, содержащего значения типа double, затем типа int и наконец типа char. Упражнение 16.54. Создайте специализированную версию шаблона функции count () для об- работки строк. Переделайте предыдущую программу так, чтобы был использован специализиро- ванный экземпляр шаблона функции. 16.6.2. Специализация шаблона класса Подобный функции compare (), рассматриваемый класс Queue “имеет пробле- мы” с использованием строк в стиле С. В данном случае причина проблемы кроется в функции push (). Эта функция копирует переданное значение, создавая новый
Глава 16. Шаблоны и общее программирование 707 элемент очереди. По умолчанию, копирование символьной строки в стиле С под- разумевает копирование только указателя, а не самих символов. В данном случае копирование указателя связано со всеми проблемами, которые возникают при со- вместном использовании указателя, а также со множеством других. Наиболее серьезная из них заключается в том, что, получив указатель на область в динами- ческой памяти, пользователь вполне сможет удалить сам массив, на который ука- зывает указатель. Определение специализированной версии шаблона класса Один из способов обеспечения правильной работы класса Queue со строками в стиле С, подразумевает создание специализированной версии класса для аргумента типа const char*. / * определение специализированной версии для const char*. * Этот класс передает всю работу классу Queue<string>; * функция push() преобразует параметр типа const char* в строку, * а функция front() возвращают строку, а не const char* ★ / templateo class Queue<const char*> { public: // нет функций управления копированием: используются // синтезируемые версии // аналогично, не нужно явно определять стандартный конструктор void push(const char*); void pop() {real_queue.pop();} bool empty() const {return real_queue.empty();} // Примечание: тип возвращаемого значения не соответствует II типу параметра шаблона std::string front() {return real_queue.front();} const std::string &front() const {return real_queue.front();} private: Queue<std::string> real_queue; // передает вызов II функции real_queue() }; Эта реализация класса Queue имеет один элемент данных: очередь строк. Его функции-члены делегируют свою работу другим функциям, например, функция pop () вызывает одноименную функцию pop () объекта real_queue. Здесь нет определения функций-членов управления копированием. Единствен- ный член данных имеет тип класса, который реально осуществляет копирование, присвоение и удаление, используя синтезируемые функции-члены управления ко- пированием. Данный класс Queue реализует похожий, но не абсолютно тот же интерфейс, что и шаблон Queue. Различие заключается в том, что функция-член front () возвра- щает тип string, а не char*. Это позволяет избежать необходимости управления символьным массивом, что было бы неизбежно в случае возвращения указателя. Следует заметить, что при специализации можно определять совершенно другие члены, отсутствующие в общем шаблоне. Если при специализации не удастся опре- делить член шаблона, его нельзя будет использовать в объектах специализированно- го типа. Для создания определения членов при явной специализации, определения члена шаблона класса не используются.
708 Часть IV. Объектно-ориентированное и общее программирование Специализированная версия шаблона класса должна обладать тем же интерфейсом, что и специализируемый шаблон. В противном случае, при попытке использовать член класса, который не был определен, пользователь будет весьма удивлен. Определение специализированного класса Когда член класса определяют вне специализированной версии класса, ему не должен предшествовать маркер templateo. Вне этого класса определен только один член. void Queue<const char*>::push(const char* val) return real_queue.push(val); } Хотя на первый взгляд может показаться, что здесь немного действий, эта функ- ция неявно копирует символьный массив, на который указывает параметр val. Ко- пирование происходит при обращении к функции real_queue .push (), которая создает новую строку из аргумента типа const char*. Этот аргумент использует конструктор класса string, который получает тип const char*. Конструктор класса string копирует символы из массива, указанного параметром val, в неиме- нованную строку, которая будет сохранена в элементе, помещаемом в объект real_queue. Упражнения раздела 16.6.2 Упражнение 16.55. Обратите внимание на комментарии в специализированной версии класса Queue для типа const char*. Здесь нет никакой необходимости в определении ни функций- членов управления копированием, ни стандартного конструктора. Объясните, почему для этой вер- сии класса Queue достаточно синтезируемых функций-членов. Упражнение 16.56. Поведение общего шаблона Queue, не специализированного для типа const char*, уже было рассмотрено. Объясните, что произойдет в следующем коде при ис- пользовании общего шаблона Queue. Queue<const char*> ql; ql.push("hi"); ql.push("bye"); ql.push("world"); Queuecconst char*> q2(ql); // q2 копия ql Queue<const char*> q3; // пустая очередь ql = q3; В частности, объясните, каковы значения объектов ql и q2 после инициализации объекта q2 и после присвоения объекта q3. Упражнение 16.57. Функция front () специализированного шаблона Queue возвращает тип string, а не const char*. Для чего это сделано? Как можно было бы реализовать шаблон Queue, чтобы она возвращала тип const char*? Укажите все преимущества и недостатки ка- ждого подхода.
Глава 16. Шаблоны и общее программирование 709 16.6.3. Специализация членов, но не класса Внимательно рассмотрев код класса, легко придти к выводу, что его вполне возможно упростить. Вместо специализации всего шаблона, достаточно специализировать только функции-члены push () и pop (). Функция push () должна скопировать символьный массив, а функция pop () — освободить память, использованную для этой копии. templateo void Queuecconst char*>::push(const char *const &val) { // создать новый символьный массив и скопировать символы // из val char* new_item = new char[strlen(val) + 1]; strncpy(new_item, val, strlen(val) + 1); // сохранить указатель на вновь созданный и // инициализированный элемент Queueltemcconst char*> *pt = new Queueltem<const char*>(new_item); // помещает элемент в существующую очередь if (empty()) head = tail = pt; // сейчас очередь имеет только I/ один элемент else { tail->next = pt; // добавить новый элемент в конец // очереди tail = pt; } } templateo void Queue<const char*>::pop() // хранение указателя на головной элемент очереди позволит // удалить его Queueltemcconst char*> *р = head; delete headoitem; // удалить массив, созданный // функцией push () head = head->next; // теперь head указывает на / / следующий элемент delete р; // удалить прежний головной элемент } Теперь экземпляр класса Queuecconst char*> будет получен из общего опре- деления шаблона класса, но не его функции push () и pop (). При вызове функций push () и pop () класса Queuecconst char*>, произойдет вызов их специализи- рованной версии, а при использовании любой другой функции-члена — их общей версии из шаблона класса для типа const char*. Объявление специализации Специализированную версию функции-члена класса объявляют аналогично лю- бой другой специализированной версии. Объявление должно начинаться пустым списком параметров шаблона. // функции push() и pop() специализированы для const char* template о void Queuecconst char*>::push(const char* const &); template <> void Queuecconst char*>::pop(); Эти объявления следует поместить в файл заголовка Queue.
710 Часть IV. Объектно-ориентированное и общее программирование Упражнения раздела 16.6.3 Упражнение 16.58. В предыдущем разделе была продемонстрирована специализация всего шаб- лона Queue, а в этом разделе — специализация лишь его функций-членов push () и pop () для типа const char*. Примените эти два способа для специализации шаблона Queue под обыч- ный ТИП char*. Упражнение 16.59. Если использована специализация только функции push (), какое значение возвратит функция front () класса Queue для символьных строк в стиле С? Упражнение 16.60. Укажите все преимущества и недостатки двух подходов реализации специа- лизированных версий шаблона для типа const char*: всего класса или только функций push () и pop (). В частности, учтите различия в поведении функции front (), а также воз- можность ошибок в пользовательском коде, способных повредить элементы очереди. 16.6.4. Частичная специализация шаблона класса Если шаблон класса имеет больше одного параметра, может возникнуть необхо- димость в специализации лишь некоторых, а не всех параметров шаблона. Для этого применяется частичная специализация (partial specialization) шаблона класса. template cclass Т1, class Т2> class some_template { } ; // частичная специализация: Т2 фиксирован как int, // а Т1 может изменяться template cclass Tl> class some_templatecTl, int> { } ; Частичная специализация шаблона класса объявляется как самостоятельный шаблон. Определение частичной специализации выглядит как определение шабло- на. Оно начинается с ключевого слова template, за которым следует список пара- метров шаблона, заключенных в угловые скобки (<>). Список параметров частичной специализации является подмножеством списка параметров определения соответст- вующего шаблона класса. Частичная специализация шаблона some_template име- ет только один параметр типа по имени Т1. Типом второго аргумента шаблона для параметра Т2 объявлен тип int. В списке параметров шаблона при частичной спе- циализации перечисляют только те параметры, для которых аргументы шаблона по- ка неизвестны. Применение частичной специализации шаблона класса Частично специализированная версия имеет то же имя, что и первоначальный шаблон класса, а именно some_template. Имя шаблона класса сопровождается списком аргументов шаблона. В предыдущем примере список аргументов шаблона был представлен как <Т1, int>. Поскольку значение аргумента для первого пара- метра шаблона неизвестно, список аргументов использует имя параметра шабло- на Т1 как знакоместо. Второй аргумент, для которого шаблон частично специализи- рован, имеет тип int.
Глава 16. Шаблоны и общее программирование 711 Подобно всем шаблонам класса, экземпляр его частично специализированной версии неявно создается при использовании в программе. some_template<int, string> foo; // использование шаблона some_template<string, int> bar; // использование его частично // специализированной версии Обратите внимание на изменение типа второго параметра при создании экземп- ляра шаблона some_template. В зависимости от его типа, string или int, экзем- пляр будет создан на основании определения общего шаблона класса или его час- тично специализированной версии. Почему во втором случае для создания экземп- ляра шаблона была выбрана его частично специализированная версия? Когда объявлена частично специализированная версия шаблона, при создании его экземп- ляра компилятор выбирает то из определений шаблона, которое наиболее точно со- ответствует типам аргументов. Если ни одна из частично специализированных вер- сий не подходит, применяется общее определение шаблона. Типы, переданные при создании экземпляра foo, не соответствуют частично специализированной версии шаблона. Таким образом, класс foo будет создан на основании общего шаблона класса, где тип int будет передан параметру Т1, а тип string — параметру Т2. Частично специализированная версия будет использована только при создании того экземпляра шаблона some_template, типом второго параметра которого яв- ляется тип int. Определение частично специализированной версии полностью отделено от опре- деления общего шаблона. Частично специализированная версия шаблона класса может иметь совершенно иной набор членов, чем общий шаблон класса. 16.7. Перегрузка и шаблоны функций Шаблон функции может быть перегружен, т.е. вполне возможно создать несколь- ко одноименных шаблонов функций, которые отличаются количеством или типом параметров. Можно также определить обычные, нешаблонные, функции, имя кото- рых совпадает с именем функции шаблона. Безусловно, объявление набора перегруженных шаблонов функции не гаранти- рует, что они правильно сработают при вызове. Перегруженные версии шаблона функции могут вести себя неоднозначно. Поиск функции и шаблоны функций Когда среди версий перегруженной функции есть и обычные функции, и шабло- ны функций, поиск используемой версии, при обращении к ней, осуществляется в следующей последовательности. 1. Создание набора функций-кандидатов для указанного имени функции. В него будут включены. • Все обычные функции, имена которых совпадают с именем, указанным при вызове. • Все экземпляры шаблона функции, для которых в результате дедукции будут найдены типы аргументов шаблона, соответствующие аргументам функции, использованным при обращении.
712 Часть IV. Объектно-ориентированное и общее программирование 2. Выявление подходящих обычных функций (раздел 7.8.2, стр. 295), если они есть. Любой из экземпляров шаблона функции является подходящим, поскольку де- дукция аргумента шаблона гарантирует возможность его вызова. 3. Ранжирование подходящих функций по виду преобразования (если они есть), необходимого для осуществления вызова. Напомним, что набор преобразований, допустимых при вызове экземпляра шаблона функции, весьма ограничен. • Если выбрана только одна функция, происходит ее вызов. • Если выбор неоднозначен, все экземпляры шаблона удаляются из набора под- ходящих функций. 4. Повторное ранжирование подходящих функций. Экземпляры шаблона функции из списка исключены. • Если выбрана только одна функция, происходит ее вызов. • В противном случае обращение признается неоднозначным. Пример поиска соответствующего шаблона функции Рассмотрим следующий набор перегруженных и обычных шаблонов функции. // сравнивает два объекта template <typename Т> int compare(const T&, const T&); // сравнивает элементы в двух последовательностях template <class U, class V> int compare(U, U, V) ; // обычная функция, обрабатывающая символьные строки в стиле С int compare(const char*, const char*); Набор перегруженных версий содержит три функции: первая является шабло- ном, обрабатывающим отдельные значения, вторая — шаблоном, сравнивающим элементы из двух последовательностей, а третья является обычной функцией, обра- батывающей символьные строки в стиле С. Поиск версии среди перегруженных шаблонов функции Эти функции можно вызвать несколькими способами. // вызов compare(const Т&, const Т&) где Т получает int compare(1, 0) ; // вызов compare(U, U, V), где U и V получает vector<int> vector<int> ivecl(10), ivec2(20); compare(ivecl.begin(), ivecl.end(), ivec2.begin()); int ial[] = {0,1,2,3,4,5,6,7,8,9}; // вызов compare(U, U, V) где U получает int*, // a V получает vector<int>::iterator compare(ial, ial + 10, ivecl.begin()); // вызов обычной функции, получающей параметр const char* const char const_arrl[] = "world", const_arr2[] = "hi"; compare(const_arrl, const_arr2); // вызов обычной функции, получающей параметр const char* char ch_arrl[] = "world", ch_arr2[] = "hi"; compare(ch_arr1, ch_arr2); iterator Рассмотрим каждый из вызовов. compare (1, 0) — оба аргумента имеют тип int. Кандидатами являются: пер- вый шаблон, у которого параметру Т передан аргумент типа int, и обычная функция compare (). Однако обычная функция не подходит, поскольку невозможно пере-
Глава 16. Шаблоны и общее программирование 713 дать значение типа int параметру, ожидающему тип char*. Будет выбран экземп- ляр функции, получающий аргумент типа int, поскольку он точно соответствует обращению. compare(ivec1.begin(), ivecl.end(), ivec2.begin()) compare(ial, ial + 10, ivecl .begin () ) — единственной подходящей функцией для этих обращений является экземпляр шаблона, который имеет три па- раметра. Ни шаблон с двумя параметрами, ни обычная функция не могут соответст- вовать этим обращениям. compare (const_arrl, const_arr2) — этому обращению, как и ожидалось, соответствует обычная функция. Подходящими являются и эта функция и первый шаблон, параметру Т которого передан тип const char*. Оба они обеспечивают точное соответствие. Однако, согласно второму пункту правила 3, обычная функ- ция имеет приоритет. Экземпляр шаблона исключается, и остается только обыч- ная функция. compare (ch_arr 1, ch_arr2) — это обращение также связано с обычной функ- цией. Кандидатами являются экземпляр шаблона функции, где параметру Т передан тип char*, и обычная функция, получающая аргумент типа const char*. Обе функции требуют обычного преобразования массивов ch_arrl и ch_arr2 в указа- тели. Поскольку обе функции равнозначны, предпочтение отдается обычной функ- ции, а не экземпляру шаблона. Преобразования и перегруженные шаблоны функций Зачастую бывает достаточно трудно выявить набор перегруженных функций, од- ни из которых являются экземплярами шаблона, а другие — обычными функциями. Для этого необходимо глубокое понимание взаимосвязи между типами и особенно- стей неявных преобразований, которые могут или не могут быть осуществлены для экземпляров шаблонов. Рассмотрим два примера, демонстрирующих, почему так сложно разработать пе- регруженные функции, которые работают правильно даже тогда, когда в наборе пе- регруженных функций имеются как шаблонные, так и нешаблонные версии. Снача- ла рассмотрим обращение к функции compare (), где вместо самих массивов ис- пользуются указатели на них. char *pl = ch_arrl, *р2 = ch_arr2; compare(pl, р2); Этому обращению соответствует шаблонная версия! Логично было бы ожидать, что при передаче массива и указателя на элемент этого массива будет использована та же функция. Но в данном случае экземпляр шаблона функции, где параметру т передан тип char*, точнее соответствует обращению. Обычная функция требует пре- образования типа char* в тип const char*, поэтому экземпляр шаблона функции предпочтительней. Другой вариант также дает удивительные результаты. Что произойдет, если шаб- лонная версия функции compare () имеет параметр типа Т, а не константной ссыл- ки на тип Т? template <typename Т> int compare2(T, Т);
714 Часть IV. Объектно-ориентированное и общее программирование В данном случае передача массива обычных переменных типа char непосредст- венно или как указателя, приводит к вызову шаблонной версии. Вызов нешаблонной версии происходит только тогда, когда элементы массива имеют тип const char или const char* (т.е. указатель на const char). // вызов compare(Т, Т) где Т получает char* compare(ch_arr1, ch_arr2); // вызов compare(T, T) где Т получает char* compare(pl, р2) ; // вызов обычной функции, получающей параметр const char* compare(const_arrl, const_arr2); const char *cpl = const_arrl, *cp2 = const_arr2; // вызов обычной функции, получающей параметр const char* compare(cpl, cp2); В этих случаях обычная функция и шаблон функции обеспечивают точное соот- ветствие. Когда точное соответствие обеспечивают обе функции, преимущество обычно имеет неперегруженная версия. /ф" Довольно трудно создать набор перегруженных функций, когда в него входят шаб- ~ лонные и нешаблонные версии. Из-за высокой вероятности непредвиденного пове- /^омендуем Дения функций в пользовательском коде, практически всегда имеет смысл создавать ~ * специализированный шаблон функции (раздел 16.6, стр. 703), а не использовать нешаблонную версию. Упражнения раздела 16.7 Упражнение 16.61. Реализуйте три версии функции compare О. Включите в каждую из них оператор print, который укажет конкретную функцию при вызове. Используйте эти функции для проверки своих ответов на следующие вопросы. Упражнение 16.62. С учетом описанной в этом разделе функции compare о и переменных объясните, какая из функций будет использована при каждом из следующих обращений и почему, compare(ch_arrl, const_arr1); compare(ch_arr2, const_arr2); compare(0, 0) ; Упражнение 16.63. Перечислите функции-кандидаты и подходящие функции для каждого из сле- дующих обращений. Укажите, является ли обращение допустимым, и если да, то какие из версий функций будут применены, template <class Т> Т calc(T, Т) ; double calc(double, double); template <> char calc<char>(char, char); int ival; double dval; float fd; calc(0, ival); calc(0.25, dval); calc(0, fd); calc(0, 'J'); Резюме Шаблоны — это отличительная особенность языка C++ и основа его стандартной библио- теки. Шаблон представляет собой независимый от типа “чертеж”, используемый компилято- ром для создания конкретных экземпляров для указанных типов. Шаблон разрабатывается один раз, а его экземпляры компилятор создает для соответствующего типа или типов по мере его применения. Можно создать шаблон функции и шаблон класса.
Глава 16. Шаблоны и общее программирование 715 Шаблоны функций являются основой библиотеки алгоритмов, а шаблоны классов — ос- новой библиотеки контейнеров и итераторов встроенных типов. Компиляция шаблонов требует поддержки среды программирования. Язык C++ предос- тавляет два способа создания экземпляров шаблонов: инклюзивная модель и раздельная мо- дель компиляции. Используемая модель влияет на способ создания системы, а также указы- вает, следует ли размещать определения шаблона в файлах заголовка или файлах исходного кода. В настоящее время инклюзивную модель компиляции реализуют все компиляторы, а раздельную модель — лишь некоторые из них. Модель, используемая конкретным компиля- тором, должна быть указана в его документации. Явное указание аргумента шаблона позволяет фиксировать тип или значение одного или нескольких параметров шаблона. Явные аргументы позволяют разрабатывать функции, в ко- торых тип параметра шаблона не выясняется на основании соответствующего аргумента, а следовательно, обеспечивает возможность преобразования типа аргумента. Специализация шаблона — это отдельное специальное определение, позволяющее создать такую версию шаблона, в которой для одного или нескольких параметров указан определен- ный тип или значение. Специализация полезна в случае, когда для некоторых типов стан- дартное определение шаблона неприменимо. Термины Аргумент шаблона (template argument). Тип или значение, указанное при создании эк- земпляра шаблона. Например, при создании объекта или именовании типа при приведении. Дедукция аргумента шаблона (template argument deduction). Процесс, в ходе которого компилятор выясняет, какой экземпляр шаблона функции следует создать. Для этого компи- лятор исследует типы аргументов, переданных в качестве параметров шаблона. На основании полученных типов или значений объектов, связанных с параметрами шаблона, компилятор автоматически создает соответствующую версию функции. Инклюзивная модель, компиляции (inclusion compilation model). Механизм, используе- мый компиляторами для поиска определения шаблона, который полагается на определения шаблона, подключаемые (included) в каждый использующий шаблон файл. Как правило, оп- ределения шаблона сохраняют в заголовке, который и следует подключить в каждый файл, использующий шаблон. Ключевое слово export. Ключевое слово, “вынуждающее” компилятор запоминать рас- положение данного определения шаблона. Используется для компиляторов, поддерживаю- щих раздельную модель компиляции при создании экземпляров шаблона. Обычно ключевое слово export используется в определении функции; класс объявляют экспортируемым, как правило, в файле его реализации. При объявлении шаблона экспортируемым, ключевое слово export применяют в программе только один раз. Общий управляющий класс (generic handle class). Класс, который содержит и манипули- рует указателем на другой класс. Общий управляющий класс получает один параметр типа, а затем создает и манипулирует указателем на объект этого типа. В управляющем классе опре- делены необходимые функции-члены управления копированием. Здесь также определены операторы обращения к значению (*) и стрелка (- >), позволяющие обращается к членам объ- екта основного класса. Общему управляющему классу не нужна информация о типе, которым он управляет. Параметр значения (nontype parameter). Параметр шаблона, представляющий значение. При создании экземпляра шаблона функции, каждый параметр значения связывается с кон- стантным выражением, переданным в качестве аргумента при обращении. Во время создания экземпляра шаблона класса, каждый параметр значения связывается с константным выраже- нием, переданным в качестве аргумента при создании экземпляра класса.
716 Часть IV. Объектно-ориентированное и общее программирование Параметр типа (type parameter). Имя, используемое в списке параметров шаблона вместо имени типа. При создании экземпляра шаблона, каждый параметр типа замещается фактиче- ским типом. Во время создания экземпляра шаблона функции, конкретный тип выясняется на основании типа аргумента или указывается в обращении явно. При использовании, шаб- лону класса следует передать аргументы, соответствующие его параметрам типа. Раздельная модель компиляции (separate compilation model). Механизм, используемый компиляторами для поиска определения шаблона, который допускает хранение определения и объявления шаблона в независимых файлах. Объявления шаблона помещают обычно в файл заголовка, а определение, содержащееся в программе только один раз, — в файл исход- ного кода. Создание экземпляра (instantiation). Процесс, в ходе которого компилятор, используя фактические аргументы шаблона, создает его экземпляр, в котором параметры заменены со- ответствующими аргументами. Экземпляр функции создается автоматически на основании аргументов, использованных при обращении. При создании экземпляра шаблона класса, ар- гументы шаблона должны быть предоставлены явно. Специализация шаблона (template specialization). Переопределение всего шаблона класса или его члена, в котором определены параметры шаблона. Специализация шаблона не может быть осуществлена до завершения определения шаблона класса, подвергающегося специали- зации. Специализация шаблона должна быть осуществлена прежде, чем он будет использован для специализированных аргументов. Список параметров шаблона (template parameter list). Список параметров типа или зна- чения (разделяемый запятыми), используемый в определении или объявлении шаблона. Частичная специализация (partial specialization). Версия шаблона класса, в которой опре- делены некоторые, но не все, параметры шаблона. Шаблон класса (class template). Определение, которое может быть использовано при соз- дании набора классов для конкретных типов. При определении шаблона класса используется ключевое слово template, за которым следует разделяемый запятыми список параметров, заключенный в угловые скобки (<>). Шаблон функции (function template). Определение, которое может быть использовано при создании набора функций для конкретных типов. При определении шаблона функции используется ключевое слово template, за которым следует разделяемый запятыми список параметров, заключенный в угловые скобки (<>). Шаблон-член (member template). Член класса или шаблона класса, который является шаблоном функции. Шаблон-член не может быть виртуальным.
Дополнительные темы В ЭТОЙ ЧАСТИ... Глава 17. Инструменты для крупномасштабных программ Глава 18. Специализированные инструменты и технологии Часть V, “Дополнительные темы”, посвящена дополнительным возможностям, которые весьма полезны в некоторых случаях, но необходимы не в каждой програм- ме на языке C++. Эти возможности делятся на две группы: те, которые используют- ся для решения крупномасштабных проблем, и те, которые применяют скорее для специфических целей, а не общих. В главе 17, “Инструменты для крупномасштабных программ”, рассматривается обработка исключений, пространства имен и множественное наследование. Эти воз- можности наиболее полезны в контексте крупномасштабных проектов. Даже когда программа достаточно проста и написана одним автором, она может извлечь немалую пользу из обработки исключений, основы которой описаны в главе 6, “Операторы”. Однако, необходимость справляться с непредвиденными ошибками во время выполнения программы не менее важна, чем решение проблем в больших группах разработчиков. В Главе 17, “Инструменты для крупномасштабных про- грамм” представлен обзор некоторых дополнительных средств обработки исключе- ний. Здесь также более подробно рассматриваются способы обработки исключений, их смысл при размещении ресурсов в памяти и их удалении. Кроме того, в этой главе описаны способы создания и применения собственных классов исключений. В крупномасштабных приложениях зачастую используют код от нескольких не- зависимых производителей. Комбинирование нескольких библиотек от независи- мых разработчиков было бы необычайно трудной, или вообще неразрешимой зада- чей, если бы все использованные в них имена располагались одном пространстве имен. В библиотеках от независимых разработчиков почти неизбежно использова- лись бы совпадающие имена. В результате, имя определенное в одной библиотеке вступило бы в конфликт с таким же именем из другой библиотеки. Чтобы избежать конфликтов имен, их следует определять внутри пространств имен (namespace). Пространства имен в этой книге используются почти с самого начала. Каждый раз, когда использовалось имя из стандартной библиотеки, происходило обращение к пространству имен std. В главе 17, “Инструменты для крупномасштабных про- грамм”, продемонстрировано, как можно определять собственные пространства имен.
718 Часть V. Дополнительные темы Глава 17 завершается очень важной, но нечасто используемой возможностью языка: множественным наследованием. Множественное наследование наиболее по- лезно в сложных иерархиях наследования. Глава 18, “Специализированные инструменты и технологии”, посвящена ряду специализированных методов и инструментальных средств. Эти методы и инстру- ментальные средства предназначены для решения ряда специфических проблем. Первая часть главы 18 демонстрирует, как осуществлять в классах собственное оп- тимизированное управление памятью. Далее рассматривается поддержка в языке C++ идентификации типов времени выполнения (RTTI — run-time type identification)1. Эти средства позволяют выяснить фактический тип объекта во время выполнения при- ложения. Далее будет продемонстрировано определение и применение указателей на чле- ны классов. Указатели на члены классов отличаются от указателей на обычные дан- ные или функции. Обычные указатели различаются только на основании типа объ- екта или функции. Указатели на члены класса должны также отражать класс, кото- рому принадлежит член. Затем рассматриваются три дополнительных составных типа: объединения, вло- женные классы и локальные классы. Глава завершается кратким обзором средств, применение которых делает код не- переносимым. Сюда относится спецификатор volatile, битовые поля и директивы компоновки. 1 Или информация о типах времени выполнения (RTTI — Runtime Type Information).— Примеч. ред.
ГЛАВА 17 Инструменты для КРУПНОМАСШТАБНЫХ ПРОГРАММ В ЭТОЙ ГЛАВЕ... 17.1. Обработка исключений 17.2. Пространства имен 17.3. Множественное и виртуальное наследование Резюме Термины 720 744 763 779 780 Язык C++ используется для решения проблем любой сложности — как незначи- тельных, которые способен решить один программист за несколько часов вечером после основной работы, так и чудовищно сложных, требующих десятков миллионов строк кода и модифицируемых впоследствии на протяжении многих лет. Средства, описанные в предыдущих разделах этой книги, полезны для решения весьма широ- кого диапазона вопросов программирования. Язык C++ обладает рядом возможностей, которые наиболее полезны при работе над системами, сложность которых превосходит возможности одного разработчика. К этим возможностям относится обработка исключений, пространства имен и мно- жественное наследование. Эти темы и рассматриваются в данной главе. Крупномасштабное программирование предъявляет к языку более высокие тре- бования чем те, выполнения которых вполне достаточно в процессе работы неболь- ших групп разработчиков. К этим требованиям относятся следующие. 1. Более строгие требования относительно периода работы и необходимость более надежного обнаружения и обработки ошибок. Система обработки ошибок зачас- тую является независимой системой. 2. Способность структурирования программ, которые состоят из библиотек, разра- ботанных более или менее независимо. 3. Возможность решения проблем наиболее сложных приложений. Для реализации этих требований язык C++ предоставляет три средства: обработ- ка исключений, пространства имен и множественное наследование. Все эти средства рассматриваются в данной главе.
720 Часть V. Дополнительные темы 17.1. Обработка исключений Обработка исключений (exception) позволяет независимо разработанным частям программы взаимодействовать и решать проблемы, возникающие во время выпол- нения программы. В одной из частей программы может возникнуть проблема, кото- рую она не способна решить. Часть программы, ответственная за обнаружение про- блемы, может передать информацию о возникшей ситуации другой части програм- мы, которая специально предназначена для решения подобных проблем. Исключения позволяют отделить код обнаружения проблем от кода их решения. В резуль- Aj/k } тате та часть программы, в которой может возникнуть проблема, не будет решать ее са- мостоятельно. В языке C++ обработка исключений основана на разделении кода на часть, обна- руживающую проблему и передающую (throwing) объект исключения другой части, обработчику (handler), который собственно и решает проблему. Тип и содержимое объекта исключения позволяют передать второй части кода программы информа- цию о возникшей проблеме. Основы концепции применения исключений в языке C++ представлены в разде- ле 6.13 (стр. 241). В нем же рассматривается более сложное гипотетическое прило- жение книжного магазина, которое сможет использовать исключения для сообще- ния о проблемах. Например, оператор суммы класса Sales_item будет передавать исключение, если значение переменной-члена isbn его операндов не совпадает. // передает исключение, если isbn объектов не совпадают Sales_item operator+(const Sales_item& Ihs, const Sales_item& rhs) { if (!Ihs.same_isbn(rhs)) throw runtime error("Data must refer to same ISBN"); // отлично, значит ISBN совпадают и можно осуществлять // суммирование Sales_item ret(Ihs); // скопировать Ihs в локальный объект, II который и будет возвращен ret += rhs; // добавить к содержимому rhs return ret; // вернуть ret по значению } Эта часть программы осуществляет сложение объектов класса Sales_item. Для обработки исключения (если оно будет передано) здесь использован блок try. // та часть приложения, которая взаимодействует с пользователем Sales_item iteml, item2, sum; while (cin » iteml >> item2) { // прочитать две транзакции try { sum = iteml + item2; // вычислить их сумму endl ; Эффективное применение обработки исключений предполагает понимание то- го, что происходит при передаче исключения и его обработке, а также назначе- ния объектов, используемых для сообщения о возникших проблемах.
Глава 17. Инструменты для крупномасштабных программ 721 17.1.1. Передача исключения типа класса При передаче исключения (throwing) фактически передается специальный объект. Тип этого объекта определяет, какой именно обработчик будет использован. Будет выбран тот из соответствующих типу объекта обработчиков, который расположен ближе других к месту его передачи. Исключения передают и получают способом, аналогичным передаче аргументов функциям. Исключение может быть объектом любого типа, который можно пере- дать как нессылочный параметр, т.е. обязательно должно быть возможным копиро- вание объекта этого типа. Напомним, что при передаче аргумента типа массива или функции, он автомати- чески преобразуется в указатель. Аналогичное автоматическое преобразование осу- ществляется и для передаваемых объектов. Как следствие, невозможными становят- ся исключения типа массива или функции. Для передачи объекта типа массива, его преобразуют в указатель на первый элемент. Аналогично, при передаче функции ее преобразуют в указатель на функцию (раздел 7.9, стр. 302). Когда выполняется оператор throw, расположенные после него выражения иг- норируются. Оператор throw передает управление соответствующему блоку catch. Блок catch может быть локальным для той же функции или функции, непосредст- венно или косвенно вызвавшей ту, в которой произошла ошибка, приведшая к пере- даче исключения. Тот факт, что управление передается из одного места в другое, имеет два важных следствия. 1. Функции можно преждевременно покидать по цепочке вызовов. Вопрос о том, что происходит при выходе из функции в связи с передачей исключения, обсуж- дается разделе 17.1.2 (стр. 723). 2. Хранилище, которое является локальным для блока, передавшего исключение, не всегда остается доступным для блока его обработки. Поскольку локальные хранилища могут оказаться освобождены на момент обра- ботки исключения, передаваемый объект не должен быть локальным. Вместо этого применяется выражение оператора throw, инициализирующего специальный объ- ект, называемый объектом исключения (exception object). Компилятор распознает объект исключения и располагает его так, чтобы он был доступен во всех блоках catch. Создаваемый оператором throw объект инициализируется копией передан- ного выражения. Объект исключения передается соответствующему блоку catch и удаляется после обработки. Объект исключения создается как копия результата выражения оператора throw. Ре- зультат должен иметь тип, который допускает копирование. Объекты исключений и наследование На практике большинство приложений передают исключения, класс которых оп- ределен в иерархии наследования. Как будет продемонстрировано в разделе 17.1.7 (стр. 730), стандартные исключения (раздел 6.13, стр. 241) определены в иерархии
722 Часть V. Дополнительные темы наследования. Однако в данный момент следует уяснить формат взаимодействия выражений оператора throw с типами, которые связаны наследованием. При передаче исключения, статический тип и тип времени компиляции передаваемого объекта, определяет тип объекта исключения. То, что для переданного объекта используется его статический тип, как правило, проблемой не является. Передача исключения подразумевает создание объекта, ко- торый предстоит передать в точку обработки. Этот объект содержит информацию о проблеме, поэтому тип исключения необходимо знать точно. Исключения и указатели Статический тип переданного исключения имеет значение только в одном случае, когда при передаче происходит обращение к значению указателя. Результатом об- ращения к значению указателя является объект, чей тип соответствует типу указате- ля. Если это указатель на тип из иерархии наследования, тип объекта, на который он указывает, может отличаются от типа указателя. Независимо от фактического типа объекта, тип объекта исключения соответствует статическому типу указателя. Если это указатель на тип базового класса, который содержит адрес объекта производного класса, объект окажется усечен (раздел 15.3.1, стр. 608), т.е. передана будет только базовая часть объекта. При передаче самого указателя может возникнуть гораздо более серьезная про- блема, чем просто усечение объекта. В частности, передача указателя на локальный объект практически всегда приводит к ошибке по той же причине, что и при возвра- щении указателя из функции (раздел 7.3.2, стр. 275). При передаче указателя следу- ет гарантировать, что объект, на который он указывает, будет существовать на мо- мент обработки. Когда обработчик находится в другой функции, передача указателя на локальный объект, вероятнее всего, приведет к ошибке, поскольку объект, указатель на который передан, больше не будет существовать на момент срабатывания обработчика. Даже если обработчик находится в той же функции, внутри блока catch стоит удостове- риться в существовании объекта, на который указывает указатель. Если указатель содержит адрес локального объекта, расположенного в блоке кода, который завер- шается перед блоком catch, он будет удален еще до блока catch. Передача указателя практически всегда является плохой идеей, т.к. это потребует, что- бы объект, адрес которого содержит указатель, существовал везде, где есть соответст- вующие обработчики. Упражнения раздела 17.1.1 Упражнение 17.1. Каким будет тип объекта исключения в следующих случаях. (a) range_error r("error"); (b) exception *p = &r; throw r; throw *p; Упражнение 17.2. Что произойдет, если второй оператор throw будет выглядеть как throw р?
Глава 17. Инструменты для крупномасштабных программ 723 17.1.2. Прокрутка стека При передаче исключения выполнение текущей функции приостанавливается и начинается поиск соответствующей директивы catch. Поиск начинается с проверки того, расположен ли оператор throw непосредственно внутри блока try. Если это так, проверяется соответствие переданного объекта одному из обработчиков того блока catch, с которым связан данный блок try. Если соответствие в блоке catch найдено, исключение обрабатывается. В противном случае осуществляется выход из текущей функции, ее память освобождается, а локальные объекты удаляются. Затем поиск продолжается в вызывающей функции. Если обращение к передавшей исключение функции находится в блоке try, про- веряются обработчики того блока catch, который связан с ним. Если соответствие найдено, исключение обрабатывается. В противном случае осуществляется выход и из вызывающей функции, а поиск продолжается в той функции, которая вызвала ее. Этот процесс, известный как прокрутка стека (stack unwinding), продолжается по цепи обращений вложенных функций до тех пор, пока не будет найден соответст- вующий исключению обработчик catch. Как только способный обрабатывать ис- ключение блок catch будет найден, выполнение продолжится внутри этого обра- ботчика. По завершении работы обработчика, выполнение продолжится с точки, расположенной непосредственно после последней директивы блока catch. Для локальных объектов применяются деструкторы В ходе прокрутки стека происходит преждевременный выход из функции, содер- жащей оператор throw, а возможно, и из других функций по цепи обращений. Как правило, функции создают локальные объекты, которые при выходе из функции уда- ляются. При выходе из функции в связи с передачей исключения, компилятор гаран- тирует правильное удаление локальных объектов. Когда завершается работа любой функции, ее локальное хранилище освобождается. Перед освобождением памяти уда- ляются все локальные объекты, которые были созданы до передачи исключения. Если локальный объект имеет тип класса, для него автоматически вызывается деструктор. Как обычно, для удаления объектов встроенного типа компилятор ничего не делает. Для освобождения памяти, занятой локальными объектами, в ходе прокрутки стека применяются деструкторы их классов. Если ресурс создан в распределяемой памяти непосредственно в блоке кода, а ис- ключение передано прежде, чем этот ресурс был освобожден, в ходе прокрутки стека он так и не будет освобожден. Например, блок может зарезервировать область в ди- намически распределяемой памяти при помощи оператора new. Если выход из блока произойдет в связи с передачей исключения, компилятор не будет применять оператор delete к указателю. В результате распределяемая память не будет освобождена. Ресурсы, зарезервированные объектами класса, обычно освобождаются правиль- но. Для локальных объектов выполняются их деструкторы, а для освобождения ре- сурсов, зарезервированных объектами классов, применяются, обычно, их деструкторы.
724 Часть V. Дополнительные темы В разделе 17.1.8 (стр. 732) описана методика программирования, которая подразу- мевает использование классов для управления размещенными в памяти ресурсами при передаче исключения. Деструкторы никогда не должны передавать исключения В ходе прокрутки стека очень часто выполняются деструкторы. Во время работы деструктора исключение уже передано, но еще не обработано. Если в ходе этого про- цесса деструктор сам передаст новое исключение, каков будет результат? Может быть, новое исключение заменит переданное ранее и еще не обработанное? Или ис- ключение, переданное в деструкторе, будет проигнорировано? Если во время прокрутки стека, вызванной передачей исключения, деструктор передаст еще одно исключение, обрабатываться оно не будет, а библиотека вызовет функцию terminate (). Обычно функция terminate () вызывает функцию abort (), которая осуществляет аварийный выход из программы. Поскольку функция terminate () завершает работу программы, было бы очень плохой идеей расположить в деструкторе действия, которые могут привести к пере- даче исключения. На практике, поскольку деструкторы освобождают ресурсы, пере- дача в них исключения маловероятна. Стандартная библиотека гарантирует, что де- структоры ее классов не будут передавать исключения. Исключения и конструкторы В отличие от деструкторов, внутри конструкторов нередки события, которые приводят к передаче исключения. Когда исключение происходит в конструкторе, процесс создания объекта еще не завершен. Некоторые из его переменных-членов могут быть уже инициализированы, а другие — нет. Даже если объект создан только частично, следует гарантировать правильное удаление уже созданных членов. Аналогично, исключение может произойти при инициализации элементов масси- ва или другого контейнера. В этом случае также следует гарантировать правильное удаление уже созданных элементов. Необработанные исключения завершают программу Исключение не может остаться необработанным. Исключение — это достаточно важное событие, после которого программа не может продолжать нормальное вы- полнение. Если соответствующий блок catch не найден, программа вызовет биб- лиотечную функцию terminate (). 17.1.3. Обработка исключения Спецификатор исключения (exception specifier) в блоке catch (обработчике) по- добен списку параметров, который содержит только один параметр. Спецификатор исключения представляет собой имя типа, сопровождаемое необязательным именем параметра. Тип спецификатора указывает, какое именно исключение обработает данный блок catch. Указанный тип должен быть законченным: это может быть либо встро- енный тип, либо тип, определенный программистом ранее. Предварительное объяв- ление типа исключения недопустимо.
Глава 17. Инструменты для крупномасштабных программ 725 Для обработки исключения блоку catch достаточно знать только его тип, а имя параметра в спецификаторе исключения может отсутствовать. Если обработчик ну- ждается в информации не только о типе исключения, в его спецификатор включают имя параметра. Блок catch использует это имя для доступа к объекту исключения. Поиск соответствующего обработчика Блок catch, найденный в ходе поиска соответствующего обработчика, не обяза- тельно является тем, который наиболее подходит данному исключению. В результа- те исключение будет обработано первым найденным блоком catch, который сможет это сделать. Как следствие, в списке блока catch наиболее специализированный об- работчик следует располагать первым. Правила поиска соответствия исключению в блоке catch, значительно жестче, чем у типов аргументов параметров. Большинство преобразований здесь недопусти- мо — тип исключения должен точно соответствовать спецификатору обработчика. Допустимо лишь несколько различий. Допустимо преобразование из неконстантного типа в константный, т.е. передан- ный неконстантный объект может соответствовать обработчику, блок catch ко- торого ожидает константную ссылку. Допустимо преобразование из производного типа в базовый. Массив преобразуется в указатель на тип массива; функция преобразуется в со- ответствующий тип указателя на функцию. Никакие другие преобразования при поиске соответствующего обработчика не- допустимы. В частности, невозможны ни стандартные арифметические преобразо- вания, ни преобразования, определенные для классов. Спецификаторы исключений Когда встречается блок catch, его параметр инициализируется объектом исклю- чения. Подобно параметрам функции, типом спецификатора исключения может быть ссылка. Объект самого исключения является копией переданного объекта. То, будет ли объект исключения снова скопирован при передаче в блок catch, зависит от типа спецификатора исключения. Если спецификатор не является ссылкой, параметр блока catch копирует объект исключения. Код блока catch работает с локальной копией объекта исключения. Все изменения, выполненные с параметром обработчика, относятся к копии объекта исключения, но не к самому объекту. Если спецификатор является ссылкой, пара- метр блока catch, подобно любому другому ссылочному параметру, является лишь другим именем объекта исключения, и никакого отдельного объекта в блоке catch не создается. В этом случае все изменения, осуществленные в блоке catch с пара- метром, происходят с самим объектом исключения. Спецификаторы исключения и наследование Подобно объявлению параметра, спецификатор исключения для базового класса подходит для объекта исключения производного класса. Также подобно объявлению параметра, статический тип спецификатора исключения определяет действия, кото-
726 Часть V. Дополнительные темы рые можно осуществлять в блоке catch. Если переданный объект исключения име- ет тип производного класса, но обрабатывается блоком catch для базового класса, в данном блоке catch нельзя использовать члены, которые специфичны лишь для производного класса. Параметр блока catch, которому приходится иметь дело с исключением, класс кото- рого связан наследственными отношениями, имеет, как правило, ссылочный тип. Если параметр блока catch имеет ссылочный тип, используемый внутри блока catch, объект является самим объектом исключения. Статический тип обрабаты- ваемого объекта и динамический тип объекта исключения, к которому он относит- ся, могут отличаться. Если спецификатор имеет не ссылочный тип, используемый внутри блока catch, объект будет копией объекта исключения. Если блок catch ожидает объект базового класса, а объект исключения имеет тип, производный от него, объект исключения окажется усечен, подобно объекту базового класса (раз- дел 15.3.1, стр. 608). Кроме того, как уже упоминалось в разделе 15.2.4 (стр. 597), объекты (в отличие от ссылок) не полиморфны. При использовании виртуальной функции от имени объекта, а не ссылки, статический и динамический типы объекта совпадают, а тот факт, что функция является виртуальной, не имеет никакого значения. Динамиче- ское связывание осуществляется только при обращении с использованием ссылки или указателя, а не объекта. Порядок обработчиков должен соответствовать иерархии типов Когда классы исключений организованы в иерархию, пользователи могут выби- рать необходимый уровень детализации. Предположим, например, что в приложе- нии достаточно освободить ресурсы и просто выйти. Его функция main () может содержать один блок try и следующий блок catch. catch(exception &е) { /I освободить ресурсы // отобразить сообщение сегг << "Exiting: " << e.what() size_t status_indicator = 42; return(status_indicator); } « endl; // задать и возвратить // индикатор ошибки “Более требовательным” программам может понадобиться более жесткий кон- троль за исключениями. Такие приложения смогут устранить причину исключения и продолжить работу. Поскольку блоки catch срабатывают по порядку их расположения, при исполь- зовании исключения, класс которого принадлежит иерархии наследования, блоки catch следует упорядочить так, чтобы в первую очередь располагались обработчики для исключения производного типа, а затем — для базового. При наличии нескольких обработчиков исключений, классы которых связаны наследст- венными отношениями, их следует упорядочить от наиболее производного типа к наиме- нее производному.
Глава 17. Инструменты для крупномасштабных программ 727 Упражнения раздела 17.1.3 Упражнение 17.3. Объясните, почему приведенный ниже блок try некорректен. Исправьте его. try { // использование стандартной библиотеки C++ } catch(exception) { } catch(const runtime_error &re) { } catch(overtlow_error eobj) { /* ... */ } 17.1.4. Повторная передача исключения Вполне возможна ситуация, когда один блок кода catch (обработчик) не смо- жет полностью обработать исключение. После некоторых корректирующих дейст- вий, обработчик может решать, что это исключение следует обработать в функции, которая расположена далее по цепи вызовов. Обработчик может передавать ис- ключение другому, внешнему, обработчику, который принадлежит функции, вы- зывавшей данную. Это называется повторной передачей исключения (rethrowing). Повторную передачу осуществляет оператор throw, после которого нет ни имени типа, ни выражения. throw; Пустой оператор throw повторно передает объект исключения. Пустой оператор throw может присутствовать только в обработчике или в функции, вызов которой осуществляется из обработчика (прямо или косвенно). Если пустой оператор throw встретится вне обработчика, будет вызвана функция terminate (). Хотя повторная передача и не определяет собственное исключение, объект ис- ключения при этом все же передается. Передаваемое исключение является первона- чальным объектом, а не параметром обработчика. Когда параметр обработчика имеет базовый тип, невозможно узнать фактический тип исключения, переданного при по- вторной передаче. Его тип зависит от динамического типа объекта исключения, а не от статического типа параметра обработчика. Таким образом, повторная передача исключения из обработчика с параметром базового класса, может фактически при- вести к передаче объекта производного класса. Как правило, в обработчике можно изменить его параметр. Если обработчик по- вторно передает исключение уже после изменения его параметра, изменения рас- пространятся на объект исключения только в том случае, когда спецификатор ис- ключения является ссылкой. catch (my_error &eObj) { // спецификатор ссылочного типа eObj.status = severeErr; // изменение объекта исключения throw; // переменная-член status объекта исключения имеет // значение severeErr } catch (other_error eObj) { // спецификатор нессылочного типа eObj.status = badErr; // изменение только локальной копии throw; // значение переменной-члена status объекта исключения // при повторной передаче не изменилось }
728 Часть V. Дополнительные темы 17.1.5. Обработчик для всех исключений Вполне возможна ситуация, когда перед выходом из функции с передачей ис- ключения необходимо выполнить некие действия, которые не обработают исключе- ние и не предотвратят его передачу. Чтобы не создавать отдельный обработчик для каждого возможного исключения (а также для непредвиденных исключений), мож- но использовать обработчик для всех исключений (catch-all). Обработчик для всех исключений имеет формат (...). // соответствует любому исключению, которое может быть передано catch (...) { // код располагается здесь } Обработчик для всех исключений подходит для исключений любого типа. Обработчик catch (...) зачастую используется в комбинации с повторной пе- редачей. Этот обработчик осуществляет все необходимые локальные действия, а за- тем снова перепередает исключение, void manip() { try { // действия, приводящие к передаче исключения } catch (...) { // действия по частичной обработке исключения throw; } Блок catch (...) может применяться как самостоятельно, так и в составе набо- ра из нескольких обработчиков. Если блок catch (...) используется в комбинации с другим блоками catch, он дол- жен располагаться последним. В противном случае все остальные обработчики никогда не сработают. Упражнения раздела 17.1.5 Упражнение 17.4. Рассмотрим следующую программу. int main() { // использование стандартной библиотеки C+ + } Измените функцию main () так, чтобы обрабатывать все исключения, переданные функциями стандартной библиотеки C++. Перед вызовом определенной в заголовке cstdlib функции abort (), завершающей работу функции main (), обработчики должны отображать сообщение об ошибке, связанное с исключением. Упражнение 17.5. С учетом следующих типов исключения и блоков catch, напишите выражение throw, которое создает объект исключения, обрабатываемого каждым из блоков catch. (a) class exceptionType { }; catch(exceptionType *pet) { } (b) catch (...) { } (c) enum mathErr { overflow, underflow, zeroDivide }; catch(mathErr &ref) { } (d) typedef int EXCPTYPE; catch(EXCPTYPE) { }
Глава 17. Инструменты для крупномасштабных программ 729 17.1.6. Блок try функции и конструкторы С теоретической точки зрения исключения могут происходить в любой точке программы. В частности, исключение может быть передано и в конструкторе, и в процессе инициализации. Инициализация в конструкторе выполняется прежде, чем его тело. Блок catch внутри тела конструктора не может обработать исключение, которое было передано при инициализации. Чтобы обработать исключение, переданное при инициализации, конструктор следует оформить как блок try функции (function try block). Блок try функции по- зволяет связать группу блоков catch с функцией в целом. Например, для обнару- жения отказа оператора new, конструктор Handle О из главы 16, “Шаблоны и об- щее программирование”, можно было бы заключить в блок try. template <class Т> Handle<T>::Handle(T *p) try : ptr(p), use(new size_t(D) { // пустое тело функции } catch(const std::bad_alloc &e) { handle_out_of_memory(e); } Обратите внимание, ключевое слово try предшествует списку инициализации переменных-членов, а набор операторов блока try заключает тело функции конст- руктора. Теперь блок catch может обработать исключения, переданные как из спи- ска инициализации переменной-члена, так и из тела конструктора. Единственный способ обработки в конструкторе исключения, переданного при инициали- зации, заключается в оформлении конструктора как блока try функции. 17.1.7. Иерархия класса исключения В разделе 6.13 (стр. 241) упоминались классы исключений стандартной библио- теки. Однако вопрос о том, что эти классы связаны наследственными отношениями, еще не обсуждался. Иерархия наследования представлена на рис. 17.1. Рис. 17.1. Иерархия стандартного класса exception
730 Часть V. Дополнительные темы К классе exception определена только одна виртуальная функция-член по имени what (). Эта функция возвращает тип const char*. Обычно она возвраща- ет сообщение, использованное при создании объекта исключения в месте передачи. Поскольку функция what () является виртуальной, в обработчике, получающем ссылку на базовый класс, обращение к функции what () приведет к выполнению той ее версии, которая соответствует динамическому типу объекта исключения. Классы исключения для приложения книжного магазина В приложениях очень часто применяются стандартные классы исключений. Кроме того, иерархию exception в приложениях зачастую расширяют за счет создания дополнительных классов, производных либо непосредственно от класса exception, либо от одного из промежуточных базовых классов. Такие вновь соз- данные производные классы применяются для создания исключений, специфиче- ских для конкретного приложения. Если бы предстояло создать реальное приложение книжного магазина, его клас- сы были бы гораздо сложнее, чем в примерах этой книги. Одной из причин усложне- ния является обработка исключений. Фактически пришлось бы создать собственную иерархию исключений, отражающую вероятные проблемы, специфические для дан- ного приложения. В этом проекте могли бы понадобиться следующие классы. // гипотетический класс исключения для приложения книжного магазина class out_of_stock: public std::runtime_error { public: explicit out_of_stock(const std::string &s): std::runtime_error(s) { } }; class isbn_mismatch: public std::logic_error { public: explicit isbn_mismatch(const std::string &s): std::logic_error(s) { } isbn_mismatch(const std::string &s, const std::string &lhs, const std::string &rhs): std::logic_error(s), left(Ihs), right(rhs) { } const std::string left, right; // более подробно деструктор рассматривается в разделе 17.1.10 II на стр. 740 virtual ~isbn_mismatch() throw() { } }; Здесь специфические для приложения классы исключения определены как про- изводные от стандартного класса исключения. Любую иерархию классов, включая иерархию исключений, можно рассматривать как слоистую структуру. По мере уг- лубления иерархии, каждый слой становится более специализированным. Напри- мер, первым и наиболее общим слоем иерархии является класс exception. При по- лучении объекта этого типа будет известно только то, что в приложении произошла какая-то ошибка. Второй слой специализирует исключение на две обширные категории: ошибки времени выполнения и логические ошибки. Классы исключений книжного магазина представляют даже более специализированный слой. Класс out_of_stock пред- ставляет проблему времени выполнения, специфическую для данного приложения. Он используется для оповещения о нарушении порядка выполнения. Класс исклю-
Глава 17. Инструменты для крупномасштабных программ 731 чения isbnjnismatch представляет собой более специализированную форму класса logic_error. В принципе, программа может обнаружить несоответствие ISBN, вы- звав функцию same_isbn (). Использование классов исключений, определенных программистом Собственные классы исключений применяются точно так же, как и классы стан- дартной библиотеки. Одна часть программы передает объект одного из этих классов, а другая получает и обрабатывает его, устраняя проблему. Например, для перегру- женного оператора суммы класса Sales_item можно создать класс исключения isbn_mismatch, передаваемого в случае обнаружения ошибки несовпадения ISBN. // передает исключение, если isbn объектов не совпадают Sales_item operators- (const Sales_item& Ihs, const Sales_item& rhs) { if (!Ihs.same_isbn(rhs)) throw isbn_mismatch("isbn mismatch", Ihs.book(), rhs.book()); Sales_item ret(Ihs); // скопировать Ihs в локальный объект, // который и будет возвращен ret += rhs; // добавить к содержимому rhs return ret; // вернуть ret по значению Обнаружив эту ошибку, использующий оператор суммы код сможет передать со- ответствующее сообщение об ошибке и продолжить работу. // применение исключения в приложении книжного магазина Sales_item iteml, item2, sum; while (cin >> iteml >> item2) { // прочитать две транзакции try { sum - iteml + item2; // вычислить их сумму II использовать сумму cout << sum << endl; } catch (const isbn_mismatch &e) { cerr << e.what() « left isbn(" << e.left << ") right isbn(" << e.right « ")" << endl; 17.1.8. Автоматическое освобождение ресурсов В разделе 17.1.2 (стр. 723) упоминалось, что при передаче исключения локаль- ные объекты удаляются автоматически. Факт применения деструкторов очень ва- жен для проекта приложения. Кроме того, это одна из многих причин, по которым имеет смысл использовать классы из стандартной библиотеки. Рассмотрим сле- дующую функцию. void f() { vector<string> v; string s; while (cin >> s) v.push_back(s); string *p = new string[v.size()]; // дальнейшая обработка // локальный вектор // заполнить вектор II динамический массив
732 Часть V. Дополнительные темы // в этом коде возможна передача исключения // при передаче исключения функция удаления будет пропущена delete [] р; } // при выходе из функции вектор v удаляется автоматически В этой функции определен локальный вектор и динамический массив. При нормаль- ной работе массив и вектор удаляются перед выходом из функции. Массив освобождает последний оператор, а вектор автоматически удаляется при выходе из функции. Но если внутри функции происходит событие, приводящее к передаче исклю- чения, вектор будет удален, а массив — нет. Проблема в том, что массивы не уда- ляются автоматически. Таким образом, передача исключения после оператора new, но перед оператором delete, приведет к тому, что массив останется неудаленным. Однако, независимо от того, когда произойдет исключение, деструктор вектора обязательно сработает. Применение классов для управления размещением ресурсов в памяти Возможность автоматического выполнения деструкторов имеет большое значение при выборе подхода программирования, который позволит повысить устойчивость ко- да приложения к исключениям. Под устойчивостью к исключениям (exception safe) подразумевается способность программы продолжать правильно работать даже тогда, когда возникает проблема, приводящая к передаче исключения. Кроме того, под “безопасностью” (safety)2 в данном случае подразумевается, что все размещенные в памяти ресурсы будут правильно освобождены при передаче исключения. Таким образом, создав класс, инкапсулирующий процессы размещения в памяти и освобождения ресурсов, можно гарантировать, что ресурсы будут правильно соз- даны и ликвидированы. Этот подход зачастую называют “созданием-инициализа- цией ресурса” (RAII — “resource allocation is initialization”). Класс управления ресурсом должен быть разработан так, чтобы его конструктор размещал ресурс в памяти, а деструктор освобождал. Когда необходимо разместить ресурс, следует создать объект этого класса. Если никаких исключений передано не было, созданный ресурс будет освобожден, когда использующий его объект выйдет из области видимости. Однако важней всего ситуация, кода приводящее к передаче исключения событие происходит после создания объекта, но до выхода объекта из области видимости. В этом случае компилятор гарантирует, что объект будет удален в процессе ликвидации области видимости, в которой он был создан. Ниже приведен пример прототипа класса, конструктор которого размещает ре- сурс, а деструктор освобождает его. class Resource { public: Resource(parms p) : r(allocate(p)) { } -Resource() { release(r); } // необходим также конструктор копий и оператор присвоения private: resource_type *r; // ресурс, управляемый этим классом resource_type *allocate(parms р); // размещение ресурса void release(resource_type*); // освобождение ресурса }; 2 Слово имеет два значения. — Примеч. ред.
Глава 17. Инструменты для крупномасштабных программ 733 Класс Resource отвечает за размещение и освобождение ресурса. Он содержит одну или несколько переменных-членов, которые представляют этот ресурс. Конст- руктор класса Resource размещает ресурс в памяти, а деструктор освобождает его. При использовании этого класса, ресурс освобождается автоматически. void fcn() { Resource res(args); // размещает II код, способный передать исключение // при передаче исключения деструктор // ср абатывает автоматически } // res выходит из области видимости и resource_type ресурса res удаляется автоматически Когда функция завершает работу стандартно, этот ресурс освобождается при вы- ходе объекта класса Resource из области видимости. При преждевременном выхо- де из функции в связи с передачей исключения, компилятор запустит деструктор класса Resource в процессе обработки исключения. В программах, где размещаются ресурсы и существует вероятность передачи ис- ключения, имеет смысл использовать классы, управляющие этими ресурсами. Как описано в этом разделе, применение классов для создания и освобождения ресур- сов гарантирует их освобождение при передаче исключения. Упражнения раздела 17.1.8 Упражнение 17.6. Объясните, что происходит при передаче исключения в следующей функции. vector<int> v(b, е) ; int *р = new int[v.size()]; ifstream in("ints"); // исключение передается здесь } Упражнение 17.7. Существует два способа обезопасить код, приведенный выше. Опишите и реа- лизуйте их. 17.1.9. Класс auto ptr Хорошим примером устойчивого к исключениям класса управления ресурсом, является класс auto_ptr из стандартной библиотеки. Класс auto_ptr — это шаблон, который получает один параметр типа. Он обеспечивает устойчивое к исключени- ям размещение объектов в динамически распределяемой памяти. Класс auto_ptr определен в заголовке memory. Класс auto_ptr применяется для управления только одиночными объектами, созда- ваемыми оператором new. Он неприменим для управления массивами, размещенными в динамической памяти. Как будет продемонстрировано вскоре, класс auto_ptr характеризуется необычным поведением при копировании и присвоении. В связи с этим объекты класса auto_ptr нельзя сохранять в библиотечных контейнерах.
734 Часть V. Дополнительные темы Класс auto_ptr способен содержать только указатель на объект и не может быть использован как указатель на массив, размещенный в динамически распре- деляемой памяти. Применение класса auto_ptr как указателя на массив в дина- мически распределяемой памяти приводит к непредсказуемому поведению во время выполнения. Объект класса auto_ptr может указывать на объект, а может и не указывать. Когда объект класса auto_ptr указывает на объект, говорят, что он “владеет” этим объектом. Когда объект класса auto_ptr выходит из области видимости или лик- видируется иным способом, объект, на который он указывает, удаляется из динами- чески распределяемой памяти автоматически. Использование класса auto_ptr для устойчивого к исключениям резервирования памяти Если память для ресурса резервирована при помощи обычного указателя, пере- дача исключения до применения оператора delete не позволит автоматически ос- вободить эту область памяти, void f() { int *ip = new int(42); // создание нового объекта в // динамической памяти // код, способный передать исключение, не обрабатываемое // внутри функции f() delete ip; // освободить память перед выходом Если исключение передается между операторами new и delete и если это ис- ключение не обрабатывается локально, оператор delete не будет выполнен, а па- мять не будет освобождена. Если вместо обычного указателя использовать класс auto_ptr, при преждевре- менном выходе из блока память будет освобождена автоматически. void f() { auto_ptr<int> ap(new int(42)); // создать новый объект // код, способный передать исключение, не обрабатываемое // внутри функции f() } // auto_ptr автоматически освободит память при выходе // из функции В данном случае компилятор гарантирует, что деструктор для объекта ар будет выполнен прежде, чем произойдет прокрутка стека для функции f (). Таблица 17.1. Класс auto jptr auto_ptr<T> ар; Создает несвязанный объект класса auto_ptr по имени ар auto_ptr<T> ар (р); Создает объект класса auto_ptr по имени ар, который владеет объ- ектом, на который указывает указатель р. Этот конструктор является яв- ным (explicit) Auto_ptr<T> api (ар2)Создает объект класса auto_ptr по имени api, который содержит указатель, первоначально хранимый в объекте ар2. Передача владения объекту api приведет к тому, что объект ар2 станет несвязанным
Глава 17. Инструменты для крупномасштабных программ 735 apl = ар2 ~ар *ар ар-> ар.reset(р) ар.release() ар.get() Окончание табл. 17.1 Передает владение объекта ар2 объекту api. Удаление объекта, кото- рым владеет объект api, а также передача ему объекта, которым вла- деет объект ар2, приведет к тому, что объект ар2 станет несвязанным Деструктор. Удаляет объект, на который указывает объект ар Возвращает ссылку на объект, с которым связан объект ар Возвращает указатель, хранимый объектом ар Если указатель р хранит не тот же адрес, что и объект ар, он удалит объект, на который указывает объект ар, и свяжет его с объектом, адрес которого содержит указатель р Возвращает указатель, хранимый объектом ар, и делает его несвязан- ным Возвращает указатель, хранимый объектом ар Класс auto_ptr является шаблоном и может содержать указатели любого типа Класс auto_ptr является шаблоном, получающим один параметр типа. Этот тип указывает тип объекта, с которым может быть связан объект класса auto_jptr. Сле- довательно, объект класса auto_ptr может быть создан для любого типа. auto_ptr<string> apl(new string("Brontosaurus")); Связывание объекта класса auto_ptr с указателем В наиболее общем случае объект класса auto_ptr инициализируется при обра- щении к адресу объекта, возвращаемого оператором new. auto_ptr<int> pi(new int(1024)); Это выражение инициализирует объект pi результатом обращения к адресу объ- екта, созданного оператором new. В данном случае оператор new инициализирует переменную типа int значением 1,024. Конструктор, получающий указатель, является явным (explicit) конструкто- ром (раздел 12.4.4, стр. 491), поэтому для создания объекта класса auto_ptr следу- ет использовать прямую форму инициализации. // ошибка: конструктор, получающий указатель, является явным II и не может быть использован неявно auto_ptr<int> pi = new int(1024); auto_ptr<int> pi (new int(1024)); // ok: используется прямая 11 инициализация Объект, созданный оператором new и переданный объекту pi, удаляется автома- тически, когда объект pi выходит из области видимости. Если объект pi является локальным, объект, которым он владеет, будет удален в конце блока, внутри которо- го создан объект pi. При передаче исключения объект pi также выйдет из области видимости. Деструктор для объекта pi будет выполнен автоматически, как часть об- работки исключения. Если объект pi является глобальным, объект, которым он вла- деет, будет удален в конце программы.
736 Часть V. Дополнительные темы Применение класса auto ptr Предположим, что необходимо получить доступ к функции класса string. С обыч- ным указателем на строку произошло бы следующее. string *pstr_type = new string("Brontosaurus"); if (pstr_type->emptyO) // Упс, что то не так В классе auto_ptr определены перегруженные версии операторов обращения к значению (*) и стрелки (->) (раздел 14.6, стр. 553). Поскольку класс auto_ptr об- ладает этими операторами, его можно использовать как указатель. // обычные для указателя операторы обращения к значению и стрелки *apl = "TRex"; // присвоить объекту, на который указывает apl, II новое значение string s = *apl; // инициализирует s копией объекта, // на который указывает apl if (apl->empty()) // выполняет функцию empty() для строки, II на которую указывает apl Главной задачей класса auto_jotr является обеспечение поведения, подобного обычному указателю, при автоматическом удалении объекта, на который он указы- вает. Как будет продемонстрировано, факт автоматического удаления объектов при- водит к серьезным различиям между объектами класса auto_ptr и обычными ука- зателями при копировании и обращении к значению по хранимому адресу. Операции копирования и присвоения класса auto_ptr деструктивны Копирование и присвоение объектов класса auto_ptr происходит совсем не так, как у обычных указателей. При копировании или присвоении объекта класса auto_ptr, его значение передается другому объекту класса auto_ptr, т.е. владение основным объ- ектом переходит от прежнего объекта класса auto_ptr к его копии. В результате ис- ходный объект класса auto_ptr оказывается в несвязанном состоянии. При копировании (и присвоении) обычных указателей, копии присваивается ад- рес оригинала. После копирования (присвоения), оба указателя содержат адрес того же объекта. После копирования (присвоения) объектов класса auto_ptr, адрес ос- новного объекта содержит новый (расположенный слева) объект класса auto_ptr, а исходный ничего не содержит (становится несвязанным). auto_ptr<string> apl(new string("Stegosaurus")); // после копирования объект apl становится несвязанным auto ptr<string> ap2(apl); // владение объекта apl передается // объекту ар2 В результате копирования или присвоения, правый объекта класса auto_ptr ос- вобождается от адреса основного объекта. В данном примере строку удалит объект ар2, а не apl. После копирования объект apl не связан ни с одним из объектов. В отличие от обычных операторов копирования и присвоения, операторы ко- пирования и присвоения класса auto_ptr изменяют правый операнд. В резуль- тате и левый, и правый операнды оператора присвоения являются изменяемыми 1-значениями.
Глава 17. Инструменты для крупномасштабных программ 737 Присвоение удаляет объект, на который указывает левый операнд Кроме того, если основные объекты являются разными, в ходе передачи владения правого операнда левому, объект, на который первоначально указывал левый операнд оператора присвоения, удаляется. Как обычно, самоприсвоение ни к чему не приводит. auto_ptr<string> арЗ(new string("Pterodactyl")); // объект, на который указывает объект арЗ, удаляется, / / а владение ар2 передается объекту арЗ арЗ = ар2; // после присвоения объект ар2 становится несвязанным В ходе присвоения объекта ар2 объекту арЗ происходит следующее. Удаляется объект, на который указывал объект арЗ. Объекту арЗ передается адрес основного объекта, хранившийся ранее в объекте ар2. Объект ар2 становится несвязанным. Поскольку операторы копирования и присвоения класса auto_ptr являются деструк- тивными, его объекты не могут быть сохранены в стандартных контейнерах. Классы библиотечных контейнеров требуют, чтобы после копирования и присвоения оба объек- та были равны. Класс auto_ptr этому требованию не соответствует. После копиро- вания и присвоения объекты ар2 и api не равны. Стандартный конструктор класса auto_ptr Без инициализирующего значения создается несвязанный (unbound) объект клас- са auto_ptr. Он не содержит указатель на основной объект. auto ptr<int> p_auto; // p_auto не указывает ни на один из объектов По умолчанию значением внутреннего указателя объект класса auto_ptr явля- ется 0. Обращение к значению несвязанного объекта класса auto_ptr приводит к тому же результату, что и обращение к значению несвязанного указателя, т.е. проис- ходит ошибка с непредвиденными последствиями. * p_auto = 1024; // ошибка: обращение к значению объекта // класса auto_ptr, не указывающего ни на что Проверка объекта класса auto ptr Чтобы выяснить, связан ли указатель с основным объектом, достаточно прове- рить его в условии оператора if, поскольку пустой указатель содержит значение 0. Непосредственная проверка объекта класса auto_ptr невозможна. // ошибка: объект класса auto__ptr в условии неприменим if (p_auto) * p_auto - 1024; В классе auto_ptr нет функции преобразования типа, позволяющей применить его объект в условии. Для проверки объекта класса auto_ptr следует воспользо- ваться его функцией-членом get (), которая возвращает хранимый им указатель. // переделанный способ проверки владения p_auto объектом if (p_auto.get()) * p_auto = 1024; Чтобы выяснить, владеет ли объект класса auto_ptr основным объектом, воз- вращаемое его функцией get () значение следует сравнить со значением 0.
738 Часть V. Дополнительные темы Функцию get () следует использовать только для опроса объекта класса auto_ptr или использования возвращенного значения указателя. Ее нельзя использовать как ар- гумент при создании другого объекта класса auto_ptr. Использование функции-члена get () для инициализации другого объекта класса auto_ptr нарушает принцип этого класса, согласно которому в любой момент време- ни только один объект класса auto_ptr может содержать данный указатель. Если тот же указатель содержат два объекта класса auto_ptr, он будет удален дважды. Функция reset () Еще одно различие между классом auto_ptr и встроенным указателем заклю- чается в том, что его объекту нельзя непосредственно присвоить адрес или дру- гой указатель. p_auto = new int(1024); // ошибка: нельзя присваивать указатель // объекту класса auto_ptr Вместо указателя следует применить функцию reset (). // переделанный способ проверки владения p_auto объектом if (p_auto.get()) * p_auto = 1024; else / / применение функции reset() для нового объекта p_auto.reset(new int(1024)); Чтобы обнулить объект класса auto_ptr, функции reset () следует передать значение 0. Перед привязкой объекта класса auto_ptr к другому объекту, вызов функции reset () позволяет удалить объект (если он есть), адрес которого он хранил. Однако, как и в случае самоприсвоения, это не даст никакого результата, если вызов функции reset О происходит для того же указателя, который объект класса auto_ptr уже содержит, т.е. объект не будет удален. Внимание! Ограничения класса auto ptr Шаблон класса auto_ptr предоставляет удобный и безопасный способ работы с объ- ектами, размещенными в динамически распределяемой памяти. Чтобы правильно ис- пользовать класс auto_ptr, следует жестко придерживаться ограничений, которые налагает этот класс. 1. Не используйте класс auto_ptr для хранения указателя на статический объект В противном случае, при удалении объекта класса auto_pt г, он попытается удалить указатель на объект, размещенный не в динамической памяти, что приведет к непредсказуемым последствиям. 2. Никогда не используйте два объекта класса auto_jptr для хранения адреса то- го же объекта. Как правило, эта ошибка происходит в случае, когда тот же ука- затель используется при инициализации двух разных объектов класса auto_ptr или применении функции reset (). Эту ошибку можно совершить
Глава 17. Инструменты для крупномасштабных программ 739 косвенно, при использовании результата выполнения функции get () одного объекта класса autojptr при инициализации другого, или при его передаче функции reset () другого объекта класса auto_p tr. 3. Не используйте объект класса auto_ptr для хранения указателя на массив в динамически распределяемой памяти. При удалении объекта класса auto_ptr он использует обычный оператор delete (который освободит только первый элемент массива), а не оператор delete [], способный удалить весь массив. 4. Не сохраняйте объекты класса autojztr в контейнере. Контейнеры требуют, чтобы классы хранимых в них объектов обладали функциями копирования и присвоения, которые ведут себя аналогично функциям встроенных типов. По- сле копирования (или присвоения) оба объекта должны иметь одинаковое зна- чение. Класс auto_ptr этому требованию не удовлетворяет. Упражнения раздела 17.1.9 Упражнение 17.8. Какое из следующих объявлений класса autojptr недопустимо или вероят- нее всего приведет к ошибке в программе? Объясните, почему. int ix = 1024, *pi = &ix, *pi2 = new int(2048); typedef auto_ptr<int> IntP; (a) IntP pO(ix); (b) IntP pl(pi); (c) IntP p2(pi2); (d) IntP p3(&ix); (e) IntP p4(new int(2048)); (f) IntP p5(p2.get()); Упражнение 17.9. Предположим, что ps — это указатель на тип string. В чем разница (если она есть) между следующими двумя обращениями к функции assign () (раздел 9.6.2, стр. 367)? Какое из них предпочтительней и почему? (a) ps.get()->assign("Danny"); (b) ps->assign("Danny"); 17.1.10. Спецификация исключений При просмотре объявления обычной функции невозможно выяснить, какие ис- ключения она может передавать. Однако, чтобы написать соответствующие обра- ботчики (блоки catch), желательно знать, какие исключения может передавать функция. Спецификация исключений (exception specification) указывает, что в случае передачи функцией исключения, это будет одно из исключений, включенных в спе- цификацию, или исключение, производное от них. Определение спецификации исключений Спецификация исключений следует за списком параметров функции. Ее начина- ет ключевое слово throw, сопровождаемое списком типов исключений (возможно пустым), заключенным в круглые скобки, void recoup(int) throw(runtime_error); Это объявление свидетельствует, что функция recoup () получает значение ти- па int и возвращает void. В случае передачи исключения, это будет исключение runtime_error или исключение, класс которого является производным от класса runtime error.
740 Часть V. Дополнительные темы Пустой список спецификации свидетельствует о том, что функция не передает никаких исключений. void no_problem() throw(); Спецификация исключений — это часть интерфейса функции. Определение функции и любые ее объявления должны иметь ту же спецификацию. Если в объявлении функции не указана спецификация исключения, она может передавать /зл/7** 1 исключения любого типа. Г^ЪпкЛ Нарушение спецификации исключения К сожалению, на момент компиляции невозможно узнать, какие исключения бу- дут (и будут ли вообще) переданы в программе. Нарушение спецификации исклю- чений функции может быть обнаружено только во время выполнения. Если функция передаст исключение, не указанное в спецификации, будет вызва- на библиотечная функция unexpected (). По умолчанию она вызывает функцию terminate (), которая и прерывает выполнение программы. Компилятор не может и не пытается проверять спецификации исключений. Даже если прочитав код функции, компилятор случайно обнаружит, что в ней возможна передача исключения, не указанного в спецификации, никаких сообщений по этому поводу он не выдаст. void f() throw() { throw exception(); } // обещано, что никаких исключений не будет // нарушение спецификации исключений Вместо этого компилятор создаст код, который гарантированно вызовет функ- цию unexpected () в случае передачи исключения, нарушающего спецификацию. Указание на отсутствие исключений в функции Поскольку проверка спецификации исключений во время компиляции не проис- ходит, практическая ценность ее весьма невысока. Спецификация исключений на самом деле полезна только в том случае, когда для функции указано, что она гарантированно не будет передавать никаких исключений. Указание на то, что функция не будет передавать никаких исключений, может быть весьма полезно и пользователям функции, и компилятору: это упрощает созда- ние устойчивого к исключениям кода, который вызывает эту функцию. Зная об от- сутствии в функции исключений, можно не заботиться об их обработке. Кроме того, если компилятор знает, что никаких исключений не будет, он может выполнить оп- тимизацию кода, которая невозможна в случае возможной передачи исключений.
Глава 17. Инструменты для крупномасштабных программ 741 Спецификация исключений и функции-члены Подобно обычным функциям, спецификация исключений в объявлении функ- ции-члена следует за списком параметров. Например, класс bad_alloc в стандарт- ной библиотеке C++ определен так, что все его функции-члены имеют пустую спе- цификацию исключений. Это обещание функций-членов не передавать исключений. // иллюстрация определения библиотечного класса bad_alloc class bad_alloc : public exception { public: bad_alloc() throw(); bad_alloc(const bad_alloc &) throw(); bad—alloc & operator=(const bad_alloc &) throw(); virtual ~bad_alloc() throw(); virtual const char* what() const throw(); }; Обратите внимание, что в объявлении константной функции-члена специфика- ция исключений следует за спецификатором const. Спецификация исключений и деструкторы В разделе 17.1.7 (стр. 730) были представлены два гипотетических класса исклю- чений приложения книжного магазина. Деструктор класса isbn_mismatch опреде- лен следующим образом. class isbn_mismatch: public std::logic_error { public: virtual ~isbn_mismatch() throw() { } }; Рассмотрим его подробнее. Класс isbn_mismatch происходит от класса logic_error, который является одним из стандартных классов исключения. Деструкторы стандартных классов ис- ключения имеют пустой спецификатор throw (). То есть они “обещают”, что не бу- дут передаваться никакие исключения. При наследовании одного из этих классов, деструктор производного класса также должен “обещать” не передавать никаких ис- ключений. Класс out_of_stock не имел никаких членов, поэтому его синтезируемый де- структор не делает ничего, что может привести к передаче исключения. Следова- тельно, компилятору известно, что синтезируемый деструктор будет “соблюдать обещание” не передавать исключений. Класс isbn_mismatch имеет два члена типа string. Следовательно, синтези- руемый деструктор класса isbn_mismatch вызывает деструктор класса string. Стандарт C++ предусматривает, что деструктор класса string, подобно деструк- тору любого другого библиотечного класса, не будет передавать исключений. Од- нако библиотечные деструкторы не обладают спецификацией исключений. В дан- ном случае разработчику известно, что деструктор класса string не будет пере- давать исключения, но не компилятору. Следовательно, чтобы восстановить “обещание” деструктора не передавать исключения, необходимо определить соб- ственный деструктор.
742 Часть V. Дополнительные темы Спецификация исключений и виртуальные функции Виртуальная функция в базовом классе может иметь спецификацию исключения, которая отличается от спецификации исключения соответствующей виртуальной функции в производном классе. Однако спецификация исключения виртуальной функции производного класса должна совпадать или быть более строгой, чем спе- цификация исключений соответствующий виртуальной функции базового класса. Это ограничение гарантирует, что при использовании указателя на тип базового класса для вызова виртуальной функции производного класса, спецификация ис- ключения из производного класса не добавит никаких новых исключений к тем, ко- торые указаны в базовом. Рассмотрим пример. class Base { public: }; class Derived : public Base { public: // ошибка: спецификация исключений менее ограничительна, !/ чем в Base::fl() double fl(double) throw(std::underflow_error); // ok: та же спецификация исключения, что и в Base::f2() int f2(int) throw(std::logic_error); // ok: спецификация производной f3() более ограничительна std::string f3() throw(); }; Объявление функции f 1 () в производном классе является ошибкой, поскольку ее спецификация исключений добавляет новое исключение к тем, которые перечис- лены для ее версии базового класса. Причина, по которой в список спецификации производного класса нельзя добавлять новые исключения, заключается в том, что пользователи иерархии должны быть способны написать код, который зависит от списка спецификации. Если обращение выполнено при помощи указателя или ссыл- ки на базовый класс, для пользователя этого класса интерес будут представлять только те исключения, которые определены в базовом классе. Ограничив количество возможных исключений в производных классах теми, ко- торые перечислены в базовым классе, можно создавать код, зная заранее, какие ис- ключения предстоит обрабатывать. То есть, создавая код, можно полагаться на то факт, что список исключений виртуальной функции в базовом классе является ос- новой списка исключений, которые способна передавать ее версия в производном классе. Например, при вызове функции f3 () известно, что обработать придется лишь исключения logic_error или runtime_error. // обещание не передавать исключений void compute(Base *pb) throw() { try { // может передать исключение типа std::logic_error !/ или типа std::runtime_error catch (const runtime_error &re) { /* ... */ } }
Глава 17. Инструменты для крупномасштабных программ 743 Для принятия решения о том, какие именно исключения возможно придется об- рабатывать в случае применения функции compute (), используется спецификация из базового класса. 17.1.11. Спецификация исключений указателя на функцию Спецификация исключения — это часть объявления типа функции. Таким обра- зом, спецификация исключений может быть использована в определении указателя на функцию. void (*pf)(int) throw(runtime_error); Данное объявление свидетельствует о том, что это указатель pf на функцию, по- лучающую значение типа int, возвращающую void и способную передавать ис- ключения лишь типа runtime_error. Если спецификация отсутствует, указатель может указывать на функции, способные передавать исключения любого типа. Когда указатель на функцию со спецификацией исключения инициализирован (или присвоен) другим указателем (или результатом обращения к адресу функции), спецификации исключений обоих указателей необязательно должны быть идентич- ными. Однако спецификация исходного указателя должна быть, по крайней мере, столь же ограничительной, как и спецификация указателя назначения. void recoup(int) throw(runtime_error); // ok: спецификация функции recoup () такая же, как и у pfl void (*pfl)(int) throw(runtime_error) = recoup; // ok: спецификация recoup() более ограничительна, чем у pf2 void (*pf2)(int) throw(runtime_error, logic_error) = recoup; // ошибка: спецификация recoup() менее ограничительна, чем у pf3 void (*pf3)(int) throw() - recoup; // ok: спецификация recoup() более ограничительна, чем у pf4 void (*pf4)(int) = recoup; Третья инициализация ошибочна. Объявление указателя pf3 гласит, что он предназначен для функции, которая не будет передавать никаких исключений. Од- нако функция recoup () объявлена как такая, что способна передать исключение типа runtime_error, которого нет среди определенных для указателя pf3. По- скольку функция recoup () недопустима для инициализации указателя pf3, во время компиляции происходит ошибка. Упражнения раздела 17.1.11 Упражнение 17.10. Какие исключения может передавать функция, если она имеет спецификацию исключения throw () ? А если она не имеет никакой спецификации исключений? Упражнение 17.11. Какие из следующих инициализаций являются ошибочными (если есть)? Почему? void example() throw(string); (a) void (*pfl)() = example; (b) void (*pf2)() throw() = example; Упражнение 17.12. Какие исключения могли бы передавать следующие функции? (a) void operate() throw(logic_error); (b) int op(int) throw(underflow_error, (c) char manip(string) throw(); (d) void process(); overflow_error);
744 Часть V. Дополнительные темы 17.2. Пространства имен Каждое имя, определенное в области видимости, должно быть уникальным. В боль- ших и сложных приложениях удовлетворить это требование может быть довольно трудно. В таких приложениях, как правило, очень много имен определено в глобаль- ной области видимости. Для сложных программ, состоящих из библиотек, которые разработаны независимо, характерна более высокая вероятность конфликта имен, чем в случае кода, разработанного самостоятельно. Большое количество глобальных имен, определенных в библиотеках, принадле- жит прежде всего шаблонам, типам и функциям. При создании приложений, исполь- зующих библиотеки от многих производителей, почти неизбежно возникают кон- фликты между используемыми в них именами. Эта проблема известна как загромо- ждение пространства имен (namespace pollution). Традиционно программисты избегали загромождения пространства имен, при- сваивая глобальным объектам очень длинные имена, которым зачастую предшеству- ет некий специфический символьный префикс. class cplusplus_primer_Query { ... }; ifstream& cplusplus_primer_open_file(ifstream&, const string&); Но это решение далеко не идеально: длинные имена громоздки и не удобны при чтении и написании кода программ. Пространства имен (namespace) предоставляют существенно более простой механизм предотвращения конфликтов имен. Простран- ства имен создают разделы в глобальном пространстве имен, облегчая таким обра- зом его использование библиотеками, разработанными независимо. Пространство имен — это область видимости. Определяя имена библиотеки внутри пространства имен, авторы библиотеки (и ее пользователи) смогут избежать ограничений, нала- гаемых на глобальные имена. 17.2.1. Определение пространств имен Определение пространства имен начинается с ключевого слова namespace, со- провождаемого именем пространства имен, namespace cplusplus_primer { class Sales_item { /* ... */}; Sales_item operator+(const Sales_item&, const Sales_item&); class Query { public: Query(const std::string&); std::ostream &display(std::ostream&) const; } Этот код определяет пространство имен cplusplus_primer с четырьмя члена- ми: двумя классами, перегруженным оператором + и функцией. Подобно другим именам, имя пространства имен должно быть уникальным внут- ри той области видимости, в которой оно определено. Пространства имен могут быть определены в глобальной области видимости или внутри другого пространства имен. Они не могут быть определены внутри функций или классов.
Глава 17. Инструменты для крупномасштабных программ 745 После имени пространства имен следует блок объявлений и определений, заклю- ченный в фигурные скобки. В пространство имен может быть помещено любое объ- явление, которое способно присутствовать в глобальной области видимости, вклю- чая классы, переменные (с инициализацией), функции (с их определениями), шаб- лоны и другие пространства имен. Область видимости пространства имен не заканчивается точкой с запятой. Каждое пространство имен является областью видимости Объекты, определенные в пространстве имен, называются членами пространства имен. Подобно любой области видимости, каждое имя в пространстве имен должно относиться к уникальному объекту внутри него. Поскольку разные пространства имен представляют разные области видимости, члены разных пространств имен мо- гут иметь одинаковые имена. К именам, определенным в пространстве имен, другие члены данного простран- ства имен могут обращаться непосредственно, а код вне пространства имен должен указывать пространство имен, в котором определено имя. cpluspluS—primer::Query q = cplusplus_primer::Query("hello"); q.display(cout); Если другое пространство имен (например AddisonWesley) тоже содержит класс TextQuery и этот класс необходимо использовать вместо определенного в пространстве имен cplusplus_primer, приведенный выше код придется изменить следующим образом. AddisonWesley::Query q = AddisonWesley::Query("hello"); q.display(cout); Использование членов пространства имен извне Безусловно, использовать полностью квалифицированные имена членов про- странства имен весьма утомительно. namespace_name::member_name Подобно именам определенным в пространстве имен std, для обеспечения пря- мого доступа можно применить объявление using (раздел 3.1, стр. 102). using cplusplus_primer::Query; Теперь, после объявления using, программа может использовать имя Query не- посредственно, без спецификатора cplusplus_primer. Чтобы упростить доступ, можно применить и другие способы, описанные в разделе 17.2.4 (стр. 752). Пространства имен могут быть разобщены В отличие от других областей видимости, пространство имен может быть опреде- лено в нескольких частях. Пространство имен составляет сумма его отдельно опре- деленных частей. То есть пространство имен способно объединить их вместе. От-
746 Часть V. Дополнительные темы дельные части пространства имен могут быть распределены между несколькими файлами. Определения пространств имен в различных файлах исходного кода также объединяются. Безусловно, обычные ограничения остаются в силе, т.е. имена види- мы только в тех файлах, в которых они объявлены. Так, если одной части простран- ства имен требуется имя, определенное в другом файле, это имя придется объявить. Синтаксис определения пространства имен либо определяет новое пространство имен, либо добавляет часть к уже существующему. namespace имя_пространства_имен { // объявления } Если пространство имен имя_пространства_имен не было определено ра- нее, создается новое пространство имен с этим именем. В противном случае это определение откроет существующее пространство имен и добавит в него данные объявления. Разделение интерфейса и реализации Возможность разобщения определений пространства имен означает, что про- странство имен может состоять из отдельных файлов интерфейса и реализации. Та- ким образом, определения пространств имен могут быть организованы точно так же, как и определения классов или функций. 1. Члены пространства имен, которые являются определениями классов, объявле- ниями функций и объектов, составляющих часть интерфейса класса, могут быть помещены в файлы заголовка. Эти заголовки могут быть подключены в те фай- лы, которые используют эти члены пространства имен. 2. Определения членов пространства имен могут быть помещены в отдельные файлы исходного кода. Организовав пространство имен таким образом, можно также удовлетворить тре- бование, согласно которому различные объекты, невстраиваемые функции, статиче- ские переменные-члены, переменные и т.д. должны быть определены в программе только один раз. Это требование справедливо и для имен, определенных в простран- стве имен. Отделив интерфейс и реализацию, можно гарантировать, что имена функций и другие имена будут определены только один раз и именно это объявле- ние будет многократно использоваться впоследствии. Для представления несвязанных типов в составных пространствах имен можно ис- пользовать отдельные файлы. Определение пространства имен cplusplusprlmer Применив описанный выше подход, отделим интерфейс и реализацию биб- лиотеки cplusplus_primer в нескольких отдельных файлах. Объявление класса Sales_item и связанных с ним функций было помещено в заголовок Sales_item.h (раздел 7.7.4, стр. 290), объявление класса Query — в заголовок Query .h (глава 15, стр. 587) и т.д. Соответствующими файлами реализации будут файлы Sales_item.cc и Query.сс.
Глава 17. Инструменты для крупномасштабных программ 747 //-----Sales_i tem. h---- namespace cplusplus_primer { class Sales_item { /* ... */}; Sales_item operator+(const Sales_item&, const Sales_item&); // объявления остальных функций интерфейса класса Sales_item } //-----Query.h------ namespace cplusplus_primer { class Query { public: Query(const std::strings); std::ostream &display(std: } ; class Query_base { /* ... */}; } ostream&) const; //-----Sales_item.cc------ #include <Sales_item.h> namespace cplusplus_primer { // определения членов класса Sales_item и перегруженных операторов } //-----Query, сс----- #include <Query.h> namespace cplusplus_primer { // определения членов класса Query и связанных функций } Подобная организация программы придает библиотеке свойство модульности, необходимое как разработчикам, так и пользователям. Каждый класс организован в виде двух файлов: интерфейса и реализации. Пользователь одного класса вовсе не должен использовать при компиляции другие классы. Их реализацию можно скрыть от пользователей, разрешив при этом компилировать и компоновать файлы Sales_item.cc и user. сс в одну программу, причем без опасений по поводу воз- никновения ошибок во время компиляции или компоновки. Кроме того, разработ- чики библиотеки могут работать над реализацией каждого класса независимо. В использующую эту библиотеку программу следует подключить все необходи- мые заголовки. Имена в этих заголовках определены внутри пространства имен cplusplus_primer. //-----user.cc------ // определение cplusplus_primer::Sales_itemclass #include "Sales_item.h" int main() { cplusplus_primer::Sales_item transl, trans2; return 0; } Определение членов пространства имен Функции, определенные внутри пространства имен, могут использовать краткую форму имен, определенных в том же пространстве имен.
748 Часть V. Дополнительные темы namespace cplusplus_primer { // члены, определенные внутри пространства имен, могут // использовать имена без уточнений std::istream& operator>>(std::istream& in, Sales_item& s) { } Член пространства имен может быть также определен вне определения про- странства имен. Для этого применяется подход, подобный определению членов класса вне его. Объявление пространства имен должно находиться в области ви- димости, а в определении следует указать пространство имен, которому принад- лежит имя. // члены пространства имен, определенные вне его, должны // использовать полностью квалифицированные имена cplusplus__primer: :Sales_item cplusplus_primer::operator+(const Sales_item& Ihs, const Sales_item& rhs) { Sales_item ret(Ihs); I Это определение очень похоже на определение функции-члена класса вне опре- деления класса. Тип возвращаемого значения и имя функции указываются полно- стью квалифицированными (включая имя пространства имен). Как только встреча- ется полностью определенное имя функции, происходит переход в область видимо- сти пространства имен. Таким образом, для обращения к членам пространства имен в списке параметров и теле функции можно использовать неуточненные имена. Члены несвязанных пространств имен не могут быть определены Хотя член класса пространства имен может быть определен вне его определения, однако существуют некоторые ограничения на месторасположения этого определе- ния. Определение члена может содержать только то пространство имен, которое со- держит его объявление. Например, оператор operator+ может быть определен ли- бо в пространстве имен cplusplus_primer либо в глобальной области видимости. Он не может быть определен в несвязанном пространстве имен. Глобальное пространство имен Имена, объявленные вне какого-либо класса, функции или пространства имен, считаются глобальными или объявленными в глобальной области видимости или внутри глобального пространства имен (global namespace). Глобальное пространство имен объявляется неявно и существует в каждой программе. Каждый файл, в кото- ром определены объекты глобальной области видимости, добавляет их имена в гло- бальное пространство имен. Для обращения к членам глобального пространства имен применяется оператор области видимости (: :). Поскольку глобальное пространство имен является неяв- ным, оно не имеет имени. Форма записи при обращении к члену глобального про- странства имен имеет следующий вид. ::имя члена
Глава 17. Инструменты для крупномасштабных программ 749 Упражнения раздела 17.2.1 Упражнение 17.13. Определите класс исключения для приложения книжного магазина, описанно- го в разделе 17.1.7 (стр. 730), как член пространства имен по имени Bookstore. Упражнение 17.14. Определите класс Saies_item и его операторы внутри пространства имен Bookstore. Определите оператор суммы как передающий исключение. Упражнение 17.15. Напишите программу, которая использует оператор суммы класса Sales_ item и обрабатывает все исключения. Сделайте эту программу членом другого пространства имен, по имени муАрр. Эта программа должна использовать классы исключения, определенные в пространстве имен Bookstore из предыдущего упражнения. 17.2.2. Вложенные пространства имен Вложенное пространство имен (nested namespace) — это вложенная область ви- димости. Имена во вложенных пространствах имен подчиняются обычным прави- лам: имена, объявленные в окружающем пространстве имен, скрыты от одноимен- ных объявлений во вложенном пространстве имен. Имена, определенные внутри вложенного пространства имен, локальны для этого пространства имен. Код во внешних частях пространства имен может обращаться к именам во вложенном про- странстве только при помощи полностью квалифицированных имен. Вложенные пространства имен могут улучшить организацию кода библиотеки, namespace cplusplus_primer { // первое вложенное пространство имен: // определение части библиотеки Query namespace QueryLib { class Query { /* ... */ }; Query operators(const Query&, const Query&); } // второе вложенное пространство имен: II определение части библиотеки Sales_item namespace Bookstore { class Item_base { /* ... */ }; class Bulk_item : public Item_base { /* ... */ }; } } Теперь пространство имен cplusplus_primer содержит два вложенных про- странства имен: QueryLib и Bookstore. Вложенные пространства имен полезны в том случае, когда поставщик библиоте- ки должен предотвратить конфликт имен между ее частями. Имя члена вложенного пространства имен состоит из имени внешнего и имени вложенного пространства имен. Например, имя класса QueryLib, объявленного во вложенном пространстве имен, выглядит следующим образом. cplusplus primer::QueryLib::Query Упражнения раздела 17.2.2 Упражнение 17.16. Организуйте программы, созданные в упражнениях предыдущих глав, та- ким образом, чтобы они использовали собственные пространства имен, т.е. чтобы пространство
750 Часть V. Дополнительные темы имен chapterrefinheritance содержало код класса Query, а пространство имен chapterrefalgs — код программы TextQuery. Используя эту структуру, откомпилируйте КОД примера Query. Упражнение 17.17. В этой книге были определены два разных класса по имени Saies_item: первый, простой, класс был определен и использован в части I, “Основы”, а второй, управляющий, класс для иерархии наследования item_base, определен в разделе 15.8.1 (стр. 630). Определи- те два пространства имен, вложенные в пространство имен cpiuspius_primer, которые спо- собны разграничить определения этих двух классов. 17.2.3. Неименованные пространства имен Пространство имен вполне может быть неименованным. Неименованное про- странство имен (unnamed namespace) — это пространство имен, которое опреде- лено без имени. Определение неименованного пространства имен начинается с ключевого слова name space, за которым следует блок объявлений, заключенных в фигурные скобки. Неименованные пространства имен не похожи на другие пространствам имен; определе- ние неименованного пространства имен локально для специфического файла и никогда не охватывает несколько файлов. Неименованное пространство имен может быть разобщено внутри одного файла, однако на несколько файлов оно не распространяется. Каждый файл имеет собст- венное неименованное пространство имен. Неименованные пространства имен используются для объявления тех объектов, которые являются локальными для данного файла. Переменные, определенные в неименованном пространстве имен, создаются при запуске программы и существуют до завершения ее выполнения. Имена, определенные в неименованном пространстве имен, используются непо- средственно, поскольку у этого пространства нет имени, чтобы его можно было ука- зать. Для обращения к членам неименованных пространств имен ненужно использо- вать оператор области видимости. Имена, определенные в неименованном пространстве имен, видимы внутри толь- ко того файла, который его содержит. Если неименованное пространство имен со- держит еще один файл, их пространства имен будут совершенно независимы. В обо- их неименованных пространствах имен могут быть определены одинаковые имена, относящиеся к разным объектам. Имена, определенные в неименованном пространстве имен, находятся в той же области видимости, в которой это пространство определено. Если неименованное пространство имен определено в наиболее удаленной области видимости файла, его имена не будут зависеть от имен, определенных в глобальной области видимости. int i; // глобальное объявление переменной i namespace { int i ; } // ошибка неоднозначности. Переменная определена как глобально, так // и во невложенном, неименованном пространстве имен i - 10;
Глава 17. Инструменты для крупномасштабных программ 751 Неименованное пространство имен, подобно любому другому пространству имен, может быть вложено внутрь другого пространства имен. В этом случае к его чле- нам следует обращаются обычным способом, указывая имена окружающих про- странств имен, namespace local { namespace { // ок: имя i определено во вложенном неименованном пространстве // имен и не зависит о глобального имени i local::i = 42; Если неименованное пространство имен определено в заголовке, имена определенных в нем локальных объектов будут доступны в каждом подключившем его файле. t/ма^ Во всем остальном члены неименованного пространства имен являются обычны- ми объектами программы. Неименованные пространства имен приходят на смену статическим файловым объектам , 3,о введения пространств имен в стандарт C++, чтобы сделать имена локальными для файла, их приходилось объявлять статическими (static). Применение статических файловых объектов (file static) унаследовано от языка С. В языке С объявленный ста- тическим глобальный объект был невидим вне того файла, в котором он объявлен. t/M3^ В соответствии со стандартом C++, применение объявлений статических файловых объектов осуждается. Эта устаревшая возможность, вероятно, не будет поддерживаться в последующих выпусках. Следует избегать применения статических файловых объектов и использовать вместо них неименованные пространства имен. Упражнения раздела 17.2.3 Упражнение 17.18. Почему необходимо создавать в программах собственные пространства имен? Когда имеет смысл использовать неименованное пространство имен? Упражнение 17.19. Предположим, имеется следующее объявление оператора operator*, яв- ляющегося ЧЛенОМ ВЛОЖенНОГО пространства Имен cplusplus_primer: :MatrixLib. namespace cplusplus_primer { namespace MatrixLib { class matrix { /* ... */ }; matrix operator* (const matrix &, const matrix &); } Как определить этот оператор в глобальной области видимости? Предоставьте только прототип определения этого оператора.
752 Часть V. Дополнительные темы 17.2.4. Использование членов пространства имен Обращение к члену пространства имен в формате имя_пространства_имен: : имя_члена является чересчур громоздким, особенно когда имя пространства имен слишком длинное. К счастью, существуют способы, которые облегчают использова- ние имен членов пространства имен. Один из этих способов, объявление using (раздел 3.1, стр. 102), уже использовался в программах, приведенных выше. Другие способы, псевдонимы пространств имен и директивы using, будут описаны в этом разделе. Файлы заголовка не должны содержать ни директив using, ни объявлений using, иначе, как внутри функций или других областей видимости. Заголовок, который со- комеидуем держит директиву или объявление using в своей области видимости верхнего уровня, введет это имя в файл, который подключит заголовок. Заголовки должны определять только те имена, которые являются частью его интерфейса, но не те, ко- торые используются в его собственной реализации. Объявления using (напоминание) В программах этой книги, использующих имена из стандартной библиотеки, под- разумевается, что предварительно было сделано соответствующее объявление using (using declaration). map<string, vector< pai s i z e_t, s i z e_t word_map; Приведенный выше код подразумевает, что ранее были сделаны следующие объ- явления using, using std::map; using std::pair; using std::size_t; using std::string; using std::vector; Объявления using представляют только один элемент пространства имен за раз. Это позволяет вполне однозначно указать имена, используемые в программе. Область видимости объявления using Имена, представленные в объявлении using, подчиняются обычным правилам области видимости. Имя видимо от точки объявления using и до конца области ви- димости, в которой оно объявлено. Объекты внешней области видимости скрывают одноименные объекты внутренней области. Короткие имена могут использоваться внутри только той области видимости, в которой они объявлены, а также в областях видимости, вложенных в нее. По завер- шении области видимости, следует использовать полные имена. Объявление using может присутствовать в глобальной и локальной области ви- димости, а также в области видимости пространства имен. Объявление using в об- ласти видимости класса ограничено именами, определенными в базовом классе оп- ределяемого класса.
Глава 17. Инструменты для крупномасштабных программ 753 Псевдонимы пространства имен Псевдоним пространства имен (namespace alias) применяется в качестве коротко- го синонима имени пространства имен. Например, длинное имя пространства имен может иметь следующий вид. namespace cplusplus_primer { /* ... */ }; Ему может быть назначен более короткий синоним следующим образом, namespace primer = cplusplus primer; Объявление псевдонима пространства имен начинается с ключевого слова name space, за которым следует имя псевдонима пространства имен (короткое), со- провождаемое знаком =, первоначальное имя пространства имен и точка с запятой. Если имя первоначального пространства имен еще не было определено как про- странство имен, произойдет ошибка. Псевдоним пространства имен может быть также применен к вложенному про- странству имен. cplusplus—primer::QueryLib::Query tq; Приведенную выше запись можно упростить использовав псевдоним для про- странства имен cplusplus_primer: .- QueryLib. namespace Qlib - cplusplus_primer::QueryLib; Qlib::Query tq; Пространство имен может иметь множество синонимов, или псевдонимов. Все псевдо- нимы и первоначальное имя пространства имен в применении равнозначны. Директива using Подобно объявлению using, директива using (using directive) позволяет ис- пользовать сокращенную форму имени пространства имен. Однако, в отличие от объявления using, здесь не сохраняется контроль над видимостью имен, поскольку все они видимы. Формат директивы using Директива using начинается с ключевого слова using, за которым следует клю- чевое слово name space, сопровождаемое именем пространства имен. Если имя про- странства не было определено ранее, произойдет ошибка. Директива using делает все имена указанного пространства имен видимыми без уточнения. Краткие формы имен применимы от директивы using и до конца облас- ти видимости, в которой находится директива using. Директива using может присутствовать в пространстве имен, функции или блоке области видимости. Однако она не может присутствовать в области видимости класса. Весьма соблазнительно применять директиву using всегда, но это вернет все про- блемы, ликвидируемые пространствами имен, такие как конфликт имен при использо- вании нескольких библиотек.
754 Часть V. Дополнительные темы Директива using и область видимости Область видимости имен, указанных директивой using, гораздо сложнее, чем в случае объявления using. Объявление using помещает имя непосредственно в ту же область видимости, в которой находится само объявление using. Объявление using подобно локальному псевдониму для члена пространства имен. Поскольку объявление ограничено, возможность конфликтов минимизирована. Директива using не объявляет локальные псевдонимы для имен членов пространства имен. Вместо этого она поднимает члены пространства имен в ближайшую область ви- димости, которая содержит и пространство имен, и саму директиву using. Рассмотрим самый простой случай. Предположим, что в глобальной области ви- димости определено пространство имен А и функция f (). Если функция f () имеет директиву using для пространства имен А, функция f () будет вести себя так, как будто имена пространства имен А присутствовали в глобальной области видимости до определения функции f (). // пространство имен А и функция f() // области видимости определены в глобальной namespace А { int i, j; } void f() using namespace A; // переводит имена из области видимости II А в глобальную область видимости cout << i * j « endl; // использует i и j из пространства / / имен А Директива using особенно полезна в файлах реализации для самих пространств имен. Пример директив us ing Рассмотрим следующий пример. namespace blip { int bi = 16, bj = 15, bk = 23; // другие объявления } int bj = 0; /I ok: bj внутри пространства имен blip скрыта void manip() { // применение директивы "добавляет" имена пространства // имен blip в глобальную область видимости using namespace blip; // конфликт между ::bj и blip::bj II обнаруживается только при использовании bj ++bi; // присваивает blip::bi значение 17 ++bj; // ошибка: неоднозначность // глобальная bj или blip::bj? ++::bj; // ok: присваивает глобальной bj значение 1 ++blip::bj; // ok: присваивает blip::bj значение 16
Глава 17. Инструменты для крупномасштабных программ 755 int bk = 97; // локальная bk скрывает blip::bk ++bk; // присваивает локальной bk значение 98 } Директива using в функции manip () делает все имена пространства имен blip доступными непосредственно. То есть функция manip () может обращаться к этим членам используя краткую форму имен. Члены пространства имен blip выглядят так, как будто они были определены в одной области видимости. Если пространство имен blip определено в глобальной области видимости, его члены будут выглядеть так, как будто они объявлены в гло- бальной области видимости. Поскольку имена находятся в разных областях видимо- сти, локальные объявления внутри функции manip () могут скрыть некоторые из имен членов пространства имен. Локальная переменная bk скрывает член простран- ства имен blip: :bk. Обращение к имени bk внутри функции manip () не приведет к неоднозначности, оно будет понято как обращение к локальной переменной bk. Имена пространства имен вполне могут войти в конфликт с другими именами, определенными в окружающей области видимости. Например, член b j пространства имен blip в функции manip () выглядит так, как будто он был объявлен в глобаль- ной области видимости. Однако в глобальной области видимости существует другой объект по имени b j. Такие конфликты вполне разрешимы, для этого достаточно яв- но указать версию необходимого имени. Следовательно, использование имени bj внутри функции manip () неоднозначно: оно относится и к глобальной переменной, и к члену пространства имен blip. В подобных случаях, чтобы точно указать необходимое имя, следует использо- вать оператор области видимости. Чтобы получить переменную b j, определенную в глобальной области видимости, используется форма : : b j, а для определенной в пространстве имен blip — полностью квалифицированное имя blip: :bj. Упражнения раздела 17.2.4 Упражнение 17.20. Объясните различия между объявлением using и директивой using. Упражнение 17.21. Рассмотрим следующий пример кода, namespace Exercise { int ivar = 0; double dvar = 0; const int limit = 1000; } int ivar = 0; // позиция 1 void manip() { // позиция 2 double dvar = 3.1416; int iobj - limit + 1; ++ivar; ++::ivar; } Каков результат объявлений и выражений в этом примере кода, если объявления using для всех членов пространства имен Exercise будут находиться в местах, помеченных комментариями позиция 1 и позиция 2? Ответьте на тот же вопрос, но в случае применения директивы using вместо объявлений using.
756 Часть V. Дополнительные темы Внимание! Избегайте директив using Простота применения директив using, переводящих в область видимости все имена из пространства имен, обманчива. Единственный оператор делает видимыми имена всех членов пространства имен. Хоть этот подход может показаться простым, он соз- дает немало проблем. Если в приложении использовано много библиотек и директива using сделает видимыми имена, определенные внутри них, то вновь возникнет про- блема загромождения глобального пространства имен. Кроме того, не исключено, что при выходе новой версии библиотеки вполне работоспо- собная в прошлом программа перестанет компилироваться. Причиной этой проблемы может быть конфликт имен новой версии с именами, которые использовались прежде. Еще одна вызванная директивой using проблема неоднозначности обнаруживается только в момент применения. Столь позднее обнаружение означает, что конфликты могут возникать значительно позже применения определенной библиотеки. То есть при использовании в программе новой библиотеки, могут возникнуть необнаружен- ные ранее конфликты. Поэтому лучше не полагаться на директиву using и использовать объявление using для каждого конкретного имени пространства имен, используемого в программе. Это уменьшит количество имен, вводимых в пространство имен. Кроме того, ошибки не- однозначности, причиной которых является объявление using, обнаруживаются в точке объявления, а это существенно упрощает их поиск. 17.2.5. Классы, пространства имен и области видимости Как уже упоминалось, пространства имен — это отдельные области видимости. Как и в любой другой области видимости, здесь имена видимы начиная с момента их объявления. Имена остаются видимыми на протяжении всех вложенных областей видимости, до конца того блока, в котором они объявлены. Поиск имен, используемых внутри пространства имен, происходит согласно обычным правилам поиска в языке C++: сначала во внутренней, а затем во внешней области видимости. Имя, используемое внутри пространства имен, может быть оп- ределено в одном из окружающих пространств имен, включая глобальное простран- ство имен. Однако учитываются только те имена, которые были объявлены перед точкой использования в блоках, которые все еще открыты. namespace А { int 1 ; namespace В { int i; // скрывает A::i внутри В int j ; int f1() { int j; // j локальна для fl() и скрывает A::B::j return i; // возвращает B::i } } // пространство имен В закрыто и его имена больше не видимы int f 2 () { return j; // ошибка: j не определена } int j = i; // инициализируется значением A::i }
Глава 17. Инструменты для крупномасштабных программ 757 Поиск имен, используемых в определении члена класса, происходит как обычно, но с одним важным отличием: если имя не локально для функции-члена, сначала предпринимается попытка его поиска среди членов класса, и только затем просмат- риваются внешние области видимости. Как уже было продемонстрировано в разделе 12.3 (стр. 473), при определении членов внутри класса могут использоваться имена, определения которых располо- жены ниже. Например, конструктор, определенный внутри тела класса, может ини- циализировать переменные-члены, даже если их объявления расположены после оп- ределения конструктора. Когда имя используется в области видимости класса, его поиск проходит сначала внутри самого члена класса, затем внутри класса и всех ба- зовых классов. Только исчерпав иерархию классов, процесс поиска распространяет- ся на окружающие области видимости. Когда класс расположен внутри пространст- ва имен, аналогичный поиск происходит в следующем порядке: содержимое члена класса, затем сам класс (включая базовые классы), окружающие области видимости, среди которых могут быть пространства имен. namespace А { int 1 ; int k; class Cl { public: Cl() : i(0) j(0) { } // ok: инициализирует Cl::i и Cl::j int fl() { return k; // возвращает A::k return h; // ошибка: h не определена int i; // скрывает A::i внутри Cl int j ; }; int h = i; // инициализируется значением A::i } // член f3() определен вне класса Cl и вне пространства имен А int А::С1::f3() { return h; // ok: возвращает A::h } За исключением определений членов класса, просмотр области видимости всегда идет снизу вверх: имя должно быть объявлено прежде его применения. Следова- тельно, оператор return функции f 2 () не будет откомпилирован. Он попытается обратиться к имени h из пространства имен А, но там оно еще не определено. Если бы это имя h было определено в пространстве имен А прежде определения класса С1, его использование было бы вполне допустимо. Аналогично, использование имени h внутри функции f 3 () вполне допустимо, потому функция f 3 () определена уже по- сле определения А: : h. веки» Порядок, в котором области видимости исследуются при поиске имени, определяется по полностью квалифицированному имени функции. Полностью квалифицированное имя указывает в обратном порядке области видимости, в которых происходит поиск.
758 Часть V. Дополнительные темы Спецификаторы А: :С1: :f3 () указывают обратный порядок, в котором про- сматриваются области видимости класса и пространств имен. Первая область види- мости — это функция f 3 (). Далее следует область видимости ее класса С1. Область видимости пространства имен А просматривается в последнюю очередь, перед пере- ходом к области видимости, содержащей определение функции f 3 (). Зависимый от аргумента поиск и параметры типа класса Рассмотрим следующую простую программу. std::string s ; // ок: вызов std::getline(std::istream&, const std::string&) getline(std::cin, s); Программа использует тип std: : string, однако без полностью квалифициро- ванного имени функции getline (). Почему же эту функцию можно использовать без спецификатора std: : или объявления using? Правило, согласно которому имена пространств имен скрыты, имеет одно важное исключение. Функции, включая перегруженные операторы, получающие параметры типа класса (или 1 указатели либо ссылки на класс) и определенные в том же пространстве имен, что и сам класс, видимы тогда, когда объект (или указатель либо ссылка) класса используется в ка- честве аргумента. getline(std::cin, s); Когда компилятор встречает приведенный выше вызов функции getline О, он ищет соответствующую функцию в текущей области видимости, области видимости, окружающей вызов getline (), и в пространстве (пространствах) имен, в которых определены типы cin и string. Следовательно, поиск продолжается в пространстве имен std, где и обнаруживается функция getline (), определенная в классе string. Причина, по которой функции становятся видимыми, если они имеют параметр типа класса, заключается в том, что это позволяет использовать функцию, которая не является членом класса, но концептуально является частью интерфейса класса без специального объявления using. Эта способность особенно полезна для функ- ций-операторов. Рассмотрим, например, следующую простую программу. std::string s; cin >> s; He принимая во внимание описанное выше исключение из правила поиска, это код пришлось бы написать следующим образом, using std::operator>>; // должно позволить cin >> s std::operator>>(std::cin, s); // ok: явно использует std::>> Эти формы записи не только громоздки, они усложнили бы использование строк и библиотеки IO. Неявные дружественные объявления и пространства имен Напомним, что на момент, когда класс объявляет функцию дружественной (раз- дел 12.5, стр. 493), объявление функции необязательно должно быть видимым. Если объявление функции еще не видимо, результатом объявления ее дружественной
Глава 17. Инструменты для крупномасштабных программ 759 окажется помещение объявления данной функции или класса в окружающую область видимости. Если класс определен внутри пространства имен, необъявленная дружест- венная функция рассматривается как принадлежащая тому же пространству имен. namespace А { class С { friend void f(const C&) ; // делает функцию f() членом // пространства имен А } ; } Поскольку дружественная функция получает аргумент типа класса и неявно объ- явлена в том же пространстве имен, что и класс, она применяется без явного специ- фикатора пространства имен. // f2() определена в глобальной области видимости void f2() } 17.2.6. Перегрузка и пространства имен Как уже упоминалось, каждое пространство имен обладает собственной областью видимости. Как следствие, функции, являющиеся членами двух разных пространств имен, не перегружают друг друга. Однако каждое пространство имен может содер- жать набор перегруженных функций-членов. В общем случае поиск функции (раздел 7.8.2, стр. 295) внутри пространства имен осуществляется уже знакомым способом. 1. Выявление набора функций-кандидатов. Функция считается кандидатом, если ее объявление видимо в момент обращения и если ее имя совпадает с использо- ванным при вызове. 2. Выявление подходящих функций из набора кандидатов. Функция считается подходящей, если она имеет то же количество параметров, которое было исполь- зовано в обращении, а также если каждый из аргументов может быть приведен к типу соответствующего параметра. 3. Выбор наилучшего соответствия из набора подходящих функций и создание ко- да вызова этой функции. Если набор подходящих функций пуст, обращение счи- тается ошибкой, поскольку наилучшее соответствие не найдено. Если набор под- ходящих функций не пуст, но наилучшее соответствие не найдено, обращение признается неоднозначным. Функции-кандидаты и пространства имен Пространства имен могут повлиять на поиск функции двумя способами. Один из них вполне очевиден: объявление или директива using может добавить функцию в набор кандидатов. Второй способ менее очевиден. Как известно из предыдущего раздела, поиск имен функций, имеющих один или несколько параметров типа класса, осуществляется также и в пространстве имен, в котором определен класс каждого параметра. Это правило влияет также и на выбор
760 Часть V. Дополнительные темы кандидатов. Каждое пространство имен, в котором определен класс, используемый в качестве типа параметра (а также те, в которых определены его базовые классы), участвуют в поиске функции-кандидата. Все функции этих пространств имен, обла- дающие именем, совпадающим с использованным при вызове, будут добавлены в набор кандидатов. Эти функции будут добавлены даже тогда, когда они не видимы в точке обращения. То есть в набор кандидатов будут добавлены функции с именами, совпадающими с использованными при вызове. namespace NS { class Item_base { /* ... */ }; void display(const Item_base&) { } } // базовый класс Bulk_item объявлен в пространстве имен NS class Bulk_item : public NS::Item_base { }; int main() { Bulk_item bookl; display(bookl); return 0; } Аргумент bookl функции display () имеет тип класса Bulk_item. Функция- ми-кандидатами для этого обращения к функции di splay (), будут не только функции с объявлениями, видимыми на момент вызова, но и те, которые объявлены в пространстве имен класса Bulk_item и его базового класса Item_base. Таким образом, функция display (const Item_base&), объявленная в пространстве имен NS, будет добавлена в набор функций кандидатов. Перегрузка и объявления using Объявление using объявляет имя. Как уже упоминалось в разделе 15.5.3 (стр. 624), нет никакого способа написать объявление using так, чтобы оно относи- лось лишь к определенному объявлению функции. using NS::print(int); // ошибка: нельзя указать список параметров using NS::print; // ok: в объявлении using указывают I/ только имена Если функция внутри пространства имен перегружена, объявление using для ее имени сделает доступными все версии функции с данным именем. Если в простран- стве имен NS функция print () существует в версиях для типа int и типа double, объявление using NS : : print; сделает обе функции видимыми в текущей области видимости. Объявление using подключает все версии перегруженной функции для того, чтобы не нарушить интерфейс пространства имен. Ведь предоставляя разные версии функции автор библиотеки имел на это весомую причину. Разрешив пользователям игнорировать некоторые (но не все) функции из набора перегруженных версий, можно получить на удивление странное поведение программы. Функции, предоставленные объявлением using, перегружают любые другие объявления одноименных функций, уже находящихся в данной области видимости. Если объявление using включает функцию в ту область видимости, в которой уже есть функция с тем же именем и тем же списком параметров, объявление using окажется ошибочным. В противном случае объявление using создаст дополнитель-
Глава 17. Инструменты для крупномасштабных программ 761 ный перегруженный экземпляр данной функции. В результате набор функций- кандидатов увеличится. Перегрузка и директивы using Директива using переводит члены пространства имен в окружающую область видимости. Если имя функции пространства имен совпадает с именем функции той области видимости, в которую помещено пространство имен, эта функция будет до- бавлена в набор перегруженных функций. namespace libs_R_us { extern void print(int); extern void print(double); } void print(const std::string &); // директива using: using namespace libs_R_us; // директива using добавила имена в набор функций-кандидатов // для обращения print: // print (int) из libs_R_us // print(double) из libs_R_us II print(const std::string &) объявлена явно void fooBar(int ival) { print("Value: "); // вызов глобальной print(const string &) print(ival); // вызов libs_R_us::print(int) ) Перегрузка при нескольких директивах using Если в коде присутствует несколько директив using, частью набора функций- кандидатов станут соответствующие функции из каждого пространства имен. namespace AW { int print(int); } namespace Primer { double print(double); } // директивы using: // формирует набор перегруженых функций из разных пространств имен using namespace AW; using namespace Primer; long double print(long double); int main() { print(1); // вызов AW: :print(int) print(3.1); // вызов Primer::print(double) return 0; } Набор перегруженных функций print () в глобальной области видимости со- держит функции print (int), print (double) и print (long double). Все они составят набор перегруженных функций, рассматриваемых при вызове функции print () внутри функции main() даже в том случае, если первоначально эти функции были объявлены в различных областях видимости пространства имен.
762 Часть V. Дополнительные темы Упражнения раздела 17.2.6 Упражнение 17.22. С учетом следующего кода укажите, какие из функций (если они есть) соот- ветствуют обращению к функции compute (). Перечислите функции-кандидаты и подходящие функции. Какая последовательность преобразований типов (если есть) будет применена к аргу- менту, чтобы он точно соответствовал параметру каждой подходящей функции? namespace primerLib { void compute(); void compute(const void *); } using primerLib::compute; void compute(int); void compute(double, double = 3.4); void compute(char*, char* = 0); int main() { compute(0); return 0; } Что произойдет в случае, если объявления using будут расположены в функции main () пе- ред обращением к функции compute () ? Ответьте на те же вопросы, что и в предыдущем уп- ражнении. 17.2.7. Пространства имен и шаблоны Объявление шаблона внутри пространства имен влияет на объявление его спе- циализированных версий (раздел 16.6, стр. 703): явное объявление специализиро- ванной версии шаблона должно располагаться в том пространстве имен, в котором определен общий шаблон. В противном случае имя специализированной версии шаблона будет отличаться от имени общего. Существует два способа определения специализированной версии: первый подразумевает повторное открытие пространства имен и добавление определения специализированной версии шаблона. Это сработает потому, что определения пространств имен могут быть разобщены. В качестве альтернативы, специализиро- ванную версию шаблона можно определить подобно любому другому члену про- странства имен вне определения пространства имен. То есть при определении спе- циализированной версии шаблона используется полностью квалифицированное имя шаблона, включающее имя пространства имен. Чтобы предоставить собственные специализированные версии шаблонов, определен- ных в пространстве имен, необходимо удостовериться, что их определения находятся в том же пространстве имен, что и исходное определение шаблона.
Глава 17. Инструменты для крупномасштабных программ 763 17.3. Множественное и виртуальное наследование Большинство приложений языка C++ используют открытое наследование одного базового класса. Но иногда одиночного наследования либо недостаточно, либо оно не соответствует модели предметной области, либо излишне усложняет структуру. В этих случаях применяется множественное наследование (multiple inheritance). Множественное наследование — это способность получить класс как производный непосредственно от нескольких базовых классов. Полученный в результате класс наследует свойства всех своих базовых классов. Несмотря на простоту концепции, одновременное использование нескольких базовых классов может создать достаточ- но много сложностей как на этапе проектирования, так и на этапе реализации. 17.3.1. Множественное наследование В этом разделе использован пример иерархии из животного мира. Животные имеют собственные имена, например Ling-ling (Линг-линг), Mowgli (Маугли) и Balou (Балу). Каждое животное можно отнести к определенному виду; Линг-линг, например, это гигантская панда. Виды в свою очередь относятся к определенным се- мействам. Гигантская панда принадлежит к семейству медведей. Каждое семейство в свою очередь является членом сообщества животного мира. Каждый уровень абстракции содержит разнообразные данные и функции. Опре- делим класс ZooAnimal как абстрактный, призванный содержать информацию, ко- торая является общей для всех животных и предоставляет открытый интерфейс. Класс Bear (Медведь) будет содержать информацию, которая является специфиче- ской для семейства медведей и т.д. Кроме классов животных, здесь можно определить дополнительные классы, ко- торые инкапсулируют различные абстракции, например, животных, подвергающих- ся опасности. В данной реализации класс Panda (Панда) будет получен в результате множественного наследования от классов Bear и Endangered (Подвергающийся опасности). Определение класса как производного от нескольких классов class Bear : public ZooAnimal { } Чтобы обеспечить множественное наследование, приведенный выше список наследования следует расширить, применив разделяемый запятыми список базо- вых классов. class Panda : public Bear, public Endangered { } Для каждого из своих базовых классов, производный класс (явно или неявно) определяет уровень доступа: открытый (public), защищенный (protected) или закрытый (private). Подобно одиночному наследованию, класс может быть ис- пользован в качестве базового класса при множественном наследованием только по-
764 Часть V. Дополнительные темы еле его определения. Язык C++ не налагает никаких ограничений на количество ба- зовых классов, из которых может быть получен производный класс. Однако базовый класс может присутствовать в списке наследования только один раз. При множественном наследовании классы наследуют состояние каждого из базовых классов При множественном наследовании объекты производного класса содержат внут- ренние объекты (раздел 15.2.3, стр. 595) каждого из его базовых классов. Рассмотрим следующую запись. Panda ying_yang("ying_yang"); Объект ying_yang состоит из объекта класса Bear (который в свою очередь содержит объект базового класса ZooAnimal), объекта класса Endangered и не- статических переменных-членов (если они есть), объявленных внутри класса Panda (рис. 17.2). ZooAnimal ) Bear j ( Panda Endangered ) Рис. 17.2. Множественное наследование в иерархии класса Panda Конструкторы производного класса инициализируют все объекты базовых классов Создание объекта производного класса подразумевает создание и инициализа- цию внутренних объектов всех его базовых классов. В случае одиночного наследова- ния, из (единого) базового класса, в списке инициализации конструктора производ- ного класса можно передать значения для любого количества его базовых классов (раздел 15.4.1, стр. 611). // явная инициализация объектов обоих базовых классов Panda::Panda(std::string name, bool onExhibit) : Bear(name, onExhibit, "Panda"), Endangered(Endangered::critical) { } // неявное применение стандартного конструктора класса Bear для II инициализации его внутреннего объекта Panda::Panda() : Endangered(Endangered::critical) { } Порядок создания Список инициализации конструктора позволяет задать только значения, которые используются для инициализации объектов базового класса, но не порядок, в кото- ром они будут созданы. Вызов конструкторов базового класса осуществляется в том
Глава 17. Инструменты для крупномасштабных программ 765 порядке, в котором они расположены в списке наследования. Для класса Panda инициализация объектов базового класса осуществляется в следующем порядке. 1. Внутренний объект класса ZooAnimal, самого первого базового класса иерар- хии класса Panda, непосредственного базового для класса Bear. 2. Внутренний объект класса Bear, первого непосредственного базового класса для класса Panda. 3. Внутренний объект класса Endangered, второго непосредственного базового класса для класса Panda. (Класс Endangered не имеет базового класса.) 4. Внутренний объект класса Panda. Сначала инициализируются члены самого класса Panda, а затем выполняется тело его конструктора. На порядок вызова конструкторов никак не влияет ни наличие базового класса в списке 1 инициализации конструктора, ни порядок, в котором базовые классы указаны внутри это- UfnKyl го списка. Например, в стандартном конструкторе класса Panda стандартный конструктор класса Bear вызывается неявно; в списке инициализации конструктора он отсутст- вует. Даже в этом случае стандартный конструктор класса Bear окажется применен до вызванного явно конструктора класса Endangered. Порядок удаления Деструкторы всегда выполняются в порядке, обратном вызову конструкторов. В данном примере порядок вызова деструкторов будет следующим: - Panda (), -Endangered (),-Bear(), -ZooAnimal (). Упражнения раздела 17.3.1 Упражнение 17.23. Какие (если они есть) из следующих объявлений являются ошибочными. Объ- ясните, почему. (a) class CADVehicle : public CAD, Vehicle { ... }; (b) class DoublyLinkedList: public List, public List { ... }; (c) class iostream: public istream, public ostream { ... }; Упражнение 17.24. Рассмотрим следующую иерархию класса, в которой у каждого класса опре- делен стандартный конструктор. class А { ... } ; class В : public А { ... } ; class С : public В { ... }; class X { ... }; class Y { ... }; class Z : public X, public Y { ... }; class MI : public C, public Z { ... }; Каков порядок выполнения конструкторов при создании следующего объекта? MI mi;
766 Часть V. Дополнительные темы 17.3.2. Преобразования и несколько базовых классов При одиночном наследовании, указатель или ссылка на производный класс могут быть автоматически преобразованы в указатель или ссылку на базовый класс. Это справедливо и для множественного наследования. Указатель или ссылка на произ- водный класс могут быть преобразованы в указатель или ссылку на любой из его ба- зовых классов. Например, указатель или ссылка на класс Panda может быть преоб- разована в указатель или ссылку на класс ZooAnimal, Bear, или Endangered. // функции, получающие ссылки на класс, базовый для класса Panda void print(const Bear&); void highlight(const Endangered&); ostream& operator«(ostream&, const ZooAnimal&) ; Panda ying yang("ying yang"); // создает объект класса Panda print (ying__yang) ; // передает объект класса Panda как // ссылку на объект класса Bear highlight (уing__yang) ; // передает объект класса Panda как // ссылку на объект класса Endangered cout << ying yang << endl; // передает объект класса Panda как // ссылку на объект класса ZooAnimal Возможность неоднозначных преобразований при множественном наследовании гораздо выше. Во время преобразования объекта производного класса компилятор даже не пытается как-то различать базовые классы. Преобразования в каждый из ба- зовых классов происходят одинаково успешно. Рассмотрим, например, перегружен- ную версию функции print О . void print(const Bear&); void print(const Endangered&); Неуточненный вызов функции print () для объекта Panda. Panda ying_yang("ying_yang"); print(ying_yang); // ошибка: неоднозначность Неоднозначность обращения приводит к ошибке во время компиляции. Упражнения раздела 17.3.2 Упражнение 17.25. Рассмотрим следующую иерархию класса, в каждом из базовых классов ко- торого определен стандартный конструктор. class X { ... } ; class А { ... } ; class В : public А { ... }; class С : private В { ... }; class D : public X, public C { ... }; Какое из следующих преобразований (если оно есть) недопустимо? D *pd = new D; (а) X *рх = pd; (с) В *pb = pd; (b) А *ра = pd; (d) С *рс = pd; Виртуальные функции при множественном наследовании Чтобы продемонстрировать влияние множественного наследования на работу механизма виртуальных функций, предположим, что рассматриваемые классы со- держат виртуальные функции-члены, перечисленные в табл. 17.2.
Глава 17, Инструменты для крупномасштабных программ 767 Таблица 17.2. Виртуальные функции иерархии классов ZooAnlmal/Endangered Функция Класс, определяющий собственную версию print ZooAnimal::ZooAnimal Bear::Bear Endangered::Endangered Panda::Panda highlight Endangered::Endangered Panda::Panda toes Bear::Bear Panda::Panda cuddle Panda::Panda Деструктор ZooAnimal: : ZooAnimal Endangered::Endangered Поиск на основании типа указателя или ссылки Как и при одиночном наследовании, указатель или ссылка на базовый класс при- меняется для доступа только к тем членам, которые определены (или унаследованы) в базовом классе. Он не позволяет обращаться к членам, определенным в производ- ном классе. Когда класс происходит от нескольких базовых классов, вполне очевидных от- ношений между этим базовыми классами может и не быть. Использование указателя на один из базовых классов не предоставит доступ к членам другого базового класса. В качестве примера можно рассмотреть применение указателя или ссылки на класс Bear, ZooAnimal, Endangered и Panda для доступа к объекту класса Panda. Класс используемого указателя определяет, какие из функций будут доступны. Если используется указатель класса ZooAnimal, для применения будут пригодны только те функции, которые определены в этом классе. Части интерфейса класса Panda, специфические для классов Bear, Panda и Endangered, окажутся недоступны. Аналогично, указатель или ссылка на класс Bear применимы только для доступа к членам классов Bear и ZooAnimal; а указатель или ссылка на класс Endangered ограничены лишь членами класса Endangered. Bear *pb = new Panda("ying_yang"); pb->print(cout); // ok: Panda::print(ostream&) pb->cuddle(); // ошибка: не является частью интерфейса Bear pb->highlight(); // ошибка: не является частью интерфейса Bear delete pb; // ok: Panda::-Panda() Если бы объект класса Panda был присвоен при помощи указателя ZooAnimal, этот набор обращений сработал бы точно так же. Когда объект класса Panda используется при помощи указателя или ссылки на класс Endangered, части объекта класса Panda, специфические для классов Panda и Bear, становятся недоступными. Endangered *ре = new Panda("ying_yang"); pe->print(cout); // ok: Panda::print(ostream&) pe->toes(); // ошибка: не является частью интерфейса // класса Endangered pe->cuddle(); // ошибка: не является частью интерфейса
768 Часть V. Дополнительные темы pe->highlight() delete ре; // класса Endangered // ok: Endangered::highlight() // ok: Panda::-Panda() Выявление используемого виртуального деструктора С учетом того, что деструкторы всех корневых базовых классов правильно опре- делены как виртуальные, работа виртуального деструктора будет корректна незави- симо от типа указателя, при помощи которого удаляется объект. // каждый указатель указывает на объект класса Panda delete pz; // pz является ZooAnimal * delete pb; II pb является Bear* delete pp; II PP является Panda * delete pe; II pe является Endangered Предположим, что каждый из этих указателей указывает на объект класса Panda, а порядок вызова деструкторов в каждом случае останется тем же. Порядок вызова деструкторов противоположен порядку вызова конструкторов: для деструктора класса Panda применяется механизм вызова виртуальных функций. Запуск дест- руктора Panda приведет к последовательному вызову деструкторов классов Endangered, Bear и ZooAnimal. Упражнения раздела 17.3.2 Упражнение 17.26. На (стр. 768) представлен ряд обращений, выполненных с использованием указателя на класс Bear, который на самом деле указывал на объект класса Panda. При этом отмечалось, что если бы это был указатель на класс zooAnimal, поведение обращений оказа- лось бы аналогичным. Объясните, почему. Упражнение 17.27. Предположим, что существует два базовых класса, Basel и Base2, в каж- дом из которых определена виртуальная функция-член по имени print () и виртуальный дест- руктор. От этих базовых классов были получены следующие классы, в каждом из которых функция print () переопределена. class Dl : public Basel { /* ... */ }; class D2 : public Base2 { /* ... */ }; class MI : public Dl, public D2 { /* ... */ }; Используя следующие определения, укажите, какая из функций используется при каждом обращении. Basel *pbl - new MI; Base2 *pb2 = new MI; Dl *pdl = new MI; D2 *pd2 = new MI; (a) pbl->print(); (b) pdl->print(); (c) pd2->print(); (d) delete pb2; (e) delete pdl; (f) delete pd2; Упражнение 17.28. Напишите определения классов согласно табл. 17.2. 17.3.3. Управление копированием при множественном наследовании Почленная инициализация, присвоение и удаление, описанные в главе 13, “Уп- равление копированием”, при множественном наследовании класса осуществляются так же, как и при одиночном наследовании. Для создания, присвоения и удаления внутреннего объекта каждого базового класса неявно используется их собственные конструкторы копий, операторы присвоения и деструкторы.
Глава 17. Инструменты для крупномасштабных программ 769 Предположим, что класс Panda использует стандартные члены управления ко- пированием. Panda ying_yang("ying yang"); Panda ling_ling = ying yang; // создание объекта класса Panda // применение конструктора копий Используемый при инициализации объекта ling_ling стандартный конст- руктор копий вызывает конструктор копий класса Bear, который перед своим выполнением запускает конструктор копий класса ZooAnimal. По завершении создания части Bear объекта ling_ling, запускается конструктор копий класса Endangered, создающий свою часть объекта. И наконец, выполняется конструктор копий класса Panda. Синтезируемый оператор присвоения ведет себя аналогично конструктору ко- пий. Сначала он присваивает части объекта класса Bear (при помощи операторов присвоения класса Bear и ZooAnimal), затем он присваивает части Endangered и наконец части Panda. Синтезируемый деструктор удаляет все члены объекта класса Panda и вызыва- ет деструкторы для частей базового класса в порядке, обратном вызову их конст- рукторов. Как и в случае одиночного наследования (раздел 15.4.3, стр. 616), если в классе, про- изводном от нескольких базовых классов, определен его собственный деструктор, этот деструктор будет отвечать за удаление только той части, которая относится к произ- водному классу. Если в производном классе определен его собственный конструктор копий или оператор присвоения, они будут отвечать за копирование и присвоение всех частей базовых классов. Автоматическое копирование и присвоение частей базовых классов происходит только тогда, когда производный класс использует синтезируемые версии этих функций-членов. 17.3.4. Область видимости класса при множественном наследовании Область видимости класса (раздел 15.5, стр. 621) при множественном наследова- нии несколько сложнее, поскольку область видимости производной части может быть заключена в несколько областей видимости базовых классов. Как обычно, по- иск имени используемого в функции-члене начинается в самой функции. Если имя не найдено локально, поиск продолжается среди членов класса, а затем в каждом ба- зовом классе по очереди. При множественном наследовании, поиск проходит во всех ветвях наследования базового класса одновременно. В рассматриваемом примере будут параллельно исследованы ветви Endangered и Bear/ZooAnimal. Если имя найдено в нескольких ветвях, использовано будет то из них, которое явно указано в базовом классе. В противном случае возникнет неоднозначность. Когда класс имеет несколько базовых классов, поиск имен происходит во всех непо- средственных базовых классах одновременно. При множественном наследовании про- изводный класс может унаследовать член с тем же именем от нескольких базовых клас- сов. При неуточненном использовании этого имени возможна неоднозначность.
770 Часть V. Дополнительные темы Множественное наследование может привести к неоднозначности Предположим, что в классах Bear и Endangered определена функция-член по имени print (). Если эта функция-член не определена в классе Panda, следующий оператор приведет к ошибке во время компиляции. уing_yang.print(cout); Создание класса Panda, унаследовавшего две одноименные функции-члена print (), вполне допустимо, поскольку приводит лишь к потенциальной возможно- сти возникновения неоднозначности. Этой неоднозначности можно избежать, если объект класса Panda не будет вызывать функции print (). Ошибки также можно избежать, если при каждом обращении к функции print () явно указывать, какая из версий функции print О необходима: Bear: :print() или Endangered: : print (). Ошибка неоднозначности происходит только при попытке использова- ния этого члена класса без уточнения его принадлежности. Если объявление обнаруживается только в одной ветви базовых классов, имя считается найденным и алгоритм поиска завершает работу. Например, класс Endangered мог бы иметь функцию-член, которая возвращает численность попу- ляции ее объекта. В этом случае следующее обращение было бы откомпилировано без проблем. уing_yang.population(); В результате имя population было бы обнаружено в базовом классе Endangered, т.е. его нет ни в классе Bear, ни в его базовых классах. Сначала происходит поиск имен Неоднозначность, вызванная двумя унаследованными функциями-членами print (), вполне очевидна и приемлема, однако весьма удивителен тот факт, что ошибка происходит даже тогда, когда две унаследованные функции имеют разные списки параметров. Аналогичная ошибка происходит, когда в одном из классов функ- ция print () объявлена закрытой, а в другом открытой или защищенной. И наконец, если бы функция print () была определена в классе ZooAnimal, а не в классе Bear, обращение все равно осталось бы ошибочным. Поиск имени, как обычно, происходит в два этапа (раздел 7.8.1, стр. 293): сначала компилятор находит соответствующие объявления (в данном случае два соответст- вующих объявления, которые приводят к неоднозначности), а потом выясняет, яв- ляются ли найденные объявления допустимыми. Как избежать неоднозначности на пользовательском уровне Чтобы избежать неоднозначности, связанной с функцией print (), достаточно указать класс, в котором находится необходимое определение. ying_yang.Endangered::print(cout); Наилучший способ предотвращения потенциальных проблем неоднозначности подразумевает указание в производном классе той версии функции, которая будет использована при неоднозначности. Например, класс Panda можно снабдить собст- венной функцией print (), в теле которой явно применены версии функции print () из базовых классов.
Глава 17. Инструменты для крупномасштабных программ 771 std::ostream& Panda::print(std::ostream &os) const { Bear::print(os); // отображение части Bear Endangered::print(os); // отображение части Endangered return os; Код для упражнений раздела 17.3.4 Struct Basel { void print(int) const; protected: int ival; double dval; char eval; private: int *id; } 7 struct Base2 { void print (double) const; protected: double fval; private: double dval; } 7 struct Derived : public Basel { void ptint(std::string) const; protected: std::string sval; double dval; struct MI public Derived, public Base2 { void print(std::vector<double>); protected: int * ival; std:vector<double> dvec; } 7 Упражнения раздела 17.3.4 Упражнение 17.29. Принимая во внимание иерархию класса на стр. 772 и приведенный ниже каркас функции-члена mi :: foo (), ответьте на следующие вопросы. int ival; double dval; void MI::foo(double dval) { int id; /* ... */ } (а) Укажите члены класса, имена которых видимы внутри класса mi. Все ли из этих имен видимы в более чем одном базовом классе? (Ь) Укажите набор членов класса, видимых внутри функции mi :: foo (). Упражнение 17.30. Принимая во внимание иерархию класса на стр. 772, ответьте, в чем ошибка приведенного ниже обращения к функции print () ? MI mi ; mi.print(42);
772 Часть V. Дополнительные темы Переделайте класс mi так, чтобы это обращение к функции print () могло быть откомпилиро- вано и выполнено правильно. Упражнение 17.31. Принимая во внимание иерархию класса на стр. 772, укажите, какие из сле- дующих операций присвоения ошибочны (если они есть). void MI::bar() { int sval; // варианты вопросов упражнения располагаются здесь ... } (a) dval = 3.14159; (d) fval = 0; (b) eval = 'a'; (e) sval = *ival; (c) id = 1; Упражнение 17.32. Принимая во внимание иерархию класса на стр. 772 и приведенный ниже каркас функции-члена mi :: f oobar (), ответьте на следующие вопросы. void MI::foobar(double eval) { int dval; // варианты вопросов упражнения располагаются здесь ... } (а) Присвойте локальному экземпляру переменной dval сумму переменных-членов dval объек- тов классов Basel И Derived. (Ь) Присвойте значение последнего элемента вектора mi :: dvec переменной-члену Base2: : fval. (с) Присвойте переменной-члену eval класса Basel первый символ строки sval класса Derived. 17.3.5. Виртуальное наследование При множественном наследовании, базовый класс может быть унаследован не- сколько раз. В приведенных ранее программах фактически уже использовался класс, который неоднократно наследовал в иерархии тот же базовый класс. Каждый библиотечный класс ввода-вывода происходит от общего абстрактного класса. Этот абстрактный класс управляет флагом состояния потока и содержит бу- фер чтения или записи потока. Классы istream и ostream происходят непосредст- венно от этого общего базового класса. В библиотеке определен еще один класс, iostream, который происходит от обоих классов, istream и ostream. Класс iostream способен осуществлять как чтение, так и запись в поток. Упрощенная версия иерархии наследования классов ввода-вывода приведена на рис. 17.3. Рис. 17.3. Виртуальное наследование в иерархии класса iostream (упрощенная схема)
Глава 17. Инструменты для крупномасштабных программ 773 Как уже упоминалось, полученные в результате множественного наследования классы наследуют возможности и функции своих предков. Если бы типы ввода- вывода использовали обычное наследование, каждый объект класса iostream со- держал бы два внутренних объекта класса ios: один экземпляр был бы внутренним объектом класса istream, а второй — класса ostream. С конструктивной точки зрения эта реализация невыгодна: класс iostream должен осуществлять чте- ние/записать в один буфер, и флаг состояния также не должен раздельно использо- ваться операторами ввода и вывода. Но если существует два разных объекта класса ios, будет невозможно наладить совместное использование. В C++ для решения этой проблемы используется виртуальное наследование (virtual inheritance). Виртуальное наследование — это механизм, позволяющий час- тям класса совместно использовать состояние их виртуального базового класса. При виртуальном наследовании, совместно использоваться будет только один экземпляр унаследованного внутреннего объекта базового класса, причем независимо от того, сколько раз виртуальный базовый класс наследуется внутри иерархии. Совместно используемый внутренний объект базового класса называется виртуальным базовым классом (virtual base class). Классы istream и ostream происходят фактически от их базового класса. Сде- лав их общий базовый класс виртуальным, можно указать, что если некий другой класс, например iostream, унаследует их обоих, в производном классе будет при- сутствовать только одна копия их общего базового класса. Чтобы сделать базовый класс виртуальным, в список наследования следует включить ключевое слово virtual. class istream class ostream virtual public public virtual // iostream унаследует только одну копию его базового класса ios class iostream: public istream, public ostream { ... }; Разные классы Panda Для демонстрации виртуального наследования продолжим рассмотрение класса Panda. Зоологи уже более 100 лет спорят о том, к какому семейству принадлежит панда (класс Panda): енотов (класс Raccoon) или медведей (класс Bear). Посколь- ку проект программы прежде всего предназначен для пользователей, наиболее прак- тичным решением будет создание класса Panda как производного от них обоих. class Panda : public Bear, public Raccoon, public Endangered { Виртуальное наследование в иерархии класса Panda изображено на рис. 17.4. Изучив эту иерархию, можно заметить неочевидный на первый взгляд аспект вирту- ального наследования: виртуальное наследование, в данном случае Bear и Raccoon, должно быть осуществлено до возникновения фактической потребности в нем. Здесь виртуальное наследование становится необходимым только при объявлении класса Panda, но если классы Bear и Raccoon еще не участвовали в виртуальном наследовании, создание класса Panda потерпит неудачу.
774 Часть V. Дополнительные темы (Виртуальный Bear Raccoon ZooAnimal (Виртуальный) Рис. 17.4. Виртуальное наследование в иерархии класса Panda На практике наличие промежуточного базового класса при виртуальном наследо- вании редко приводит к проблемам. Обычно иерархия классов, в которой используется виртуальное наследование, разрабатывается сразу и одним лицом (или группой разра- ботчиков). Ситуации, когда разработку виртуального базового класса необходимо поручить независимому производителю, чрезвычайно редки, а разработчик нового базового класса не может внести изменения в существующую иерархию. 17.3.6. Объявление виртуального базового класса Базовый класс объявляется виртуальным при помощи ключевого слова virtual. Например, приведенные ниже объявления сделают класс ZooAnimal виртуальным базовым классом для классов Bear и Raccoon. // порядок расположения ключевых слов public и virtual // несущественен class Raccoon : public virtual ZooAnimal { /* ... */ } class Bear : virtual public ZooAnimal { /* ... */ } Объявление виртуального наследования воздействует только на те классы, которые были получены как производные от класса, объявленного виртуальным базовым классом. Те есть воздействие будет оказано на объекты не данного класса, а классов, производ- ных от него. Ключевое слово virtual позволяет совместно использовать единый экземпляр име- нованного базового класса, внутри производных классов, полученных впоследствии. Любой класс, который может быть объявлен базовым, может быть также объяв- лен виртуальным базовым. Виртуальный базовый класс может содержать любые члены, которые способен содержать обычный, невиртуальный базовый класс. Для базовых классов поддерживаются стандартные преобразования Манипулировать объектом производного класса можно как обычно, при помощи указателя или ссылки на базовый класс, хотя он и является виртуальным. Например, все преобразования для классов, являющихся базовыми для класса Panda, будут выполнятся правильно несмотря на то, что среди них есть виртуальный базовый класс ZooAnimal. void dance(const Bear*); void rummage(const Raccoon*); ostreams operator<<(ostreamS, const ZooAnimal&); Panda yinq yang;
Глава 17. Инструменты для крупномасштабных программ 775 dance (&ying__yang) ; rummage(&ying_yang); cout « ying_yang; // ok: преобразует адрес в указатель на // класс Bear // ok: преобразует адрес в указатель на // класс Raccoon // ok: передает ying_yang как ZooAnimal Видимость членов виртуальных базовых классов Применение виртуальных классов в иерархии множественного наследования по- зволяет снизить количество проблем неоднозначности. К совместно используемым членам виртуального базового класса можно обращаться од- нозначно и непосредственно. Аналогично, если член виртуального базового класса пере- определен только в одной ветви наследования, к этому переопределенному члену класса можно обратиться непосредственно. При невиртуальном наследовании, оба вида доступа привели бы к неоднозначности. Предположим, что член класса по имени X унаследован по нескольким ветвям наследования. Здесь возможны три варианта. 1. Если член X в каждой ветви представляет тот же член виртуального базового класса, никакой неоднозначности не будет, поскольку он окажется единствен- ным совместно используемым экземпляром. 2. Если в одной из ветвей член X принадлежит виртуальному базовому классу, а в другой ветви последовательно производному классу, никакой неоднозначности также не будет, поскольку специализированный экземпляр производного класса имеет приоритет над совместно используемым экземпляром виртуального базо- вого класса. 3. Если член X в каждой ветви наследования представляет разные члены последо- вательно производного класса, прямое обращение к нему приведет к неодно- значности. Как и в иерархии с невиртуальным множественным наследованием, подобная не- однозначность лучше всего устраняется переопределением экземпляра в производ- ном классе. Упражнения раздела 17.3.6 Упражнение 17.33. Рассмотрим следующую иерархию класса. Можно ли внутри класса vmi об- ращаться к унаследованным членам без уточнения? Какие из них требуют полностью квалифици- рованных имен? Объясните, почему. class Base { public: class Derivedl : virtual public Base { public:
776 Часть V. Дополнительные темы }; class Derived2 : virtual public Base { public: foo(int); protected: int ival; char eval; }; class VMI : public Derivedl, public Derived2 17.3.7. Семантика специальной инициализации Обычно каждый класс инициализирует лишь свой непосредственный базовый класс (классы). Будучи применена к виртуальному базовому классу, эта стратегия инициализации потерпит неудачу. Если использовать обычные правила, виртуаль- ный базовый класс может быть инициализирован несколько раз, поскольку он со- держится в каждой ветви наследования. В рассматриваемом примере применение обычных правил инициализации привело бы к попытке инициализировать часть класса ZooAnimal объекта класса Panda в частях классов Bear и Raccoon. Для решения проблемы двойной инициализации, классы, унаследованные от классов, имеющих виртуальный базовый класс, осуществляют в процессе инициали- зации специальную обработку. При виртуальном наследовании, виртуальный базо- вый класс инициализирует конструктор самого последнего производного класса. В рас- сматриваемом примере, при создании объекта класса Panda, инициализацию членов базового класса ZooAnimal осуществит объект класса Panda. Хотя часть, относящаяся к виртуальному базовому классу, инициализируется конструктором последнего производного класса, все классы, непосредственно или косвенно производные от него, также попытаются инициализировать внутренний объект виртуального базового класса. Поскольку вполне возможно создать незави- симый объект класса, производного от виртуального базового класса, этот класс должен инициализировать свой внутренний объект виртуального базового класса. Однако эта инициализация осуществляется только тогда, когда создается независи- мый объект промежуточного класса. В рассматриваемой иерархии могут быть созданы объекты класса Bear, Raccoon или Panda. Когда создается объект класса Panda, в большинстве из его базовых классов осуществляется попытка инициализации совместно используемых членов базового класса ZooAnimal. Когда создается объект класса Bear (или Raccoon), никаких последующих производных классов нет. В этом случае конструкторы клас- сов Bear и Raccoon должны сами инициализировать свой внутренний объект базо- вого класса ZooAnimal как обычно. Bear::Bear(std::string name, bool onExhibit) : ZooAnimal(name, onExhibit, "Bear") { } Raccoon::Raccoon(std::string name, bool onExhibit) : ZooAnimal(name, onExhibit, "Raccoon") { } Конструктор класса Panda инициализирует внутренний объект класса ZooAnimal несмотря на то, что он не является его непосредственным базовым классом.
Глава 17. Инструменты для крупномасштабных программ 777 Panda::Panda(std::string name, bool onExhibit) : ZooAnimal(name, onExhibit, "Panda"), Bear(name, onExhibit), Raccoon(name, onExhibit), Endangered(Endangered::critical), sleeping_flag(false) { } Когда создается объект класса Panda, именно его конструктор инициализирует часть ZooAnimal объекта Panda. Как создается объект при виртуальном наследовании Рассмотрим, как создаются объекты при виртуальном наследовании. Bear winnie("pooh"); Raccoon meeko("meeko"); Panda yolo("yolo"); // конструктор класса Bear // инициализирует объект класса // конструктор класса Raccoon // инициализирует объект класса // конструктор класса Panda /! инициализирует объект класса ZooAnimal ZooAnimal ZooAnimal Когда создается объект класса Panda, происходит следующее. 1. Сначала создается часть ZooAnimal. При этом используется список инициали- зации конструктора, определенный в классе Panda. 2. Затем создается часть Bear. Список инициализации конструктора класса Bear для класса ZooAnimal игнорируются. 3. Затем создается часть Raccoon. Список инициализации конструктора для клас- са ZooAnimal снова игнорируется. 4. Наконец создается часть Panda. Если конструктор класса Panda не инициализирует явно часть базового класса ZooAnimal, будет использован стандартный конструктор класса ZooAnimal. Если класс ZooAnimal не имеет стандартного конструктора, произойдет ошибка. Компи- лятор выдаст сообщение об ошибке при попытке откомпилировать определение кон- структора класса Panda. Порядок выполнения конструкторов и деструкторов Части виртуальных базовых классов всегда создаются прежде, чем внутренние объекты Ц невиртуальных базовых классов, причем независимо от того, где они расположены в ие- рархии наследования. Например, в следующей иерархии наследования класса TeddyBear (Медвежо- нокТедди) существует два виртуальных базовых класса: непосредственный базовый класс ToyAnimal (ИгрушечноеЖивотное) и косвенный базовый класс ZooAnimal, от которого происходит класс Bear. class Character { /* ... */ } class Bookcharacter : public Character { /* class ToyAnimal { /* ... */ } class TeddyBear : public Bookcharacter, public Bear, public virtual ToyAnimal
778 Часть V. Дополнительные темы j ToyAnimal J Character ZooAnimal (Виртуальный) (Виртуальный) Рис. 17.5. Виртуальное наследование в иерархии класса TeddyBear BookCharacter Объявления непосредственных базовых классов проверяются на наличие вирту- альных базовых классов. В данном примере сначала исследуется ветвь наследования Bookcharacter, затем ветвь Bear и наконец ветвь ToyAnimal. Каждая ветвь ис- следуется начиная с корневого класса и до последнего производного. Для объекта класса TeddyBear части виртуальных базовых классов создаются так: сначала внутренний объект класса ZooAnimal, а затем класса ToyAnimal. Как только будут созданы внутренние объекты виртуальных базовых классов, конструк- торы невиртуальных базовых классов будут вызваны в порядке их объявления: кон- структор класса Bookcharacter, который вызовет конструктор класса Character, а затем конструктор класса Bear. Таким образом, в ходе создания объекта класса TeddyBear конструкторы будут вызваны в следующем порядке. ZooAnimal(); // виртуальный базовый класс Bear ToyAnimal(); // непосредственный виртуальный базовый класс Character(); // невиртуальный базовый класс Bookcharacter Bookcharacter(); // непосредственный невиртуальный базовый класс Bear(); // непосредственный невиртуальный базовый класс TeddyBear(); // последний производный класс Списки инициализирующих значений, используемые для классов ZooAnimal и ToyAnimal, определены в большинстве производных классов иерархии TeddyBear. Тот же порядок создания используется в синтезируемом конструкторе копий и синтезируемом операторе присвоения. Вызов деструкторов базовых классов осуще- ствляется в порядке, обратном порядку вызова конструкторов. Упражнения раздела 17.3.7 Упражнение 17.34. Существует один случай, когда производный класс не должен предоставлять инициализирующих значений для своих виртуальных базовых классов. Что это за случай? Упражнение 17.35. Рассмотрим следующую иерархию класса. class Class { ... }; class Base : public Class { ... }; class Derivedl : virtual public Base { ... }; class Derived2 : virtual public Base { ... }; class MI : public Derivedl, public Derived2 { ... }; class Final : public MI, public Class { ... }; (а) Каков порядок вызова конструкторов и деструкторов при создании объектов класса Final? (b) Сколько внутренних объектов класса Base находится в объекте класса Final? А сколько внутренних объектов класса class?
Глава 17. Инструменты для крупномасштабных программ 779 (с) Какие из следующих случаев присвоения приведут к ошибке во время компиляции? Base *pb; Class *рс; MI *pmi; Derived2 *pd2; (a) pb = new Class; (c) pmi = pb; (b) pc = new Final; (d) pd2 = pmi; Упражнение 17.36. Учитывая предыдущую иерархию и подразумевая, что в классе Base опреде- лены три следующих конструктора, укажите классы, которые происходят от класса Base (с учетом того, что каждый класс имеет те же три конструктора). Каждый конструктор должен использовать свой аргумент для инициализации внутренней части класса Base. struct Base { Base () ; Base(std::string); Base(const Base&); protected: std::string name; }; Резюме Язык C++ применяется для решения широкого диапазона проблем: от требующих лишь нескольких часов работы, до занимающих годы работы больших группы разработчиков. Не- которые из возможностей языка C++ наиболее полезны при создании крупномасштабных приложений. Имеется ввиду обработка исключений, пространства имен и множественное или виртуальное наследование. Обработка исключений позволяет отделить ту часть кода, где может произойти ошибка, от той части кода, где она обрабатывается. Тема обработки исключений, начатая в разделе 6.13 (стр. 241), завершается в данной главе. При передаче исключения, выполнение текущей функции приостанавливается и начинается поиск ближайшей директивы catch. Локальные переменные, определенные внутри покидаемых при поиске директив catch функций, уда- ляются в ходе обработки исключения. Факт удаления объектов позволяет использовать очень важную методику программирования, известную под названием “создание-инициализация ресурса” (RAII). Пространства имен — это механизм управления большими и сложными приложениями, формируемыми из кода, созданного независимыми поставщиками. Пространство имен является областью видимости, в которой могут быть определены объекты, типы, функции, шаблоны и другие пространства имен. Стандартная библиотека определена внутри пространства имен std. Объявление using позволяет сделать имена пространства имен доступными в текущей области видимости по одному. В качестве альтернативы, но намного менее безопасно, мож- но внести в текущую область видимости все имена пространства имен при помощи дирек- тивы using. С концептуальной точки зрения множественное наследование — довольно простое поня- тие: производный класс может быть унаследован от нескольких непосредственных базовых классов. Объект производного класса состоит из частей, представляющих собой внутренние объекты всех своих базовых классов. Концепция действительно проста, но на практике со- пряжена со многими сложностями. В частности, наследование от нескольких базовых классов создает вероятность конфликтов имен и в результате порождает неоднозначные обращения к именам из базовых частей объекта. Если класс происходит от нескольких непосредственных базовых классов, не исключена ситуация, когда эти классы сами могут совместно использовать другой базовый класс. В та- ких случаях промежуточные классы могут применить виртуальное наследование, позволяю-
780 Часть V. Дополнительные темы щее другим классам иерархии, унаследовавшим тот же базовый класс, совместно использо- вать его внутренний объект. Таким образом, объект производного класса будет иметь только одну совместно используемую копию внутреннего объекта виртуального базового класса. Термины Блок catch (catch clause). Часть программы, которая обрабатывает исключение. Блок обработчика состоит из ключевого слова catch, за которым следует спецификатор исключе- ния и блок операторов. Код внутри блока catch предназначен для обработки исключений класса, указанного в его спецификаторе. Блок try (try block). Блок операторов, начинающийся ключевым словом try и содер- жащий один или несколько блоков catch. Если код внутри блока try передает исключение и один из блоков catch соответствует типу переданного исключения, переданное исключение будет обработано этим обработчиком. В противном случае исключение будет передано из блока try другому обработчику, далее по цепи обращений. Блок try функции (function try block). Блок try, являющийся телом функции. Он на- чинается ключевым словом try, которое располагается перед открывающей фигурной скоб- кой тела функции и обладает блоками catch, следующими после закрывающей фигурной скобки тела функции. Как правило, в блоки try функций заключают реализацию конструк- тора, чтобы перехватывать исключения, передаваемые в ходе инициализации. Виртуальное наследование (virtual inheritance). Форма множественного наследования, при котором производные классы совместно используют одну копию экземпляра базового класса, даже если в иерархии он встречается несколько раз. Виртуальный базовый класс (virtual base class). Базовый класс, при наследовании которого было использовано ключевое слово virtual. В объекте производного класса, часть виртуально- го базового класса содержится только в одном экземпляре, даже если в иерархии этот класс при- сутствует несколько раз. При невиртуальном наследовании, конструктор может инициализиро- вать только непосредственный базовый класс (классы). При виртуальном наследовании, этот класс мог бы быть инициализирован несколькими производными классами, которые должны предоставить инициализирующие значения для всех его виртуальных предков. Выражение throw е (передача исключения). Выражение, которое прерывает текущий поток выполнения. Каждый оператор throw передает управление ближайшему окружающе- му блоку catch, который способен обработать исключение переданного типа. Выражение е будет скопировано в объект исключения. Глобальное пространство имен (global namespace). Неявное пространство имен, содер- жащее все определения глобальных объектов, которыми обладает каждая программа. Директива using (using directive). Механизм, позволяющий сделать все имена про- странства имен доступными в ближайшей области видимости, содержащей и директиву using, и само пространство имен. Загромождение пространства имен (namespace pollution). Термин, используемый для опи- сания ситуации, когда все имена классов и функций располагаются в глобальном пространстве имен. Большие программы, использующие код, который создан несколькими независимыми производителями, зачастую сталкиваются с конфликтами имен, если эти имена глобальны. Класс runtime_error. Определенный в стандартной библиотеке класс, являющийся производным от класса exception. Используется для описания ошибки, которая может быть обнаружена только во время выполнения. Производными от класса runtime_error являются классы overtlow_error, underf low_error и range_error. Множественное наследование (multiple inheritance). Наследование, при котором класс имеет несколько непосредственных базовых классов. Производный класс наследует члены
Глава 17. Инструменты для крупномасштабных программ 781 всех своих базовых классов. Имена нескольких базовый классов указываются в списке насле- дования класса. Для каждого базового класса необходим отдельный маркер доступа. Неименованное пространство имен (unnamed namespace). Пространство имен, определен- ное без имени. К именам, определенным в неименованном пространстве имен, можно обращать- ся непосредственно, без оператора области видимости. Каждый файл имеет собственное, уни- кальное неименованное пространство имен. Имена в файле невидимы вне данного файла. Обработка исключений (exception handling). Механизм уровня языка, предназначенный для ликвидации аномалий времени выполнения. Один независимо разработанный раздел ко- да может обнаружить проблему и передать исключение, которое может получить и обработать другая независимо разработанная часть программы. Часть кода, обнаруживающая ошибку, передает исключение, а часть кода, получающая его, осуществляет обработку. Обработчик для всех исключений (catch-all). Блок обработчика, спецификатором исклю- чения которого является (...). Этот обработчик предназначен для исключений любого ти- па. Обычно он используется для предварительной обработки исключения, осуществляемой локально. Затем исключение повторно передается другой части программы, в которой и осу- ществляется устранение причины проблем. Обработчик исключения (exception handler). Еще одно название блока catch. Объект исключения (exception object). Объект, используемый для передачи сообщения между блоками throw и catch. Объект создается в точке передачи и является копией ис- пользованного выражения. Объект исключения существует, пока не сработает последний об- работчик для его типа. Тип объекта соответствует типу использованного выражения. Объявление using (using declaration). Механизм, позволяющий ввести одно имя из пространства имен в текущую область видимости. using std::cout; Это объявление сделает имя cout из пространства имен std доступным в текущей облас- ти видимости, благодаря чему имя cout можно применять без спецификатора std: :. Оператор области видимости (scope operator). Оператор (: :) используется для доступа к именам пространства имен или класса. Передача (raise). Синоним термина “throw” (передача). Программисты C++ используют термины “throwing” и “raising” как синонимы, означающие передачу исключений. Повторная передача исключения (rethrow). Пустой оператор throw повторно передает объект исключения. Повторная перепередача возможна только изнутри блока catch (обработчика) или в функции, прямо или косвенно вызываемой обработчиком. В результате будет повторно передан полученный ранее объект исключения. Порядок выполнения деструкторов (destructor order). Внутренние объекты производных классов удаляются в порядке, обратном их созданию: сначала удаляется часть объекта произ- водного класса, а затем классов, указанных в списке наследования, начиная с последнего ба- зового. У классов, которые предполагается использовать как базовые классы в иерархии множественного наследования, деструкторы обычно определяют как виртуальные. Порядок выполнения конструкторов (constructor order). Базовые классы обычно созда- ются для использования в списке наследования класса. Конструктор производного класса должен явно инициализировать объект каждого базового класса, указанного в списке ини- циализации конструктора. Порядок, в котором базовые классы указаны в списке инициали- зации конструктора, никак не влияет на порядок, в котором создаются внутренние объекты базовых классов. При виртуальном наследовании, внутренние объекты виртуальных базовых классов создаются раньше внутренних объектов любых других базовых классов. Это необхо- димо для того, чтобы они могли быть совместно использованы в объектах производных клас- сов, в списке наследования которых они присутствуют прямо или косвенно. Только самый последний производный класс может инициализировать объект виртуального базового клас- са; упоминания этого базового класса в списках инициализации конструкторов промежуточ- ных базовых классов игнорируются.
782 Часть V. Дополнительные темы Прокрутка стека (stack unwinding). Термин, используемый для описания процесса выхо- да из функции при передаче исключения и перехода к поиску его обработчика. Локальные объекты, созданные перед передачей исключения, следует удалить перед началом поиска со- ответствующего обработчика. Пространство имен (namespace). Механизм, используемый для сбора всех имен, опреде- ленных в библиотеке или другом фрагменте программы, в единую область видимости. В от- личие от других областей видимости языка C++, область видимости пространства имен мо- жет быть определена в нескольких частях. Пространство имен может быть открыто, закрыто и открыто вновь, причем в разных частях программы. Псевдоним пространства имен (namespace alias). Синтаксис создания синонима для про- странства имен имеет следующий вид. namespace Nl = N; Здесь Nl — это лишь другое имя пространства имен N. Пространство имен может иметь несколько псевдонимов, причем псевдонимы и реальное имя пространства имен могут ис- пользоваться попеременно. Спецификатор исключения (exception specifier). Задает типы исключений, которые мо- жет обработать данный блок catch. Спецификатор исключения подобен списку параметров, единственный параметр которого инициализирует объект исключения. Подобно передаче обычных параметров, если спецификатор исключения имеет нессылочный тип, объект ис- ключения копируется при передаче в блок catch. Спецификация исключений (exception specification). Используется в объявлении функ- ции для указания типа исключения (если есть), передаваемого функцией. Типы (классы) ис- ключений указывают в списке, заключенном в скобки и разделяемом запятыми, который рас- положен после ключевого слова throw, следующего за списком параметров функции. Пустой список означает, что функция не передает никаких исключений. Функция без спецификации исключений может передавать любые исключения. Статический файловый объект (file static). Локальное для файла имя, которое было объ- явлено с использованием ключевого слова static. В языке С и версиях языка C++, выпу- щенных до появления стандарта, статические файловые объекты использовались для объяв- ления таких объектов, которые применимы только в одном файле. Применение статических файловых объектов осуждено стандартом C++. Сейчас они заменены неименованными про- странствами имен. Устойчивость к исключению (exception safe). Термин, используемый для указания на способность программы правильно работать даже в случае передачи исключения. Функция abort (). Библиотечная функция, аварийно завершающая выполнение про- граммы. Обычно функцию abort () вызывает функция terminate (). Однако функцию abort () можно вызывать и непосредственно. Она определена в заголовке cstdlib. Функция terminate (). Библиотечная функция, вызов которой происходит в случае, ко- гда переданное исключение либо так и не обработано, либо если оно было передано внутри обработчика исключений. Обычно вызывает функцию abort (), аварийно завершающую выполнение программы. Функция unexpected (). Библиотечная функция, вызов которой происходит в случае, если переданное исключение нарушает спецификацию исключений функции. Шаблон auto_ptr. Библиотечный шаблон класса, который предоставляет защищенный от исключений доступ к объектам, размещенным в динамической памяти. Экземпляр шабло- на auto_ptr не может быть связан ни с массивом, ни с указателем на переменную. Копиро- вание и присвоение экземпляра шаблона auto_ptr являются деструктивными операторами: владение объектом переходит от правого операнда к левому. Присвоение нового объекта эк- земпляру шаблона auto_ptr удаляет объект левого операнда. В результате экземпляр шаб- лона auto_ptr не может быть сохранен в контейнере.
ГЛАВА 18 Специализированные ИНСТРУМЕНТЫ И ТЕХНОЛОГИИ В ЭТОЙ ГЛАВЕ... 18.1. Оптимизация распределения памяти 783 18.2. Идентификация типов времени выполнения 803 18.3. Указатель на член класса 811 18.4. Вложенные классы 817 18.5. Объединение: экономный класс 823 18.6. Локальные классы 826 18.7. Возможности, снижающие переносимость 828 Резюме 835 Термины 836 В первых четырех частях этой книги обсуждались элементы языка C++, которые используются практически всеми программистами C++. Кроме того, язык C++ пре- доставляет некоторые специализированные инструменты. Возможностями, описан- ными в этой главе, большинство программистов пользуются крайне редко или не пользуются вообще. 18.1. Оптимизация распределения памяти Распределение памяти в языке C++ — типизированная операция: оператор new (раздел 5.11, стр. 199) выделяет область памяти для размещения объекта указанного типа и создает в ней объект. Для инициализации объекта указанного класса, разме- щаемого в динамической памяти, оператор new автоматически запускает соответст- вующий конструктор. То, что во время выполнения оператор new резервирует память на основании ти- па объекта, в некоторых классах может привести к излишним дополнительным за- тратам. Самостоятельное размещение в памяти объектов таких классов может прой- ти гораздо быстрее. Общепринятый подход реализации этой стратегии подразумева- ет предварительное резервирование (preallocate) памяти, которая будет использована впоследствии, при создании нового объекта.
784 Часть V. Дополнительные темы Некоторым классам может понадобиться минимизация затрат на размещение в памяти их собственных переменных-членов. Например, библиотечный класс vector осуществляет предварительное резервирование дополнительной памяти (раздел 9.4, стр. 358) для новых элементов. Новые элементы добавляются именно в эту, заранее зарезервированную емкость. Предварительное резервирование места для новых элементов позволяет вектору повысить эффективность процесса добав- ления и хранения элементов в непрерывной области памяти. В каждом из этих случаев предварительное резервирование памяти для размеще- ния объекта или хранения внутренних данных объекта класса, позволяет отделить резервирование памяти от создания объекта. Вполне очевидным основанием для этого является то, что создавать объект в предварительно зарезервированный памя- ти гораздо практичнее. Ведь созданный объект может никогда и не использоваться. Перед фактическим использованием таких предварительно зарезервированных объ- ектов, им следует присвоить новые значения. То есть некоторые классы не могут ис- пользовать предварительно зарезервированную память, если в ней ничего не созда- но. Рассмотрим, например, вектор, который использует стратегию предварительного резервирования. Если бы объекты в предварительно зарезервированный памяти должны были быть созданы сразу, невозможно было бы создать вектор для элемен- тов класса, который не имеет стандартного конструктора, поскольку вектору не был бы известен способ создания этих объектов. Рассматриваемые в этом разделе методики не гарантируют ускорения работы всех программ. Улучшив производительность, они могут привести к другим издержкам, на- пример, к повышенному использованию ресурсов или затруднению отладки. К оптими- зации желательно прибегать только тогда, когда уже вполне понятно, что программа работает, но не так быстро как требуется, и что улучшение способа резервирования памяти решит проблемы производительности. 18.1.1. Резервирование памяти в языке С++ В языке C++ резервирование памяти и создание объектов тесно связаны между собой, как удаление объекта и освобождение занимаемой им памяти. Когда исполь- зуется оператор new, в распределяемой памяти резервируется область, в которой и создается объект. Когда используется оператор delete, происходит вызов деструк- тора, уничтожающего объект. Используемая объектом память возвращается системе. Приняв на себя ответственность за резервирование памяти, придется самому иметь дело как с созданием, так и с удалением объектов. Зарезервировав область па- мяти, необходимо создать в ней объект (объекты), а перед ее освобождением необхо- димо удостовериться в правильном удалении объекта (объектов). В отличие от инициализации, результат присвоения значения объекту, который еще не был создан в зарезервированной области памяти, непредсказуем. У многих классов это f ) становится причиной отказа во время выполнения. Присвоение подразумевает удале- ние существовавшего ранее объекта. Но если объект ранее не существовал, попытка его удаления (а следовательно, и вызов оператора присвоения) может привести к ава- рийной ситуации. Язык C++ предоставляет два способа резервирования и освобождения памяти. 1. С помощью класса allocator, который позволяет зарезервировать область па- мяти на основании типа объекта. Этот класс предоставляет абстрактный интер-
Глава 18. Специализированные инструменты и технологии 785 фейс резервирования памяти и последующего использования полученной облас- ти для размещения объектов. 2. С помощью библиотечных функций operator new() и operator delete (), которые резервируют и освобождают нетипизированную память указанного объема. Язык C++ предоставляет также различные способы создания и удаления объек- тов в нетипизированной памяти. 1. Класс allocator обладает функциями-членами construct () и destroy (), действие которых совпадает с их именами (создать и уничтожить). Функция- член construct () инициализирует объект в нетипизированной памяти, а функция-член destroy () запускает соответствующий деструктор объекта. 2. Оператор new получает указатель на нетипизированную область памяти и ини- циализирует объект или массив в этом пространстве. 3. Чтобы уничтожить объект, деструктор его класса можно вызывать непосредст- венно. Однако в результате выполнения деструктора не освобождается память, в которой объект располагался. 4. Алгоритмы uninitialized_f ill и uninitialized_copy работают подобно алгоритмам fill и сору за исключением того, что они создают объекты в ука- занном месте, а не присваивают или копируют их. В современных программах C++ для резервирования памяти обычно используют класс allocator. Этот способ гораздо надежнее и гибче. Однако при создании объектов, оператор new оказывается гибче, чем функция-член allocator::construct (). Поэтому в некоторых случаях применение оператора new предпочтительнее. 18.1.2. Класс allocator Класс allocator является шаблоном, обеспечивающим типизированное рас- пределение памяти, а также создание и удаление объектов. Предоставляемые шаб- лоном allocator функции приведены в табл. 18.1. Таблица 18.1. Стандартный шаблон allocator и специальные алгоритмы allocator<T> а; а.allocate(п) a.deallocate(р, п) а.construct(р, t) Определяет объект класса allocator по имени а, который может резервировать память и создавать объекты класса т Резервирует нетипизированную, несозданную область памяти, предна- значенную для хранения п объектов класса т Освобождает область памяти, содержавшую п объектов класса ^начи- ная с адреса, содержащегося в указателе типа т* по имени р. Ответ- ственность за вызов функции destroy () для каждого созданного в этой области памяти объекта, перед вызовом функции deallocate (), лежит на пользователе Создает новый элемент в области памяти, адрес которой содержит ука- затель р типа т*. Для инициализации объекта t используется конст- руктор копий класса т
786 Часть V. Дополнительные темы Окончание табл. 18.1 a.destroy(р) Выполняет деструктор объекта, адрес которого содержит указатель р типа т* uninitialized_copy (Ь, е, Ь2) Uninitialized_fill (b, e, t) Uninitialized_fill_n (b, e, t, n) Копирует элементы исходного диапазона, обозначенного итераторами ь и е, в несозданную, нетипизированную область памяти, начинаю- щуюся с итератора Ь2. Функция создает элементы в области назначе- ния, а не присваивает их. Область назначения, обозначенная итерато- ром Ь2, должна быть достаточно большой, чтобы содержать копию элементов исходного диапазона Инициализирует объекты в диапазоне, обозначенном итераторами ь и е копией объекта t. Поддиапазоном подразумевается несозданная, нетипизированная область памяти. Для создания объектов использует- ся конструктор копий Инициализирует п объектов копией объекта t в диапазоне, обозначен- ном итераторами ь и е. Подразумевается, что размер диапазона дос- таточен для размещения п элементов. Для создания объектов исполь- зуется конструктор копий Класс allocator отделяет резервирование памяти от создания объекта. Когда объект класса allocator резервирует память, он выделяет область памяти, размер которой достаточен для хранения объекта указанного типа. Однако зарезервирован- ная область памяти останется нетипизированной. Пользователь класса allocator должен самостоятельно создавать и удалять объекты, помещаемые в зарезервиро- ванную область памяти. Применение класса allocator для управления данными-членами Чтобы продемонстрировать применение стратегии предварительного резервиро- вания и использование класса allocator при манипулировании внутренними дан- ными класса, давайте рассмотрим, как осуществляется резервирование памяти в классе vector. Напомним, что класс vector хранит свои элементы последовательно. Чтобы до- биться приемлемой производительности, вектор предварительно резервирует боль- шее количество элементов, чем ему необходимо (раздел 9.4, стр. 358). Каждая функ- ция-член вектора, которая добавляет элементы в контейнер, проверяет, достаточно ли места для нового элемента. Если это так, функция-член инициализирует объект в следующей доступной области предварительно зарезервированной памяти. Если свободного места нет, вектор резервирует новое, большее по размеру пространство, копирует в него существующие элементы, добавляет новый, а затем освобождает прежнее пространство. Используемое вектором хранилище создается как нетипизированная область па- мяти, которая еще не содержит никаких объектов. При копировании или добавлении элементов в эту предварительно зарезервированную область, их следует создавать при помощи функции-члена construct () класса allocator. Чтобы проиллюстрировать эту концепцию, реализуем небольшую часть вектора. Присвоим этому классу имя Vector, чтобы отличить его от стандартного класса vector.
Глава 18. Специализированные инструменты и технологии 787 // псевдореализация стратегии резервирования памяти для класса // имитирующего вектор template <class Т> class Vector { public: private: static std::allocator<T> alloc; // объект для получения // нетипизированной памяти void reallocate(); // получить больше пространства и // скопировать существующие элементы Т* elements; // указатель на первый элемент массива Т* first_free; // указатель на первый свободный элемент Т* end; II массива / / указатель на следующий элемент после // конца массива Каждый класс Vector<T> содержит статическую переменную-член типа allocator<T>, предназначенную для резервирования места и создания элементов в векторах данного типа. Каждый объект вектора хранит свои элементы во встроенном массиве указанного типа, а также содержит три следующих указате- ля на этот массив. Указатель е 1 ement s, который указывает на первый элемент массива. Указатель first_free, который указывает на следующий элемент после по- следнего фактически существующего. Указатель end, который указывает на следующий элемент после конца самого массива. Смысл этих указателей иллюстрирует рис. 18.1. 0 1 2 3 4 место для новых элементов т t elements first free end Puc. 18.1. Стратегия резервирования памя- ти вектора Эти указатели можно использовать для определения размера и емкости вектора. Размер (size) вектора (количество фактически используемых элементов) равен first_free - elements. Емкость (capacity) вектора (общее количество элементов, которые могут быть размещены в векторе без переразмещения) равна end - elements. Свободная емкость вектора (количество элементов, которые могут быть добав- лены в вектор без переразмещения) равна end - first_free. Применение функции construct () Функция-член push_back () использует эти указатели при добавлении новых элементов в конец вектора.
788 Часть V. Дополнительные темы template cclass Т> void Vector<T>::push_back(const T& t) // есть ли еще место? if (first_free == end) reallocate(); // выделить большее пространство и скопировать // в него существующие элементы alloc.construct(first_fгее, t) ; ++first_free; } Сначала функция push_back () проверяет наличие свободного места. При его от- сутствии она вызывает функцию reallocate () (переразместить). Эта функция-член резервирует новое пространство и копирует в него существующие элементы, а затем переназначает указатели так, чтобы они указывали на вновь созданное пространство. Как только функция push_back () убедится, что места для нового элемента дос- таточно, она обращается к функции construct () объекта класса allocator, что- бы создать новый последний элемент. Чтобы скопировать значение t в элемент, обо- значенный указателем first_free, функция construct О использует конструк- тор копий класса Т. Затем она увеличивает значение указателя f irst_free таким образом, чтобы он указывал на следующий элемент. Переразмещение и копирование элементов Функция reallocate () осуществляет достаточно много действий. template <class Т> void Vector<T>::reallocate() { // вычислить размер текущего массива и зарезервировать // пространство для вдвое большего количества элементов std::ptrdiff_t size = first_free - elements; std::ptrdiff_t newcapacity = 2 * max(size, 1); // зарезервировать пространство для хранения нового // количества элементов класса Т Т* newelements - alloc.allocate(newcapacity); // создать копии существующих элементов в новом пространстве uninitialized_copy(elements, first_free, newelements); // удалить прежние элементы в обратном порядке for (Т *р = first_free; р != elements; /* удаление */ ) alloc.destroy(--р); // функцию deallocate () нельзя применить к нулевому указателю if (elements) // освободить память, занятую элементами alloc.deallocate(elements, end - elements); // откорректировать структуру данных с учетом новых элементов elements - newelements; first_free = elements + size; end = elements + newcapacity; } Здесь использована простая, но удивительно эффективная стратегия резервирова- ния вдвое большего количества памяти при каждом переразмещении. Функция начи- нается с вычисления текущего количества используемых элементов. Полученное чис- ло удваивается и используется объектом allocator для резервирования пространст- ва необходимого объема. Если вектор пуст, резервируется место для двух элементов.
Глава 18. Специализированные инструменты и технологии 789 Если вектор хранит целые числа, обращение к объекту allocate приведет к ре- зервированию пространства для newcapacity (новая емкость) целых чисел. Если вектор содержит строки, это приводит к резервированию пространства для указан- ного количества строк. При обращении к функции uninitialized_copy () используется специализи- рованная версия стандартного алгоритма сору. Эта версия предназначена для рабо- ты с несозданной, нетипизированная областью памяти. Вместо присвоения элемен- тов исходного диапазона результирующему, она создает каждый результирующий элемент. Для копирования элементов исходного диапазона в результирующий, ис- пользуется конструктор копий класса Т. Цикл for вызывает функцию-член destroy () класса allocator для каждого объекта в старом массиве. Она удаляет элементы в обратном порядке начиная с послед- него элемента массива и заканчивая первым. Чтобы освободить все ресурсы, используе- мые прежними элементами, функция destroy () использует деструктор класса Т. Как только прежние элементы будут скопированы и удалены, используемое исход- ным массивом пространство освобождается. Перед вызовом функции deallocate () необходимо удостовериться в том, что указатель elements фактически указывает на массив. Функции deallocate () передают указатель на пространство, которое было зарезер- вировано объектом allocate. Функции deallocate О нельзя передавать нулевой указатель. И наконец, необходимо переназначить указатели так, чтобы они содержали адре- са вновь размещенного и инициализированного массива. Указателям f irst_f гее и end присваиваются адреса элементов, следующих после последнего фактически су- ществующего и после конца самого массива. Упражнения раздела 18.1.2 Упражнение 18.1. Реализуйте собственную версию класса vector, обладающего функциями- членами reserve () (раздел 9.4, стр. 358), resize () (раздел 9.3.5, стр. 351), а также кон- стантным и неконстантным операторами индексирования (раздел 14.5, стр. 551). Упражнение 18.2. Создайте определение типа, который использует указатель на соответствую- щий тип как итератор для вектора. Упражнение 18.3. Чтобы проверить собственный класс вектора, повторно реализуйте созданные ранее программы, которые использовали класс vector так, чтобы вместо него использовался класс Vector. 18.1.3. Функции operator new() и operator delete() В предыдущем подразделе на примере класса vector было продемонстрировано применение класса allocator для управления пулом памяти при хранении данных внутри класса. В трех следующих разделах рассматривается реализация той же стра- тегии на примере более простых библиотечных средств.
790 Часть V. Дополнительные темы Однако сначала имеет смысл подробнее рассмотреть работу операторов new и delete. // оператор new string * sp = new string("initialized"); Когда используется оператор new, фактически выполняются три следующих дей- ствия. Сначала выражение вызывает библиотечную функцию operator new (), резервирующую нетипизированную область памяти, причем достаточно большую, чтобы содержать объект указанного типа. Затем, чтобы создать объект на основании переданного инициализирующего значения, запускается конструктор соответст- вующего класса. И наконец, возвращается указатель на вновь созданный объект, delete sp; Когда используется оператор delete (), удаляющий объект, расположенный в динамической памяти, происходят два следующих действия. Сначала выполняется деструктор, соответствующий классу объекта, на который указывает указатель sp. Затем используемая объектом память освобождается при помощи библиотечной функции operator delete (). Терминология. Оператор new или функция operator new () Имена библиотечных функций operator new() и operator delete () могут вве- сти в заблуждение. В отличие от других функций-операторов (таких как operator=), эти функции не перегружают операторы new и delete. Фактически переопределить поведение операторов new и delete нельзя. В процессе выполнения оператор new вызывает функцию operator new (), чтобы зарезервировать область памяти, в которой он затем создает объект. Оператор delete уничтожает объект, а затем вызывает функцию operator delete (), чтобы освобо- дить память, которая использовалась объектом. Поскольку имена операторов new и delete совпадают с именами соответствующих библиотечных функций, их легко можно перепутать. Интерфейс функций operator new () и operator delete () Существует две перегруженные версии функций operator new () и operator delete (). Каждая из версий поддерживает соответствующий оператор new и delete, void *operator new(size_t); // размещает объект void *operator new[](size_t); // размещает массив void Operator delete(void*); // освобождает объект void *operator delete[](void*); // освобождает массив Применение функций операторов размещения в памяти Хотя функции operator new() и operator delete () предназначены для использования оператором new, они являются вполне доступными функциями биб-
Глава 18. Специализированные инструменты и технологии 791 лиотеки. Их можно использовать для создания нетипизированных областей в памя- ти. Они несколько похожи на функции-члены allocate () и deallocate () клас- са allocator. Например, в собственном классе Vector вместо объекта класса allocator можно использовать функции operator new() и operator delete О. Для создания нового пространства в динамически распределяемой памяти можно использовать следующий код. // зарезервировать пространство для хранения newcapacity // элементов класса Т Т* newelements = alloc.allocate(newcapacity); Этот код можно переписать следующим образом. // зарезервировать пространство для хранения newcapacity // элементов класса Т Т* newelements = ststic_cast<T*> (operator new[](newcapacity * sizeof(T))); Аналогично, при освобождении области, на которую указывает указатель elements вектора, можно использовать следующий код. // освободить память, занятую элементами alloc.deallocate(elements, end - elements); Этот код можно переписать следующим образом. // освободить память, занятую элементами operator delete[](elements); Эти функции ведут себя аналогично функциям-членам allocate () и deallocate () класса allocator. Однако у них есть одно очень важное отличие: способность работать с указателями типа void*, а не типизированными указателями. Как правило, безопаснее использовать класс allocator, а не сами функции- члены operator new() И operator delete(). Поскольку функция-член allocate () резервирует типизированную область памяти, использующие ее программы могут избежать необходимости вычисления требуемого объема памяти. Кроме того, они могут также избежать преобразования (раздел 5.12.4, стр. 209) значения, возвращаемого функцией operator new(). Аналогично, функция-член deallocate () освобождает память специфического типа, тоже позволяя избежать необходимости преобразования в тип void*. 18.1.4. Размещающий оператор new Библиотечные функции operator new () и operator delete () являются низкоуровневыми версиями функций-членов allocate () и deallocate () класса allocator. Они резервируют память, но не инициализируют ее. Существуют также низкоуровневые альтернативы функций-членов construct () и destroy () класса allocator. Эти функции-члены инициализируют и удаляют объекты в области памяти, зарезервированной объектом класса allocator. Аналогией функции-члена construct () является третий вид оператора new, упоминаемый как размещающий оператор new (placement new). Он инициализирует объект в нетипизированной области памяти, которая уже была зарезервирована. Эта
792 Часть V. Дополнительные темы версия отличается от других операторов new тем, что он не резервирует память. Вместо этого он получает указатель на нетипизированную, но уже созданную, об- ласть памяти и инициализирует в ней объект. Размещающий оператор new фактиче- ски позволяет создавать объект в определенной, предварительно зарезервированной области памяти. Размещающий оператор new имеет следующий формат. new(адрес_размещения) тип new{адрес_размещения) тип (список_инициализации) Здесь адрес_размещения представляет собой указатель, а список_инициали- зации является списком (возможно, пустым) инициализирующих значений, ис- пользуемых при создании нового объекта. Размещающий оператор new можно использовать для замены обращения к функции construct () в реализации класса Vector. // создать копию t в элементе, на который указывает first_free alloc.construct(first_fгее, t); Приведенный выше код можно заменить эквивалентным выражением, исполь- зующим размещающий оператор new. // скопировать t в элемент, адрес которого содержит first_free new(first_free) T(t); При использовании размещающего оператора new выражения получаются более гибкими, чем при использовании функции-члена construct () класса allocator. Когда размещающий оператор new инициализирует объект, он может использовать любой конструктор и непосредственно создать объект, а функция construct () всегда использует конструктор копий. Например, используя пару итераторов любым из двух следующих способов мож- но инициализировать строку. allocator<string> alloc; string *sp = alloc.allocate(2); // зарезервировать область для двух / / строк // существует два способа создания строки по двум итераторам new(sp) string(b, е); // создание непосредственно II на месте alloc.construct(sp + 1, string(b, e)); // создать и скопировать из // временного объекта Размещающий оператор new использует конструктор класса string, который получает два итератора и создает строку непосредственно в той области, на которую указывает указатель sp. Когда происходит вызов функции construct (), необхо- димо сначала создать строку, используя два итератора, а полученный объект класса string передать функции construct (). Затем, чтобы скопировать эту неимено- ванную, временную строку в объект, на который указывает указатель sp, эта функ- ция использует конструктор копий класса string. Зачастую, это различие несущественно: у классов подобных значению никакого вполне очевидного различия между созданием объекта непосредственно на месте и копированием временного объекта нет. Различие в производительности тоже редко бывает значительным. Однако для некоторых классов применение конструктора ко- пий либо невозможно (поскольку он объявлен закрытым), либо нежелательно. В этих случаях применение размещающего оператора new может оказаться неизбежным.
Глава 18. Специализированные инструменты и технологии 793 Упражнения раздела 18.1.4 Упражнение 18.4. Почему функция construct () ограничена применением конструктора копий класса элемента? Упражнение 18.5. Почему выражения с использованием размещающего оператора new могут быть более гибкими? 18.1.5. Явный вызов деструктора Подобно тому, как размещающий оператор new является низкоуровневой аль- тернативой функции-члену allocate () класса allocator, явное обращение к де- структору можно использовать как низкоуровневый вариант вызова функции destroy(). В собственной версии класса Vector, использующей класс allocator, для уда- ления каждого элемента использовался вызов функции destroy (). // удаление прежних элементов в обратном порядке for (Т *р = first_free; р != elements; /* удаление */ ) alloc.destroy(--р); В программах, которые используют размещающий оператор new, для создания объекта можно явно вызвать деструктор. for (Т *р = first_free; р != elements; /* удаление */ ) р->~Т(); // вызов деструктора Здесь происходит явный вызов деструктора. Оператор стрелки обращается к зна- чению итератора р, чтобы получить объект, на который он указывает. Затем проис- ходит вызов деструктора, имя которого соответствует имени класса, с предваряю- щим символом ~. В результате явного вызова деструктора, объект сам удаляется правильно. Одна- ко память, в которой находился объект, при этом не освобождается. В случае необ- ходимости ее можно использовать многократно. При вызове функция operator delete () не запускает деструктор, она лишь осво- бождает указанную область памяти. Упражнения раздела 18.1.5 Упражнение 18.6. Переделайте класс vector так, чтобы он использовал функции operator new (), operator delete (), размещающий оператор new и непосредственное обращение к деструктору. Упражнение 18.7. Проверьте новую версию класса vector, использовав ее в тех же програм- мах, что и ранее. Упражнение 18.8. Какая из версий лучше и почему?
794 Часть V. Дополнительные темы 18.1.6. Операторы new и delete, специфические для класса В предыдущих разделах описано, как класс может использовать управление па- мятью в своей внутренней структуре данных. Еще один способ оптимизации резер- вирования памяти подразумевает улучшение способа применения оператора new. Рассмотрим, например, класс Queue, описанный в главе 16, “Шаблоны и общее про- граммирование”. Этот класс не хранит сами элементы. Вместо этого он использует оператор new, чтобы создавать объекты класса Queue Item. Эффективность класса Queue можно было бы улучшить, предварительно заре- зервировав свободный участок памяти для размещения новых объектов класса Queue Item. Впоследствии, при создании нового объекта класса Queue Item, его можно будет разместить в этой предварительно зарезервированной области. При удалении объектов класса Queue Item, занимаемая ими память остается в предвари- тельно зарезервированной области (и может быть использована следующими объек- тами), а не возвращается системе. Различие между данным случаем и проблемой в реализации класса Vector за- ключается в том, что здесь необходимо оптимизировать поведение операторов new и delete применительно к объектам специфического типа. Обычно оператор new ре- зервирует память при помощи библиотечной версии функции operator new (). Класс может манипулировать памятью, занятой объектами ее типа, используя собст- венные функции-члены по имени operator new () и operator delete О . Когда компилятор встречает оператор new или delete, примененные к клас- су, он проверяет, имеет ли данный класс функции-члены operator new () и operator delete (). Если класс обладает (или наследует) собственными функ- циями-членами new() и delete (), то именно их использует компилятор при ре- зервировании и освобождении области памяти для объекта. В противном случае ис- пользуются версии этих функций из стандартной библиотеки. Чтобы оптимизировать поведение операторов new и delete, достаточно опреде- лить собственные версии функций-членов operator new() и operator delete (). О создании и удалении объектов операторы new и delete “позаботятся” сами. Функции-члены new () и delete () Если в классе необходима любая из этих функций-членов, их придется определить обе. Функция-член класса operator new () должна получать параметр типа size_t, а ее возвращаемое значение должно иметь тип void*. Параметр типа size_t задает размер резервируемый области памяти в байтах. Возвращаемое значение функции-члена operator delete () должно иметь тип void. Она может быть определена как получающая один параметр типа void* или два параметра типа void* и size_t. Параметр типа void* функции delete () предна- значен для указателя на удаляемый объект. Этот указатель может быть нулевым. Если
Глава 18. Специализированные инструменты и технологии 795 параметр типа size_t присутствует, компилятор автоматически инициализирует его размером в байтах того объекта, который представлен первым параметром. Параметр типа size_t необходим в случае, если класс принадлежит иерархии наследования. При удалении указателя на тип в иерархии наследования, он может оказаться указателем на объект базового или производного класса. Обычно объект производного класса превосходит объект базового класса по размеру. Если базовый класс имеет виртуальный деструктор (раздел 15.4.4, стр. 618), размер переданный функции operator delete () варьируется в зависимости от динамического типа объекта, на который указывает удаляемый указатель. Если базовый класс виртуаль- ного деструктора не имеет, результат удаления указателя на объект при помощи указателя на базовый класс, как обычно, окажется непредсказуем. Эти функции являются неявно статическими (раздел 12.6.1, стр. 498). Нет ника- кой необходимости явно объявлять их статическими, хоть это и вполне допустимо. Функции-члены new О и delete () должны быть статическими, поскольку они ис- пользуются, соответственно, перед созданием объекта (operator new () ) и после его удаления (operator delete О ). Следовательно, никаких данных-членов, ко- торыми могут манипулировать эти функции, не может быть. Подобно любым дру- гим статическим функциям-членам, функции new() и delete () способны непо- средственно обращаться только к статическим членам класса. Операторы массива new [] и delete [] Чтобы манипулировать массивом объектов класса, можно также определить функции-члены new[] и delete []. Если эти функции существуют, компилятор использует их вместо глобальных версий. Типом возвращаемого значения функции-члена класса operator new[] дол- жен быть void*, а типом ее первого параметра — size_t. Параметр типа size_t автоматически инициализируется значением, которое соответствует количеству байтов, необходимых для сохранения указанного количества элементов массива данного типа. Типом возвращаемого значения функции-члена delete [] должен быть void, а типом ее первого параметра — void*. Параметр типа void* автоматически инициа- лизируется значением, которое соответствует началу области памяти, в которой хранится массив. Оператор delete [] может также иметь второй параметр типа size_t. Если до- полнительный параметр представлен, компилятор автоматически инициализирует его размером области памяти в байтах, необходимой для хранения массива. Переопределение функций, специфических для класса Пользователь класса, в котором определены его собственные функции-члены new () и delete О , вполне может принудительно применить глобальные библио- течные функции new () или delete (), явно указав при вызове глобальную область видимости. Туре ::new Туре; ::delete р; // используется глобальная // функция operator new() // используется глобальная // функция operator delete ()
796 Часть V. Дополнительные темы Здесь пользователь использовал глобальную функцию operator new (), несмотря на то, что в классе Туре определена его собственная, специфическая именно для дан- ного класса функция operator new (). То же самое касается функции delete (). Если новая область памяти была зарезервирована при помощи глобальной функции operator new (), для ее удаления также следует использовать глобальную функ- цию operator delete(). Упражнения раздела 18.1.6 Упражнение 18.9. Объявите функции-члены new () и delete () для класса Queueitem. 18.1.7. Базовый класс системы резервирования памяти Как и в случае с общим управляющим классом (раздел 16.5, стр. 698), данный пример рассматривает довольно сложную концепцию языка C++. Полное понимание этого при- F Л мера предполагает наличие глубоких знаний в областях наследования и шаблонов. Возможно, изучение данного примера имеет смысл отложить до тех пор, пока эти темы не будут вполне понятны. Ранее уже было продемонстрировано объявление и применение специфических для класса Queue Item функций-членов new () и delete (). Однако прежде чем по- ступать таким образом, необходимо знать, как обеспечить преимущество по сравне- нию со встроенными библиотечными функции new () и delete (). Одна из обще- принятых стратегий подразумевает предварительное резервирование нетипизиро- ванной области памяти, предназначенной для последующего создания объектов. При создании новых элементов, их можно размещать в этом предварительно заре- зервированном пространстве. При удалении элементов, освобождаемая память оста- ется в предварительно зарезервированной области, а не возвращается системе. Эту стратегию зачастую называют хранением списка свободных блоков (freelist). Список свободных блоков может быть реализован как связанный список объектов, место для которых зарезервировано, но сами они еще не созданы. Однако прежде чем реализовать ориентированную на список свободных блоков стратегию резервирования для класса Queue Item, заметим, что класс Queue Item не уникален в желании оптимизировать размещение в памяти объектов своего типа. Необходимость в этом может возникнуть у многих классов. Поскольку подобное по- ведение может быть полезным впоследствии, создадим для реализации списка сво- бодных блоков новый класс по имени CachedObj. Такой класс как Queueitem, ну- ждающийся в оптимизации размещения своих объектов, сможет использовать класс CachedOb j, а не реализовать собственные функции-члены new () и delete (). Класс CachedOb j будет иметь очень простой интерфейс: его задача заключается лишь в резервировании памяти и управлении списком свободных блоков, разме- щенных, но несозданных объектов. В этом классе будет определена функция-член operator new (), которая возвращает следующий элемент из списка свободных
Глава 18. Специализированные инструменты и технологии 797 блоков и, соответственно, удаляет его из списка свободных блоков. Каждый раз, ко- гда список свободных блоков оказывается пуст, функция operator new () резер- вирует новую область памяти. В классе определена также функция operator delete (), возвращающая элемент обратно в список свободных блоков, когда объект оказывается удален. Классы, в которых предполагается использовать стратегию резервирования спи- сков свободных блоков для их собственных объектов, следует наследовать от класса CachedObj. Унаследовав класс CachedObj, эти классы смогут использо- вать его определения функций-членов operator new () и operator delete () наряду с переменными-членами, необходимыми для списка свободных блоков. Поскольку класс CachedOb j задуман как базовый, снабдим его открытым вирту- альным деструктором. Как будет продемонстрировано, класс Cachedobj можно использовать только для классов, которые не включены в иерархию наследования. В отличие от функций-членов f Л new о и delete (), класс Cachedobj не может резервировать области памяти для объектов разного размера, в зависимости от его фактического типа. Его список ^i/ма^ свободных блоков содержит объекты одинакового размера. Следовательно, он может быть использован только для таких классов как Queue item, которые не будут исполь- зованы как базовые. В классе Cachedobj определены и наследуются его производными классами следующие переменные-члены: статический указатель на начало списка свободных блоков; переменная-член next, указывающая на следующий объект класса Cachedobj. Указатель next связывает элементы списка свободных блоков в цепочку. Каж- дый класс, производный от класса Cachedobj, будет содержать собственные дан- ные, специфические именно для него, плюс один указатель, унаследованный от ба- зового класса Cachedobj. Этот дополнительный указатель, которым обладает объект, используется при резервировании памяти, но не наследующим классом непосредст- венно. Во время применения объекта, этот указатель не имеет никакого смысла и не используется. Когда объект доступен для применения и находится в списке свобод- ных блоков, указатель next указывает на следующий доступный объект. Если применить класс Cachedobj для оптимизации размещения объектов клас- са Screen, объект класса Screen (концептуально) будет выглядеть, как на рис. 18.2. next Объект класса, производного от класса CachedObj Часть CachedObj Переменные-члены класса Screen contents cursor height width Puc. 18.2. Схема объекта класса, про- изводного от класса Cachedobj
798 Часть V. Дополнительные темы Класс CachedOb j Последним нерешенным вопросом остается следующий: какие типы использо- вать для указателей в части CachedOb j ? Поскольку список свободных блоков хоте- лось бы использовать для любых классов, класс CachedOb j следует сделать шабло- ном. В результате указатели на объект будут иметь тип шаблона. / * класс резервирования памяти: предварительно резервирует * пространство для объектов, а также поддерживает список * свободных блоков неиспользуемых объектов * После освобождения, объект возвращается в список * свободных блоков * Освобождение памяти происходит только при выходе из программы * / template <class Т> class CachedObj { public: void *operator new(std::size_t); void operator delete(void *, std::size_t); virtual -CachedObj() { } protected: T *next; private: static void add_to_freelist(T*); static std::allocator<T> alloc_mem; static T *freeStore; static const std::size_t chunk; }; Этот класс довольно прост. Он предоставляет только три открытые функции- члена: operator new (), operator delete () и виртуальный деструктор. Функ- ции-члены new () и delete () извлекают и возвращают объекты в список свобод- ных блоков. Списком свободных блоков управляют статические члены. Эти члены объявлены статическими потому, что существует только один список свободных блоков, со- вместно используемый всеми объектами данного класса. Указатель frees tore указывает на начало списка свободных блоков. Переменная-член chunk задает количество областей объектов, которые будут зарезервированы, когда список сво- бодных блоков окажется пуст. И наконец, функция-член add_to_f reelist () помещает объекты в список свободных блоков. Чтобы поместить вновь зарезерви- рованные области объектов в список свободных блоков, эта функция использует функцию operator new (). Чтобы вернуть область объекта обратно в список сво- бодных блоков при его удалении, эта функция использует функцию operator delete (). Применение класса CachedObj Единственная сложность при использовании класса CachedObj сопряжена с пара- метром шаблона. При наследовании, чтобы получить объект класса CachedObj, соот- ветствующий данному производному классу, используется тип шаблона CachedObj. Чтобы обеспечить многократное использование шаблона CachedObj для управле- ния списком свободных блоков, его класс необходимо унаследовать. Однако класс CachedObj имеет указатель на тип объекта, которым он управляет. Типом этого указателя является класс, производный от класса CachedObj.
Глава 18. Специализированные инструменты и технологии 799 Например, чтобы оптимизировать управление памятью класса Screen, его сле- дует объявить следующим образом. class Screen: public CachedObj<Screen> { // интерфейс и реализация членов класса Screen неизменны }; Это объявление придает классу Screen новый базовый класс — экземпляр шаб- лона CachedObj, типом параметра которого является класс Screen. Теперь, кроме прежних членов, определенных внутри класса Screen, каждый его объект будет со- держать дополнительный унаследованный член по имени next. Поскольку Queue Item является шаблоном класса, получение класса, производ- ного от него, несколько усложняется, template <class Туре> class Queueitem: public CachedObj< QueueItem<Type> > { // остальная часть объявления класса и все определения // его членов неизменны } ; Это объявление свидетельствует о том, что Queue Item является шаблоном клас- са, который происходит от экземпляра шаблона CachedObj, получающего объект класса Queueltem<Type>. Например, при определении очереди целых чисел, класс Queueltem<int> будет получен как производный от класса CachedObj< Queueltemcint> >. Никаких других изменений в этот класс вносить не нужно. Теперь класс Queueitem i имеет механизм автоматического резервирования памяти, который использует список / свободных блоков для уменьшения количества случаев резервирования памяти, необхо- димой для создания новых элементов очереди. Как происходит резервирование Поскольку класс Queue Item происходит от класса CachedObj, при любом слу- чае применения оператора new для резервирования области памяти, например, при вызове функции Queue : : push (), происходит не только резервирование области памяти, но и создание нового объекта класса Queue 11 em. // создает новый объект класса Queueitem QueueItem<Type> *pt = new QueueItem<Type>(val); При каждом обращении к оператору new происходит следующее. 1. Для резервирования области под объект из списка свободных блоков использу- ется функция QueueItem<T>::operator new(). 2. Для создания объекта в этой области памяти используется конструктор копий класса хранимого элемента (т.е. типа Т). Аналогично, при уничтожении указателя на класс Queue Item, сначала выполня- ется его деструктор (чтобы удалить объект, на который указывает указатель pt), а затем вызывается функция-член класса operator delete (). delete pt; Данный оператор возвращает используемую область памяти обратно в список свободных блоков.
800 Часть V. Дополнительные темы Определение функции operator new () Функция-член operator new () возвращает область объекта из списка свобод- ных блоков. Если список свободных блоков пуст, функция new () должна сначала зарезервировать chunk новых областей памяти. template <class Т> void *CachedObj<T>::operator new(size_t sz) { // new должен лишь запросить создание Т, а не объект // производный от Т; удостовериться в правильности /! запрошенного размера ("Cachedobj: wrong size object in operator new"); if (!freeStore) { // список пуст: зарезервировать новую порцию памяти // allocate резервирует chunk областей под объекты типа Т Т * array = alloc_mem.allocate(chunk); // теперь установить все указатели next / / зарезервированной памяти for (size_t i = 0; i ! = chunk; + + i) add_to_freelist(&array[i]); } T *p = freeStore; freeStore - freeStore->CachedObj<T>::next; return p; // конструктор T создаст часть T объекта } Работа функции начинается с выяснения того, следует ли запросить резервиро- вание дополнительного количества пространства. Необходимость этой проверки обуславливает упомянутое ранее ограничение конструкции класса Cachedobj, предписывающее использовать его только для тех классов, которые не являются базовыми. Поскольку класс Cachedobj резервирует в списке свободных блоков место для объектов фиксированного размера, он не может быть использован для управления резервированием памяти для классов из иерархии наследования. Объекты классов иерархии наследования почти всегда имеют разный размер. Единый механизм резервирования памяти, применимый для таких классов, должен был бы быть значительно сложнее, чем тот, который реализован здесь. Затем функция operator new () проверяет, есть ли в списке свободных блоков место для объектов. Если нет, она применяет функцию-член класса allocator для резервирования chunk новых областей под объекты. Затем она перебирает вновь созданные области и устанавливает указатель next. После обращения к функции add_to_f reelist (), каждый объект в списке свободных блоков останется в ос- новном еще не созданным (unconstructed), кроме его указателя next, который уже содержит адрес следующего доступного объекта. В результате список свободных блоков будет выглядеть так, как на рис. 18.3. Удостоверившись в наличии доступных объектов, функция operator new () воз- вращает адрес первого элемента в списке свободных блоков, а указателю freeStore присваивает адрес следующего элемента в списке свободных блоков. Возвращенный объект остается еще не занятым. Поскольку функцию operator new () вызывает оператор new, создание объекта становится именно его задачей.
Глава 18. Специализированные инструменты и технологии 801 Queueltem<T> Рис. 18.3. Схема списка свободных блоков класса CachedObj Определение функции operator delete () Функция-член operator delete () отвечает только за управление памятью. Сам объект уже был удален деструктором, который оператор delete вызывает перед об- ращением к функции operator delete (). Функция-член operator delete О довольно проста. template cclass Т> void CachedObj<Т>::operator delete(void *p, size_t) { if (p != 0) // вернуть "удаленный" объект обратно в начало II списка свободных блоков add_to_freelist(static_cast<T*>(р)); } Вызов функции add_to_f reelist () возвращает освобожденный объект в спи- сок свободных блоков. Весьма интересна та часть кода, в которой происходит приведение (раздел 5.12.4, стр. 209). Вызов функции operator delete () происходит при удалении объекта класса в динамически распределяемой памяти. Компилятор передает адрес этого объекта функции operator delete (). Однако типом параметра-указателя явля- ется void*. Перед вызовом функции add_to_f reelist () указатель типа void* следует привести обратно к его фактическому типу. В данном случае указатель при- водится к типу Т, который в свою очередь является указателем на объект класса, унаследованного от класса CachedObj. Функция-член add_to_f reelist () Задача этой функции-члена заключается в установке указателя next и модифи- кации указателя frees tore при добавлении объекта в список свободных блоков. // поместить объект в начало списка свободных блоков template cclass Т> void CachedObj<Т>::add_to_freelist(Т *p) { p->CachedObj<T>::next = freeStore; freeStore = p; } Единственная сложность связана с использованием переменной-члена next. На- помним, что объект CachedObj предназначен для использования в качестве базово- го класса. Классом размещаемых объектов является вовсе не CachedObj, а класс, производный от него. Следовательно, класс Т будет производным, а указатель р —
802 Часть V. Дополнительные темы указателем на класс Т, а не на класс CachedObj. Если бы производный класс имел собственный член по имени next, следующий код относился бы к члену next про- изводного класса! p->next Однако адрес необходимо присвоить переменной-члену next базового класса, т.е. класса CachedObj. Во избежание любых возможных конфликтов с именами, определенными в производном 1 классе, необходимо явно указать, что адрес присваивается переменной-члену next ^у! именно базового класса. Определение статических переменных-членов Осталось определить лишь статические переменные-члены. template <class Т> allocator< Т > CachedObj< Т >::alloc_mem; template cclass T> T *CachedObj< Т >::freeStore = 0; template cclass T> const size_t CachedObjc T >::chunk = 24; Как обычно, для шаблона класса статическими должны быть члены каждого типа, используемого при создании экземпляра шаблона CachedObj. Переменная-член chunk инициализируется произвольным значением, в данном случае 24. Указатель frees tore инициализируется нулевым значением, что свидетельствует о том, что список свободных блоков вначале пуст. Переменная-член alloc_mem не требует инициализации, но ее следует определить. Упражнения раздела 18.1.7 Упражнение 18.10. Объясните каждый из следующих случаев инициализации. Укажите, есть ли здесь ошибки и какие именно. class iStack { public: iStack(int capacity): stack(capacity), top(0) { } private: }; (a) iStack *ps = new iStack(20); (b) iStack *ps2 = new const iStack(15); (c) iStack *ps3 = new iStack[ 100 ]; Упражнение 18.11. Объясните, что происходит в следующих операторах new и delete. struct Exercise { Exercise(); -Exercise(); }; Exercise *pe = new Exercise[20]; deleted pe; Упражнение 18.12. Реализуйте систему резервирования памяти, специфическую для класса Queue или другого класса по выбору. Чтобы выяснить, насколько полезен этот подход и полезен ли он вообще, оцените изменение производительности.
Глава 18. Специализированные инструменты и технологии 803 18.2. Идентификация типов времени выполнения Идентификация типов времени выполнения (RTTI — Run-time Type Identification)! позволяет программам, которые используют указатели или ссылки на базовые клас- сы, выяснять фактические типы объектов производных классов, к которым относят- ся эти указатели или ссылки. RTTI обеспечивает два следующих оператора. 1. Оператор typeid, который возвращает фактический тип объекта, к которому относится указатель или ссылка. 2. Оператор dynamic_cast, который осуществляет безопасное преобразование указателя или ссылки на базовый класс в указатель или ссылку на производный класс. Эти операторы возвращают информацию о динамическом типе только для тех классов, . ч 1 которые обладают одной или несколькими виртуальными функциями. Для всех остальных классов возвращается информация о статическом типе (т.е. типе времени компиляции). Во время выполнения операторы RTTI срабатывают лишь для классов с вирту- альными функциями, однако во время компиляции они применимы и для всех ос- тальных типов. Динамическое приведение необходимо в случае, когда имеется ссылка или указа- тель на базовый класс, но действия необходимо выполнить для той части производ- ного класса, которая не является частью базового класса. Как правило, для обеспе- чения применения членов производного класса по указателю на базовый класс, используют виртуальную функцию. При использовании виртуальных функций, компилятор автоматически выберет правильную версию функции, т.е. согласно фак- тическому типу объекта. Однако в некоторых ситуациях применить виртуальные функции невозможно. В этих случаях RTTI предоставляет альтернативный механизм. Правда этот меха- низм в большой степени чреват ошибками, чем виртуальные функции: чтобы приве- дение прошло успешно, программист должен заранее знать, к какому именно классу должен быть приведен объект. Динамическое приведение следует использовать осторожно. При каждой возможности желательно создавать и использовать виртуальные функции, а не прибегать к непо- средственному управлению типами. 18.2.1. Оператор dynamic_cast Оператор dynamic_cast применяется для преобразования ссылки или указателя на объект базового класса в ссылку или указатель на другой класс той же иерархии. 1 Или информация о типах времени выполнения (RTTI — Runtime Type Information). — Примеч. ред.
804 Часть V. Дополнительные темы Указатель, используемый оператором dynamic_cast, должен быть допустимым, т.е. он должен указывать на существующий объект или 0. В отличие от других способов приведения, оператор dynamic_cast подразуме- вает контроль соответствия типов во время выполнения. Если объект, связанный со ссылкой или указателем, не является объектом результирующего класса, оператор dynamic_cast приводит к ошибке. Если неудачу потерпит динамическое приведе- ние типа указателя, результатом оператора dynamic_cast окажется значение 0. Если неудачу потерпит динамическое приведение типа ссылки, будет передано ис- ключение типа bad_cast. Следовательно, оператор dynamic_cast выполняет два действия сразу. Его вы- полнение начинается с проверки допустимости запрошенного приведения. Только ес- ли приведение допустимо, оператор фактически осуществляет его. Как правило, класс объекта, к которому будет приведена ссылка или указатель, на момент компиляции не известен. Указатель на базовый класс может быть использован для указания на объект производного класса. Аналогично, ссылка на базовый класс может быть ини- циализирована объектом производного класса. В результате осуществляемая опера- тором dynamic_cast проверка должна выполняться во время выполнения. Применение оператора dynamic_cast В качестве простого примера рассмотрим класс Base, у которого есть по крайней мере одна виртуальная функция-член и класс Derived, производный от класса Base. Предположим, что существует указатель на класс Base по имени basePtr, который предстоит привести к типу Derived во время выполнения. if (Derived *derivedPtr = dynamic_cast<Derived*>(basePtr)) { // применение объекта, класса Derived, на который указывает II указатель derivedPtr } else { // BasePtr указывает на объект класса Base // применение объекта класса Base, на который указывает // указатель basePtr } Если во время выполнения указатель basePtr фактически указывает на объект класса Derived, приведение пройдет успешно и указатель derivedPtr окажется инициализирован адресом того объекта класса Derived, на который указывает ука- затель basePtr. В противном случае результатом приведения окажется 0, а при- своение нулевого значения указателю derivedPtr приведет к невыполнению усло- вия в операторе if. Оператор dynamic_cast может быть применен к указателю, значением которого яв- ляется о. Результатом выполнения также окажется о. 7 / Проверив значение указателя derivedPtr, код внутри оператора if может быть “уверен”, что он работает с объектом на самом деле класса Derived и может без опа- сений использовать его функции. Если оператор dynamic_cast потерпит неудачу, поскольку указатель basePtr указывает на объект класса Base, обработку продол- жит оператор else, код которого предназначен для объекта класса Base. Еще одно преимущество проверки внутри условия оператора i f заключается в том, что это не
Глава 18. Специализированные инструменты и технологии 805 позволяет расположить код между оператором dynamic_cast и проверкой резуль- тата приведения. Следовательно, такой подход не позволяет случайно использовать указатель derivedPtr перед проверкой успешности приведения. Третье преиму- щество заключается в том, что указатель не доступен вне оператора if. В случае ошибки приведения, несвязанный указатель окажется недоступен для применения впоследствии, там, где о проверке уже будет забыто. Выполнение оператора dynamic_cast в условии гарантирует, что приведение и проверка его результата будут осуществлены в одном выражении. Оператор dynamic_cast и ссылочные типы В предыдущем примере оператор dynamic_cast использовался для преобразо- вания указателя на базовый класс в указатель на производный. Оператор dynamic_ cast может быть также использован для преобразования ссылки на базовый класс в ссылку на производный. Синтаксис такого оператора dynamic_cast имеет сле- дующий вид. dynamic_cast< Туре& >(val) Здесь Туре — это тип результата преобразования, a val — объект базового класса. Оператор dynamic_cast приводит операнд val к необходимому типу (Туре&) только тогда, когда операнд val фактически относится к объекту класса Туре или класса, производного от него. Поскольку нулевых ссылок не существует, для них невозможно применить тот же способ проверки, который используется для указателя. Вместо этого при ошибке в ходе приведения передается исключение std: :bad_cast. Оно определено в биб- лиотечном заголовке type info. Предыдущий пример можно переписать так, чтобы в нем использовались ссылки, void f(const Base &b) { try { const Derived &d = dynamic_cast<const Derived&>(b); // применение объекта класса Derived, на который II ссылается ссылка b } catch(bad_cast) { // обработка ошибки приведения } } Упражнения раздела 18.2.1 Упражнение 18.13. Рассмотрим следующую иерархию классов, где у каждого класса определен открытый стандартный конструктор и виртуальный деструктор. class А { /* ... */ }; class В : public А { /* ... */ }; class С : public В { /* ... */ }; class D : public В, public А { /* ... */ }; В каких из следующих операторов dynamic_cast есть ошибки (если есть)? (а) А *ра = new С; В *pb = dynamic_cast< В* >(ра);
806 Часть V. Дополнительные темы (Ь) В *pb = new В; С *рс = dynamic_cast< С* >(pb); (с) А *ра = new D; В *pb = dynamic_cast< В* >(ра); Упражнение 18.14. Что произойдет, если в преобразованиях предыдущего упражнения классы d и в будут производными от виртуального базового класса а? Упражнение 18.15. Используя иерархию классов, определенную в предыдущем упражнении, пе- репишите следующий фрагмент кода так, чтобы используя оператор dynamic_cast привести выражение *ра к типу с&. if (С *рс = dynamic_cast< С* >(ра)) // используются члены класса С } else { // используются члены класса А } Упражнение 18.16. Объясните, когда имеет смысл использовать оператор dynamic_cast вместо виртуальной функции. 18.2.2. Оператор typeid Второй оператор поддержки RTTI — это оператор typeid. Оператор typeid по- зволяет выяснить текущий тип объекта. Оператор typeid имеет следующий формат. typeid(е) Здесь е — это любое выражение или имя класса. Если типом выражения является класс и этот класс содержит одну или несколько виртуальных функций, динамический тип выражения во время компиляции может отличаться от его статического типа. Например, если выражение обращается к зна- чению указателя на базовый класс, статическим типом во время компиляции этого выражения будет базовый класс. Однако если указатель фактически содержит адрес объекта производного класса, оператор typeid сообщит, что выражение имеет тип производного класса. Оператор typeid применим к выражениям любого типа. Операндами оператора typeid могут быть как выражения встроенного типа, так и константы. Когда опе- ранд имеет тип, отличный от класса или класса без виртуальных функций, оператор typeid укажет его статический тип. Когда типом операнда является класс, в кото- ром определена по крайней мере одна виртуальная функция, его тип будет выяснен во время выполнения. Результатом операнда typeid является ссылка на объект библиотечного типа по имени type_info. Более подробно этот тип описан в разделе 18.2.4 (стр. 811). Что- бы использовать класс type_inf о, необходимо подключить библиотечный заголо- вок type inf о. Применение оператора typeid Чаще всего оператор typeid используют для сравнения типов двух выражений или для сравнения типа выражения с определенным типом.
Глава 18. Специализированные инструменты и технологии 807 Base *bp; Derived *dp; // сравнить типы двух объектов во время выполнения if (typeid(*bp) == typeid(*dp)) { // bp и dp указывают на объекты того же типа } // проверить, совпадает ли тип времени выполнения с указанным типом if (typeid(*bp) == typeid(Derived)) { // bp на самом деле указывает на класс Derived } В первом операторе if сравниваются фактические типы объектов, на которые указывают указатели Ьр и dp. Если оба они указывают на тот же тип, результат про- верки положителен. Результат проверки второго оператора if будет положитель- ным, если указатель Ьр в настоящий момент указывает на объект класса Derived. Обратите внимание, операндами оператора typeid являются проверяемые объ- екты (*Ьр), а не указатели (Ьр). // результат проверки всегда отрицателен: типом Ьр является // указатель на класс Base if (typeid(bp) == typeid(Derived)) { // код, который никогда не будет выполнен } Здесь сравнивается тип Base* с типом Derived. Эти типы неравнозначны, по- этому результат проверки всегда будет отрицательным, независимо от типа объек- та, на который указывает указатель Ьр. Информация о динамическом типе возвращается только тогда, когда типом операнда оператора typeid является объект класса с виртуальной функцией. Результатом про- верки указателей во время компиляции (в отличие от объектов, на которые они указы- вают) является статический тип указателя. Когда значением указателя р является 0, оператор typeid (*р) передаст исклю- чение bad_typeid, но только если класс указателя р содержит виртуальную функ- цию. Если тип указателя р виртуальных функций не имеет, значение указателя р окажется несоответствующим. Подобно оператору sizeof (раздел 5.8, стр. 191), компилятор не может вычислить указатель *р. Он использует статический тип р, который не требует, чтобы сам указатель р был допустимым указателем. Упражнения раздела 18.2.2 Упражнение 18.17. Напишите выражение, динамически преобразующее указатель на класс Query_base в указатель на класс AndQuery. Проверьте правильность приведения, использо- вав объект класса AndQuery и другого класса запроса. Отобразите результат выполнения опе- ратора, свидетельствующий о правильности работы приведения, а также убедитесь, совпадает ли полученный результат с ожидаемым. Упражнение 18.18. Напишите то же приведение, но преобразуйте объект класса Query_base в ссылку на класс AndQuery. Повторите проверку и удостоверьтесь, что приведение работает правильно. Упражнение 18.19. Напишите выражение с использованием оператора typeid, позволяющее удостовериться в том, что два указателя класса Query_base указывают на тот же тип. Проверь- те, сработает ли данный подход для класса AndQuery.
808 Часть V. Дополнительные темы 18.2.3. Применение RTTI Для демонстрации целесообразности применения RTTI, рассмотрим иерархию класса, для которого необходима реализация оператора равенства. Два объекта счи- таются равными, если значения определенного набора их переменных-членов совпа- дают. Любой производный класс может добавить в набор собственные данные, кото- рые при выяснении равенства также необходимо будет проверить. Поскольку рассматриваемые в определении оператора равенства значения про- изводного класса могут отличаться от соответствующих значений базового класса, для каждой пары классов иерархии, вероятнее всего, понадобятся различные опера- торы равенства. Кроме того, чтобы использовать данный тип в качестве левого или правого операнда, фактически понадобятся два оператора для каждой пары классов. Таким образом, если иерархия имеет только два класса, понадобятся четыре функции. bool operator==(const Base&, const Base&) bool operator==(const Derived&, const Derived&) bool operator==(const Derived&, const Base&); bool operator==(const Base&, const Derived&); Но если иерархия имеет несколько классов, количество подлежащих определе- нию операторов стремительно растет: для 3 классов понадобится 9 операторов, для 4 классов — 16 и т.д. Казалось бы, эту проблему можно решить определив набор виртуальных функ- ций, которые проверяют равенство на каждом уровне иерархии. Сделав оператор ра- венства виртуальным, можно было бы определить одну функцию, которая работает со ссылкой на базовый класс. Этот оператор мог бы передать свою работу виртуаль- ному оператору equal, который и осуществил бы все необходимые действия. К сожалению, виртуальные функции не очень хороши для решения этой задачи. Проблема кроется в определении типа параметра оператора equal. Параметры вир- туальной функции должны иметь одинаковые типы и в базовом, и в производных классах. В результате, виртуальный оператор equal должен иметь параметр, яв- ляющийся ссылкой на базовый класс. Однако при выяснении равенства двух объектов производного класса, необходи- мо будет сравнить и те переменные-члены, которые специфичны для производного класса. Если параметр является ссылкой на базовый класс, использовать можно бу- дет только те переменные-члены, которые определены в базовом классе. Но обра- титься к переменным-членам, определенным в производном классе не удастся. Рассмотрев эту проблему детально, можно придти к выводу, что оператор equal должен возвращать значение false, если предпринимается попытка сравнивать объекты разных классов. С учетом этого замечания, приступим к решению данной проблемы с использованием RTTI. Здесь будет определен только один оператор равенства. Однако в каждом клас- се будет определена виртуальная функция equal (), которая в первую очередь при- водит свой операнд к правильному типу. Если приведение проходит удачно, осу- ществляется реальное сравнение. В противном случае функция equal () вернет значение false.
Глава 18. Специализированные инструменты и технологии 809 Иерархия класса Чтобы сделать концепцию более конкретной, предположим, что рассматривае- мые классы выглядят следующим образом. class Base { friend bool operator==(const Bases, const BaseSc); public: // члены интерфейса для класса Base protected: virtual bool equal(const BaseSc) const; // данные и другие члены реализации класса Base }; class Derived: public Base { friend bool operator==(const Bases, const BaseSc); public: // другие члены интерфейса класса Derived private: bool equal(const BaseSc) const; // данные и другие члены реализации класса Derived }; Оператор равенства, чувствительный к типу Давайте рассмотрим, как можно было бы определить общий оператор равенства. bool operator==(const Base &lhs, const Base &rhs) { // возвращает false, если типы разные, а в противном случае II возвращает Ihs.equal(rhs) return typeid(Ihs) == typeid(rhs) && Ihs.equal(rhs); } Этот оператор возвращает значение false, если операнды имеют разный тип. Если они имеют одинаковый тип, оператор делегирует реальную работу по сравне- нию операндов соответствующей виртуальной функции equal (). Если операнды являются объектами класса Base, происходит вызов функции Base: : equal (), а если класса Derived — функции Derived: : equal (). Виртуальная функция equal () Каждый класс иерархии должен иметь собственную версию функции equal (). Начало у функций всех производных классов будет одинаковым: они приводят ар- гумент к типу собственного класса. bool Derived::equal(const Base &rhs) const if (const Derived *dp - dynamic_cast<const Derived*>(&rhs)) { // действия по сравнению двух объектов класса Derived // и возвращение результата } else return false; } Приведение всегда должно быть успешным, ведь оператор равенства вызывает эти функции только после проверки того, что два операнда имеют одинаковый тип. Одна- ко приведение необходимо для того, чтобы функция могла обращаться к производным членам правого операнда. Поскольку операнд имеет тип Base&, при необходимости доступа к членам класса Derived, сначала придется осуществить приведение.
810 Часть V. Дополнительные темы Функция equal () базового класса Эта функция гораздо проще других. bool Base::equal(const Base &rhs) const { // действия по сравнению двух объектов класса Base } Здесь нет никакой необходимости в приведении аргументов перед применением. Оба они, и *this и параметр, являются объектами класса Base, поэтому все дос- тупные для него функции содержатся в классе объекта. 18.2.4. Класс type_inf о Точное определение класса type_inf о зависит от компилятора, но стандарт га- рантирует, что все его реализации будут обладать, по крайней мере, теми действия- ми, которые перечислены в табл. 18.2. Этот класс обладает также открытым виртуальным деструктором, поскольку он предназначен для использования в качестве базового класса. Если компилятор по- зволяет предоставить дополнительную информацию о типе, для этого следует вос- пользоваться классом, производным от класса type_inf о. Таблица 18.2. Действия класса type info ti == t2 Возвращает значение true, если оба объекта (11 и 12) имеют тот же тип, и зна- чение false — в противном случае ti != t2 Возвращает значение true, если оба объекта (ti и t2) имеют разные типы, и значение false — в противном случае t. name () Возвращает символьную строку в стиле С, содержащую отображаемую версию имени типа. Имена типов создаются способом, не зависящим от системы ti .before (t2) Возвращает логическое значение (тип bool), указывающее на то, следует ли тип ti прежде типа t2. Порядок следования зависит от компилятора Стандартный конструктор, конструктор копий и оператор присвоения определе- ны как закрытые, поэтому объекты класс type_inf о нельзя ни создать, ни скопиро- вать непосредственно. Единственный способ создать объекты класса type_inf о — это использовать оператор typeid. Функция name () возвращает символьную строку в стиле С, содержащую имя класса объекта. Значение, используемое для данного типа, зависит от компилятора и не обязательно соответствует имени класса, использованному в программе. Единст- венное, что гарантирует функция name () — это уникальность возвращаемой ей строки для данного типа. Тем не менее, функция-член name () вполне применима для отображения имени объекта type_inf о. int iobj; cout typeid(iobj).name() << endl typeid(8.16).name() « endl typeid(std::string).name() « endl typeid(Base).name() « endl typeid(Derived).name() << endl;
Глава 18. Специализированные инструменты и технологии 811 Формат и значение, возвращаемые функцией name (), зависят от компилятора. Приведенный выше код вернул на машине автора следующий результат. d Ss 4Base 7Derived Класс type_info зависит от компилятора. Некоторые компиляторы предоставляют и другие функции-члены, которые возвращают дополнительную информацию о типах, ис- пользуемых в программе. Чтобы выяснить реальные возможности класса type_info для конкретного компилятора, необходимо обратиться к его документации. Упражнения раздела 18.2.4 Упражнение 18.20. С учетом приведенной ниже иерархии классов, в которой каждый класс обла- дает открытым стандартным конструктором и виртуальным деструктором, укажите, какие имена типов отобразят следующие операторы? class А { /* ... */ } ; class В : public А { /* ... */ }; class С : public В { /* ... */ }; (а) А *ра = new С; cout « typeid(pa).name() << endl; (b) C eobj ; A& ra = eobj; cout « typeid(&ra).name() << endl; (с) В *px = new B; A& ra = *px; cout << typeid(ra).name() « endl; 18.3. Указатель на член класса Известно, что используя оператор стрелки (- >) и указатель на объект, можно по- лучить доступ к члену класса этого объекта. Но иногда имеет смысл начать с самого члена класса. То есть сначала получить указатель на определенный член класса, а за- тем обращаться по указателю к данному члену того или иного объекта. Для этого можно использовать специальный вид указателя, известный как указа- тель на член класса (pointer to member). Указатель на член класса воплощает как тип класса, так и тип его члена. Этот факт влияет на способ определения указателя на член класса, его связь с функцией или переменной-членом, а так же способ его применения. Указатели на член класса применимы только к нестатическим членам класса. Статические члены класса не являются частью одного определенного объекта, по- этому указать на статический член класса не требует никакого специального синтак- сиса. Указатели на статические члены являются обычными указателями. 18.3.1. Объявление указателя на член класса Для демонстрации работы указателей на члены класса, используем упрощенную версию класса Screen из главы 12, “Классы”. class Screen { public:
812 Часть V. Дополнительные темы typedef std::string::size_type index; char get() const; char get(index ht, index wd) const; private: std::string contents; index cursor; index height, width; }; Определение указателя на переменную-член Переменная-член contents класса Screen имеет тип std: : string. Полный тип переменной contents можно выразить в качестве “переменной-члена класса Screen, типом которой является std: : string”. Следовательно, полным типом указателя, способным указывать на переменную-член contents, будет “указатель на член класса Screen, типом которого является std: :string”. Этот тип можно записать следующим образом. string Screen::* Определить указатель на член типа string класса Screen можно следующим образом. string Screen::*ps_Screen; Указатель ps_Screen может быть инициализирован адресом переменной-члена contents так. string Screen::*ps_Screen = &Screen contents; Следующим образом можно также определить указатель, который способен со- держать адреса переменных-членов height, width и cursor. Screen::index Screen::*pindex; Здесь указано, что pindex представляет собой указатель на член класса Screen, типом которого является screen: : index. Адрес члена width ему можно присво- ить следующим образом. pindex = &Screen::width; Указателю pindex может быть присвоен адрес любого из членов (width, height или cursor), т.к. все три переменные-члена класса Screen имеют тип index. Определение указателя на функцию-член Указатель на функцию-член должен соответствовать типу функции по трем сле- дующим критериям. 1. Тип и количество параметров функции с учетом того, являются ли они кон- стантными. 2. Тип возвращаемого значения. 3. Тип, членом которого является функция. В определении указателя на функцию-член используют тип возвращаемого зна- чения функции, список ее параметров и класс. Ниже приведен пример указателя на функцию-член get () класса Screen, не получающую никаких параметров. char (Screen::*)() const
Глава 18. Специализированные инструменты и технологии 813 Здесь определен указатель на константную функцию-член класса Screen, не получающую никаких параметров и возвращающую значение типа char. Указа- тель на эту версию функции get () может быть определен и инициализирован следующим образом. // pmf указывает на функцию-член get() класса Screen, II не получающую никаких аргументов char (Screen::*pmf) () const = SScreen::get; Можно также определить указатель на версию функции get () с двумя пара- метрами. char (Screen::*pmf2)(Screen::index, Screen::index) const; pmf2 = SScreen::get; Приоритет оператора обращения выше, чем приоритет оператора “указатель на член 1 класса”. Следовательно, круглые скобки вокруг части screen:: * необходимы. Без них компилятор посчитает следующий код объявлением функции (причем ошибочным). // ошибка: не являющаяся членом класса функция р() II не может иметь спецификатор const char Screen::*р() const; Применение определений типов для указателей на члены класса Определения типов могут существенно упростить код создания указателей на члены класса. Например, приведенное ниже определение типа Action является альтернативным именем типа для версии функции-члена get () с двумя пара- метрами. // Action является именем типа typedef char (Screen::*Action)(Screen::index, Screen::index) const; Action — это имя типа “указатель на константную функцию-член класса Screen, получающую два параметра типа index и возвращающую тип char”. Использова- ние определения типа может упростить определение указателя на функцию get () следующим образом. Action get = SScreen::get; Тип указателя на функцию-член может быть использован для объявления пара- метров функции и типа ее возвращаемого значения. // action получает ссылку на класс Screen и указатель на II функцию-член класса Screen Screens action (Screens, Action = SScreen::get); Эта функция объявлена как получающая два параметра: ссылку на объект класса Screen и указатель на функцию-член класса Screen, получающую два параметра типа index и возвращающую тип char. Функцию action () можно вызвать пере- дав ей либо указатель, либо адрес соответствующей функции-члена класса Screen. Screen myScreen; // эквивалентные обращения: action (myScreen); // применение аргумента по умолчанию action (myScreen, get); // применение определенной ранее // переменной get action (myScreen, SScreen::get); // явная передача адреса
814 Часть V. Дополнительные темы Упражнения раздела 18.3.1 Упражнение 18.21. В чем разница между указателем на обычные переменные или функции и ука- зателем на переменные или функции, являющиеся членами класса? Упражнение 18.22. Определите тип, который представляет собой указатель на переменную-член isbn класса Sales_item. Упражнение 18.23. Определите указатель, который может содержать адрес функции-члена same_isbn(). Упражнение 18.24. Напишите определение типа, который является синонимом указателя на функцию-член avg_price () КЛЭССЭ Sales_item, 18.3.2. Применение указателя на член класса Новые операторы . * и . - >, позволяющие связывать указатель на член класса с фактическим объектом, аналогичны операторам обращения к членам класса . и - >. Левым операндом этих операторов должен быть объект или указатель на класс, а правый операнд — указателем на член этого класса. Оператор обращения к значению указателя на член класса (.*), обеспечивает доступ к члену объекта, указанного непосредственно или по ссылке. Оператор стрелки для указателя на член класса ( - > * ) обеспечивает доступ к члену по указателю на объект. Применение указателя на функцию-член Используя указатель на член класса, можно следующим образом вызывать вер- сию функции get (), не получающей никаких параметров. // pmf указывает на функцию-член get() класса Screen, // не получающую никаких аргументов char (Screen::*pmf)() const = &Screen::get; Screen myScreen; char cl - myScreen.get(); // вызов функции get() класса myScreen char c2 = (myScreen.*pmf)(); // эквивалентное обращение // к функции get() Screen *pScreen = &myScreen; cl = pScreen->get(); // вызов функции get() того объекта, II на который указывает // указатель pScreen с2 = (pScreen->*pmf)(); // эквивалентное обращение II к функции деt() Круглые скобки В обращениях (myScreen. *pmf) () и (pScreen->*pmf) () не- обходимы потому, что приоритет оператора обращения (()) выше, чем у оператора ука- зателя на член класса. Рассмотрим следующее обращение без круглых скобок. myScreen.*pmf() Оно будет интерпретировано следующим образом. myScreen.*(pmf())
Глава 18. Специализированные инструменты и технологии 815 То есть код рассматривается как вызов функции pmf (), возвращаемое значение которой передается оператору указателя на член класса (. *) объекта. Безусловно, тип pmf не обеспечивает такого способа применения, поэтому во время компиляции произойдет ошибка. Подобно вызовам любых других функций, при обращении с использованием ука- зателя на функцию-член вполне можно передавать аргументы. char (Screen::*pmf2)(Screen::index, Screen::index) const; pmf2 = SScreen::get; Screen myScreen; char cl = myScreen.get(0, 0); // вызов версии функции get () / / с двумя параметрами char с2 = (myScreen.*pmf 2)(0, 0); // эквивалентное обращение II к функции get() Применение указателя на переменную-член Те же операторы указателя на член класса применимы для доступа к перемен- ным-членам. Screen::index Screen::*pindex = SScreen::width; Screen myScreen; // эквивалентные способы доступа к переменной-члену width II класса хпу Screen Screen::index indl = myScreen.width; // непосредственное Screen::index ind2 = myScreen.*pindex; // обращение к значению II указателя Screen *pScreen; // эквивалентные способы доступа к переменной-члену width II по указателю *pScreen indl = pScreen->width; // непосредственное ind2 = pScreen->*pindex; // обращение к значению // указателя pindex Таблицы указателей на функции-члены Чаще всего используют указатели на функции и указатели на функции-члены класса, сохраняя их в таблице функций. Таблица функций (function table) представ- ляет собой коллекцию указателей на функции, которая позволяет во время выпол- нения выбрать и выполнить функцию, необходимую в данный момент. В классе, который обладает несколькими функциями-членами того же назначе- ния, подобная таблица применяется для выбора одной из них. Предположим, что рассматриваемый класс Screen был дополнен несколькими функциями-членами, каждая из которых перемещает курсор в определенном направлении. class Screen { public: // остальные члены интерфейса и реализации как прежде Screens home(); // функции перемещения курсора Screens forward(); Screens back(); Screens up(); Screens down(); }; Каждая из этих новых функций не получает никаких параметров и возвращает ссылку на тот объект класса Screen, для которого она была вызвана.
816 Часть V. Дополнительные темы Применение таблиц указателей на функции Вполне возможно определить функцию move (), которая будет вызывать любые из перечисленных выше функций, чтобы выполнить необходимое действие. Для обеспечения работы этой новой функции, добавим в класс Screen статический член, представляющий собой массив указателей на функции перемещения курсора, class Screen { public: // остальные члены интерфейса и реализации как прежде // Action является указателем, которому может быть присвоен // адрес любой из функций-членов перемещения курсора typedef Screens (Screen::*Action)(); static Action Menu[]; // таблица функций public: // выяснить направление перемещения enum Directions { HOME, FORWARD, BACK, UP, DOWN }; Screen& move(Directions); }; Указатели на функции перемещения курсора будет содержать массив по имени Menu. Они будут сохранены согласно значениям перечисления Directions. Функ- ция move () получает значение перечисления и выполняет обращение к соответст- вующей функции. Screens Screen::move(Directions cm) { // доступ к элементу массива Menu по индексу cm // выполнить выбранную функцию от имени данного объекта (this->*Menu[cm])(); return *this; } Обращение внутри функции move () осуществляется следующим образом: сна- чала выбирается элемент массива Menu по индексу ст. Этот элемент является указа- телем на функцию-член класса Screen. Далее, от имени объекта, связанного с ука- зателем this, происходит вызов той функции-члена, на которую указывает элемент массива Menu. При вызове, функции move () передается значение перечислителя, задающее на- правление перемещения курсора. Screen myScreen; myScreen.move(Screen::HOME); // вызов myScreen.home myScreen.move(Screen::DOWN); // вызов myScreen.down Определение таблиц указателей на функции-члены Теперь осталось определить и инициализировать саму таблицу. Screen::Action Screen::Menu[] = { &Screen::home, SScreen::forward, SScreen::back, SScreen::up, SScreen::down, };
Глава 18. Специализированные инструменты и технологии 817 Упражнения раздела 18.3.2 Упражнение 18.25. Каков тип переменных-членов contents и cursor класса screen? Упражнение 18.26. Определите указатель на член класса, способный хранить адрес переменной- члена cursor класса screen. Получите при помощи этого указателя значение переменной- члена screen::cursor. Упражнение 18.27. Создайте определения типов для всех функций-членов класса screen. Упражнение 18.28. Указатели на члены класса вполне могут быть объявлены как переменные- члены самого класса. Измените определение класса screen так, чтобы он содержал указатель на свои функции-члены того же типа, что и у функций home () или down (). Упражнение 18.29. Напишите конструктор класса Screen, получающий в качестве параметра указатель на функцию-член класса screen, список параметров и тип возвращаемого значения которой такие же, как и у функций-членов home () или down (). Упражнение 18.30. Снабдите эти параметры аргументами по умолчанию. Используйте данный параметр для инициализации переменной-члена, описанной в предыдущем упражнении. Упражнение 18.31. Снабдите класс screen функцией-членом, устанавливающей значение этой переменной-члена. 18.4. Вложенные классы Класс может быть определен внутри другого класса. Такой класс называется вложенным классом (nested class) или вложенным типом (nested type). Вложенные классы обычно используются для создания классов реализации, как, например, класс Queue Item из главы 16, “Шаблоны и общее программирование”. Вложенные классы являются вполне независимыми и, как правило, не связаны в значительной степени с классом, который их содержит. Следовательно, объекты со- держащих и вложенных классов не зависимы друг от друга. Объект вложенного класса не имеет членов содержащего класса, а объект содержащего класса не имеет членов, определенных во вложенным классе. Имя вложенного класса видимо в области видимости класса, его содержащего, но не в областях видимости других классов или областях видимости, в которых содер- жащий класс определен. Имя вложенного класса не будет входить в конфликт с тем же именем, объявленным в другой области видимости. Вложенный класс может содержать члены тех же видов, что и невложенный класс. Подобно любым другим классам, вложенный класс способен управлять доступом к сво- им членам, используя маркеры доступа. Его члены могут быть объявлены открытыми (public), закрытыми (private) или защищенными (protected). Содержащий класс не имеет никаких специальных прав доступа к членам вложенного класса, а вложен- ный класс не имеет привилегий в доступе к членам класса, который его содержит. В содержащем классе, вложенный класс представляет собой член, типом которо- го является класс. Подобно любому другому члену, содержащий класс задает уро- вень доступа к этому типу. Вложенный класс, определенный в разделе public со- держащего класса, может быть использован везде. Вложенный класс, определенный в разделе protected, доступен только содержащему классу, его производным и дружественным классам. Вложенный класс, определенный в разделе private, дос- тупен лишь для членов содержащего класса и классов, дружественных для него.
818 Часть V, Дополнительные темы 18.4.1. Реализация вложенного класса Класс Queue, рассматриваемый в главе 16, “Шаблоны и общее программирова- ние”, содержал определение и реализацию вспомогательного класса Queue Item. Этот был закрытый (private) класс, содержащий только закрытые члены, но оп- ределен он был в глобальной области видимости. Общепользовательский код не мог использовать объекты класса Queue Item: ведь все его члены, включая конст- рукторы, являются закрытыми. Хотя имя Queue Item было видимо глобально, од- нако ни определить собственный тип по имени Queue Item, ни создать его объект было нельзя. Наилучшим решением была бы конструкция, где класс Queue Item являлся бы закрытым членом класса Queue. Таким образом, класс Queue (и классы, дружест- венные для него) вполне могли бы использовать класс Queue Item, но сам класс Queue Item не был бы видим в общепользовательском коде. Но если сам класс явля- ется закрытым, его члены вполне можно оставить открытыми: поскольку обращать- ся к классу Queue Item будет только класс Queue или классы, дружественные ему, нет никакой необходимости защищать его члены от доступа со стороны общепользо- вательского кода. Чтобы оставить члены класса открытыми, в его определении мож- но использовать ключевое слово struct. Новая конструкция класса будет выглядеть следующим образом. template cclass Туре> class Queue { // функции интерфейса класса Queue остались неизменными private: // открытые члены в порядке: класс Queueitem является // закрытым членом класса Queue // доступ к членам класса Queueitem имеют только класс Queue // и классы, дружественные ему struct Queueitem { Queueitem(const Type &); Type item; // значение, хранимое в данном элементе Queueitem *next; // указатель на следующий элемент очереди }; Queueitem *head; // указатель на первый элемент очереди Queueitem *tail; // указатель на последний элемент // очереди }; Поскольку класс Queue Item является закрытым членом, использовать его могут только члены класса Queue и классов, дружественных для него. Сделав сам класс Queue Item закрытым, его члены можно оставить открытыми. Это позволит избе- жать необходимости объявления дружественных отношений для класса Queue Item. Классы, вложенные внутрь шаблона класса, являются шаблонами Поскольку класс Queue является шаблоном, его члены также неявно являются шаблонами. В частности, вложенный класс Queue Item неявно представляет собой шаблон класса. Подобно любому другому члену класса Queue, параметр шаблона для класса Queue Item будет тем же параметром шаблона, что и у класса Queue, ко- торый его содержит.
Глава 18. Специализированные инструменты и технологии 819 При создании каждого экземпляра класса Queue, создается его собственный эк- земпляр класса Queue Item с соответствующим аргументом шаблона для класса Туре. Между экземпляром шаблона класса Queue Item и экземпляром шаблона со- держащего класса Queue, нет точного соответствия. Определение членов вложенного класса В данной версии класса Queue Item его конструктор будет определен не внутри класса, а отдельно. Единственная сложность этого определения будет связана с ука- занием имени. Определение члена вложенного класса, расположенное вне его, должно находиться в той же области видимости, в которой определен содержащий класс. Определение чле- на вложенного класса, расположенное вне его, не может находиться непосредственно внутри содержащего класса. Член вложенного класса не является членом содержащего класса. Конструктор класса Queue Item не является членом класса Queue. Следователь- но, он не может быть определен в любом месте тела класса Queue. Но он должен быть определен в той же области видимости, что и класс Queue, но вне этого класса. При определении члена класса вне тела вложенного класса, следует учесть, что вне класса его имя не видимо. Чтобы определить конструктор, необходимо указать, что класс Queue Item вложен внутри области видимости класса Queue. Для этого сле- дует применить полностью квалифицированное имя, содержащее имя класса Queue Item, и имя содержащего его класса Queue. // определение конструктора класса Queueitem, // вложенного внутрь класса Queue<Type> template cclass Туре> Queue<Type>: -.Queueltem: :Queueitem (const Type &t) : item(t), next(O) { } Безусловно, поскольку и Queue и Queue I tern являются шаблонами класса, кон- структор тоже будет шаблоном. Этот код определяет шаблон функции, единственным параметром типа которого является класс Туре. Читая имя функции справа налево, можно уяснить, что эта функция представляет собой конструктор класса Queue Item, который вложен в об- ласть видимости класса Queue<Type>. Определение вложенного класса вне содержащего класса Вложенные классы зачастую содержат детали реализации содержащего класса. Однако может возникнуть необходимость предотвратить возможность просмотра кода реализации вложенного класса пользователями содержащего класса. Например, может возникнуть необходимость поместить определение класса Queue Item в его собственный файл, который будет подключаться в те файлы, кото- рые содержат реализацию класса Queue и его членов. Аналогично тому, как члены вложенного класса можно определить вне тела класса, вне тела содержащего класса можно определить весь класс. template cclass Туре> class Queue { // функции интерфейса класса Queue остались неизменными private:
820 Часть V. Дополнительные темы struct QueueItem; // предварительное объявление вложенного // типа QueueItem Queueitem *head; // указатель на первый элемент очереди Queueitem *tail; // указатель на последний элемент // очереди }; template <class Туре> struct Queue<Type>::Queueitem { Queueitem(const Type &t): item(t), next(0) { } Type item; // значение, хранимое в данном элементе Queueitem *next; // указатель на следующий элемент очереди }; Чтобы определить тело класса вне класса, который его содержит, необходимо указать полностью квалифицированное имя вложенного класса, включающее имя его содержащего класса. Обратите внимание, класс Queue Item все же нужно объя- вить в теле класса Queue. Вложенный класс также может быть сначала объявлен, а впоследствии определен в теле содержащего класса. Подобно любым другим предварительным объявлениям, предварительное объявление вложенного класса позволяет членам вложенного класса обращаться к нему и наоборот. Пока не встретится фактическое определение вложенного класса, расположенное вне его тела, этот класс считается незавершенным (раздел 12.1.4, стр. 466). При использо- вании такого незавершенного класса, к нему применимы все ограничения, присущие незавершенным типам. Определение статических членов вложенных классов Если бы класс Queue Item обладал статическим членом, его определение также должно было бы располагаться во внешней области видимости. Предположим, что класс Queue Item таки имеет статический член. Его определение выглядело бы при- мерно так. // определение статического члена типа int класса Queueitem, II вложенного внутрь класса Queue<Type> template cclass Туре> int Queue<Type>::Queueitem::static_mem = 1024; Применение членов содержащего класса Между объектами в окружающей области видимости и объектами ее вложенных классов I (типов) нет никакой взаимосвязи. Нестатические функции во вложенном классе имеют неявный указатель this, который указывает на объект вложенного класса. Объект вложенного класса содер- жит члены только вложенного типа. Указатель this не может быть использован для доступа к членам содержащего класса. Аналогично, нестатические функции-члены содержащего класса также имеют указатель this, который указывает на объект вложенного класса. Этот объект может иметь только те члены, которые определены в содержащем классе.
Глава 18. Специализированные инструменты и технологии 821 Применение нестатических данных или функций-членов содержащего класса всегда предполагает использование указателя, ссылки или объекта содержащего класса. Функция pop () класса Queue не может использовать члены item или next непосредственно. template cclass Туре> void Queue<Type>::рор() { // операция без проверки: удаление из пустой очереди невозможно Queueitem* р - head; // хранение указателя на головной / / элемент очереди позволит удалить / / его head = head->next; // теперь head указывает на // следующий элемент delete р; // удалить прежний головной элемент } Объекты класса Queue не имеют членов по имени item или next. Для доступа к членам класса Queue Item, функции-члены класса Queue вполне могут использо- вать члены head и tail, являющиеся указателями на объекты класса Queue Item. Применение членов статических и других типов Вложенный класс может непосредственно обращаться к статическим членам, именованным типам и перечислениям (раздел 2.7, стр. 84) содержащего класса. Без- условно, обращение к имени типа или статического члена класса вне области види- мости содержащего класса, предполагает применение оператора области видимости. Создание экземпляра шаблона вложенного класса При создании экземпляра шаблона содержащего класса, экземпляр шаблона вложенного класса не создается автоматически. Подобно любой функции-члену, эк- земпляр вложенного класса создается только тогда, когда он используется. Напри- мер, следующее определение создает экземпляр шаблона Queue для типа int, одна- ко экземпляр шаблона Queue Item< int > оно не создает. Queue<int> qi; // создает экземпляр Queue<int>, // а не Queueltem<int> Переменные-члены head и tail класса Queue являются указателями на класс QueueItem< int >. Для определения указателя на этот класс, нет никакой необхо- димости создавать экземпляр шаблона Queueltem<int>. Вложение класса Queue Item в шаблон класса Queue не изменит правил созда- ния экземпляра шаблона Queue Item. Класс Queue Item< int > будет создан только тогда, когда класс Queue Item используется. В данном случае — только при обраще- нии к значениям head и tail в функции-члене класса Queue<int >. 18.4.2. Поиск имен в области видимости вложенного класса Поиск имен (раздел 12.3.1, стр. 475), используемых во вложенном классе, осущест- вляется точно так же, как и в случае обычного класса. Единственное различие заклю- чается в наличии одной или нескольких областей видимости содержащего класса.
822 Часть V. Дополнительные темы Объявление имени любого члена класса должно быть расположено до места его приме- 1 нения. Объявления всех вложенных классов и содержащего класса должны находиться в одной области видимости. В качестве примера поиска имен во вложенном классе, рассмотрим следующие объявления классов. class Outer { public: struct Inner { // ok: ссылка на незавершенный класс void process(const Outers); Inner2 val; // ошибка: Outer::Inner2 вне области видимости public: // ok: Inner2::val используется в определении Inner2(int i = 0): val(i) { } // ok: процесс компиляции определения происходит после II завершения компиляции содержащего класса void process(const Outer &out) { out.handle(); } private: int val; }; void handle() const; // член класса Outer }; Компилятор сначала обрабатывает объявления членов Outer: : Inner и Outer: : Inner2 класса Outer. Применение имени Outer в качестве параметра функции Inner: :process () связано с содержащим классом. В момент объявления функции process () этот класс еще не завершен, однако параметр является ссылкой, поэтому его применение вполне допустимо. Объявление переменной-члена Inner: : val является ошибкой. Тип Inner2 еще не является видимым. Объявление класса Inner2 не создаст никаких проблем, оно использует одну пе- ременную-член встроенного типа int. Единственным исключением является функ- ция-член process (). Ее параметр имеет незавершенный тип Outer. Но поскольку параметр является ссылкой, факт незавершенности класса Outer не имеет ника- кого значения. Определения конструктора и функции-члена process () не обрабатываются компилятором до тех пор, пока не станут видимы отложенные объявления в содер- жащем классе. В конце объявлений класса Outer расположено объявление функции handle(). Когда компилятор ищет имена, используемые в определениях класса Inner2, он просматривает объявления всех имен в областях видимости классов Inner2 и Outer. Применение имени val до его объявления вполне допустимо, поскольку это ссылка на переменную-член класса Inner2. Аналогично, вполне допустимо приме- нение имени handle из класса Outer в теле функции-члена Inner2 : : process (), поскольку при компиляции членов класса Inner2 весь класс Outer является видимым.
Глава 18. Специализированные инструменты и технологии 823 Применение оператора области видимости для управления поиском имен К глобальной версии функции handle () можно обратиться используя оператор области видимости. class Inner2 { public: // ок: программист указывает явно, какая версия II функции handle О будет использована void process(const Outer &out) { ::handle(out); } }; Упражнения раздела 18.4.2 Упражнение 18.32. Повторно реализуйте классы Queue и Queueitem из главы 16, “Шаблоны и общее программирование” так, чтобы класс Queue item был вложен внутрь класса Queue. Упражнение 18.33. Объясните все преимущества и недостатки исходной конструкции класса Queue и его версии с вложенным классом. 18.5. Объединение: экономный класс Объединение (union) — это класс специального вида. Объединение может иметь несколько переменных-членов, однако в каждый момент времени только одна из них будет содержать значение. Когда значение присваивается одному из членов объеди- нения, все остальные переходят в неопределенное состояние. Для объединения резервируется такой объем памяти, которого хватит, по край- ней мере, для размещения его самой большой переменной-члена. Подобно любому другому классу, определение объединения создает новый тип. Определение объединения Объединения позволяют создать набор взаимоисключающих значений, которые могут иметь разные типы. Предположим, например, что существует процесс, в ходе которого обрабатываются различные виды числовых или символьных данных. Для хранения этих значений можно было бы использовать следующее объединение. // объект типа TokenValue содержит один член, который может // иметь любой из следующих типов union TokenValue { char eval; int ival; double dval; }; Определение объединения начинается с ключевого слова union, за которым следу- ет имя объединения (не обязательно) и набор его членов, заключенный в фигурные скобки. В приведенном выше коде определено объединение по имени TokenValue, которое может содержать значение типа char, int, double или указателя на тип char. В разделе 18.5 (стр. 826) будет описано, что происходит, когда имя объедине- ния не указывается.
824 Часть V. Дополнительные темы Подобно любому классу, объем памяти, резервируемой для объекта объединения, определяет его тип. Во время компиляции размер каждого объекта объединения фиксирован: он должен быть достаточен для хранения самой большой переменной- члена объединения. Никаких статических или ссылочных переменных-членов Для объединений доступны некоторые возможности классов, но не все. Напри- мер, подобно любому классу, в объединении можно применять маркеры доступа, по- зволяющие сделать его члены открытыми, закрытыми или защищенными. По умол- чанию объединения ведут себя подобно структурам: если не указано иное, члены объединения считаются открытыми. В объединении могут быть также определены функции-члены, включая конст- рукторы и деструкторы. Однако объединение не может выступать в качестве базово- го класса, поэтому его функции-члены не могут быть виртуальными. Объединение не может иметь статических переменных-членов, а также перемен- ных-членов, являющихся ссылкой. Кроме того, объединения не могут иметь пере- менных-членов, типом которых является класс, обладающий конструктором, дест- руктором или оператором присвоения. union illegal_members { Screen s; // ошибка: имеет конструктор static int is; // ошибка: статический член int &rfi; // ошибка: ссылочный член Screen *ps; // ok: указатель на обычный встроенный тип }; Это ограничение распространяется на классы, членами которых являются конст- руктор, деструктор или оператор присвоения. Применение типа union Имя объединения является именем типа. TokenValue first_token = {'а'}; // инициализированный объект // объединения Token Value TokenValue last_token; // неинициализированный объект II объединения TokenValue TokenValue *pt = new TokenValue; // указатель на объект II объединения TokenValue Подобно другим встроенным типам, по умолчанию объединения являются не- инициализированными. Объединение можно инициализировать явно, как и обыч- ный класс (раздел 12.4.5, стр. 492). Однако здесь инициализирующее значение пре- доставляется только для первого члена. Инициализирующее значение заключают в пару фигурных скобок. При инициализации объекта f irst_token, значение при- сваивается члену eval. Применение членов объединения Для доступа к членам объекта объединения применяются обычные операторы обращения (. и - >). last_token.eval = 'z'; pt->ival = 42;
Глава 18. Специализированные инструменты и технологии 825 Присвоение значения переменной-члену объекта объединения, приводит к тому, что другие его переменные-члены переходят в неопределенное состояние. Исполь- зуя объединение, всегда следует помнить, какое именно значение оно хранит в на- стоящий момент. Попытка получения хранимого в объединении значения при по- мощи несоответствующей переменной-члена, может привести к отказу, аварийному завершению программы или другой неприятности. Наилучший способ избежать обращения к значению объединения при помощи несо- ответствующих переменных-членов, подразумевает создание специального объекта, следящего за значением, хранимым в объединении. Этот дополнительный объект называют дискриминантом (discriminant) объединения. Вложенные объединения Обычно, объединения используют как вложенные типы, где дискриминант явля- ется членом содержащего класса, class Token { public: // указывает, какой вид значений хранит член val enum TokenKind {INT, CHAR, DEL}; TokenKind tok; union { // неименованное объединение double dval; } val; // член val представляет собой объединение // из 3 указанных типов }; Перечисление tok в этом классе служит для указания вида значения, хранимого членом val. Член val представляет собой неименованное объединение, способное содержать значение типа char, int или double. Для проверки дискриминанта и последующей обработки текущего значения объединения, зависящей от его типа, зачастую используется оператор switch (раз- дел 6.6, стр. 225). Token token; switch (token.tok) { case Token::INT: token.val.ival = 42; break; case Token::CHAR: token.val.eval = 'a'; break; case Token::DBL: token.val.dval - 3.14; break; } Анонимные объединения Неименованное объединение, не используемое при определении объекта, назы- вают анонимным объединением (anonymous union). Имена членов анонимного объе- динения располагаются в окружающей области видимости. Рассмотрим, например, класс Token, переделанный таким образом, чтобы в нем использовалось анонимное объединение. class Token { public:
826 Часть V. Дополнительные темы // указывает, какой вид значений хранит член val enum TokenKind {INT, CHAR, DBL}; TokenKind tok; union { // анонимное объединение char eval; int ival; double dval; }; }; Поскольку анонимное объединение не предоставляет никаких способов доступа к своим членам, обращаться к ним можно непосредственно, т.е. они доступны в той облас- ти видимости, в которой анонимное объединение определено. Перепишем предыдущий оператор switch так, чтобы в нем использовалась анонимная версия объединения. Token token; switch (token.tok) { case Token::INT: token.ival = 42; break; case Token::CHAR: token.eval = 'a'; break; case Token::DBL: token.dval = 3.14; break; } Анонимное объединение не может иметь ни закрытых, ни защищенных членов, ни функ- ций-членов. 18.6. Локальные классы Класс может быть определен внутри тела функции. Такой класс называют ло- кальным классом (local class). Локальный класс представляет собой тип, который ви- дим только в той локальной области видимости, в которой он определен. В отличие от вложенных классов, члены локального класса жестко ограничены. Все члены локального класса, включая функции, должны быть полностью определены /зал!) 1 внутри тела класса. В результате локальные классы гораздо менее полезны, чем вложен- ные классы. На практике требование полностью определять члены внутри самого класса, суще- ственно ограничивает сложность, а следовательно и возможности функций-членов ло- кального класса. Функции локальных классов редко имеют размер, превышающий не- сколько строк кода. Более длинный код функций труднее прочитать и понять. Кроме того, в локальном классе нельзя объявлять статические переменные- члены. Это связано со способом их определения. Локальные классы не могут использовать переменные из области видимости функции Локальный класс может обращаться далеко не ко всем именам из окружающей области видимости. Он может обращаться только к именам типов, статических пе- ременных (раздел 7.5.2, стр. 281) и перечислений, определенных внутри окружаю-
Глава 18. Специализированные инструменты и технологии 827 щей локальной области видимости. Локальный класс не может использовать обыч- ные локальные переменные той функции, в которой класс определен. int a, val; void foo(int val) { static int si; enum Loc { a = 1024, b // Bar локален для foo() class Bar { public: Loc locVal; int barVal; void fooBar(Loc 1 = { barVal = val; barVal = ::val; barVal = si; locVal = b; } }; // ок: используется локальное имя типа a) // ok: аргумент по умолчанию Loc: :a // ошибка: val локален для foo() II ok: используется глобальный I/ объект II ok: используется статический // локальный объект II ok: используется значение // перечисления } К локальным классам применимы обычные правила доступа Содержащая функция не имеет никаких специальных прав доступа к закрытым членам локального класса. Безусловно, локальный класс вполне может сделать со- держащую функцию дружественной. На практике закрытые члены в локальном классе встречаются крайне редко. Как правило, все его члены открыты. Та часть программы, которая может обращаться к локальному классу, весьма ог- раниченна. Локальный класс сосредоточен (инкапсулирован) внутри своей локаль- ной области видимости. Дальнейшая инкапсуляция, подразумевающая сокрытие информации, безусловно, является излишней. Поиск имен внутри локального класса Поиск имен внутри тела локального класса осуществляется таким же образом, как и у остальных классов. Имена, используемые в объявлениях членов класса, должны быть объявлены в области видимости до их применения. Имена, используе- мые в определениях членов, могут располагаться в любой части области видимости локального класса. Поиск имен, не найденных среди членов класса, осуществляется сначала в содержащей локальной области видимости, а затем вне области видимо- сти, заключающей саму функцию. Вложенные локальные классы Вполне возможно вложить класс внутрь локального класса. В данном случае оп- ределение вложенного класса может располагаться вне тела локального класса. Од-
828 Часть V. Дополнительные темы нако вложенный класс следует определить в той же локальной области видимости, в которой определен локальный класс. Как обычно, имя вложенного класса должно быть снабжено именем содержащего класса, а объявление вложенного класса долж- но присутствовать в определении локального класса. class Ваг { public: class Nested; // объявление класса Nested }; // определение класса Nested class Bar::Nested { Класс, вложенный в локальный класс, сам является локальным классом, со всеми соответствующими ограничениями. Все члены вложенного класса должны быть оп- ределены внутри тела самого вложенного класса. 18.7. Возможности, снижающие переносимость Одним из преимуществ языка программирования С является возможность соз- дания низкоуровневых программ, которые можно легко переносить с одной машины на другую. Процесс перемещения программы на новую машину называют переносом (porting), а допускающую перенос программу считают переносимой (portable). Для поддержки низкоуровневого программирования, язык С предоставляет не- которые возможности, применение которых может привести к ухудшению перено- симости. Несовпадение размеров арифметических типов у разных машин (табл. 2.1, стр. 56) — это одна из причин невозможности переноса. В этом разделе будут описа- ны две дополнительные возможности, способные ликвидировать переносимость. Они унаследованы языком C++ от языка С. Речь идет о битовых полях и специфи- каторе volatile. Как правило, эти возможности существенно упрощают непосред- ственное взаимодействие с аппаратными средствами. К ухудшающим переносимость возможностям, унаследованным от языка С, язык C++ добавляет еще одну: директивы компоновки, которые позволяют создать программу из компонентов, написанных на других языках. 18.7.1. Битовые поля Специальная переменная-член класса, называемая битовым полем (bit-field), предназначена для хранения определенного количества битов. Битовые поля обычно используют в случае, когда программа должна передать бинарные данные другой программе или аппаратному устройству. Расположение в памяти битовых полей зависит от конкретной машины.
Глава 18. Специализированные инструменты и технологии 829 Битовое поле представляет собой целочисленный тип данных. Оно может быть знаковым (signed) или беззнаковым (unsigned). Чтобы объявить член класса би- товым полем, после его имени располагают двоеточие и константное выражение, указывающее количество битов. typedef unsigned int Bit; class File { Bit mode: 2; Bit modified: 1; Bit prot_owner: 3; Bit prot_group: 3; Bit prot_world: 3; Битовое поле mode имеет размер в два бита, битовое поле modified — только один, а другие по три бита. Битовые поля, определенные в последовательном поряд- ке внутри тела класса, если это возможно, упаковываются внутри смежных битов то- го же целого числа. Таким образом достигается уплотнение хранилища. Например, пять битовых полей в приведенном выше объявлении, будут сохранены в одной пе- ременной типа unsigned int, ассоциированной с первым битовым полем mode. Способ упаковки битов в целое число зависит от машины. Для битовых полей обычно лучше подходит беззнаковый тип. Поведение битовых полей, хранимых в переменной знакового типа, определяет конкретная реализация. Применение битовых полей К битовым полям обращаются так же, как и к другим переменным-членам класса. Например, к закрытому битовому полю можно обращаться только из функций- членов самого класса и классов, дружественных для него. void File::write() { modified = 1; void File::close() { if (modified) // ... сохранить содержимое Для манипулирования битовыми полями с несколькими битами обычно исполь- зуют встроенные побитовые операторы (раздел 5.3, стр. 179). enum { READ - 01, WRITE =02 }; // режимы файла int main() { File myFile; myFile.mode 1= READ; // установить бит READ if (myFile.mode & READ) // если бит READ установлен cout << myFile.mode READ is set\n";
830 Часть V. Дополнительные темы Классы, в которых определены члены, представляющие собой битовые поля, обычно также обладают набором встраиваемых функций-членов, облегчающих ус- тановку и проверку значений битового поля. Например, класс File мог бы содер- жать функции-члены isRead () и isWrite (). inline inline File File isRead() { return mode & READ; } isWrite() { return mode & WRITE; } if (myFile.isRead()) /* ... Обладая этими функциями-членами, битовые поля можно объявить закрытыми членами класса File. Оператор обращения к адресу (&) не может быть применен к битовому полю, по- скольку не существует указателей на битовые поля. Кроме того, битовое поле не мо- жет быть статическим членом класса. 18.7.2. Спецификатор volatile Назначение спецификатора volatile полностью зависит от конкретной машины и может быть выяснено только в документации компилятора. При переносе на новые ма- шины или компиляторы, программы, использующие спецификатор volatile, обычно приходится переделывать. Программы, которым приходится работать непосредственно с аппаратными сред- ствами, зачастую имеют элементы данных, значением которых управляют процессы, не контролируемые самой программой. Например, программа могла бы содержать переменную, значение которой изменяет системный таймер. Такой объект должен быть объявлен со спецификатором volatile тогда, его значение может быть изме- нено способами, не контролируемыми или не обнаруживаемыми компилятором. Ключевое слово volatile — это приказ компилятору не выполнять оптимизацию для таких объектов. Спецификатор volatile используется аналогично спецификатору const, т.е. как дополнительный модификатор типа. volatile int display„register; volatile Task *curr_task; volatile int ixa[max_size]; volatile Screen bitmap_buf; Здесь объявлен асинхронно-изменяемый (volatile) объект display_register типа int, указатель curr_task на асинхронно-изменяемый объект класса Task, асинхронно-изменяемый массив ixa целых чисел, каждый элемент которого явля- ется асинхронно-изменяемым, и асинхронно-изменяемый объект bitmap_buf клас- са Screen. Каждая из его переменных-членов считается асинхронно-изменяемой. Подобно тому, как в классе можно определять константные функции-члены, в нем можно определять асинхронно-изменяемые функции-члены. К асинхронно- изменяемым объектам могут обращаться только асинхронно-изменяемые функ- ции-члены. В разделе 4.2.5 (стр. 151) описано взаимодействие указателей со спецификатором const. Аналогичное взаимодействия существует между указателями и специфика- тором volatile. Можно объявлять асинхронно-изменяемые указатели на объекты,
Глава 18. Специализированные инструменты и технологии 831 указатели на асинхронно-изменяемые объекты и асинхронно-изменяемые указатели на асинхронно-изменяемые объекты. volatile int v; // vis - асинхронно-изменяемый объект типа int int *volatile vip; // vip - асинхронно-изменяемый указатель !/ на объект типа int volatile int *ivp; // ivp - указатель на асинхронно-изменяемый // объект типа int II vivp - асинхронно-изменяемый указатель на асинхронно-изменяемый // объект типа int volatile int *volatile vivp; int *ip = &v; // ошибка: необходим указатель на / / асинхронно-изменяемый объект * ivp = &v; // ok: ivp - указатель на // асинхронно-изменяемый объект vivp = &v; // ok: vivp - асинхронно-изменяемый указатель II на асинхронно-изменяемый объект Подобно константам, адрес асинхронно-изменяемого объекта можно присвоить (или скопировать указатель на асинхронно-изменяемый тип) только асинхронно- изменяемому указателю. При инициализации ссылки на асинхронно-изменяемый объект следует использовать только асинхронно-изменяемые ссылки. Синтезируемые функции управления копированием не применимы к асинхронно-изменяемым объектам Между константными и асинхронно-изменяемыми объектами есть одно важное различие: для инициализации и присвоения асинхронно-изменяемых объектов не применимы синтезируемые версии функций копирования и присвоения. Синтези- руемые функции-члены управления копированием получают параметры, типами ко- торых являются константные ссылки на класс. Однако асинхронно-изменяемый объект не может быть передан при помощи обычной или константной ссылки. Если класс должен обеспечить копирование или присвоение асинхронно- изменяемых объектов в (или из) асинхронно-изменяемый операнд, в нем следу- ет определить его собственные версии операторов присвоения и/или конструк- торов копий. class Foo { public: Foo(const volatile Foo&); // копирование из II асинхронно-изменяемого объекта и присвоение значения // асинхронно-изменяемого объекта II неасинхронно-изменяемому объекту Foo& operators(volatile const Foo&); // присвоение значения асинхронно-изменяемого объекта II асинхронно-изменяемому объекту Foo& operator=(volatile const Foo&) volatile; // остальная часть класса Foo }; Определив параметры функций-членов управления копированием как констант- ные асинхронно-изменяемые (const volatile) ссылки, можно организовать ко- пирование и присвоение объектов класса Foo любых видов: обычных, константных, асинхронно-изменяемых или константных асинхронно-изменяемых.
832 Часть V. Дополнительные темы Хотя создать функции-члены управления копированием можно так, чтобы они были способны работать с асинхронно-изменяемыми объектами, имеет смысл задаться вопросом: а нужно ли копировать асинхронно-изменяемый объект? Ответ на этот во- прос зависит от конкретных причин, по которым в программе используется асин- хронно-изменяемый объект. 18.7.3. Директивы компоновки: extern "С" Иногда в программах C++ необходимо применять функции, написанные на дру- гом языке программирования. Как правило, это язык С. Подобно любому имени, имя функции, написанной на другом языке, следует объявить. Это объявление должно указать тип возвращаемого значения и список параметров. Компилятор проверяет обращения к внешним функциям на другом языке точно так же, как и обращения к обычным функциям языка C++. Однако для вызова функций, написанных на дру- гих языках, компилятор обычно вынужден создавать иной код. Чтобы указать язык для функций, написанных на языке, отличном от C++, используются директивы компоновки (linkage directive). Объявление функций, написанных на языке, отличном от С++ Директива компоновки может существовать в двух формах: одиночной и со- ставной. Директивы компоновки не могут располагаться внутри определения класса или функции. Директива компоновки должна присутствовать при первом объяв- лении функции. В качестве примера рассмотрим некоторые из функций языка С, объявленные в заголовке cstdlib. Объявления в этом заголовке могли бы выглядеть примерно следующим образом. // гипотетические директивы компоновки, которые могли бы // присутствовать в заголовке C++ <cstring> // одиночная директива компоновки extern "С" size_t strlen(const char *) ; // составная директива компоновки extern "С" { int strcmp(const char*, const char*); char *strcat(char*, const char*); } Первая форма состоит из ключевого слова extern, сопровождаемого строковым литералом и “обычным” объявлением функции. Строковый литерал указывает язык, на котором написана функция. Аналогичную директиву компоновки можно применить и для нескольких функ- ций сразу, заключив их объявления в фигурные скобки, расположенные после клю- чевого слова extern. Фигурные скобки группируют объявления, к которым приме- няется директива компоновки. Директивы компоновки и файлы заголовков Составная форма объявления применима ко всему файлу заголовка. Например, заголовок cstring языка C++ может выглядеть следующим образом. // составная директива компоновки extern "С" {
Глава 18. Специализированные инструменты и технологии 833 #include <string.h> // функции языка С, манипулирующие // строками в стиле С } Когда директива #include заключена в фигурные скобки составной директивы компоновки, все объявления обычных функций в файле заголовка будут восприня- ты как написанные на языке, указанном в директиве компоновки. Директивы ком- поновки допускают вложенность, т.е. если заголовок содержит функцию с директи- вой компоновки, на данную функцию это не повлияет. Функции, унаследованные языком C++ от языка С, могут быть определены как функции языка С, но это не является обязательным условием для каждой реализации языка C++. Экспорт функций, созданных на языке C++, в другой язык Используя директиву компоновки в определении функции, написанной на язы- ке C++, эту функцию можно сделать доступной для программы, написанной на другом языке. // функция calc() может быть вызвана из программы на языке С extern "С" double calc(double dparm) { /* ... */ } Код, создаваемый компилятором для этой функции, будет соответствовать ука- занному языку. Объявление каждой функции, в определении которой использована директива компонов- ки, должно содержать ту же директиву компоновки. Поддержка препроцессора при компоновке Иногда возникает необходимость в компиляции того же файла исходного кода на компиляторах обоих языков — и С, и C++. При компилировании на языке C++, имя препроцессора,__cplusplus (два символа подчеркивания), определяется автомати- чески, а значит, с помощью директивы условной компиляции можно организовать подключение фрагментов кода, компилируемых только в C++. #ifdef __cplusplus // ok: компилируется только в C++ extern “С" #endif int strcmp(const char*, const char*); Языки, поддерживаемые директивами компоновки Компилятор обязательно должен поддерживать директивы компоновки для язы- ка С. Однако компилятор может предоставлять директивы компоновки и для других языков. Например: extern "Ada", extern "FORTRAN" ит.д. Набор поддерживаемых языков зависит от конкретного компилятора. Более подробная & | информация о поддерживаемых языках, отличных от языка С, должна содержаться в до- etTtky) кументации на компилятор.
834 Часть V. Дополнительные темы Перегруженные функции и директивы компоновки Взаимодействие между директивами компоновки и перегрузкой функций зави- сит от конкретного языка. Если язык поддерживает перегрузку функций, компи- лятор, обрабатывая директивы компоновки для, того языка, вероятней всего, вы- полнит ее. Единственным гарантированно поддерживаемым языком является язык С. Язык С не поддерживает перегрузку функций, поэтому нет ничего удивительного в том, что директива компоновки языка С может быть определена только для од- ной из функций в наборе перегруженных функций. Поэтому объявление несколь- ких одноименных функций в директиве компоновки языка С будет ошибкой. // ошибка: в директиве extern "С" указаны две функции // из набора перегруженных функций extern "С" void print(const char*); extern "C" void print(int); Перегрузка функций языка С в программах на языке C++ — довольно обычное дело. Однако все остальные функции в наборе перегруженных функций пригодны только для языка C++. class Smalllnt { /* ... */ }; class BigNum { /* ... */ }; // функция С может быть вызвана из программ С и С ++ II версия функции C++, перегружающая предыдущую функцию, II может быть вызвана только из программ на языке C++ extern "С" double calc(double); extern Smalllnt calc(const Smalllnt&); extern BigNum calc(const BigNum&); Версия функции calc () для языка С может быть вызвана как из программ на языке С, так и из программ на языке C++. Дополнительные функции с параметрами типа класса, могут быть вызваны только из программ на языке C++, причем порядок объявления не имеет значения. Указатели на функции, объявленные в директиве extern " С " Язык, на котором написана функция, является частью ее типа. Чтобы объявить указатель на функцию, написанную на другом языке программирования, следует использовать директиву компоновки. / / pf указывает на функцию языка С, получающую аргумент // типа int и возвращающую тип int extern "С” void (*pf)(int); Когда указатель pf используется для вызова функции, созданный при компиля- ции код подразумевает, что происходит обращение к функции С. Тип указателя на функцию С не совпадает с типом указателя на функцию C++. Указатель 33/^ j на функцию С не может быть инициализирован (или присвоен) значением указателя на функцию C++ (и наоборот). В результате такого несоответствия, во время компиляции происходит ошибка. void (*pfl)(int); // указатель на функцию C++ extern "С" void (*pf2)(int); // указатель на функцию С pfl - pf2; // ошибка: pfl и pf2 имеют разные типы
Глава 18. Специализированные инструменты и технологии 835 Некоторые модернизированные компиляторы C++ могут допускать присвоение, приве- денное выше, хотя, строго говоря, оно некорректно. Директивы компоновки применимы ко всем объявлениям Директива компоновки, использованная для функции, применяется также к любым указателям на нее, используемым как тип возвращаемого значения или параметр. // fl() - функция С, ее параметр также является указателем // на функцию С extern "С" void f1(void(*)(int)); Это объявление свидетельствует о том, что f 1 () является функцией С, которая не возвращает никаких значений. Она имеет один параметр, который является ука- зателем на функцию, которая ничего не возвращает и получает один параметр типа int. Эта директива компоновки применяется как к самой функции f 1 (), так и к указателю на нее. Когда происходит вызов функции f 1 (), ей необходимо передать имя функции С или указатель на нее. Поскольку директива компоновки применяется ко всем функциям в объявлении, для передачи функции C++ указателя на функцию С, необходимо использовать оп- ределение типа. // FC - указатель на функцию С extern "С" typedef void FC(int); // f 2 () - функция C++, параметром который является указатель // на функцию С void f2(FC *) ; Упражнения раздела 18.7.3 Упражнение 18.34. Объясните объявления, указанные ниже, и укажите, являются ли они до- пустимыми. extern "С" int compute(int *, int); extern "C" double compute(double *, double); Резюме Язык C++ предоставляет несколько специализированных средств, которые предназначе- ны для решения некоторых видов проблем. Специальное управление памятью класс может осуществлять двумя способами: класс мо- жет внутренне резервировать области памяти, что позволяет ему рационализировать разме- щение в памяти собственных переменных-членов. Класс может определить собственные, спе- цифические функции operator new () и operator delete О, которые и будут использо- ваны при создании новых и удалении существующих объектов данного класса. Некоторым программам необходимо непосредственно выяснять динамический тип объек- та во время выполнения. Идентификация типов времени выполнения (RTTI — Run-Time Type Identification) предоставляет поддержку этого вида программирования на уровне языка. RTTI применима только к тем классам, которые обладают виртуальными функциями; ин- формация о типах без виртуальных функций также доступна, но она соответствует статиче- скому типу.
836 Часть V. Дополнительные темы Указатели на объекты имеют тип. При определении указателя на член класса, в состав его типа должен также входить тот класс, на член которого указывает указатель. Указатель на член класса может быть связан с членом любого объекта того же класса. При обращении к значению указателя на член класса, необходимо указать объект, о члене которого идет речь. В языке C++ определено несколько дополнительных составных типов. Вложенные классы, которые определены в области видимости другого класса. Такие классы зачастую применяют для реализации содержащего их класса. Объединения — это специальный вид класса, объект которого может содержать только простые переменные-члены. В любой момент времени объект такого типа может содер- жать значение только в одной из его переменных-членов. Как правило, объединения вхо- дят в состав другого класса. Локальные классы представляют собой очень простые классы, определенные локально внутри функции. Все члены локального класса должны быть определены в его теле. Для локального класса недопустимы статические переменные-члены. Язык C++ предоставляет также несколько возможностей, которые могут привести к ухуд- шению переносимости программы. Сюда относятся битовые поля, спецификатор volatile, упрощающий взаимодействие с аппаратными средствами, и директивы компоновки, упро- щающие взаимодействие с программами, написанными на других языках. Термины Анонимное объединение (anonymous union). Неименованное объединение, которое не применимо для создания объекта. К членам анонимного объединения обращаются непосред- ственно. Такие объединения не могут иметь ни функций-членов, ни закрытых или защищен- ных членов. Битовое поле (bit-field). Знаковый или беззнаковый целочисленный член класса, который определяет количество резервируемых для него битов. Битовые поля класса определяют в обычном порядке. По возможности они упаковываются в обычные целочисленные значения. Вложенный класс (nested class). Класс, определенный внутри другого класса. Вложенный класс определен внутри окружающей области видимости: имена вложенных классов должны быть уникальны внутри области видимости того класса, в котором они определены, но могут повторяться в областях видимости вне содержащего класса. Доступ к вложенному классу из- вне содержащего класса, предполагает применение оператора области видимости, позволяю- щего указать область (области) видимости, в которую вложен класс. Вложенный тип (nested type). Синоним вложенного класса. Директива компоновки (linkage directive). Механизм, позволяющий вызвать в программе на языке C++ функции, написанные на другом языке. Вызов функций С должен обеспечи- ваться всеми компиляторами языка C++. Поддержка других языков зависит от конкретного компилятора. Дискриминант (discriminant). Технология программирования, подразумевающая исполь- зование специального объекта для хранения информации о фактическом типе значения, со- держащегося в объединении в данный момент времени. Идентификация типов времени выполнения (run-time type identification). Термин, ис- пользуемый для описания языковых и библиотечных средств, позволяющих выяснить дина- мический тип ссылки или указателя во время выполнения. Операторы RTTI, typeid и dynamic_cast, обеспечивают возвращение динамического типа только для ссылок и указа- телей на классы с виртуальными функциями. Будучи примененными к другим типам, они возвращают статический тип ссылки или указателя.
Глава 18. Специализированные инструменты и технологии 837 Класс allocator. Класс стандартной библиотеки, который обеспечивает резервирование областей памяти. Класс allocator является шаблоном класса, в котором определены функ- ции-члены allocate (), deallocate (), construct () и destroy (), применимые к объ- ектам класса, переданного шаблону allocator в качестве параметра шаблона. Локальный класс (local class). Класс, определенный внутри функции. Локальный класс видим внутри только той функции, в которой он определен. Все его члены должны быть оп- ределены внутри тела класса. Он не может иметь статических членов. Локальные члены клас- са не могут обращаться к локальным переменным, определенным в содержащей функции. Однако они могут использовать имена типов, статические переменные и перечисления, опре- деленные в содержащей функции. Объединение (union). Подобный классу составной тип, в котором может быть определено несколько переменных-членов, однако значение в каждый момент времени может иметь только один из них. Членами объединения должны быть простые типы: это могут быть встро- енные типы, составные типы или классы, в которых нет ни конструктора, ни деструктора, ни оператора присвоения. Объединения могут иметь функции-члены, включая конструкторы и деструкторы, но они не могут быть использованы в качестве базового класса. Оператор delete. Удаляет объект указанного типа, размещенный в динамически распре- деляемой памяти, и освобождает занимаемую им область. Оператор delete [] удаляет эле- менты массива, размещенного в динамически распределяемой памяти, и освобождает зани- маемую ими область. Для освобождения области памяти, содержащей объект или массив, эти операторы используют соответствующую версию (библиотечную или класса) функции operator delete(). Оператор dynamic_cast. Осуществляет приведение типа базового класса к типу произ- водного с проверкой. В базовом классе должна быть определена по крайней мере одна вирту- альная функция. Оператор проверяет динамический тип объекта, с которым связана ссылка или указатель. Приведение осуществляется только тогда, когда тип объекта совпадает с типом приведения или является типом, производным от него. В противном случае возвращается ну- левой указатель (при приведении указателя) или исключение (при приведении ссылки). Оператор new. Резервирует область памяти и создает в ней объект указанного типа. Опера- тор new [ ] резервирует область памяти и создает в ней массив объектов. Для резервирования области память, в которой будет создан объект или массив указанного типа, эти операторы ис- пользуют соответствующую версию (библиотечную или класса) функции operator new (). Оператор typeid. Унарный оператор, получающий выражение и возвращающий ссылку на объект библиотечного типа type_inf о, который описывает тип выражения. Когда выра- жение является объектом класса, имеющего виртуальные функции, оператор возвращает ди- намический тип. Если типом является ссылка, указатель или другой тип, в котором не опре- делены виртуальные функции, будет возвращен его статический тип. Переносимость (portable). Термин, используемый для описания программ, которые мож- но переносить на другую машину без особых усилий. Размещающий оператор new (placement new). Форма оператора new, создающая объект в указанной области памяти. Память он не резервирует, а область, предназначенную для объек- та, указывает получаемый аргумент. Представляет собой низкоуровневый аналог функции- члена construct () класса allocator. Спецификатор volatile. Спецификатор типа, указывающий компилятору на то, что значение переменной данного типа может быть изменено извне программы. Это запрещает компилятору осуществлять некоторые виды оптимизации кода. Список свободных блоков (freelist). Технология управления памятью, подразумевающая предварительное резервирование области памяти, предназначенной для размещения объек- тов, которые будут созданы при необходимости. При удалении объектов, занимаемая ими па- мять передается в список, а не возвращается системе.
838 Часть V. Дополнительные темы Тип type_inf о. Библиотечный тип, способный описывать тип данных. Класс type_inf о жестко зависит от конкретной машины, однако любая библиотека должна определять класс type_inf о как содержащий члены, перечисленные в табл. 18.2. Объекты класса type_inf о не могут быть скопированы. Указатель на член класса (pointer to member). Инкапсулирует тип класса, а также тип элемента, на который он указывает. Определение указателя на член класса должно содержать имя класса, а также тип элемента (элементов), на который он может указывать. Т С'. : *pmem = &С: : {member} ; Это выражение определяет указатель pmem, который способен указывать на члены класса по имени С, которые имеют тип Т, и инициализирует его адресом члена класса С по имени member. Перед обращением к значению такого указателя, он должен быть предварительно связан с объектом или указателем класса С. classobj.*pmem; classptr->*pmem; Обращение к члену member объекта classobj или указателя classptr. Функции-члены operator new () и operator delete (). Функции-члены класса, пе- реопределяющие стандартные глобальные библиотечные функции резервирования памяти operator new() и operator delete (). Могут быть определены обе формы этих функ- ций: для объектов (new) и массив (new [ ]). Неявно объявленные статическими, функции- члены new () и delete () резервируют и освобождают память соответственно. Они автома- тически используются операторами new и delete при инициализации и удалении объектов. Функция operator delete (). Библиотечная функция, освобождающая область памяти, за- резервированную функцией operator new (). Библиотечная функция operator delete [] освобождает область памяти зарезервированную функцией operator new [ ] и использован- ную для хранения массива. Функция operator new (). Библиотечная функция, резервирующая нетипизированную область памяти указанного размера. Библиотечная функция operator new[] резервирует область памяти для массива. Эти библиотечные функции предоставляют механизм резервирования памяти на более низком уровне, чем у библиотечного класса allocator. В современных программах следует использовать класс allocator, а не данные библиотечные функции.
И Е Библиотека В ЭТОЙ ГЛАВЕ... А. 1. Имена и заголовки стандартной библиотеки А.2. Краткий обзор алгоритмов А.З. Возвращаясь к библиотеке ввода-вывода 839 841 856 Это приложение содержит дополнительные сведения о библиотеке. В начале приведена табл. А. 1, содержащая имена и заголовки стандартной библиотеки, упо- минаемые в книге. В главе 11, “Общие алгоритмы”, рассматривались библиотечные алгоритмы, при- водились примеры применения некоторых из наиболее популярных алгоритмов, а также описывалась архитектура, лежащая в основе библиотеки алгоритмов. В этом приложении перечислены все алгоритмы, причем отсортированы они по видам вы- полняемых операций. Приложение завершается описанием некоторых дополнительных возможностей библиотеки ввода-вывода: управление форматом, бесформатный ввод-вывод и про- извольный доступ к файлам. Каждый класс ввода-вывода содержит набор данных о формате и соответствующие функции для управления ими. Флаги формата (format state) позволяют осуществлять более подробный контроль над процессом ввода и вывода. Все операции ввода-вывода обладают неким форматом: т.е. функции ввода и вывода имеют информацию об используемых типах и оформляют вводимые или выводимые данные соответственно. Существуют также бесформатные (unformatted) функции ввода-вывода, которые работают с потоком на уровне отдельных символов, никак не интерпретируя данные. В главе 8, “Библиотека ввода-вывода”, уже было продемонстрировано, что класс f stream позволяет читать и записывать данные в тот же файл. В данном приложении это будет описано более подробно. А. 1. Имена и заголовки стандартной библиотеки В программах этой книги директивы #include, необходимые для их компиля- ции, практически нигде не приводились. Для удобства читателей в табл. А. 1 пере- числены все использованные в программах книги библиотечные имена и заголовки, в которых они определены.
840 Приложение А Таблица А.1. Имена и заголовки стандартной библиотеки Имя Заголовок Имя Заголовок abort <cstdlib> ios_base <ios_base> accumulate <numeric> isalpha <cctype> allocator <memory> islower <cctype> auto_ptr <memory> ispunct <cctype> back_inserter <iterator> isspace <cctype> bad_alloc <new> istream <iostream> bad_cast <typeinfo> istream_iterator <iterator> bind2nd <functional> istringstream <sstream> bitset <bitset> isupper <cctype> boolalpha <iostream> left <iostream> cerr <iostream> lessequal <functional> cin <iostream> ~l *1 g <list> copy <algorithm> logic_error <stdexcept> count <algorithm> 1ower_bound <algorithm> count_if <algorithm> make_pair < U. 1111 t > cout <iostream> map <map> dec <iostream> max <algorithm> deque <deque> min <algorithm> endl <iostream> multimap <map> ends <iostream> multiset <set> equal_range <algorithm> negate <functional> exception <exception> noboolalpha <iostream> fill <algorithm> noshowbase <iostream> fill_n <algorithm> noshowpoint <iostream> find <algorithm> noskipws <iostream> find_end <algorithm> notl <functional> find_first_of <algorithm> nounitbuf <iostream> fixed <iostream> nouppercase <iostream> flush <iostream> nth_element <algorithm> for_each <algorithm> oct <iostream> front_inserter <iterator> ofstream <fstream> fstream <fstream> ostream <iostream> getline <string> ostream_iterator <iterator> hex <iostream> ostringstream <sstream> ifstream <fstream> out_of_range <stdexcept> inner_product <numeric> pair < U. t i. 11 t > inserter <iterator> partial_sort <algorithm> internal <iostream> plus <functional>
Библиотека 841 Окончание табл. А. 1 Имя Заголовок Имя Заголовок priority_queue ptrdiff_t queue range_error replace replace_copy reverse_iterator right runt ime_e rror scientific set set_difference set_intersection set_union setfill setprecision setw showbase showpoint size_t skipws sort <queue> ccstddef> <queue> <stdexcept> <algorithm> <algorithm> <iterator> <iostream> <stdexcept> <iostream> <set> <algorithm> <algorithm> <algorithm> <iomanip> <iomanip> <iomanip> <iostream> <iostream> ccstddef> <iostream> <algorithm> sqrt stable_sort stack strcmp strepy string stringstream strlen strnepy terminate tolower toupper type_info unexpected uninitialized_copy unitbuf unique unique_copy upper_bound uppercase vector <cmath> <algorithm> <stack> <cstring> <cstring> <string> <sstream> <cstring> <cstring> <exception> <cctype> <cctype> <typeinfo> <exception> <memory> <iostream> <algorithm> <algorithm> <algorithm> <iostream> <vector> A. 2. Краткий обзор алгоритмов В главе 11, “Общие алгоритмы”, представлены общие алгоритмы и лежащая в их основе архитектура. В библиотеке определено более 100 алгоритмов. Чтобы нау- читься их использовать, следует понять структуру, а не запоминать подробности применения каждого из них. В этом разделе описан каждый из алгоритмов, причем они организованы в соответствии с типом выполняемых ими действий. А.2.1. Алгоритмы поиска объекта Алгоритмы find и count осуществляют поиск указанных значений в исходном диапазоне. Алгоритм find возвращает итератор на элемент, а алгоритм count — количество соответствующих элементов. Простой алгоритм поиска Этим алгоритмам следует передать итератор. Алгоритмы find и count ищут указанные элементы. Алгоритм find возвращает итератор на первый соответст-
842 Приложение А вующий элемент. Алгоритм count возвращает количество соответствующих эле- ментов в исходной последовательности. find(beg, end, val) count(beg, end, val) Здесь в исходном диапазоне осуществляется поиск элемента со значением val. При этом используется оператор равенства (==) базового типа. Функция find О возвращает итератор на первый соответствующий элемент или итератор end, если он не найден. Функция count () возвращает количество элементов со зна- чением val. find_if(beg, end, unaryPred) countif(beg, end, unaryPred) Здесь в исходном диапазоне осуществляется поиск элемента, для которого пре- дикат unaryPred возвращает значение true. Предикат должен получать один па- раметр типа value_type исходного диапазона и возвращать тип, значение которого применяется в условии. Функция f ind () возвращает итератор на первый элемент, для которого преди- кат unaryPred возвращает значение true или итератор end, если объект не найден. Функция count () применяет предикат unaryPred к каждому элементу и возвра- щает количество элементов, для которых он вернул значение true. Алгоритм поиска одного из нескольких значений Этим алгоритмам необходимы две пары итераторов. Они ищут в первом диапа- зоне первый (или последний) элемент, значение которого равно значению любого элемента во втором диапазоне. Типы итераторов begl и endl должны точно соот- ветствовать типам итераторов Ьед2 и end2. Однако типы итераторов begl и Ьед2 необязательно должны точно соответство- вать друг другу. Кроме того, должна существовать возможность сравнивать элемен- ты этих двух последовательностей. Так, например, если первая последовательность имеет тип list<string>, то типом второй может быть vector<char*>. Каждый алгоритм может быть перегружен. По умолчанию для сравнения эле- ментов используется оператор == типа элемента. В качестве альтернативы можно определить предикат, который получает два параметра и возвращает значение типа bool, указывающее результат сравнения двух элементов. find_first_of(begl, endl, beg2, end2) Это выражение возвращает итератор на первый элемент первого диапазона, зна- чение которого совпадает со значением любого элемента из второго диапазона. Если соответствие не найдено, возвращается итератор endl. find_first_of(begl, endl, beg2, end2, binaryPred) Для сравнения элементов каждой последовательности, здесь использован преди- кат binaryPred. Алгоритм возвращает итератор на первый элемент в первом диа- пазоне, для которого предикат binaryPred возвращает значение true. Если соот- ветствие не найдено, возвращается итератор endl. flnd_end(begl, endl, beg2, end2) find end(begl, endl, beg2, end2, binaryPred)
Библиотека 843 Работает аналогично функции f ind_f irst_of (), за исключением того, что осуществляется поиск последнего соответствующего элемента из второй последова- тельности. Например, если первая последовательность содержит значения 0,1,1, 2, 2, 4, О и 1, а вторая — 1, 3, 5, 7 и 9, функция f ind_end () возвратит итератор на последний элемент в исходной последовательности (значение 1), а функция f ind_f irst_of () — на второй. Алгоритм поиска последовательности Этим алгоритмам нужны прямые итераторы. Они осуществляют поиск последо- вательности, а не отдельного элемента. Если последовательность найдена, возвраща- ется итератор на ее первый элемент. Если последовательность не найдена, возвраща- ется итератор end исходного диапазона. Каждый алгоритм может быть перегружен. По умолчанию для сравнения эле- ментов используется оператор ==. В качестве альтернативы вместо него можно ис- пользовать предикат. adjacent_find(beg, end) adjacent_find(beg, end, binaryPred) Возвращает итератор на первую смежную пару совпадающих элементов. Возвра- щает итератор end, если никаких смежных двойных элементов не найдено. В первом случае для поиска дубликатов используется оператор ==, а во втором — предикат binaryPred. search(begl, endl, beg2, end2) search(begl, endl, beg2, end2, binaryPred) Возвращает итератор на первую позицию в исходном диапазоне, с которой начи- нается совпадение с диапазоном второй последовательности. Если последователь- ность не найдена, возвращает итератор endl. Типы итераторов begl и Ьед2 могут отличаться, но они обязательно должны быть совместимы: это позволит сравнивать элементы двух последовательностей. search_n(beg, end, count, val) search_n(beg, end, count, val, binaryPred) Возвращает итератор на начало последовательности из count совпадающих эле- ментов. Возвращает итератор end, если последовательность не существует. Первая версия ищет count позиций указанного значения value, а вторая — count пози- ций, для которых предикат binaryPred возвращает значение true. А.2.2. Другие алгоритмы, осуществляющие только чтение В качестве первых двух аргументов этим алгоритмам необходимы два итерато- ра исходного диапазона. Алгоритмы equal и mismatch получают также дополни- тельный итератор, который обозначает второй диапазон. Во второй последова- тельности должно быть по крайней мере столько же элементов, сколько и в пер- вом. Если во втором диапазоне элементов больше, они игнорируются. Если эле-
844 Приложение А ментов окажется меньше, во время выполнения произойдет ошибка с непредви- денными последствиями. Как обычно, типы итераторов, обозначающих исходный диапазон, должны точно совпадать. Тип итератора Ьед2 должен быть совместим с типом итератора begl. То есть они должны позволить сравнивать элементы в обеих последовательностях. Функции equal () и mismatch () перегружены: одна из версий использует для сравнения оператор равенства элемента (==), а вторая — предикат. for_each(beg, end, f) Функция (или объект функции (раздел 14.8, стр. 560)) f () применяется к каж- дому элементу в исходном диапазоне. Возвращаемое функцией f () значение (если оно есть) игнорируется. Используемые итераторы являются итераторами ввода, по- этому функция f () не может записывать значения в элементы. Как правило, алго- ритм f or_each используется совместно с функцией, которая осуществляет вспомо- гательные действия, например, отображает значения диапазона. mismatch(begl, endl, beg2) mismatch(begl, endl, beg2, binaryPred) Сравнивает элементы двух последовательностей. Возвращает пару (pair) итера- торов, обозначающих первые несоответствующие элементы. Если все элементы со- ответствуют друг другу, первый итератор возвращенной пары окажется равным endl, а итератор Ьед2 — смещению, равному размеру первой последовательности. equal(begl, endl, beg2) equal(begl, endl, beg2, binaryPred) Выявляет равенство двух последовательностей. Возвращает значение true, если каждый элемент в исходном диапазоне равен соответствующему элементу последо- вательности, начинающейся с позиции Ьед2. Например рассмотрим две последовательности: meet и meat. В результате при- менения к ним функции mismatch () будет возвращена пара (pair), содержа- щая итератор, указывающий на вторую букву е в первой последовательности, и букву а — во второй последовательности. Если бы второй последовательностью бы- ла meeting, возвращенная функцией equal пара содержала бы итератор endl и итератор, обозначающий элемент i второго диапазона. Л.2.3. Алгоритмы бинарного поиска Хотя эти алгоритмы можно использовать с прямыми итераторами, они обладают специализированными версиями, которые работают с итераторами прямого доступа и выполняются гораздо быстрей. Эти алгоритмы выполняют бинарный поиск, что подразумевает сортировку ис- ходной последовательности. Данные алгоритмы работают с исходной последова- тельностью как с ассоциативным контейнером (раздел 10.5.2, стр. 404). Алгоритмы equal_range, lower_bound и upper_bound возвращают итератор, соответст- вующий позиции контейнера, в которую может быть вставлен данный элемент, при сохранении прежнего порядка контейнера. Если значение элемента превосходит значения всех остальных элементов контейнера, будет возвращен итератор, указы- вающий позицию после конца контейнера.
Библиотека 845 Каждый алгоритм представлен в двух версиях: первая использует для сравнения оператор меньше (<) класса элемента, а вторая — указанную функцию сравнения. lower_bound(beg, lowerbound(beg, end, val) end, val, comp) Возвращает итератор на первую позицию, в которую может быть вставлено зна- чение val при сохранении текущего порядка. upper_bound(beg, end, val) upper_bound(beg, end, val, comp) Возвращает итератор на последнюю позицию, в которую может быть вставлено значение val при сохранении текущего порядка. equal_range(beg, end, val) egual_range(beg, end, val, comp) Возвращает пару итераторов, указывающую диапазон, в который может быть вставлено значение val при сохранении текущего порядка. binary_search(beg, binary_search(beg, end, val) end, val, comp) Возвращает логическое значение (тип bool), свидетельствующее о наличии в по- следовательности элемента, значение которого равно val. Два значения, х и у, счи- таются равными, если оба выражения, х < уиу < х, возвращают значение false. А.2.4. Алгоритмы записи в элементы контейнера Запись в элементы контейнера осуществляется многими алгоритмами. Эти алго- ритмы могут отличаться видом итераторов, с которыми они работают, а также тем, осуществляют ли они запись в элементы исходного диапазона или указанного ре- зультирующего диапазона. Простейшие алгоритмы читают элементы последовательности. Им необходимы лишь итераторы ввода. Для записи в исходную последовательность нужны прямые итераторы. Некоторое алгоритмы читают последовательность в обратном порядке, им необходим двунаправленный итератор. Алгоритмы, осуществляющие запись в указанную результирующую последовательность, подразумевают, что она доста- точно велика, чтобы вместить результат. Алгоритмы, которые записывают, но не читают значения элементов Этим алгоритмам необходим итератор вывода, который обозначает получателя. Они получают второй аргумент, который задает количество элементов, записывае- мых по назначению. flll_n(dest, ent, val) generate_n(dest, ent, Gen) Записывает ent значений в dest. Функция f ill_n () записывает ent копий значения val, а функция generate_n () запускает генератор Gen () ent раз. Гене- ратор (generator) — это функция (или объект функции (раздел 14.8, стр. 560)), которая, как и предполагает ее название, создает значения, возвращаемые при ее вызове.
846 Приложение А Алгоритмы, записывающие значения в элементы с помощью итераторов ввода Каждая из этих функций читает исходную последовательность и осуществляет запись в результирующую последовательность, обозначенную параметром de st. Параметр de st должен быть итератором вывода, а итераторы, обозначающие исход- ный диапазон, — итераторами ввода. Ответственность за то, что обозначенная пара- метром de st последовательность сможет вместить все элементы, записываемые из исходной последовательности, несет вызывающая функция. Эти алгоритмы возвра- щают итератор dest, увеличенный так, чтобы он указывал на следующий элемент после последнего записанного. copy(beg, end, dest) Копирует исходный диапазон в последовательность, начинающуюся с итерато- ра dest. transform(beg, end, dest, unaryOp) transform(beg, end, beg2, dest, binaryOp) Применяет указанную операцию к каждому элементу исходного диапазона и за- писывает результат в последовательность, указанную итератором dest. Первая вер- сия применяет к элементам исходного диапазона унарную операцию, а вторая — би- нарную к парам элементов. Первый аргумент бинарного оператора она получает из последовательности, обозначенной итераторами beg и end, а второй — из последо- вательности, начало которой указано итератором Ьед2. Ответственность за доста- точный объем последовательности, указанной итератором Ьед2, несет программист. replace_copy(beg, end, dest, old_val, newval) replacecopyif(beg, end, dest, unaryPred, newval) Копирует элементы в последовательность, указанную итератором dest, заменяя определенные элементы значением new_val. Первая версия заменяет те элементы, значения которых равны old_val. Вторая версия заменяет те элементы, для кото- рых предикат unaryPred возвращает значение true. merge(begl, endl, beg2, end2, dest) merge(begl, endl, beg2, end2, dest, comp) Обе исходные последовательности должны быть отсортированы. Объединенный результат записывается в последовательность, указанную итератором dest. Первая версия сравнивает элементы используя оператор <, а вторая применяет предостав- ленную функцию сравнения. Алгоритмы, записывающие значения в элементы с помощью прямых итераторов Этим алгоритмам нужны прямые итераторы, поскольку они осуществляют за- пись в элементы исходной последовательности. swap (elemi, elem2) iterswap(Iterl, iter2) Параметрами этих функций являются ссылки, поэтому аргументы допускают пе- резапись. Эти функции меняют указанные элементы, обозначенные итераторами.
Библиотека 847 swap_ranges(begl, endl, beg2) Заменяет элементы исходного диапазона элементами второй последовательно- сти, начиная с позиции Ьед2. Диапазоны не должны пересекаться. Программист должен гарантировать, что последовательность, начинающаяся с позиции Ьед2, бу- дет иметь достаточный размер. Возвращает итератор Ьед2, увеличенный так, чтобы указывать на элемент после последнего измененного элемента. fill(beg, end, val) generate(beg, end, Gen) Присваивает новое значение каждому элементу исходной последовательности. Алгоритм fill присваивает значение val, а алгоритм generate запускает функ- цию Gen (), которая создает новые значения. replace(beg, end, old_val, newval) replace!f(beg, end, unaryPred, newval) Заменяет значение каждого соответствующего элемента значением new_val. Первая версия использует для сравнения значений элементов со значением old_val оператор ==, а вторая выполняет предикат unaryPred для каждого элемента, заме- няя те из них, для которых предикат вернул значение true. Алгоритмы, записывающие значения в элементы при помощи двунаправленных итераторов Этим алгоритмам нужна возможность перемещения по последовательности впе- ред и назад, поэтому для них требуются двунаправленные итераторы. copy_backward(beg, end, dest) Копирует элементы в последовательность, указанную итератором dest, в обрат- ном порядке. Возвращает итератор Ьед2, увеличенный так, чтобы указывать на эле- мент после последнего измененного. inplace_merge(beg, mid, end) inplace_merge(beg, mid, end, comp) Объединяет две смежные подпоследовательности из той же последовательности в единую упорядоченною последовательность. То есть последовательности, указан- ные итераторами от beg до mid и от mid до end, объединяются в последователь- ность от beg до end. Первая версия сравнивает элементы используя оператор <, а вторая применяет предоставленную функцию сравнения. Возвращает тип void. А.2.5. Алгоритмы сортировки и разделения Алгоритмы сортировки и разделения предоставляют разные способы упорядочи- вания элементов контейнера. Алгоритм partition разделяет элементы исходного диапазона на две группы. Первая группа состоит из тех элементов, которые удовлетворяют условию предика- та, а вторая — из остальных. Например, элементы контейнера можно разделить на основании четности или нечетности элементов, или на основании того, начинается ли слово с заглавной буквы и т.д.
848 Приложение А Каждый из алгоритмов сортировки и разделения имеет стабильные (stable) и не- стабильные (unstable) версии. Стабильный алгоритм поддерживает относительный порядок равных элементов. Рассмотрим, например, следующую последовательность. { "pshew", "Honey", "tigger", "Pooh" } Стабильный алгоритм, разделяющий элементы на основании того, начинается ли слово с заглавной буквы, создает последовательность, в двух категориях которой под держивается прежний порядок расположения слов относительно друг друга. { "Honey", "Pooh", "pshew", "tigger" } Стабильные алгоритмы предполагают выполнение большего количества дейст- вий, поэтому они могут работать медленнее и использовать больше памяти, чем их нестабильные аналоги. Алгоритмы разделения Этим алгоритмам необходимы двунаправленные итераторы. stable_partition(beg, end, unaryPred) partition(beg, end, unaryPred) Используют для разделения исходной последовательности предикат unaryPred. Элементы, для которых предикат unaryPred возвращает значение true, помеща- ются в начало последовательности, а элементы, для которых предикат возвращает значение false, — в конец. Возвращает итератор на элемент, следующий за послед- ним, для которого предикат unaryPred вернул значение true. Алгоритмы сортировки Эти алгоритмы требуют итераторов прямого доступа. Каждый из алгоритмов сортировки предоставляется в двух перегруженных версиях. В одной из них для сравнения элементов используется оператор < типа элемента, а во второй преду- смотрен дополнительный параметр для функции сравнения. Эти алгоритмы воз- вращают тип void, только алгоритм partial_sort_copy возвращает итератор получателя. Алгоритмы partial_sort и nth_element выполняют частичную сортировку последовательности. Их используют в случае, когда в результате сортировки всей последовательности могут возникнуть проблемы. Поскольку эти операции являются менее трудоемкими, они выполняются быстрее, чем сортировка всего исходного диапазона. sort(beg, end) stable_sort(beg, end) sort(beg, end, comp) stablesort(beg, end, comp) Сортирует весь диапазон. partial_sort(beg, mid, end) partial_sort(beg, mid, end, comp) Сортирует набор элементов, количество которых равно mid-beg. То есть если mid-beg равно 42, эта функция помещает элементы с самыми низкими значениями, в отсортированном порядке, в первые 42 позиции последовательности. После завер-
Библиотека 849 шения работы алгоритма partial_sort, окажутся отсортированы элементы в диа- пазоне от beg и далее, но не включая mid. Ни один из элементов в отсортированном диапазоне не больше, чем любой из элементов в диапазоне после mid. Порядок неот- сортированных элементов не определен. В качестве примера рассмотрим набор результатов соревнований. Допустим, не- обходимо выяснить, кто занял первое, второе и третье место, порядок всех осталь- ных результатов не важен. Такую последовательность можно отсортировать сле- дующим образом. partial_sort(scores.begin(), scores.begin() + 3, scores.end()); partial_sort_copy(beg, end, destBeg, destEnd) partialsortcopy(beg, end, destBeg, destEnd, comp) Сортирует элементы исходного диапазона и помещает их (в отсортированном порядке) в последовательность, указанную итераторами destBeg и destEnd. Если получающий диапазон имеет тот же размер или превосходит исходный, в него со- храняется весь исходный диапазон в отсортированном виде, начиная с позиции destBeg. Если размер получающего диапазона меньше, в него будет скопировано столько отсортированных элементов, сколько поместится. Алгоритм возвращает итератор в получающем диапазоне, указывающий на сле- дующий элемент после последнего отсортированного. Если получающая последова- тельность меньше исходного диапазона или равна ему по размеру, будет возвращен итератор destEnd. nth_element(beg, nth, end) nth_element(beg, nth, end, comp) Аргумент nth должен быть итератором, указывающим на элемент в исходной по- следовательности. Обозначенный этим итератором элемент, после выполнения алго- ритма nth_element, имеет значение, которое находилось бы там после сортировки всей последовательности. Кроме того, элементы контейнера вокруг позиции nth также отсортированы: перед ним располагается значение меньше или равное значению в по- зиции nth, а после него значение большее или равное. Для поиска ближайшего к ме- диане значения, алгоритм nth_element можно использовать следующим образом. nth_element(scores.begin(), scores.begin() + scores.size()/2, scores.end()); A.2.6. Общие функции изменения порядка Некоторые алгоритмы переупорядочивают элементы определенным способом. Первые два, remove и unique, переупорядочивают контейнер так, чтобы элементы в первой части последовательности удовлетворяли некоему критерию. Они возвра- щают итератор, отмечающий конец этой подпоследовательности. Другие, например reverse, rotate и random_shuf f 1е, реорганизуют всю последовательность. Эти алгоритмы работают “на месте”, т.е. они реорганизуют элементы непосредст- венно в исходной последовательности. Три алгоритма изменения порядка, remove_ copy, rotate_copy и unique_copy, предоставляют копирующие версии. Они за- писывают переупорядоченные значения в получающую последовательность, а не непосредственно в исходную.
850 Приложение А Переупорядочивающие алгоритмы, использующие прямые итераторы Эти алгоритмы переупорядочивают исходную последовательность. Им необхо- димы по крайней мере прямые итераторы. remove(beg, end, val) remove_if(beg, end, unaryPred) “Удаляет” элементы из последовательности, записывая поверх них те элементы, которые должны быть сохранены. Удаляются те элементы, которые равны значению val или те, для которых предикат unaryPred вернул значение true. Возвращает итератор на следующий элемент после последнего удаленного. Например, если исходной последовательностью является hello world, а зна- чением val — символ о, обращение к функции remove () перепишет два элемента, содержащие символ о, а остальные символы последовательности сдвинет влево на две позиции. Новой последовательностью будет hell wrldld. Возвращенный ите- ратор обозначит элемент после первого символа d. unique(beg, end) unique(beg, end, binaryPred) “Удаляет” все совпадающие элементы, кроме первого. Возвращает итератор на элемент, который следует за последним уникальным элементом. Первая версия ис- пользует для определения равенства смежных элементов оператор ==, а вторая — указанный предикат. Например, если исходной последовательностью является boohiss, после обра- щения к функции unique () она будет содержать символы bohisss. Возвращен- ный итератор указывает на элемент после первого символа s. Значения двух осталь- ных элементов последовательности не определены. rotate(beg, mid, end) “Поворачивает” элементы вокруг элемента, указанного итератором mid. Элемент, указанный итератором mid, становится первым элементом, затем идет последо- вательность от mid+1 до end, далее следует диапазон от beg до mid. Возвраща- ет тип void. Предположим, например, что исходная последовательность содержит символы hissboo. Если итератор mid указывает на символ Ь, перевернутая последователь- ность примет вид boohiss. Переупорядочивающие алгоритмы, использующие двунаправленные итераторы Поскольку эти алгоритмы обрабатывают исходную последовательность в обрат- ном порядке, им необходимы двунаправленные итераторы. reverse(beg, end) reverse_copy(beg, end, dest) Меняет порядок элементов последовательности на обратный. Алгоритм reverse реорганизует порядок элементов исходной последовательности, а алгоритм reverse_ сору копирует элементы (в обратном порядке) в последовательность, указанную
Библиотека 851 итератором вывода dest. Как обычно, программист должен удостовериться, что в принимающей последовательности достаточно места. Алгоритм reverse возвраща- ет тип void, а алгоритм reverse_copy возвращает итератор принимающей после- довательности на элемент, который расположен за последним скопированным. Переупорядочивающие алгоритмы, осуществляющие запись в итераторы вывода Этим алгоритмам необходимы прямые итераторы для исходной последователь- ности и итераторы вывода для получающей последовательности. Каждый из приведенных выше общих алгоритмов переупорядочивания имеет копирующую версию (их имена заканчиваются частью _сору). Копирующие версии выполняют то же действие, но результат записывается в указанную получающую последовательность, а не в исходную. За исключением алгоритма rotate_copy, ко- торому необходимы прямые итераторы, исходный диапазон определяют итераторы ввода. Итератор dest должен быть итератором вывода, и, как обычно, разработчик должен гарантировать, что получающая последовательность будет иметь достаточ- ный размер. Алгоритмы возвращают итератор dest, увеличенный так, чтобы обо- значить следующий элемент после последнего скопированного. гemove_copy(beg, end, dest, val) remove_copy_if(beg, end, dest, unaryPred) Копирует в получающую последовательность, обозначенную итератором dest, все элементы, за исключением тех, значения которых равны val, или тех, для кото- рых предикат unaryPred возвращает значение true. unique_copy(beg, end, dest) unique_copy(beg, end, dest, binaryPred) Копирует в получающую последовательность, обозначенную итератором dest, все уникальные элементы. rotate_copy(beg, mid, end, dest) Действует аналогично алгоритму rotate, за исключением того, что исходная по- следовательность остается неизменной, а результат записывается в получающую по- следовательность, обозначенную итератором dest. Возвращает тип void. Переупорядочивающие алгоритмы, использующие итераторы прямого доступа Поскольку эти алгоритмы реорганизуют элементы в произвольном порядке, им нужны итераторы прямого доступа. random_shuffie(beg, end) random_shuffie(beg, end, rand) Осуществляет перестановку элементов в исходной последовательности. Второй версии передают генератор случайных чисел. Эта функция должна получать и воз- вращать значение итератора dif f erence_type. Обе версии возвращают тип void.
852 Приложение А А.2.7. Алгоритмы перестановки Рассмотрим последовательность из трех символов abc. Для нее возможны шесть вариантов перестановки: abc, acb, bac, bca, cab и cba. Эти варианты перечислены в лексикографическом порядке, т.е. так, как их оценивает оператор меньше (<). То есть abc — это первая перестановка, поскольку ее первый элемент меньше или равен первому элементу в каждом другом варианте, второй элемент меньше, чем у любого варианта, использующего соответствующий первый элемент, и т.д. Аналогично, acb является следующим вариантом перестановки, поскольку кроме первого варианта, она “меньше” любого остального варианта перестановки. Те варианты, которые на- чинаются с символа Ь, располагаются перед теми, которые начинаются с символа с. Для каждого описанного выше варианта перестановки можно выяснить, который из них должен располагаться прежде, а которые после него. Например, о варианте перестановки Ьса можно сказать, что предыдущим для нее будет вариант Ьас, а сле- дующим — cab. Для варианта abc нет предыдущего, а для варианта cba — после- дующего варианта перестановки. Библиотека предоставляет два алгоритма, которые осуществляют перестановку последовательности в лексикографическом порядке. Эти алгоритмы переупорядо- чивают последовательность так, чтобы она оказалась в следующем или предыдущем (лексикографически) порядке. Они возвращают логическое (bool) значение, ука- зывающее на следующий или предыдущий вариант перестановки. Каждый из алгоритмов имеет две версии: с использованием оператора < типа элемента и дополнительного аргумента, позволяющего указать используемый для сравнения элементов предикат. Эти алгоритмы подразумевают, что элементы после- довательности уникальны. То есть элементы последовательности не должны иметь одинаковых значений. Алгоритмы перестановки, использующие двунаправленные итераторы Поскольку для перестановки последовательности необходимо перемещаться вперед и назад, алгоритмам нужны двунаправленные итераторы. next_permutation(beg, end) next_permutation(beg, end, comp) Если последовательность соответствует последней перестановке, алгоритм next_ permutation переупорядочивает ее так, чтобы она соответствовала самой младшей версии и возвращает значение false. В противном случае последовательность пре- образуется в следующий вариант перестановки и возвращает значение true. Первая версия использует для сравнения элементов оператор < типа элемента, а вторая — указанную функцию сравнения. prev_permutatlon(beg, end) prev_permutation(beg, end, comp) Подобен алгоритму next__permutation (), но преобразует последовательность в предыдущую версию перестановки. Если текущая версия является самой млад- шей, переупорядочивает последовательность в самую старшую и возвращает зна- чение false.
Библиотека 853 А.2.8. Алгоритмы набора для отсортированных последовательностей Алгоритмы набора реализуют присущие набору операции, применяемые для от- сортированной последовательности. Не следует путать эти алгоритмы с функциями библиотечного контейнера set (набор). Они обеспечивают присущее набору поведение на базе обычного последовательного А контейнера (например, vector, list и т.д.) или другой последовательности (например, потока ввода). За исключением алгоритма includes, всем им необходим итератор вывода. Как обычно, программист должен удостовериться в том, что получающая последова- тельность имеет достаточный размер для сохранения полученных элементов. Алго- ритмы возвращают итератор dest, увеличенный так, чтобы указывать на следую- щий элемент после последнего записанного. Каждый алгоритм предоставлен в двух формах: использующей для сравнения элементов оператор < или функцию сравнения. Алгоритмы набора, использующие итераторы ввода Поскольку эти алгоритмы обрабатывают элементы последовательно, им необхо- димы итераторы ввода. includes(beg, end, beg2, end2) includes(beg, end, beg2, end2, comp) Возвращает значение true, если каждый элемент во второй последовательно- сти содержится в исходной последовательности. В противном случае возвращает значение false. set_union(beg, end, beg2, end2, dest) set_union(beg, end, beg2, end2, dest, comp) Создает сортируемую последовательность элементов, которые находятся в обеих последовательностях. Элементы, которые находятся в обеих последовательностях, записываются в указанную итератором dest результирующую последовательность в одном экземпляре. set_intersection(beg, end, beg2, end2, dest) set_intersection(beg, end, beg2, end2, dest, comp) Создает сортируемую последовательность элементов, представленных в обеих последовательностях. Результат сохраняется в последовательности, указанной ите- ратором dest. set_difference(beg, end, beg2, end2, dest) set_difference(beg, end, beg2, end2, dest, comp) Создает сортируемую последовательность элементов, представленных в первом контейнере, но не во втором. set_symmetric_difference(beg, end, beg2, end2, dest) set_symmetric_difference(beg, end, beg2, end2, dest, comp)
854 Приложение А Создает сортируемую последовательность элементов, представленных в любом из контейнеров, но не в обоих контейнерах. Л.2.9. Минимальные и максимальные значения Первая из этих групп библиотечных алгоритмов уникальна тем, что они работают со значениями, а не с последовательностями. Второй набор получает последователь- ность, которая обозначена итераторами ввода. min(vail, val2) min(vail, val2, max(vail, val2) max(vail, val2, comp) comp) Эти алгоритмы возвращают минимум или максимум значений vail и val2. Тип аргументов должен точно совпадать. В одной из версий для сравнения ис- пользуется оператор < типа элемента, а во втором — указанная функция. Посколь- ку типом аргумента и возвращаемого значения являются константные ссылки, са- ми объекты не копируются. min_element(beg, end) min_element(beg, end, max_e1ement(beg, end) max_e1ement(beg, end, comp) comp) Эти алгоритмы возвращают итератор на элемент с самым маленьким или самым большим значением в исходной последовательности. Для сравнения элементов ис- пользуется либо оператор <, либо указанная функция. Лексикографическое сравнение При лексикографическом сравнении исследуются соответствующие элементы двух последовательностей на предмет выявления первой несовпадающей пары. По- скольку алгоритмы обрабатывают элементы последовательно, им необходимы ите- раторы ввода. Если одна последовательность коррче другой и все ее элементы равны соответствующим элементам более длинной последовательности, считается, что ко- роткая последовательность лексикографически меньше. Если размер последова- тельностей одинаков и все их элементы совпадают, ни одна из последовательностей не будет лексикографически меньше другой. lexicographical_compare(begl, endl, beg2, end2) lexicographicalcompare(begl, endl, beg2, end2, comp) Осуществляет поэлементное сравнение двух последовательностей. Возвращает значение true, если первая последовательность лексикографически меньше второй, в противном случае возвращает значение false. Для сравнения элементов исполь- зуется либо оператор <, либо указанная функция. А.2.10. Числовые алгоритмы Числовым алгоритмам необходимы итераторы ввода, если осуществляется чте- ние, и итераторы вывода — если осуществляется запись.
Библиотека 855 Эти функции выполняют простые арифметические манипуляции с исходной по- следовательностью. Для применения числовых алгоритмов в код следует подклю- чить заголовок numeric. accumulate(beg, end, init) accumulate(beg, end, init, BinaryOp) Возвращает сумму всех значений в исходном диапазоне. Суммирование начина- ется с исходного значения, заданного параметром init. Тип возвращаемого значе- ния задает тип параметра init. Если последовательность содержит значения 1, 1, 2, 3, 5 и 8, а исходным значе- нием является 0, результатом будет 20. Первая версия использует оператор + типа элемента, а вторая — указанный бинарный оператор. innerproduct(begl, endl, beg2, init) inner_product(begl, endl, beg2, init, BinOpl, BinOp2) Возвращает произведение элементов двух последовательностей. Обе последова- тельности исследуются поэлементно, а их значения перемножаются. Результат ум- ножения суммируется. Исходное значение суммы задает параметр init. Подразу- мевается, что вторая последовательность, начало которой указывает итератор Ьед2, имеет по крайней мере столько элементов, сколько и в первой. Лишние элементы второй последовательности игнорируются. Тип возвращаемого значения задает тип параметра init. Первая версия использует для умножения элементов оператор *, а для сложе- ния оператор +. Предположим, что существует две последовательности 2, 3, 5, 8 и 1, 2, 3, 4, 5, б, 7. Результатом будет сумма исходного значения и следующих пар произведений. initial_value +(2*1) +(3*2) +(5*3) +(8*4) Если исходным будет значение 0, то результат составит 55. Там, где в первой версии используются операторы сложения и умножения, вто- рая версия применяет указанные бинарные операторы. Например, алгоритм inner_ product можно было бы использовать для создания списка параметрических пар имя-значение элементов, где имя взято из первой исходной последовательности, а соответствующее значение — из второй. / / объединить элементы в разделяемую запятой пару string combine(string х, string у) { return "(" + х + ", " + у + ")"; } // добавить две строки, разделенные запятой string concatenate(string х, string у) { } cout « inner_product(names.begin(), names.end(), values.begin(), string(), concatenate, combine); Если первая последовательность содержит слова if, string и sort, а вторая — keyword, library type и algorithm, результат получится таким.
856 Приложение А (if, keyword), (string, library type), (sort, algorithm) partial_sum(beg, end, dest) partial_sum(beg, end, dest, BinaryOp) Записывает в новую последовательность, указанную итератором dest, элементы, значения которых представляют собой сумму всех предыдущих элементов исходно- го диапазона, включая соответствующий в текущей позиции. Первая версия исполь- зует оператор + типа элемента, а вторая — указанный бинарный оператор. Програм- мист должен гарантировать, что указанная итератором dest последовательность, по крайней мере, не меньше исходной. Итератор dest возвращается увеличенным, с тем чтобы указывать на следующий элемент после последнего записанного. Если исходная последовательность содержит значения 0,1,1,2,3,5,8,в резуль- тате получится последовательность, содержащая значения 0, 1, 2, 4, 7, 12, 20. Чет- вертый элемент, например, будет суммой трех предыдущих значений (0, 1, 1) плюс его собственное значение (2). В результате получается значение 4. adjacentdifference(beg, end, dest) adjacent_difference(beg, end, dest, BinaryOp) Записывает в новую последовательность, указанную итератором dest, элементы, значения которых представляют собой разницу текущего и предыдущего элементов. Первая версия использует оператор - типа элемента, а вторая — указанный бинар- ный оператор. Программист должен гарантировать, что указанная итератором dest последовательность, по крайней мере, не меньше исходной. Если исходная последовательность содержит значения 0, 1, 1, 2, 3, 5, 8, первый элемент новой последовательности будет копией первого элемента исходной, т.е. 0. Вторым элементом будет разница первых двух элементов, т.е. 1. Третий элемент — разница между вторым и третьим элементом, т.е. 0, и т.д. Новая последовательно- стью будет содержать значения 0,1,0,1,1,2,3. Л.З. Возвращаясь к библиотеке ввода-вывода В главе 8, “Библиотека ввода-вывода”, была представлена упрощенная архитек- тура библиотеки ввода-вывода и ее наиболее популярные части. Этот раздел прило- жения завершает описание библиотеки ввода-вывода. А.3.1. Флаги формата Кроме флагов состояния (раздел 8.2, стр. 314), каждый объект класса iostream содержит также флаги формата (format state), контролирующие способ оформления вводимых и выводимых данных. Флаги формата контролируют такие аспекты оформ- ления, как национальные особенности отображения целочисленных значений, точ- ность значений с плавающей запятой, ширина поля вывода и т.д. В библиотеке опреде- лен также набор манипуляторов, перечисленных в табл. А.2 (стр. 862) и А.З (стр. 866), позволяющих изменять флаги формата объекта. Говоря проще, манипулятор — это функция или объект, применяемые в качестве операнда в операторе ввода или вывода. Поскольку манипулятор возвращает тот потоковый объект, к которому он был приме- нен, в одном операторе можно использовать несколько манипуляторов.
Библиотека 857 При чтении или записи манипулятора, никакие данные не читаются и не записы- ваются. Вместо этого принимается некое действие. В описанных ранее программах уже использовался один из манипуляторов, endl, “запись” которого в поток вывода похожа на передачу значения. Однако манипулятор endl — это не значение, а при- каз выполнить операцию записи символа новой строки и сброса буфера. Л.3.2. Большинство манипуляторов изменяют флаг формата Большинство манипуляторов изменяют флаг формата потока. Они изменяют та- кие аспекты отображения, как формат чисел с плавающей запятой, способ представ- ления логических значений (как число или как литерал true либо false) и т.д. Флаг формата потока, измененный манипулятором, обычно остается примененным для всего последующего ввода-вывода. Большинство манипуляторов, изменяющих флаг формата, существуют в двух ва- риантах, для установки и сброса; т.е. один манипулятор устанавливает новое значе- ние флага формата, а другой возвращает его в состояние, принятое по умолчанию. Тот факт, что манипулятор изменяет состояние флага формата на постоянной основе, может быть очень полезен в случае, когда тот же формат следует использовать для цело- го набора операций ввода-вывода. На практике подобное поведение манипуляторов очень удобно для установки или отмены некоторых правил форматирования всего по- тока ввода или вывода. В некоторых случаях такое поведение весьма желательно. Однако большинство программ (и программистов тоже) ожидают, что поток бу- дет находиться в стандартном состоянии. Кроме того, если поток останется в не- стандартном, измененном состоянии, может произойти ошибка. Обычно имеет смысл отменять все изменения состояния, сделанные манипулятором. Как правило, после каждой операции ввода-вывода, поток следует возвращать в стандартное, заданное по умолчанию состояние. Применение функции f lags () для восстановления флагов формата Лучше всего манипулировать флагами формата с помощью функции flags О. Функция flags () подобна функциям rdstate () и setstate (), позволяющим работать с флагами состояния потока. В настоящее время в библиотеке определены две версии функции f lags (). Функция flags () без аргументов возвращает текущий флаг формата потока. Возвращенное значение имеет библиотечный тип f mtf lags. Функция flags (arg) получает аргумент типа fmtflags и устанавливает ука- занный им формат потока. Эти функции можно использовать для установки или сброса флагов формата по- тока ввода или вывода.
858 Приложение А void display(ostream& os) { // запомнить текущий флаг формата ostream::fmtflags curr_fmt = os.flagsO; // вывести данные, используя манипуляторы изменения флага // формата объекта os os.flags(curr_fmt); // восстановить исходный флаг формата os } А.3.3. Управление форматом вывода Большинство манипуляторов позволяют изменять внешний вид выводимых дан- ных. Существует две обширных категории средств управления выводом: средства управления представлением числовых значений и средства управления расположе- нием данных. Управление форматом логических значений Манипулятор bool alpha является одним из примеров манипулятора, который изменяет состояние формата своего объекта. По умолчанию логические значения (тип bool) отображаются как 1 или 0. Значению true соответствует целое число 1, а значению false — 0. Манипулятор boolalpha позволяет изменить это стандарт- ное поведение потока, cout « "default bool values: " « true « " " « false « "\nalpha bool values: " « boolalpha « true « " "« false << endl; При выполнении эта программа отобразит следующий результат, default bool values: 1 0 alpha bool values: true false Как только манипулятор boolalpha будет “записан” в поток cout, изменится способ отображения им логических значений. В результате последующих операций вывода значений типа bool, отображаться будут литералы true или false. Чтобы сбросить флаг формата, примененный к объекту cout, следует применить манипулятор noboolalpha, bool bool_val; cout << boolalpha // изменить внутреннее состояние объекта cout « bool_val << noboolalpha; // восстановить стандартное состояние Здесь формат отображения логических значений изменен только во время ото- бражения значения переменной bool_val. После этого поток возвращается к ис- ходному состоянию. Указание основания целочисленных значений По умолчанию чтение и запись целочисленных значений происходит в десятич- ном формате. Используя манипуляторы hex, oct и dec, программист может изме- нить основание целых чисел на восьмеричное, шестнадцатеричное и десятичное (кроме значений с плавающей запятой).
Библиотека 859 const int ival = 15, jval = 1024; // const, значения не изменятся cout « "default: ival = " « ival << " jval - " << jval « endl; cout << "printed in octal: ival = " << oct << ival « " jval = " << jval « endl; cout « "printed in hexadecimal: ival - " << hex << ival « " jval = " << jval « endl; cout << "printed in decimal: ival = " << dec << ival << " jval - " << jval << endl; После компиляции и выполнения программа отобразит следующий результат, default: ival = 15 jval = 1024 printed in octal: ival = 17 jval = 2000 printed in hexadecimal: ival = ? jval = 400 printed in decimal: ival = 15 jval = 1024 Эти манипуляторы, подобно манипулятору boolalpha, изменяют флаг формата. Их воздействие начинается непосредственно после применения и продолжается да- лее, при выводе всех остальных целочисленных значений, пока формат не будет из- менен или восстановлен другим манипулятором. Индикация основания выводимых данных По умолчанию, при отображении числа, нет никакого визуального индикатора, указывающего используемое основание. Означает ли 2 0 действительно число 20 или восьмеричное представление числа 16? При отображении чисел в десятичном режи- ме, число соответствует ожидаемому. При отображении восьмеричного или шестна- дцатеричного значения имеет смысл использовать манипулятор showbase. Он формирует поток вывода так, чтобы в нем использовались те же соглашения, кото- рые используются при указании основания целочисленных констант. Префикс Ох означает шестнадцатеричное основание. Префикс 0 означает восьмеричное основание. Отсутствие префикса означает десятичное число. Ниже приведен вариант программы, переделанный с использованием манипуля- тора showbase. const int ival - 15, jval - 1024; // const, значения не изменятся cout « showbase; // отображать основание при // выводе целочисленных значений cout "default: ival ival << " jval - " << jval << endl; cout << "printed in octal: ival = " « oct << ival << " jval = " « jval << endl; cout << "printed in hexadecimal: ival = " << hex << ival << " jval = " << jval « endl; cout << "printed in decimal: ival = " << dec << ival « " jval = " « jval << endl; cout << noshowbase; // восстановить состояние потока Теперь в отображаемом результате указано основание каждого значения. default: ival = 15 jval = 1024 printed in octal: ival = 017 jval = 02000 printed in hexadecimal: ival = Oxf jval = 0x400 printed in decimal: ival = 15 jval = 1024
860 Приложение А Манипулятор noshowbase восстанавливает состояние объекта cout так, чтобы он перестал отображать основание целочисленных значений. По умолчанию символы шестнадцатеричного значения отображаются в нижнем регистре (включая символ х). Применив манипулятор uppercase, шестнадцате- ричные цифры A—F и символ X можно отображать в верхнем регистре. cout « uppercase « showbase « hex « "printed in hexadecimal: ival - " « ival << " jval = " « jval « endl « nouppercase « endl; Эта программа создает следующий результат. printed in hexadecimal: ival = OXF jval = 0X400 Для возвращения в нижний регистр используется манипулятор nouppercase. Управление форматом значений с плавающей запятой Существует три аспекта оформления значений с плавающей запятой, которыми можно управлять. Точность: количество отображаемых цифр. Форма записи: десятичная или экспоненциальная. Указание десятичной точки для значений с плавающей запятой, являющихся целыми числами. По умолчанию значения с плавающей запятой отображаются с точностью в шесть цифр. Если значение не имеет дробной части, десятичная точка отсутствует. То, бу- дет ли число с плавающей запятой отображено в десятичном формате или экспонен- циальном, зависит от его значения. Автоматический выбор формата способствует удобочитаемости числа. Экспоненциальный формат используется для отображения очень больших и очень маленьких значений. Для других значений используется де- сятичный формат. Задание отображаемой точности Точность задает общее количество отображаемых цифр. При отображении значе- ния с плавающей запятой округляются до текущей точности, а не усекаются. Таким образом, если текущей является точность в четыре цифры, число 3.14159 отобра- жается как 3.142, а если точность составляет три символа — как 3.14. Изменить точность можно при помощи функции-члена precision () или мани- пулятора setprecision. Функция-член precision () перегружена (раздел 7.8, стр. 291): одна из ее версий получает аргумент типа int, задающий устанавли- ваемую точность. Она возвращает предыдущее значение точности. Вторая вер- сия не получает никаких аргументов и возвращает текущее значение точности. Манипулятор setprecision получает аргумент, который задает устанавливае- мую точность.
Библиотека 861 Таблица А.2. Манипуляторы, определенные в заголовке iostream boolalpha X noboolalpha showbase X noshowbase showpoint X noshowpoint showpos X noshowpos uppercase X nouppercase X dec hex oct left right internal fixed scientific flush ends endl unitbuf X nounitbuf X skipws noskipws ws Отображать true и false как строки Отображать true и false как о и 1 Создает префикс, указывающий основание числа Отмена создания префикса, указывающего основание числа Всегда отображать десятичную точку Отображать десятичную точку только для дробных чисел Отображать для положительных чисел символ + Не отображать для положительных чисел символ + Отображать префиксы ох и е в верхнем регистре Отображать префиксы ох и е в нижнем регистре Отображать числа в десятичном формате Отображать числа в шестнадцатеричном формате Отображать числа в восьмеричном формате Добавить заполняющие символы справа от значения Добавить заполняющие символы слева от значения Добавить заполняющие символы между знаком и значением Отображать числа с плавающей запятой в десятичном формате Отображать числа с плавающей запятой в экспоненциальном формате Сброс буфера объекта класса ostream Вставляет нулевой символ, а затем сбрасывает буфер объекта класса ostream Вставляет символ новой строки, а затем сбрасывает буфер объекта класса ostream Сброс буфера после каждой операции вывода Восстановить стандартные правила сброса буфера Пропустить отступ в операторах ввода Не пропускать отступ в операторах ввода “Удалить” отступ X означает стандартное состояние потока. Приведенная ниже программа иллюстрирует разные способы управления точно- стью отображения значения с плавающей запятой. // cout.precision сообщает о текущем значении точности cout « "Precision: " « cout.precision() << ", Value: " << sqrt(2.0) « endl; // cout.precision(12) задает точность отображения в 12 цифр cout.precision(12); cout « "Precision: " << cout.precision() « ", Value: " << sqrt(2.0) « endl; // альтернативный способ задания точности при помощи !/ манипулятора setprecision cout << setprecision(3); cout << "Precision: " << cout.precision() << ", Value: " << sqrt(2.0) « endl;
862 Приложение А После компиляции и выполнения программа отобразит следующий результат. Precision: Precision: Precision: 6, Value: 1.41421 12, Value: 1.41421356237 3, Value: 1.41 Эта программа использует библиотечную функцию sqrt О, определенную в за- головке cmath. Функция sqrt () перегружена и может быть вызвана с аргументами float, double и long double. Она возвращает квадратный корень своего аргумента. Манипуляторы setprecision и другие получающие аргументы манипуляторы опреде- лены В заголовке iomanip. Управление формой записи По умолчанию форма записи значения с плавающей запятой зависит от его раз- мера: если число очень большое или очень маленькое, оно будет отображено в экс- поненциальном формате, в противном случае — в десятичном. Форма записи выби- рается исходя из удобочитаемости. При отображении обычного числа с плавающей запятой (в отличие от отображения денежных сумм или процентов, где внешний вид значения должен быть фиксиро- ван), как правило, имеет смысл позволить библиотеке самой выбрать используемую форму записи. Но иногда, особенно при отображении таблиц, следует фиксировать положение десятичной точки. Если необходимо установить экспоненциальную или фиксированную форму за- писи, можно использовать соответствующий манипулятор. Манипулятор scientific изменяет поток вывода так, чтобы использовался экспоненциальный формат. По- добно символу х шестнадцатеричных целочисленных значений, регистр символа е экспоненциальной формы можно изменить с помощью манипулятора uppercase. Манипулятор f ixed возвращает поток к применению формата фиксированных де- сятичных чисел. Эти манипуляторы изменяют стандартный смысл принятых по умолчанию зна- чений точности для потока. После выполнения манипуляторов scientific или fixed, точность отображения значений задает количество цифр после десятичной точки. По умолчанию точность определяет общее количество цифр: и до и после де- сятичной точки. Применение манипуляторов scientific или fixed позволяет отображать числа, совмещенные в столбцы. Эта стратегия гарантирует, что десятич- ная точка всегда будет располагаться в фиксированной позиции относительно ото- бражаемой дробной части. Возвращение к стандартной форме записи значений с плавающей запятой В отличие от других случаев, не существует манипулятора, который возвращает поток в стандартное состояние после изменения формы записи значений с плаваю- щей запятой. Чтобы отменить изменение, внесенное манипуляторами scientific или fixed, необходимо вызвать функцию-член unset f (). Для этого ей необходимо передать определенное в библиотеке значение по имени float field.
Библиотека 863 // вернуться к стандартной форме записи cout.unsetf(ostream::floatfield); За исключением способа отмены, применение этих манипуляторов подобно ис- пользованию любых других манипуляторов. cout « sqrt(2.0) « '\n' « endl; cout « "scientific: " « scientific « sqrt(2.0) « '\n' « "fixed decimal: " « fixed « sqrt(2.0) « "\n\n"; cout « uppercase « "scientific: " « scientific « sqrt(2.0) « 1 \n' « "fixed decimal: " « fixed « sqrt(2.0) « endl « nouppercase; // вернуться к стандартной форме записи cout.unsetf(ostream::floatfield); cout « '\n' « sqrt(2.0) « endl; При выполнении эта программа отобразит следующий результат. 1.41421 scientific: 1.414214е+00 fixed decimal: 1.414214 scientific: 1.414214Е+00 fixed decimal: 1.414214 1.41421 Отображение десятичной точки По умолчанию когда дробная часть значения с плавающей запятой равна 0, деся- тичная точка не отображается. Манипулятор showpoint позволяет отображать де- сятичную точку принудительно. cout « 10.0 « endl; cout « showpoint « 10.0 « noshowpoint « endl; // отображает 10 // отображает 10.0000 !/ вернуться к стандартной форме // отображения десятичной точки Манипулятор noshowpoint восстанавливает стандартное поведение. Сле- дующие операторы вывода будет иметь стандартное поведение, при котором деся- тичная точка не будет отображаться, если значение с плавающей запятой не имеет дробной части. Дополнение отображаемого результата При отображении данных в столбцах, зачастую необходим дополнительный кон- троль за их размещением. Библиотека предоставляет несколько манипуляторов, об- легчающих размещение отображаемых данных. Манипулятор setw задает минимальное пространство для следующего числового или строкового значения. Манипулятор left выравнивает вывод по левому краю. Манипулятор right выравнивает вывод по правому краю. Это состояние при- нято по умолчанию.
864 Приложение А Манипулятор internal контролирует размещение символа отрицательного значения. Манипулятор internal выравнивает знак по левому краю, а значе- ния по правому, заполняя пространство между ними пробелами. Манипулятор set fill указывает альтернативный символ, используемый при дополнении вывода. По умолчанию для этого используется пробел. Манипулятор setw, подобно манипулятору endl, не изменяет внутреннее состояние потока вывода. Он задает только размер пространства для следующего вывода. Применение этих манипуляторов демонстрирует следующий код. int i = -16; double d = 3.14159; // дополнят первый столбец так, чтобы вывод II содержал минимум 12 позиций cout << "i: " << setw(12) << i << "next col" « '\n' << "d: " « setw(12) « d « "next col" « '\n'; // дополнят первый столбец так, чтобы выровнять // все столбцы по левому краю cout « left « "i: " « setw(12) « i « "next col" << '\n' « "d: " « setw(12) << d << "next col" « ’ \n' << right; // восстанавливает стандартное // выравнивание // дополнят первый столбец так, чтобы выровнять // все столбцы по правому краю cout « right « "i: " << setw(12) << i « "next col" « '\n' « "d: " « setw(12) << d << "next col" « '\n'; // дополнят первый столбец, но с применением манипулятора internal cout « internal « "i: " << setw(12) « i « "next col" « ’ \n' « "d: " « setw(12) « d << "next col" « '\n'; // дополняет первый столбец, используя для заполнения символ # cout « setfill('#') « "i: " << setw(12) « i « "next col" « '\n' « "d: " « setw(12) « d « "next col" << '\n' « setfill(' '); // восстанавливает стандартный II символ заполнения При выполнении эта программа отобразит следующий результат. i: -16next col d: 3.14159next col i: -16 next col d: 3.14159 next col i: -16next col d: 3.14159next col i: - 16next col d: 3.14159next col i: -#########16next col d: #####3.14159next col
Библиотека 865 Таблица А.З. Манипуляторы, определенные в заголовке iomanip setfill (ch) Задает символ заполнения ch setprecision (п) Задает точность п для значений с плавающей запятой s е tw (w) Читать или записывать значения в w символов setbase (Ь) Основание выводимых целых чисел ь Л.3.4. Управление форматом ввода По умолчанию операторы ввода игнорируют отступ (символы пробела, табуля- ции, новой строки, перехода на новую страницу и возврата каретки), while (cin » ch) cout « ch; Предположим, что цикл приведенный выше, читает следующую последователь- ность символов. а Ь с d Цикл выполняется четыре раза и читает символы от а до d, игнорируя располо- женные между ними пробелы, символы табуляции и символы новой строки. В ре- зультате выполнения программа отобразит следующий результат, abed Манипулятор noskipws заставляет оператор ввода читать все, включая символы отступа. Чтобы вернуть стандартное поведение, применяется манипулятор skipws. cin » noskipws; // заставить cin читать отступы while (cin » ch) cout « ch; cin » skipws; // вернуть cin в стандартное состояние, // чтобы он игнорировал отступ При вводе тех же данных, что и ранее, этот цикл сделает семь итераций, читая также и символы отступа. В результате он отобразит следующее, а b с d А.3.5. Бесформатные операции ввода-вывода В программах, рассмотренных до сих пор, использовались только форматирован- ные операции ввода-вывода. Операторы ввода и вывода (<< и >>) читают или запи- сывают данные, формат которых определяет их тип. Операторы ввода игнорируют отступ, а операторы вывода применяют дополнение, точность и т.д. Библиотека предоставляет также богатый набор низкоуровневых операторов, ко- торые обеспечивают бесформатный ввод и вывод. Эти операторы позволяют рабо- тать с потоком как с последовательностью неинтерпретируемых байтов, а не пере- менных определенных типов, например char, int, string и т.д.
866 Приложение А Л.3.6. Однобайтовые операторы Некоторые из бесформатных операторов работают с потоком по одному байту за раз. Они читают операторы отступа, не игнорируя их. Например, для чтения символов по одному, можно использовать бесформатные операторы ввода и вывода get и put. char ch; while (cin.get(ch)) cout.put(ch); Эта программа прочитает все введенные символы отступа. Ее вывод идентичен вводу. Если ввести данные, как и в прежних случаях, результат будет тем же, что и при использовании манипулятора noskipws. а Ь с d Таблица А.4. Однобайтовые низкоуровневые операторы ввода-вывода is.get(ch) os .put (ch) is.get() is.putback(ch) is.unget() is. peek () Помещает следующий байт из объекта is объекта istream в символ ch. Возвращает объект is Помещает символ ch в объект os класса ostream. Возвращает объект os Возвращает следующий байт из объекта i s как тип int Помещает символ ch обратно в объект is. Возвращает объект is Перемещает объект is на один байт обратно. Возвращает объект is Возвращает следующий байт как тип int, но не удаляет его Возвращение символа обратно в поток ввода Иногда, чтобы убедиться в непригодности читаемого символа, его все же следует прочитать. В таких случаях символ следует вернуть обратно в поток. Библиотека предоставляет для этого три разных способа, каждый из которых несколько отлича- ется от других. Функция реек () возвращает копию следующего символа из потока ввода, но сам поток не изменяет. Возвращенное функцией реек () значение остается в по- токе и будет следующим в очереди на получение. Функция unget () возвращает поток ввода на одно значение назад, поэтому по- следнее возвращенное значение остается в потоке. Функцию unget () можно вызывать даже тогда, когда неизвестно само значение, полученное из потока по- следним. Функция putback () представляет собой более специализированную версию функции unget (). Она возвращает в поток последнее прочитанное значение, но получает аргумент, который должен быть последним прочитанным значением. Функция putback () используется в программах нечасто, поскольку функция unget () осуществляет те же действия, но гораздо проще. Обычно гарантированно можно вернуть только одно прочитанное значение. То есть нет никакой гарантии, что последовательный вызов функций putback () или unget () позволит вернуть несколько прочитанных ранее значений.
Библиотека 867 Возвращение значения типа int, полученного операторами ввода Версия функции get () без параметров и функция реек () возвращают символ из потока ввода как переменную типа int. Казалось бы, для этих функций было бы более естественным возвращать символ (тип char). Эти функции возвращают тип int для того, чтобы можно было получить символ конца файла. Такой способ интерпретации символов позволяет использовать каждое значение диапазона char для представления фактических символов. В этом случае нет необходимости в дополнительном значении, используемом для представления конца файла. Данные функции преобразуют символ в тип unsigned char, а затем представ- ляют его как значение типа int. В результате, даже если набор символов содержит символы, соответствующие отрицательным значениям, их интерпретация в качестве типа int приведет к возвращению положительных значений (раздел 2.1.1, стр. 58). Возвращая конец файла как отрицательное значение, библиотека гарантирует, что это значение будет отличаться от любого настоящего символьного значения. Чтобы избавить разработчика от необходимости выяснять фактическое значение символа конца файла, в заголовке iostream определена константа EOF, которую можно ис- пользовать для проверки соответствия возвращенного функцией get () значения символу конца файла. Таким образом, для хранения значения, возвращаемого этими функциями, необходимо использовать именно переменную типа int. int ch; // Примечание: int, а не char!!!! // цикл, читающий и отображающий все введенные данные while ((ch = cin.getO) != EOF) cout.put(ch); Эта программа работает аналогично приведенным на стр. 867. Единственное раз- личие заключается в использовании функции get () для чтения вводимых данных. Внимание! Низкоуровневые функции подвержены ошибкам Обычно рекомендуется использовать высокоуровневые абстракции, предоставляемые библиотекой. Функции ввода-вывода, возвращающие значение типа int, являются хорошим подтверждением правильности этой рекомендации. Обычной ошибкой программирования является присвоение значения, возвращаемого функцией get () или другой функцией, возвращающей тип int, переменной типа char, а не переменной типа int. Это, безусловно, будет ошибкой, но компилятор ее не обнаружит. То, что произойдет в результате этой ошибки, зависит от конкретной ма- шины и введенных данных. Например, если машина интерпретирует символ как без- знаковое целое число, приведенный ниже цикл окажется бесконечным. cnar ch; // Применение типа char здесь приведет к катастрофе! // значение, возвращенное функцией get() объекта cin, // преобразуется из int в char, а затем сравнивается с int while ((ch = cin.getO) ! = EOF) cout.put(ch); Проблема заключается в том, что когда функция get () возвращает значение EOF, оно пре- образуется в беззнаковое значение типа unsigned char. Это преобразованное значение не будет равно целочисленному значению EOF, поэтому цикл не закончится никогда.
868 Приложение А Подобная ошибка, по крайней мере, вероятней всего таки будет обнаружена при про- верке. У тех машин, где символы интерпретируются как знаковы. i тип, нельзя быть уверенным в том, что поведение цикла будет аналогичным. Ведь результат переполне- ния переменной беззнакового типа зависит от компилятора. На большинстве машин этот цикл будет работать нормально, если только во вводимых данных не встретится символ, соответствующий значению EOF. Поскольку в обычных данных такие симво- лы маловероятны, низкоуровневые операторы ввода-вывода могут пригодиться при чтении только бинарных значении, которые не соответствуют непосредственно обыч- ным символам и числовым значениям. На машине автора, например, цикл прежде- временно завершается в случае ввода символа, значением которого является ' \377 ’. Когда значение ' \3771 на машине автора преобразуется в тип signed char, получа- ется значение -1. Если во введенных данных встретится это значение, оно будет рас- сматриваться как символ (преждевременного) конца фагла. При чтении и записи типизированных значений такие ошибки не возникают. Поэто- му, по возможности следует использовать предоставляемые библиотекон высоко- уровневые операторы, что гораздо безопас ней. А.3.7. Многобайтовые операторы Другие бесформатные операторы ввода-вывода предназначены для одновремен- ной работы с порциями данных. Эти операторы могут быть очень полезны, особенно если скорость критически важна. Однако, подобно другим низкоуровневым опера- торам, они подвержены ошибкам. В частности, эти операторы требуют создания и уп- равления символьными массивами (раздел 4.3.1, стр. 159), используемыми этими операторами для хранения получаемых данных. Многобайтовые операторы перечислены в табл. А.5. Следует заметить, что функ- ция-член get () перегружена. Существует ее третья версия, которая читает после- довательность символов. Таблица А.5. Многобайтовые низкоуровневые операторы ввода-вывода is.get(sink, size, delim) Читает size байтов из объекта is и сохраняет их в символьном массиве, указанном параметром sink. Чтение продолжается до тех пор, пока не встре- тится символ delim или конец файла, либо пока не будет прочитано size байтов. Символ delim, если он встретится, остается в потоке ввода и не чи- тается В sink is.getline (sink, delim) size, Ведет себя так же, как и версия функции get () с тремя аргументами, но читает и отбрасывает символ delim is.read (sink, size) Читает size байтов в символьный массив sink. Возвращает объект is is.gcount() Возвращает количество байтов, прочитанных из потока is при последнем обра- щении к бесформатной функции чтения os.write (source, size) Записывает size байтов из символьного массива source в объект os. Воз- вращает объект os is.ignore (size, delim) Читает и игнорирует size символов, исключая символ delim. По умолчанию size равен 1, a delim — символу конца файла
Библиотека 869 Функции get () и getline () получают те же параметры и действуют подобно, но не идентично. В каждом случае параметр sink представляет собой массив эле- ментов типа char, в который помещены данные. Функция продолжает чтение до тех пор, пока не выполнится одно из следующих условий. Прочитано size - 1 символов. Достигнут конец файла. Достигнут символ, указанный в качестве разделителя. При выполнении любого из этих условий, в следующую открытую позицию мас- сива помещается нулевой символ. Отличаются эти функции обработкой разделите- ля. Функция get () оставляет разделитель как следующий символ объекта класса istream, а функция getline () читает и отбрасывает разделитель. В любом слу- чае, разделитель в массиве sink не сохраняется. Указание количества читаемых символов Некоторые из функций способны читать из потока ввода неизвестное заранее ко- личество байтов. Чтобы выяснить количество символов, прочитанных при послед- ней бесформатной операции чтения, можно применить функцию gcount (). Вызов функции gcount () должен располагаться перед любой из операций бесформатного ввода. В частности, односимвольные операторы, которые возвращают символы об- ратно в поток, также выполняют бесформатные операции ввода. Если перед вызовом функции gcount () будет вызвана функция реек (), unget () или putback (), она вернет значение 0! А.3.8. Произвольный доступ к потоку Некоторые из потоковых классов обеспечивают произвольный доступ к данным связанного с ним потока. Положение в потоке можно изменить так, чтобы прочитать сначала последнюю строку, затем первую и т.д. Для установки (seek) необходимой позиции и сообщения (tell) текущей позиции в потоке, библиотека предоставляет пару функций. Произвольный доступ для чтения и записи напрямую зависит от системы. Чтобы выяс- нить способ применения этой возможности, следует обратиться к документации на сис- тему. Функции установки и сообщения Для обеспечения произвольного доступа, классы ввода-вывода обладают мар- кером (marker), который указывает позицию следующей операции чтения или за- писи. Они обладают также двумя функциями: одна устанавливает (seek) маркер в новую позицию, а вторая сообщает (tell) текущую позицию маркера. Фактически в библиотеке определены две пары функций установки и сообщения, которые опи- саны в табл. А.6. Одна пара функций используется потоками ввода, а вторая — по- токами вывода. Версии для ввода и вывод различаются суффиксом. Суффикс g
870 Приложение А (getting) означает получение данных (чтение), а суффикс р (putting) — помещение данных (запись). Таблица А.б. Функции установки и сообщения seekg() tellg () seekp() tellp() Перемещает маркер потока ввода Возвращает текущую позицию маркера потока ввода Перемещает маркер потока вывода Возвращает текущую позицию маркера потока вывода Вполне логично, что для класса istream, а также производных от него классов ifstream и istringstream, можно использовать только версии д, а для классов ostream, а также производных от него классов ofstream и ostringstream мож- но использовать только версии р. Классы iostream, f st ream и stringstream способны читать и записывать данные в поток, поэтому для них можно использовать обе версии, дир. Существует только один маркер Тот факт, что библиотека различает версии функций seek () и tell () для чтения и записи, может ввести в заблуждение. Хотя библиотека и различает эти функции, в файле существует только один маркер, т.е. нет разных маркеров для чтения и записи. Когда речь идет о потоке только ввода или вывода, различие не столь очевидно. В таких потоках можно использовать версии только g или р. Если попытаться вы- звать функцию tellp () для объекта класса ifstream, компилятор сообщит об ошибке. Аналогично он поступит при попытке вызвать функцию seekg () для объекта класса ostringstream. При использовании классов f stream и stringstream, допускающих чтение и запись, применяется один буфер для хранения подлежащих чтению и записи дан- ных, а также один маркер, обозначающий текущую позицию в буфере. Библиотеч- ные функции версий дир используют тот же маркер позиции. Поскольку существует только один маркер, для переустановки маркера при каждом пере- ключении между чтением и записью следует применять функцию seek (). Объект класса iostr earn обычно не допускает произвольного доступа к данным Функции seek () и tell () определены для всех потоковых классов. Их дейст- вия определяются видом объекта, с которым связан поток. В большинстве систем поток, с которым связан потоковый объект cin, cout, сегг или clog, не обеспечи- вает возможности произвольного доступа — в конце концов, как можно перейти на десять позиций обратно, если запись осуществляется непосредственно в объект cout? Применить функции seek () и tell () конечно, можно, но во время выпол- нения это приведет к ошибке и переходу потока в недопустимое состояние.
Библиотека 871 Поскольку классы istream и ostream обычно не обеспечивают произвольного дос- тупа, в остальной части этого раздела речь идет только о классах fstream и sstream. Смена позиции маркера Функции seekg () и seekp () используются для изменения позиции чтения и записи в файле или строке. Функция seekg () изменяет позицию чтения в потоке, а функция seekp () задает позицию, начиная с которой будет осуществляться запись. Имеются две версии функции установки позиции: одна обеспечивает переход к указанной позиции внутри файла, а другая осуществляет смещение от текущей позиции. // установить маркер на указанную позицию внутри файла или строки seekg(new_position); // установка маркера для чтения seekp(new_position); // установка маркера для записи // сместить позицию на указанную дистанцию от текущей seekg(offset, dir); // установка маркера для чтения seekp(offset, dir); // установка маркера для записи Первая версия устанавливает текущую позицию непосредственно. Вторая полу- чает смещение (offset) и индикатор направления (dir). Возможные значения для смещения перечислены в табл. А.7. Таблица А.7. Аргументы смещения функции seek () beg Начало потока cur Текущая позиция потока end Конец потока Типы аргумента и возвращаемого значения этих функций являются машинно- зависимыми, они определены в классах istream и ostream. Типы pos_type и of f_type представляют позицию внутри файла и смещение от этой позиции соот- ветственно. Значение типа of f_type может быть положительным или отрицатель- ным, что соответствует смещению вперед или назад. Доступ к маркеру Текущая позиция возвращается функциями tellg () или tellp (), в зависимо- сти от того, осуществляется чтение или запись. Как прежде, р означает запись, ад — чтение. Функция tell () обычно используется для того, чтобы запомнить текущее положение и вернуться к нему впоследствии, при помощи функции seek (). // запомнить текущую позицию записи в переменную mark ostringstream writeStr; // поток вывода в строку ostringstream::pos_type mark = writeStr.tellp(); if (cancelEntry) // возврат к отмеченной позиции writeStr.seekp(mark);
872 Приложение А Функции tell () возвращают значение, которое указывает позицию в соответ- ствующем потоке. Подобно типу size_type классов string или vector, фактиче- ский тип значения, возвращаемого функциями tellg () или tellp (), неизвестен. Поэтому вместо него следует использовать член pos_type соответствующего пото- кового класса. Л.3.9. Чтение и запись в тот же файл Давайте рассмотрим пример программы, которая читает файл и записывает в его конец новую строку, которая содержит относительную позицию начала каждой строки. Предположим, например, что работать придется со следующим файлом, abed efg hi J Модифицированный программой файл должен выглядеть следующим образом, abed efg hi J 5 9 12 14 Обратите внимание, программа не записывает смещение для первой строки, она всегда начинается с позиции 0. Однако программа должна указать смещение, кото- рое соответствует концу части данных файла. То есть она должна записать позицию после конца введенных данных, чтобы можно было узнать, где заканчиваются дан- ные и где можно начать вывод. Эту программу можно создать на базе цикла, осуществляющего построчное чтение. int main() { // открыть файл для ввода и вывода, а затем перейти в его конец fstream inOut("copyOut", fstream::ate I fstream::in I fstream::out); if (!inOut) { cerr « "Unable to open file!" << endl; return EXIT_FAILURE; } // inOut открыт в режиме ate, поэтому исходной позицией // файла будет его конец ifstream::pos_type end_mark = inOut.tellg(); inOut.seekg(0, fstream::beg); // перейти к началу файла int ent = 0; // счетчик байтов string line; // содержит все введенные строки // пока не произошла ошибка, и чтение всех исходных // данных успешно, читать из файла следующую строку while (inOut && inOut.tellg() != end_mark && getline(inOut, line)) { ent += line.size() + 1; // добавить 1 (символ новой строки) // запомнить текущее положение маркера чтения ifstream::pos_type mark = inOut.tellg() ; inOut.seekp(0, fstream::end); // маркер записи в конец inOut « ent; // запись подсчитанного количества // запись разделителя, если это не последняя строка
Библиотека 873 if (mark ’ = end_mark) inOut « " inOut.seekg(mark); // восстановить позицию чтения } inOut.clear(); // сбросить флаги на случай, если // произойдет ошибка inOut.seekp(0, fstream::end); // перейти в конец inOut << "\n"; // запись символа новой строки в конец файла return 0; Эта программа открывает поток f stream в режимах in, out и ate. Первые два режима означают, что предполагается чтение и запись в тот же файл. Режим ate оз- начает, что начальной позицией открытого файла будет его конец. Как обычно, не- обходимо удостовериться, что файл открыт корректно, если это не так, следует вый- ти из программы. Исходное положение Ядром программы является цикл, который построчно читает файл и записывает относительную позицию каждой прочитанной строки. Цикл должен читать содер- жимое файла не включая символ новой строки. Соответствующая ему единица будет добавлена в счетчик. Поскольку предполагается запись в файл, конец файла не мо- жет быть условием выхода из цикла. Цикл должен закончиться тогда, когда он дос- тигнет точки, в которой заканчиваются исходные данные. Для этого необходимо сначала запомнить первоначальную позицию конца файла. Маркер файла, открытого в режиме ate, устанавливается в конец. Начальное по- ложение маркера, т.е. исходный конец файла, сохраняется в переменной end_mark. Запомнив конечную позицию, маркер чтения следует установить в начало файла, чтобы можно было приступить к чтению данных. Главный цикл Цикл while имеет три условия выхода. Сначала проверяется допустимость потока. Если объект inOut пройдет первую проверку, далее проверяется, не достигнут ли конец исходных данных. Для этого те- кущая позиция чтения, возвращаемая функцией tel 1g О , сравнивается со значени- ем, заранее сохраненным в переменной end_mark. И наконец, если обе проверки пройдены успешно, происходит вызов функции getline О, которая читает сле- дующую строку из файла. Если вызов функции getline О завершается успешно, выполняется тело цикла. Задачей цикла while является увеличение значения счетчика так, чтобы оно со- ответствовало началу следующей строки, а также запись этого значения в конец файла. Обратите внимание, что при каждом цикле конец файла отодвигается. Сначала текущая позиция сохраняется в переменной mark. Это значение необхо- димо сохранить потому, что для записи следующего относительного смещения те- кущую позицию в файле придется изменить. Вызов функции seekp () перемещает маркер текущей позиции в конец файла. Здесь происходит запись значения счетчи- ка, а затем текущая позиция восстанавливается с использованием значения, храни- мого в переменной mark. В результате маркер возвращается в ту же позицию, где он был после последней операции чтения. Восстановив положение маркера, можно снова проверить условие выхода из цикла while.
874 Приложение А Завершение работы с файлом После выхода из цикла все строки будут прочитаны, а начальные смещения вы- числены. Остается лишь записать смещение последней строки. Подобно другим операциям записи, здесь происходит вызов функции seekp (), осуществляющей пе- реход в конец файла перед записью символа новой строки. Единственной сложно- стью является вызов функции clear () для потока. Выход из цикла может произой- ти в связи с достижением конца файла или ошибкой ввода. Если бы не было вызова функции clear (), объект inOut остался бы в недопустимом состоянии и вызов функции seekp () потерпел бы неудачу.
Предметный указатель А Abstract base class, 628; 653 Abstract data type, 101; 130; 457; 501 Abstract interface, 640 Access label, 88; 98; 460; 502 Access level, 88 334; 376; 380 Address, 57; 96 Address-of, 139 Ambiguous, 296 Ambiguous call, 306 Anonymous union, 825; 836 Argument, 4 7; 50; 253; 306 Arithmetic conversion, 205; 214 Arithmetic type, 56; 57; 96 Array, 98; 134 Arrow, 553 Assignment operator, 506; 533 Associative array, 387; 416 Associative container, 383; 416 Associativity, 216 Automatic object, 280; 306 В Base class, 311; 329; 588; 590; 653 Best match, 295; 306 Bidirectional iterator, 444; 452 Binary function object, 564; 583 Binary operator, 172; 215 Binder, 565; 583 Bit pattern, 126 Bit-field, 828; 836 Bitwise operator, 130; 179 Block, 24; 35; 50; 219; 249 Buffer, 50 Built-in type, 24; 50 Byte, 57; 96 c Call operator, 4 7; 252; 306; 560 Candidate function, 296; 307 Capacity, 359 Cast, 209; 216; 577 Catch clause, 780 Catch-all, 728; 781 Class, 42; 50; 85; 97; 502 declaration, 502 derivation list, 594; 654 member, 100 scope, 77; 502 template, 114; 132; 659; 716 type, 52 Class-type conversion, 583 Comma expression, 192 Comment, 32; 50 Comparator, 636 Compiler extension, 169 Compound assignment operator, 35 Compound expression, 193; 216 Compound statement, 219; 250 Compound type, 81; 99; 169 Concatenation, 110 Concrete class, 502 Condition, 35; 52; 216 Condition state, 314; 330 Conditional operator, 190 Const member function, 286; 306; 502 Const pointer, 152 Const reference, 82; 97 Constant expression, 84; 97; 134 Constructor, 71; 88; 98; 288; 306; 459; 480 Constructor initializer list, 289; 307; 503 Constructor order, 781 Container, 131; 333; 381 Conversion, 204; 216; 566 Conversion constructor, 502 Conversion operator, 567; 583 Copy constructor, 505; 533 Copy control, 534 Copy-initialization, 71; 97 Cover, 630 C-style character string, 154 C-style string, 169 Curly brace, 24; 52 D Dangling else, 224; 250 Dangling pointer, 202; 214
876 Предметный указатель Data abstraction, 460; 501 Data member, 87; 99 Data structure, 52 Declaration, 75; 99 Declaration statement, 219; 250 Decrement, 556 Default argument, 278 Default constructor, 74; 99; 288; 307; 487; 503 Definition, 75; 99 Definition statement, 219 Deque, 334 Dereference, 553 Dereference operator, 121; 554 Derived class, 311; 329; 588; 654 Destination iterator, 424 Destructor, 505; 514; 533 Destructor order, 781 Dimension, 134; 169 Direct base class, 654 Direct-initialization, 71; 99 Discriminant, 825; 836 Dot operator, 47; 189 Dynamic binding, 589; 653 Dynamic type, 598; 653 Dynamically allocated object, 159; 168 E Ellipses parameter, 270 Encapsulation, 460; 502 End-of-file, 41; 50 Enumeration, 84; 99 Enumerator, 84; 99 Escape sequence, 62; 100 Exception, 24/; 720 class, 241; 249 handler, 249; 781 handling, 781 object, 721; 781 safe, 732; 782 specification, 739; 782 specifier, 243; 250; 724; 782 Explicit constructor, 503 Explicit conversion, 209; 216 Expression, 29; 50; 171,214 Expression statement, 218; 250 F File mode, 323; 330 File static, 751; 782 Flow of control, 217; 250 Format state, 839; 856 Forward declaration, 466; 503 Forward iterator, 444; 453 Free store, 159; 168 Freelist, 796; 837 Friend, 502 Friend mechanism, 493 Function, 24; 52; 251; 307 adaptor, 565; 583 body, 24; 252; 307 matching, 295; 306 name, 24; 50 object, 561; 583 overload resolution, 295 overloading, 291 pointer, 302 prototype, 277; 307 table, 815 template, 657; 716 try block, 729; 780 Function-call operator, 560 G Generator, 845 Generic algorithm, 417; 453 Generic handle class, 699; 715 Global namespace, 748; 780 Global scope, 76; 96 H Handle class, 630; 654 Handler, 24/; 720 Header, 29; 50; 96 Header file, 89 Header guard, 94; 96 Heap, 159; 169 High-order, 131 I IDE, 25 Identifier, 68; 96 Immediate base class, 596; 653 Implementation, 86; 99 Implementation inheritance, 603 Implicit conversion, 2/4 Implicit type conversion, 204 Inclusion compilation model, 715 Incomplete type, 466; 502 Increment, 121; 556 Index, 111; 130
Предметный указатель 877 Indirect base class, 596; 653 Infinite recursion, 276 Inheritance, 311; 329; 588 Inheritance hierarchy, 588; 653 Initialization, 31 Initialized variable, 97 Inline, 282 Inline function, 306 Input iterator, 443; 452 Input operator, 30 Input range, 421; 447 Input/Output, 27; 309 Insert iterator, 425; 432; 452 Inserter, 432 Instance, 667 Instantiate, 658 Instantiation, 667; 716 Integral literal constant, 84 Integral promotion, 205; 216 Integral type, 57; 100 Interface, 86; 97; 277 Invalidate, 343 Invalidated iterator, 381 10,27 lostream iterator, 432 Iterator, 101; 120; 381; 419 Iterator categories, 453 Iterator range, 341; 380 К Key, 383; 389 Key-value pair, 383 Keyword, 69 L Label, 250 Labeled statement, 239; 250 Library type, 50 Lifetime, 280; 307 Link, 97 Linkage directive, 832; 836 List, 334 Literal constant, 61; 98 Local class, 826; 837 Local scope, 76; 98 Local static object, 306 Local variable, 253; 306 Low-order, 131 Lvalue, 67; 96 L-значение, 67; 96 M Magic number, 98 Manipulator, 30; 50 Marker, 869 Member, 87 Member function, 46; 52; 87; 503 Member template, 692; 716 Memberwise assignment, 513; 534 Memberwise initialization, 509; 533 Memory leak, 162; 515 Method, 50 Modulus, 175 Multidimensioned array, 165 Multiple inheritance, 763; 780 Mutable data member, 471; 502 N Name, 96 Name lookup, 475; 502 Namespace, 30; 52; 744; 782 alias, 753; 782 pollution, 744; 780 scope, 77 Negator, 565; 583 Nested class, 817; 836 Nested namespace, 749 Nested type, 817; 836 Nonconst reference, 82; 98 Nonprintable character, 62; 98 Nonreference type, 256 Nontype parameter, 657; 715 Null character, 62 Null statement, 218; 250 О Object, 98 Object-oriented, 311 Object-oriented library, 329 Object-oriented programming, 588; 653 Off-the-end, 149 Off-the-end iterator, 131; 420', 452 OOP, 588 Operand, 29; 171; 214 Operator, 29; 171,214 Operator arrow, 555 Operator overloading, 216; 535 Option, 269 Order of evaluation, 216 Output iterator, 443; 452
878 Предметный указатель Output operator, 29 Overload resolution, 306 Overloaded, 291 function, 306 operator, 5 72; 533; 536 P Parameter, 251; 306 Parameter list, 24; 52; 254 Partial specialization, 710; 716 Placement new, 791; 837 Pointer, 138; 169 Pointer arithmetic, 147; 168 Pointer to const, 151 Pointer to member, 811; 838 Pointerlike, 530 Polymorphic type, 588 Polymorphism, 588; 654 Pool, 159 Portable, 828; 837 Porting, 828 Preallocate, 783 Precedence, 148; 169; 173; 216 Predicate, 429; 453 Prefix increment operator, 35 Preprocessor, 93; 99 directive, 29; 50 macro, 247; 249 Printable character, 62 Priority queue, 334 Private inheritance, 602; 653 Private member, 96; 502 Protected inheritance, 602; 653 Public inheritance, 602; 653 Public member, 99; 502 Pure virtual, 654 Pure virtual function, 627 Queue, 334 R Raise, 250; 781 Random-access iterator, 444; 453 Recursive function, 276; 307 Redeclaration, 292 Redefinition, 77 Refactoring, 614; 654 Reference, 81; 99; 145 Reference count, 526; 534 Reference parameter, 259 Relational operator, 349 Remainder, 175 Result, 171; 216 Rethrow, 781 Rethrowing, 727 Return type, 24; 50; 251; 307 Reverse iterator, 344; 432; 439; 453 Root, 588 RTTI, 803; 836 Rule of Three, 515; 534 Run time, 96 Run-time Type Identification, 803 Rvalue, 67; 96 R-значение, 67; 96 s Scope, 76; 98; 280; 621 Scope operator, 30; 459; 781 Separate compilation, 89; 99 Separate compilation model, 677; 716 Sequential container, 333; 381 Short-circuit evaluation, 177 Signed, 58; 96 Size, 359 Sliced, 654 Smart pointer, 522; 525; 533; 583 Source file, 26; 52 Stack, 334 Stack unwinding, 723; 782 Standard conversion, 216 Standard error, 28; 52 Standard input, 28; 52 Standard library, 27; 52 Standard output, 28; 52 Statement, 24; 51 Statement scope, 76 Static local object, 281 Static member, 496; 503 Static member function, 496 Static type, 598; 654 Statically typed, 66; 99 Stopping condition, 276 Stream, 27 Stream iterator, 434; 453 Strict weak ordering, 416 String literal, 31; 52 Subscript, 111 Symbol, 172 Synthesized assignment operator, 513; 534
Предметный указатель 879 Synthesized copy constructor, 509; 534 Synthesized default constructor, 290; 307; 488; 503 T Template, 655 Template argument, 658; 715 Template argument deduction, 669; 715 Template parameter, 657 Template parameter list, 657; 716 Template spacialization, 704 Template specialization, 716 Temporary, 306 Temporary object, 273; 306 Ternary operator, 172 Throwing, 721 Try block, 780 Type parameter, 657; 716 Type specifier, 70; 99; 134 Type-checking, 66; 98 Typedef, 83 Typing, 66 u Unary function object, 564; 583 Unary operator, 172; 216 Undefined behavior, 98 Uninitialized variable, 50; 98 Union, 823; 837 Unnamed namespace, 750; 781 Unsigned, 58; 96 Use count, 526; 534 Using declaration, 752; 781 Using directive, 753; 780 Value, 67; 389 Value initialized, 116; 130 Value semantics, 530; 534 Valuelike, 530 Valuelike behavior, 523 Variable, 30; 52; 66 Variable initialization, 97 Vector, 114,333 Viable function, 296; 306 Virtual, 589 base class, 773; 780 function, 653 inheritance, 773; 780 Volatile, 830 w Word, 57 Абстрактный базовый класс, 653 интерфейс, 640 класс, 628 тип данных, 101; 130; 457; 501 Абстракция данных, 460; 501 Автоматический объект, 280; 306 Адаптер, 334; 376; 380 back_inserter, 432; 452 bind 1st, 565 bind2nd, 565 front_inserter, 433; 452 inserter, 432; 433; 452 not1,565 not2,565 priority queue, 379; 380 queue, 379; 380 stack, 377; 380 Адаптер вставки, 432 Адаптер функции, 565; 583 Адрес, 57; 96 Алгоритм count, 842 find, 841 Анонимное объединение, 825; 836 Аргумент, 4 7; 50; 253; 306 Аргумент шаблона, 658; 715 Арифметические операции с указателями, 147; 168 Арифметический тип, 56; 57; 96 Арифметическое преобразование, 205; 214 Асинхронно-изменяемый объект, 830 Ассоциативный контейнер, 383; 416 Ассоциативный массив, 387; 416 Б Базовый класс, 311; 329; 588; 590; 653 Байт, 57; 96 Беззнаковый тип, 96 Библиотека iostream, 27 ostream, 29 стандартная, 52 Библиотечный тип, 50 Бинарный объект функции, 583
880 Предметный указатель Битовая схема, 126 Битовое поле, 828; 836 Блок, 50; 219; 249 catch, 721; 724; 780 try, 242; 249; 720; 780 try функции, 729; 780 Блок операторов, 24; 35 Буфер, 50 В Ввод и вывод, 27; 309 Ввод и вывод в файл, 319 Вектор, 114; 333; 381 Виртуальная функция, 589; 653 Виртуальное наследование, 773; 780 Виртуальный базовый класс, 773; 780 Вложенное пространство имен, 749 Вложенный класс, 817; 836 Вложенный тип, 817; 836 Возвращаемый тип, 24; 50; 251 Временный объект, 273; 306 Время выполнения, 96 Встраиваемая функция, 282; 306 Встроенный тип, 24; 50 Выражение, 29; 50; 171; 214 throw е, 780 составное, 193; 216 Вычисление по сокращенной схеме, 177 Г Генератор, 845 Глобальная область видимости, 76; 96 Глобальное пространство имен, 748; 780 Данные-члены, 87 Двунаправленный итератор, 444; 452 Двухсторонняя очередь, 334; 381 Дедукция аргумента шаблона, 669; 715 Деструктор, 505; 514; 533 Диапазон итераторов, 341; 380 Динамическая память, 159; 168 Динамически созданный объект, 168 Динамический тип, 598; 653 Динамическое связывание, 589; 653 Динамическое создание массивов, 159 Директива #define, 94 #endif, 94; 246 #ifndef, 94; 246 #include, 29; 93; 99; 839 else, 223 using, 753; 780 Директива компоновки, 832; 836 Директива препроцессора, 29; 50 Дискриминант, 825; 836 Дружественные отношения, 502 Е Емкость, 359 3 Заголовок, 29; 50; 96 algorithm, 354; 420; 421 cassert, 247 cctype, 112; 130 cmath, 862 cstddef, 128; 147 cstdlib, 142; 273; 782 cstring, 832 exception, 245 fstream, 311; 313; 319 functional, 563 iomanip, 862 iostream, 311; 313 iterator, 432 map, 387 memory, 733 new, 245 numeric, 421 set, 400 sstream, 311;313; 326 stdexcept, 242 typeinfo, 245 typeinfo, 805 utility, 384 защита, 96 исключения stdexcept, 245 Загромождение пространства имен, 744; 780 Закрытое наследование, 602; 653 Закрытый член, 96 Закрытый члены класса, 502 Защита заголовка, 94; 96 Защищенное наследование, 602; 653 Знаковый тип, 96 Значение, 389 badbit, 315 eofbit,3/5
Предметный указатель 881 failbit, 315 Значение параметра по умолчанию, 278 Значение переменной, 67 И Идентификатор, 68; 96 Идентификация типов времени выполнения, 803 Иерархия наследования, 588; 653 Изменяемая переменная-член, 471; 502 Имя, 96 Имя переменной, 68 Имя функции, 24; 50 Инвертор, 565; 583 Индекс, 111; 130 Инициализация, 31; 70 копии, 71; 97 переменной, 97 прямая, 71 Инициализированная переменная, 97 Инициализирующее значение, 116; 130 Инкапсуляция, 460; 502 Инклюзивная модель компиляции, 715 Интегрированная среда разработки, 25 Интеллектуальный указатель, 522; 525; 533; 583 Интерфейс, 86; 97; 277 Исключение, 241; 720 badalloc, 201 badcast, 805 Исходный диапазон, 421; 44 7 Итератор, 101; 120; 131; 339; 381; 419 istreamiterator, 434; 437; 452 ostreamiterator, 434; 436; 452 ввода, 443; 452 ввода-вывода, 432 вставки, 425; 432; 452 вывода, 443; 452 константный, 123 назначения, 424 недопустимый, 343 после конца, 131; 420; 452 прямого доступа, 444; 453 К Карта, 387; 416 Категории итераторов, 453 Класс, 42; 50; 85; 97; 502 allocator, 785; 837 auto_ptr, 733 bad alloc, 741 bitset, 125; 131 equalto, 564 exception, 780 fstream, 311; 319; 329 ifstream, 311; 319 iostream, 311 istream, 309; 311 istringstream, 311; 326 modulus, 564 ofstream, 311;319 ostream, 309; 311 ostringstream, 311; 326 overflow error, 780 plus, 563 range error, 780 runtime error, 780 string, 102; 104; 363 stringstream, 311; 326; 329 underflow error, 780 vector, 102 wfstream, 313 wifstream, 313 wiostream, 312 wistream, 312 wistringstream, 313 wofstream,313 wostream, 312 wostringstream, 313 wstringstream, 313 базовый косвенно, 596 непосредственно, 596 ввода-вывода, 309 имя, 87 исключения, 241; 249 badalloc, 245 bad_cast, 245 domainerror, 245 exception, 245 invalid_argument, 245 lengtherror, 245 logicerror, 245 out_of_range, 245 overflowerror, 245 rangeerror, 245 runtimeerror, 245 underflowerror, 245 объектно-ориентированный, 311 тело, 87 Ключ, 383; 389
882 Предметный указатель Ключевое слово, 69 catch, 241; 780 class, 87; 97; 461; 502; 605; 657; 663 const, 286; 459 delete, 162 enum, 84 explicit, 491 export, 677; 715 extern, 75; 99; 832 friend, 493 inline, 283 mutable, 471 namespace, 744; 753 operator, 536; 567 static, 497; 782 struct, 88; 97; 461; 502; 605 switch, 226 template, 657; 682 throw, 242; 739; 782 try, 241; 729; 780 typedef, 83; 97; 167 typename, 657; 663 union, 823 virtual, 591; 773; 774; 780 void, 254 volatile, 830 Комментарий, 32; 50 однострочный, 32 парный, 32 Компаратор, 636 Компоновка, 90; 97 Компоновщик, 565; 583 Конец файла, 41; 50 Конкатенация, 110 Конкретный класс, 461; 502 Константа EOF, 867 литеральная,61 Константная ссылка, 82; 97 Константная функция-член, 286; 306; 502 Константное выражение, 84; 92; 97; 134 Константный итератор, 123 Константный указатель, 152 Конструктор, 71; 88; 98; 288; 306; 459; 480 Конструктор копий, 506; 533 Конструктор преобразования, 502 Контейнер, 114; 131; 333; 381 deque, 381 list, 381 map, 383; 387; 416 multimap, 384; 403; 416 multiset, 384; 403; 416 set, 383; 399; 416 vector, 381 Контроль соответствия типов, 66; 98 Контроль типов, 66 Корневой класс, 588 Косвенный базовый класс, 653 Круглые скобки, 194 Л Литерал, 61 логический, 62 многострочный, 64 с плавающей запятой, 61 символьный, 62 строковый, 63 составной, 63 целочисленный, 61 Литеральная константа, 61; 98 Локальная область видимости, 76; 98 Локальная переменная, 253; 306 Локальный класс, 826; 837 Локальный статический объект, 306 м Магическое число, 98 Макрос assert, 247; 249 препроцессора, 249 Манипулятор, 30; 50 boolalpha, 858 dec, 858 endl, 318; 857 ends, 318 fixed, 862 flush, 318 hex, 858 internal, 864 left, 863 noboolalpha, 858 noshowpoint, 863 noskipws, 865 nounitbuf, 318 nouppercase, 860 oct, 858 right, 863 scientific, 862 setfill, 864 setprecision, 860 setw, 863
Предметный указатель 883 showbase, 859 showpoint, 863 skipws, 865 unitbuf, 373 uppercase, 860 Маркер, 869 Маркер доступа, 88; 98; 460; 502 private, 98 protected, 592; 653 public, 98 Массив, 98; 134 Метка, 250 case, 226; 249 default, 228; 249 Метка доступа private, 591 public, 591 Метод, 47; 50 Механизм дружественных отношений, 493 Младший бит, 131 Многомерный массив, 165 Множественное наследование, 763; 780 Модель компиляции инклюзивная, 676; 715 раздельная, 677,716 н Набор, 399; 416 Набор битов, 125; 131 Наилучшее соответствие, 295; 306 Наследование, 311; 329; 588 закрытое, 602 защищенное, 602 открытое, 602 реализации, 603 Незавершенный тип, 466; 502 Неименованное пространство имен, 750; 781 Неинициализированная переменная, 50; 98 Неконстантная ссылка, 82; 98 Некорректный итератор, 381 Неоднозначное обращение, 306 Неопределенное поведение, 98 Непечатаемый символ, 62; 98 Непосредственный базовый класс, 653 Нессылочный параметр, 256 Неявное преобразование, 214 Неявное преобразование типов, 204 Нулевой оператор, 218 Нулевой символ, 62 Область видимости, 30; 76; 98; 280; 621 блока, 98 глобальная, 76; 96; 98 класса, 77; 98; 502 локальная, 76; 98 операторная, 76; 98 пространства имен, 77; 98 Оболочка класса, 630 Обработка исключений, 781 Обработчик, 241; 720 для всех исключений, 728; 781 исключения, 249; 781 Общий алгоритм, 417; 453 Общий управляющий класс, 699; 715 Объединение, 823; 837 Объект, 68; 98 сегг, 28; 50; 310 cin, 28; 51; 309 clog, 28; 51 cout, 28; 51; 310 wcerr, 313 wcin, 313 wcout, 313 автоматический, 306 инициализация, 71 исключения, 721; 781 Объект функции, 561; 583 бинарной, 564 унарной,564 Объектно-ориентированная библиотека, 329 Объектно-ориентированное программирование, 588; 653 Объектно-ориентированный класс, 311 Объектный файл, 90 Объявление, 75; 99 using, 102; 131; 604; 625; 752; 781 Объявление класса, 502 Операнд, 29; 171; 214 Оператор, 24; 29; 51; 171; 214 !, 177 \-,51 %, 175 &, 139; 169; 180; 214 &&, 176 0,47; 51 *, 121; 131; 144; 169; 416 „ 192 ; 215 47; 51 *,814 Л 175
884 Предметный указатель ::,ЗО,51; 102; 131; 781 ?:,215 [], 111; 119; 131; 169; 416 Л, 180, 214 |, 181; 214 ||, 176 ~, 179; 214 +, 110, 174 ++, 35; 51; 121; 131; 169; 187; 215; 556 +=,35; 51; 110 <, 51; 109 «, 29; 51; 129; 131; 180, 215; 310 <=, 35; 51; 109 =, 51; 109 ==, 51; 109 ->, 189 >, 51; 109 ->*,814 >=, 51; 109 »,ЗО,51; 131; 180; 215; 310 break, 226; 237; 249 case, 226 const_cast, 209 continue, 239; 249 delete, 159; 169; 199; 201; 215; 784; 790, 837 delete[], 161; 837 dynamic cast, 209; 803; 837 else, 40 for, 36; 51 goto, 239; 249 if, 39; 51; 221; 249 if...else, 223; 249 new, 159; 169; 199; 215; 783; 790, 837 new[], 837 reinterpret_cast, 209 return, 25; 253; 271 sizeof, 191 static_cast, 209 Switch, 225; 249 throw, 242; 249-, 727; 780 typeid, 803; 806; 837 while, 34; 52 vboj&,30, 131 вывода, 29; 129; 131 вызова функции, 560 выражения, 218; 250 вычитания парный, 174 унарный, 174 декремент, 187; 556 постфиксный, 187,215 префиксный, 187,215 деления, 174; 175 деления по модулю, 175 запятая, 192,215 индексирования, 111; 119; 128; 131; 169,416 ъюф&лет, 121; 131; 169; 187; 556 постфиксный, 187,215 префиксный, 35; 187; 215 логический AND, 176 NOT, 177 меньше или равно, 35 нулевой, 218 области видимости, 30,102; 131,459; 781 обращения, 47; 252; 306; 560 к адресу, 139; 169 к значению, 121; 131; 144; 169; 416; 553; 554 объявления, 219; 250 определения, 219 отношения, 177 парный, 172; 215 вычитания, 174 суммы, 174 побитовый, 130,179 AND, 180,214 NOT, 179; 214 OR, 181,214 XOR, 180 исключающее или, 214 сдвига, 180 помеченный, 239; 250 преобразования, 567; 583 приведения constcast, 210,215 dynamic_cast, 210,215 reinterpret_cast, 211,215 static_cast, 210,215 именованный, 210 присвоения, 109; 110,184; 506; 512; 533 составной, 186 присвоения с суммой, 35 пустой, 218; 250 равенства, 109 сдвига влево, 215 сдвига вправо, 215 составной, 250 присвоения, 186 сравнения, 349 сравнения строк, 109 стрелка, 189; 553; 555
Предметный указатель 885 суммы, 174 парный, 174 унарный, 174 точечный, 4 7; 189 тройственный, 172 умножения, 174 унарный, 172,216 вычитания, 174 суммы, 174 управления потоком выполнения, 217 условный, 190,215 цикла do...while, 236 for, 233 while, 231 Операторная бласть видимости, 76 Определение, 75; 99 Определение имен типов, 83 Опция, 269 Открытое наследование, 602; 653 Открытый член, 99 Открытый член класса, 502 Очередь, 334; 379; 380 Ошибка не совпадения типа, 38 неоднозначности, 296 объявления, 38 синтаксическая, 38 п Пара, 384; 416 Пара ключ-значение, 383 Параметр, 251; 306 argc, 270 argv, 270 rhs, 285 this, 286; 468 значения, 657; 715 командной строки, 269 константный, 257 многоточие, 270 нессылочный, 256 нетипа, 657 ссылочный, 259 типа, 657; 716 указатель, 256 функции, 253 шаблона, 657 Парный оператор, 172; 215 Перегруженная функция, 291; 306 Перегруженный оператор, 512; 533; 536 Перегрузка оператора, 216; 535 Перегрузка функции, 291; 292 Передача, 250; 781 Передача исключения, 721; 780 Переменная, 30; 52; 66 внешняя, 75 инициализация, 97 константная, 79 неинициализированная, 30; 73 объявление, 75 определение, 75 препроцессора NDEBUG, 246 Переменная-член, 87; 99 Перенос, 828 Переносимая программа, 828 Переносимость, 837 Переобъявление функции, 292 Переопределение, 77 Перечисление, 84; 99 Перечислитель, 84; 99 Печатаемый символ, 62 Побитовый оператор, 130; 179 Повторная передача исключения, 727; 781 Подбор функции, 295; 306 Поддержка целочисленных действий, 205 Подходящая функция, 306 Поиск имени, 475; 502 Поиск перегруженной функции, 295; 306 Полиморфизм, 588; 654 Полиморфный тип, 588 Помеченный оператор, 239; 250 Порядок, 194,216 Порядок выполнения деструкторов, 781 Порядок выполнения конструкторов, 781 Порядок вычисления, 216 После конца, 149 Последовательный контейнер, 333; 381 Потерянный оператор else, 250 Потерянный указатель, 202; 214 Поток, 27 Потоковый итератор, 434; 453 Почленная инициализация, 509; 533 Почленное присвоение, 513; 534 Правило трех, 515; 534 Предварительное объявление, 466; 503 Предварительное резервирование, 783 Предикат, 429; 453 Преобразование, 216; 566 арифметическое, 205; 214 неявное, 214
886 Предметный указатель стандартное, 216 типа, 204 класса, 583 неявное, 204 целочисленное, 216 явное, 209; 216 Препроцессор, 93; 99 Префиксный оператор инкремента, 35 Приведение, 577 Приведение типов, 209; 216 Приоритет, 148; 169; 173,216 Приоритет операторов, 195 Приоритетная очередь, 334; 379; 380 Приращение, 35 Продолжительность существования, 280; 307 Производный класс, 311; 329; 588; 654 Прокрутка стека, 723; 782 Пространство имен, 30; 52; 744; 782 std, 30; 52 Прототип функции, 277; 284; 307 Прямая инициализация, 71; 99 Прямой итератор, 444; 453 Псевдоним пространства имен, 753; 782 Пул, 159 Пустой оператор, 218; 250 Р Раздел catch, 241; 250 private, 88 public, 88 Раздельная компиляция, 89; 99 Раздельная модель компиляции, 716 Размер, 169; 359 Размер типа, 56 Размерность, 134 Размещающий оператор new, 791,837 Распределяемая память, 159; 169 Расширение компилятора, 169 Реализация, 86; 99 Реверсивный итератор, 344; 432; 439; 453 Режим арр, 324 ate, 324 binary, 324 in, 324 out, 324 trunc, 324 файла, 323; 330 Результат, 171,216 Рекурсивная функция, 276; 307 Рефакторинг, 614; 654 С Семантика значения, 530; 534 Символ, 172 непечатаемый, 62; 113 печатаемый, 62; 113 пунктуации,113 Символьная строка в стиле С, 154 Синтаксическая ошибка, 38 Синтезируемый конструктор копий, 509; 534 оператор присвоения, 513; 534 стандартный конструктор, 290; 307; 503 Создание экземпляра, 658; 667; 716 Составное выражение, 193,216 Составной оператор, 219; 250 присвоения, 35 Составной тип, 81; 99; 169 Специализация шаблона, 704; 716 Спецификатор const, 78; 79; 151 volatile, 837 исключения, 243; 250; 724; 782 типа, 70; 99; 134 Спецификация исключений, 739; 782 Список, 334; 381 инициализации конструктора, 503 инициализирующих значений, 289; 307 наследования класса, 594; 654 параметров, 24; 52; 254 параметров шаблона, 657; 716 свободных блоков, 796; 837 Ссылка, 81; 99; 145 Ссылочный параметр, 259 Стандартная библиотека, 27; 52 Стандартная ошибка, 28; 52 Стандартное преобразование, 216 Стандартный ввод, 28; 52 вывод, 28; 52 конструктор, 74; 99; 288; 307; 487; 503 синтезируемый, 488 Старший бит, 131 Статическая переменная-член, 496 Статическая типизация, 99 Статическая функция-член, 496 Статический локальный объект, 281 Статический тип, 598; 654 Статический файловый объект, 751; 782
Предметный указатель 887 Статический член класса, 503 Стек, 334; 377; 380 Строгое сравнение, 416 Строка в стиле С, 169 Строковый литерал, 31; 52; 63 Структура данных, 42; 52 Счетчик пользователей, 526; 534 Счетчик ссылок, 526; 534 т Таблица функций, 815 Тело функции, 24; 52; 252; 307 Тип, 43 bool, 57; 96 char, 57; 58; 96 const void *, 169 const void, 151 constiterator, 123; 344 constreference, 344 const_reverse_iterator, 344 container_type, 377 difference_type, 131,344 double, 60; 96 float, 60; 96 fmtflags, 857 int, 24; 30; 57; 96 iostate, 315 iostream, 52 istream, 27; 52 iterator, 120; 131; 344 key_type, 389; 416 long, 57; 60; 96 long double, 60; 96 mapped_type, 389; 416 npos, 371 offtype, 871 ostream, 27; 28; 52 pair, 384; 416 pos_type, 871 ptrdiff_t, 147; 169 reference, 344 reverse iterator, 344; 432 short, 57; 59; 96 signed, 58; 96 signed char, 58 sizet, 128; 132; 169 size_type, 108; 132; 344; 377 string, 104 type_info, 838 unsigned, 58; 96 unsigned char, 58 unsigned int, 58 unsigned long, 58 value_type, 344; 377; 416 vector, 114 void, 56; 99; 143; 253 void *,169 wchar_t, 57; 96; 312 word, 100 абстрактный, 101 арифметический, 56; 57; 96 беззнаковый, 58; 96 знаковый, 58; 96 размер, 56 с плавающей запятой, 60 спецификатор, 70 целочисленный, 57 Тип возвращаемого значения, 307 Тип данных, 55 Тип класса, 52 Типизация, 66 Точечный оператор, 47; 189 Тройственный оператор, 172 Указатель, 138; 169 this, 307 на константу, 151 на функцию, 302 на член класса, 811; 838 Унарный объект функции, 583 Унарный оператор, 172; 216 Управление копированием, 506; 534 Управление потоком, 250 Управляющая последовательность, 62; 100 Управляющий класс, 630; 654 Уровень доступа, 88 Усечение, 654 Условие, 35; 52; 216 Условие выхода, 276 Условный оператор, 190; 215 Устойчивость к исключению, 732; 782 Утечка памяти, 162; 203; 515 ф Файл заголовка, 89 Файл исходного кода, 26; 52 Фигурная скобка, 24; 52 Флаг состояния, 314; 330 Флаг формата, 839; 856 Функция, 24; 52; 251; 307
888 Предметный указатель abort(), 724; 782 accumulate(), 421 any(), 128 append(), 370 assign(), 356 at(), 353 back(), 352 back_inserter(), 425; 432 ЪаЦ),315 base 0,447 begin(), 121; 345; 381; 396 c_str(), 164,320 capacity (), 359 clear(), 315; 316; 322; 355; 874 close(), 322; 329 compare(), 374 construct(), 785; 787 count(), 128; 395 count_if(), 430 deletef], 795 destroy(), 785; 789 empty(), 108; 132; 351 end(), 121; 345; 381; 396 eof(), 315 equal_range(), 406 erase(), 343; 354; 396; 429 fail(), 375 fill(), 424 fill_n(), 424 ШО, 355; 371; 395; 419 find_first_not_of(), 373 find_first_of(), 422 find_if(), 431 find_last_not_of(), 374 find_last_of(), 374 flags(), 857 flip(), 129 front(), 352 gcount(), 868; 869 get(), 866 getline(), 106; 132; 310; 868 good(),576 ignore(), 868 insert(), 347; 392; 401 isalnum(), 112 isalpha(), 112 iscntrl(), 112 isdigit(), 112 isgraph(), 112 islower(), 112 isprint(), 112 ispunct(), 112 isspace(), 112 isupper(), 112 isxdigit(), 112 lower_bound(), 404 main(), 24; 52; 269 make_pair(), 385 Toax._sae(),351 name(), 810 new[], 795 none(), 128 open(), 320; 329 operator delete(), 790; 838 operator new(), 790; 838 peek(), 866 pop_back(), 353 pop_front(), 353; 354 precision(), 860 push_backO, 117; 132,345; 787 push_front(), 346 put(), 866 putback(), 866 rbegin(), 345; 432 rdstate(), 316 read(), 868 reallocate(), 788 rend(), 345; 432 replace(), 370 reserve(), 359 reset(), 128; 129; 738 resize(), 351 reverse(), 449 reverse copy(), 449 rfmd(), 373 seek(), 870 seekg(), 871 seekp(), 871 set(), 128; 129 setstate(), 315; 316 size(), 107; 128; 132; 351 sizeof(), 191 sort(), 428 sqrt(), 862 stable_sort(), 430 str(), 327 strcat(), 156 strcmp(), 156 strcpy(), 156 strlen(), 156 strm(), 327 strn(), 157
Предметный указатель 889 stmcat(), 156 stmcpy(), 156 substr(), 369 tell(), 870 tellg(), 871 tellp(), 871 terminate(), 244; 250; 724; 782 test(), 128; 129 tie(), 319 to_ulong(), 129 tolower(), 112 toupper(), 112 unexpected(), 740; 782 unget(), 866 unique(), 428 unique_copy(), 434 unsetf(), 862 upper_bound(), 404 what(), 244 write(), 868 встраиваемая, 282; 306 имя, 50 перегруженная, 291; 306 перегрузка, 292 переобъявление, 292 подходящая, 296; 306 прототип, 284 рекурсивная, 276; 307 тело, 52 Функция-кандидат, 296; 307 Функция-член, 46; 52; 87; 503 ц Целочисленное преобразование, 216 Целочисленный константный литерал, 84 Целочисленный тип, 57; 100 Цикл while, 250 ч Частичная специализация, 710; 716 Чистая виртуальная функция, 627; 654 Член класса, 87; 100 ш Шаблон, 655 Шаблон auto_ptr, 782 Шаблон класса, 114; 132; 659; 716 Шаблон функции, 657; 716 Шаблон-член, 692 э Экземпляр, 667 Я Явное преобразование, 209; 216 Явный конструктор, 503
Научно-популярное издание Стенли Б. Липпман, Барбара Э. Му, Жози Лажойе Язык программирования C++. Вводный курс 4-е издание Литературный редактор С. Г. Татаренко Верстка М.А. Удалов Художественный редактор В. Г. Павлютин Корректоры Л.А. Гордиенко, Т.А. Корзун, О. В. Мишу тина, Л. В. Чернокозинская Издательский дом “Вильямс” 127055, г. Москва, ул. Лесная, д. 43, стр. 1 Подписано в печать 10.10.2006. Формат 70x100/16. Гарнитура Times. Печать офсетная. Усл. печ. л. 72,24. Уч.-изд. л. 52,3. Тираж 3000 экз. Заказ № 3020. Отпечатано по технологии CtP в ОАО “Печатный двор” им. А. М. Горького 197110, Санкт-Петербург, Чкаловский пр., 15.
“Книга Язык программирования C++. Вводный курс — это весьма популярный учебник для начинающих программистов на языке C++. Он также полезен для разработчиков любой квалификации. Нынешнее, четвертое, издание не только сохранило установившуюся традицию, но и претерпело существенные улучшения". Стив Виноски, главный инженер по новейшим технологиям компании IONA Technologies. "Вводный курс на самом деле помогает быстро освоить такой большой и сложный язык, как C++”. Джастин Шоу, глава технического совета по электронике и программному обеспечению корпорации Aerospace Corporation. “Эта книга не только содержит советы новичкам, но и позволяет получить хорошую практику программирования”. Невин “:-)”Либер, ведущий инженер (разработчик C++ начиная с 1988 года). Нынешнее издание столь популярного вводного курса стандартного языка C++ было полностью переделано, реорганизовано и переписано так, чтобы помочь быстрее и эффективнее научиться программировать на этом языке. По мере развития языка C++ автор старается вносить в последующие издания соответствующие изменения. Теперь стандартная библиотека C++ описана с самого начала, что позволяет читателю сразу приступить к созданию работоспособных программ, еще до изучения подробностей языка. Здесь содержатся полезные советы, которые помогут облегчить создание программ, а также повысить их эффективность. Примеры, в которых используются возможности библиотек, позволяют продемонстрировать достоинства языка C++, а также наиболее эффективные приемы его применения. Как и в предыдущих изданиях, здесь обсуждаются фундаментальные концепции и методы языка C++, что делает книгу ценнейшим ресурсом даже для опытных программистов. Это обновленное классическое издание позволит научиться программировать быстрее и эффективнее! Изменение структуры позволяет быстрее изучить применение стандартной библиотеки C++. Переделано описание наиболее современных стилей и способов программирования. Задействованы новые методики обучения, позволяющие подчеркнуть важные моменты, предупредить о наиболее распространенных ошибках, выработать правильный стиль программирования и предоставить весьма полезные советы. Дополнены упражнения, позволяющие закрепить полученные знания. Исчерпывающее описание тем. Исходный код некоторых примеров книги доступен на Web-сайте, адрес которого приведен ниже. Все авторы книги являются признанными специалистами в области программирования на языке C++. Стенли Б. Липпман, являющийся в настоящее время архитектором группы Visual C++ корпорации Microsoft, начал работать на языке C++ с момента его создания Бьёрном Страуструпом в лаборатории Bell Labs в 1984 году. Он занимался анимацией в кинокомпаниях Disney и DreamWorks, а также работал старшим консультантом в лаборатории Jet Propulsion Laboratories. Липпман является также автором книги Inside the C++ Object Model (издательство Addison-Wesley, 1996 год). Жози Лажойе, бывший член канадской группы разработчиков компилятора C/C++ корпорации IBM, уже семь лет работает в комитете стандартов языка C++ в составе международной организации по стандартизации ISO. Его перу принадлежат комментарии к отчету C++ Report. Барбара Му — независимый консультант, обладающий двадцатипятилетним опытом в области программного обеспечения. Вместе со Страуструпом и Липпманом она работала в компании AT&T над реализацией сложнейших проектов на языке C++. В соавторстве с Эндрю Коэном Барбара выпустила книги Accelerated C++ (издательство Addison-Wesley, 2000 год) и Ruminations on C++ (1997 год). www.awprofessional.com/cpp_primer Дизайн обложки Шуги Прасерсис (Chuti Prasertsith) Автор фотографии на обложке Стив Коул (Steve Cole)/Photodisc/Getty Images, Inc. ISBN 5-8459-1121-4 Категория: программирование Содержание: C++ ВИЛЬЯМС К4 Издательский дом “Вильямс” www.williamspublishing.com Addison-Wesley Pearson Education 9