Язык программирования С++: базовый курс [5 ed.] 9785845918390

Книга "Язык программирования C++. Базовый курс" — новое издание популярного и исчерпывающего бестселлера по яз

1,318 125 18MB

Russian Pages 1118 Year 2014

Report DMCA / Copyright

DOWNLOAD FILE

Polecaj historie

Язык программирования С++: базовый курс [5 ed.]
 9785845918390

Table of contents :
Содержание
Введение
Для кого написана эта книга
Изменения в пятом издании
Структура книги
Соглашения, принятые в книге
Примечание о компиляторах
Благодарности
От издательства
Глава 1. Первые шаги
1.1. Создание простой программы на языке С++
1.1.1. Компиляция и запуск программы
1.2. Первый взгляд на ввод-вывод
1.3. Несколько слов о комментариях
1.4. Средства управления
1.4.1. Оператор while
1.4.2. Оператор for
1.4.3. Ввод неизвестного количества данных
1.4.4. Оператор if
1.5. Введение в классы
1.5.1. Класс Sales_item
1.5.2. Первый взгляд на функции-члены
1.6. Программа для книжного магазина
Резюме
Термины
Часть I. Основы
Глава 2. Переменные и базовые типы
2.1. Простые встроенные типы
2.1.1. Арифметические типы
2.1.2. Преобразование типов
2.1.3. Литералы
2.2. Переменные
2.2.1. Опредедения переменных
2.2.2. Объявдения и определения переменных
2.2.3. Идентификаторы
2.2.4. Обдасть видимости имен
2.3. Составные типы
2.3.1. Ссылки
2.3.2. Указатели
2.3.3. Понятие описаний составных типов
2.4. Спецификатор const
2.4.1. Ссылка на константу
2.4.2. Указатели и спецификатор const
2.4.3. Спецификатор const верхнего уровня
2.4.4. Переменные constexpr и константные выражения
2.5. Работа с типами
2.5.1. Псевдонимы типов
2.5.2. Спецификатор типа auto
2.5.3. Спецификатор типа decltype
2.6. Определение собственных структур данных
2.6.1. Определение типа Sales_data
2.6.2. Использование класса Sales_data
2.6.3. Создание собственных файлов заголовка
Резюме
Термины
Глава 3. Типы string, vector и массивы
3.1. Пространства имен и объявления using
3.2. Библиотечный тип string
3.2.1. Определение и инициализация строк
3.2.2. Операции со строками
3.2.3. Работа с символами строки
3.3. Библиотечный тип vector
3.3.1. Определение и инициализация векторов
3.3.2. Добавление элементов в вектор
3.3.3. Другие операции с векторами
3.4. Знакомство с итераторами
3.4.1. Использование итераторов
3.4.2. Арифметические действия с итераторами
3.5. Массивы
3.5.1. Определение и инициализация встроенных массивов
3.5.2. Доступ к элементам массива
3.5.3. Указатели и массивы
3.5.4. Символьные строки в стиле C
3.5.5. Взаимодействие с устаревшим кодом
Резюме
Термины
Глава 4. Выражения
4.1. Основы
4.1.1. Фундаментальные концепции
4.1.2. Приоритет и порядок
4.1.3. Порядок вычисления
4.2. Арифметические операторы
4.3. Логические операторы и операторы отношения
4.4. Операторы присвоения
4.5. Операторы инкремента и декремента
4.6. Операторы доступа к членам
4.7. Условный оператор
4.8. Побитовые операторы
4.9. Оператор sizeof
4.10. Оператор запятая
4.11. Преобразование типов
4.11.1. Арифметические преобразования
4.11.2. Другие неявные преобразования
4.11.3. Явные преобразования
4.12. Таблица приоритетов операторов
Резюме
Термины
Глава 5. Операторы
5.1. Простые операторы
5.2. Операторная область видимости
5.3. Условные операторы
5.3.2. Оператор switch
5.4. Итерационные операторы
5.4.1. Оператор while
5.4.2. Традиционный оператор for
5.4.3. Серийный оператор for
5.4.4. Оператор do while
5.5. Операторы перехода
5.5.1. Оператор break
5.5.2. Оператор continue
5.5.3. Оператор goto
5.6. Блоки try и обработка исключений
5.6.1. Оператор throw
5.6.2. Блок try
5.6.3. Стандартные исключения
Резюме
Термины
Глава 6. Функции
6.1. Основы функций
6.1.1. Локальные объекты
6.1.2. Объявление функций
6.1.3. Раздельная компиляция
6.2. Передача аргументов
6.2.1. Передача аргумента по значению
6.2.2. Передача аргумента по ссылке
6.2.3. Константные параметры и аргументы
6.2.4. Параметры в виде массива
6.2.5. Функция main(): обработка параметров командной строки
6.2.6. Функции с переменным количеством параметров
6.3. Типы возвращаемого значения и оператор return
6.3.1. Функции без возвращаемого значения
6.3.2. Функции, возвращающие значение
6.3.3. Возвращение указателя на массив
6.4. Перегруженные функции
6.4.1. Перегрузка и область видимости
6.5. Специальные средства
6.5.1. Аргументы по умолчанию
6.5.2. Встраиваемые функции и функции constexpr
6.5.3. Помощь в отладке
6.6. Подбор функции
6.6.1. Преобразование типов аргументов
6.7. Указатели на функции
Резюме
Термины
Глава 7. Классы
7 .1. Определение абстрактных типов данных
7.1.1. Разработка класса Sales_data
7.1.2. Определение пересмотренного класса Sales_data
7.1.3. Определение функций, не являющихся членом класса, но связанных с ним
7.1.4. Конструкторы
7.1.5. Копирование, присвоение и удаление
7.2. Управление доступом и инкапсуляция
7.2.1. Друзья
7.3. Дополнительные средства класса
7.3.1. Снова о членах класса
7.3.2. Функции, возвращающие указатель *this
7.3.3. Типы классов
7.3.4. Снова о дружественных отношениях
7.4. Область видимости класса
7.4.1. Поиск имен в области видимости класса
7.5. Снова о конструкторах
7.5.1. Список инициализации конструктора
7.5.2. Делегирующий конструктор
7.5.3. Роль стандартного конструктора
7.5.4. Неявное преобразование типов класса
7.5.5. Агрегатные классы
7.5.6. Литеральные классы
7.6. Статические члены класса
Резюме
Термины
Часть II. Библиотека С++
Глава 8. Библиотека ввода и вывода
8.1. Классы ввода-вывода
8.1.1. Объекты ввoдa-выoдa не допускают копирования и присвоения
8.1.2. Флаrи состояния
8.1.3. Управление буфером вывода
8.2. Ввод и вывод в файл
8.2.1. Использование объектов файловыx потоков
8.2.2. Режимы файла
8.3. Строковые потоки
8.3.1. Использование класса istringstream
8.3.2. Использование класса ostringstream
Резюме
Термины
Глава 9. Последовательные контейнеры
9.1. Обзор последовательных контейнеров
9.2. Обзор библиотечных контейнеров
9.2.1. Итераторы
9.2.2. Типы-члены классов контейнеров
9.2.3. Функции-члены begin() и end()
9.2.4. Определение и инициализация контейнера
9.2.5. Присвоение и функция swap()
9.2.6. Операции с размером контейнера
9.2.7. Операторы сравнения
9.3. Операции с последовательными контейнерами
9.3.1. Добавление элементов в последовательный контейнер
9.3.2. Доступ к элементам
9.3.3. Удаление элементов
9.3.4. Специализированные функции контейнера forward_list
9.3.5. Изменение размеров контейнера
9.3.6. Некоторые операции с контейнерами делают итераторы недопустимыми
9.4. Как увеличивается размер вектора
9.5. Дополнительные операции со строками
9.5.1. Дополнительные способы создания строк
9.5.2. Другие способы изменения строки
9.5.3. Операции поиска строк
9.5.4. Сравнение строк
9.5.5. Числовые преобразования
9.6. Адаптеры контейнеров
Резюме
Термины
Глава 10. Обобщенные алгоритмы
10.1. Краткий обзор
10.2. Первый взгляд на алгоритмы
10.2.1. Алгоритмы только для чтения
10.2.2. Алгоритмы, записывающие элементы контейнера
10.2.3. Алгоритмы, переупорядочивающие элементы контейнера
10.3. Перенастройка функций
10.3.1. Передача функций алгоритму
10.3.2. Лямбда-выражения
10.3.3. Захват и возвращение значений лямбда-выражениями
10.3.4. Привязка аргументов
10.4. Возвращаясь к итераторам
10.4.1. Итераторы вставки
10.4.2. Потоковые итераторы
10.4.3. Реверсивные итераторы
10.5. Структура обобщенных алгоритмов
10.5.1. Пять категорий итераторов
10.5.2. Параметрическая схема алгоритмов
10.5.3. Соглашения об именовании алгоритмов
10.6. Алгоритмы, специфические для контейнеров
Резюме
Термины
Глава 11. Ассоциативные контейнеры
11.1. Использование ассоциативных контейнеров
11.2. Обзор ассоциативных контейнеров
11.2.1. Определение ассоциативного контейнера
11.2.2. Требования к типу ключа
11.2.3. Тип pair
11.3. Работа с ассоциативными контейнерами
11.3.1. Итераторы ассоциативных контейнеров
11.3.2. Добавление элементов
11.3.3. Удаление элементов
11.3.4. Индексация карт
11.3.5. Доступ к элементам
11.3.6. Карта преобразования слов
11.4. Неупорядоченные контейнеры
Использование неупорядоченного контейнера
Управление ячейками
Требования к типу ключа неупорядоченных контейнеров
Резюме
Термины
Глава 12. Динамичская память
12.1. Динамическая память и интеллектуальные указатели
12.1.1. Класс shared_ptr
12.1.2. Непосредственное управление памятью
12.1.3. Использование указателя shared_ptr с оператором new
12.1.4. Интеллектуальные указатели и исключения
12.1.5. Класс unique_ptr
12.1.6. Класс weak_ptr
12.2. Динамические массивы
12.2.1. Оператор new и массивы
12.2.2. Класс allocator
12.3. Использование библиотеки: программа запроса текста
12.3.1. Проект программы
12.3.2. Определение классов программы запросов
Резюме
Термины
Часть III. Инструменты для разработчиков классов
Глава 13. Управление копированием
13.1. Копирование, присвоение и удаление
13.1.1. Конструктор копий
13.1.2. Оператор присвоения копии
13.1.3. Деструктор
13.1.4. Правило три/пять
13.1.5. Использование спецификатора = defаult
13.1.6. Предотвращение копирования
13.2. Управление копированием и ресурсами
13.2.1. Классы, действующие как значения
13.2.2. Определение классов, действующих как указатели
13.3. Функция swap()
13.4. Пример управления копированием
13.5. Классы, управляющие динамической памятью
13.6. Перемещение объектов
13.6.1. Ссылки на r-значение
13.6.2. Конструктор перемещения и присваивание при перемещении
13.6.3. Ссылки на r-значение и функции-члены
Резюме
Термины
Глава 14. Перегрузка операторов и преобразований
14.1. Фундаментальные концепции
14.2. Операторы ввода и вывода
14.2.1. Перегрузка оператора вывода
14.3. Арифметические операторы и операторы отношения
14.3.1. Операторы равенства
14.3.2. Операторы отношения
14.4. Операторы присвоения
14.5. Оператор индексирования
14.6. Операторы инкремента и декремента
14.7. Операторы доступа к членам
14.8. Оператор вызова функции
14.8.1. Лямбда-выражения - объекты функции
14.8.2. Библиотечные объекты функций
14.8.3. Вызываемые объекты и тип function
14.9. Перегрузка, преобразование и операторы
14.9.1. Операторы преобразования
14.9.2. Избегайте неоднозначных преобразований
14.9.3. Подбор функций и перегруженные операторы
Резюме
Термины
Глава 15. Объектно-ориентированное программирование
15.1. Краткий обзор ООП
15.2. Определение базовых и производных классов
15.2.1. Определение базового класса
15.2.2. Определение производного класса
15.2.3. Преобразования и наследование
15.3. Виртуальные функции
15.4. Абстрактные базовые классы
15.5. Управление доступом и наследование
15.6. Область видимости класса при наследовании
15.7. Конструкторы и функции управления копированием
15.7.1. Виртуальные деструкторы
15.7.2. Синтезируемые функции управления копированием и наследование
15.7.3. Функции-члены управления копированием производного класса
15.7.4. Унаследованные конструкторы
15.8. Контейнеры и наследование
15.8.1. Разработка класса Basket
15.9. Возвращаясь к запросам текста
15.9.1. Объектно-ориентированное решение
15.9.2. Классы Query_base и Query
15.9.3. Производные классы
15.9.4. Виртуальные функции eval()
Резюме
Термины
Глава 16. Шаблоны и обобщенное программирование
16.1. Определение шаблона
16.1.1. Шаблоны функций
16.1.2. Шаблоны класса
16.1.3. Параметры шаблона
16.1.4. Шаблоны-члены
16.1.5. Контроль создания экземпляра
16.1.6. Эффективность и гибкость
16.2. Дедукция аргумента шаблона
16.2.1. Преобразования и параметры типа шаблона
16.2.2. Явные аргументы шаблона функции
16.2.3. Замыкающие типы возвращаемого значения и трансформация типа
16.2.4. Указатели на функцию и дедукция аргумента
16.2.5. Дедукция аргумента шаблона и ссылки
16.2.6. Функция std::move()
16.2.7. Перенаправление
16.3. Перегрузка и шаблоны
16.4. Шаблоны с переменным количеством аргументов
16.4.1. Шаблоны функции с переменным количеством аргументов
16.4.2. Развертывание пакета
16.4.3. Перенаправление пакетов параметров
16.5. Специализация шаблона
Резюме
Термины
Часть IV. Дополнительные темы
Глава 17. Специализированные средства библиотек
17.1. Тип tuple
17.1.1. Определение и инициализация кортежей
17.1.2. Использование кортежей для возвращения нескольких значений
17.2. Тип bitset
17.2.1. Определение и инициализация наборов битов
17.2.2. Операции с наборами битов
17.3. Регулярные выражения
17.3.1. Использование библиотеки регулярных выражений
17.3.2. Типы итераторов классов соответствия и regex
17.3.3. Использование подвыражений
17.3.4. Использование функции regex_replace()
17.4. Случайные числа
17.4.1. Процессоры случайных чисел и распределения
17.4.2. Другие виды распределений
17.5. Еще о библиотеке ввода и вывода
17.5.1. Форматированный ввод и вывод
17.5.2. Не форматированные операции ввода-вывода
17.5.3. Произвольный доступ к потоку
Резюме
Термины
Глава 18. Инструменты для крупномасштабных программ
18.1. Обработка исключений
18.1.1. Передача исключений
18.1.2. Обработка исключения
18.1.3. Блок try функции и конструкторы
18.1.4. Спецификатор исключения noexcept
18.1.5. Иерархии классов исключений
18.2. Пространства имен
18.2.1. Определение пространств имен
18.2.2. Использование членов пространства имен
18.2.3. Классы, пространства имен и области видимости
18.2.4. Перегрузка и пространства имен
18.3. Множественное и виртуальное наследование
18.3.1. Множественное наследование
18.3.2. Преобразования и несколько базовых классов
18.3.3. Область видимости класса при множественном наследовании
18.3.4. Виртуальное наследование
18.3.5. Конструкторы и виртуальное наследование
Резюме
Термины
Глава 19. Специализированные инструменты и технологии
19.1. Контроль распределения памяти
19.1.1. Перегрузка операторов new и delete
19.1.2. Размещающий оператор new
19.2. Идентификация типов времени выполнения
19.2.1. Оператор dynamic_cast
19.2.2. Оператор typeid
19.2.3. Использование RTTI
19.2.4. Класс type_info
19.3. Перечисления
19.4. Указатель на член класса
19.4.1. Указатели на переменные-члены
19.4.2. Указатели на функции-члены
19.4.3. Использование функций-членов как вызываемых объектов
19.5. Вложенные классы
19.6. Класс объединения, экономящий место
19.7. Локальные классы
19.8. Возможности, снижающие переносимость
19.8.1. Битовые поля
19.8.2. Спецификатор volatile
19.8.3. Директивы компоновки: extern "С"
Резюме
Термины
Приложение А. Библиотека
А.1. Имена и заголовки стандартной библиотеки
А.2. Краткий обзор алгоритмов
А.2.1. Алгоритмы поиска объекта
А.2.2. Другие алгоритмы, осуществляющие только чтение
А.2.3. Алгоритмы бинарного поиска
А.2.4. Алгоритмы записи в элементы контейнера
А.2.5. Алгоритмы сортировки и разделения
А.2.6. Общие функции изменения порядка
А.2.7. Алгоритмы перестановки
А.2.8. Алгоритмы набора для отсортированных последовательностей
А.2.9. Минимальные и максимальные значения
А.2.10. Числовые алгоритмы
А.3. Случайные числа
А.3.1. Распределение случайных чисел
А.3.2. Процессоры случайных чисел
Предметный указатель

Citation preview

Язык программирования

++

Базовый курс

Пятое издание

Стенли Б. Липпман Жози Лажойе Барбара Э. Му

Москва· Санкт-Петербург· Киев 2014

++

Primer

Fifth Edition Stanley В. Lippman Josee Lajoie Barbara Е. Моо

"•" Addison-Wesley Upper Saddle River, NJ New York Capetown

• •

Toronto Sydney

• •





Boston

Montreal Tokyo





lndianapolis London

Singapore







San Francisco

Munich



Mexico City

Paris



Madrid

ББК 32.973.26-01 8.2.75 Л61 УДК 681 .3.07

Издательский дом " Вильяме " Зав. редакцией С.Н . Тригуб Перевод с английского и редакция В.А. Коваленко

По общим вопросам обращайтесь в Издательский дом " Вильяме " по адресу : [email protected], http://www.williamspublishing.com

Липпман, Стенли Б., Лажойе, Жози, Му, Бар бара Э. Л61

Язык программирования С++. Базовый курс, 5-е изд. : Пер. с англ. М. : ООО "И.Д. Вильяме", 201 4. - 1 1 20 с. : ил. - Парал. тит. англ.

ISBN 978-5-8459-1 839-0 (рус.) ББК 32.973.26-018.2.75 Все названия программных продуктов являются зарегистрированными торговыми мар­ ками соответствующих фирм. Никакая часгь настоящего издания ни в каких целях не может бьrгь воспроизведена в какой бы то ни было форме и какими бы то ни было средствами, будь то электронные или механиче­ ские, включая фотокопирование и запись на магнитный носитель, если на это нет rmсьменного разрешения издательства Addison-Wesley PuЫishing Company, Inc. Authorized translation from the English language edition puЫished Ъу Addison-Wesley Pub­ lishing Company, Inc, Copyright © 2013 Objectwrite Inc., Josee Lajoie and Barbara Е. Моо All rights reserved. No part of this book shall Ье reproduced, stored in а retrieval system, or transmitted Ъу any means, electronic, mechanical, photocopying, recording, or otherwise, without written permission from the puЫisher. No patent liaЬility is assumed with respect to the use of the information contained herein. All terms mentioned in this book that are known to Ье trademarks or service marks have been appropriately capitalized. Russian language edition is puЫished Ьу Williams PuЫishing House according to the Agree­ ment with R&I Enterprises Intemational, Copyright © 2014.

Н аучно-популярное издание Стенли Б. Липпман, Жози Лажойе, Барбара Э. Му

Язы1< программирования С++. Базовый 1 > 14.3. Арифметические операторы и операторы отношения 1 4.3. 1 . Операторы равенства 1 4.3.2. Операторы отношения 14.4. Операторы присвоения 14.5. Оператор индексирования 1 4.6. Операторы инкремента и декремента 1 4.7. Операторы доступа к членам 1 4.8. Оператор вызова функции 1 4.8. 1 . Лямбда-выражения - объекты функции 1 4.8.2. Библиотечные объекты функций 1 4.8.3. Вызываемые объекты и тип fu n c t i o n 1 4.9. Перегрузка, преобразование и операторы 1 4.9. 1 . Операторы преобразования 1 4.9.2. Избегайте неоднозначных преобразований 1 4.9.3. Подбор функций и перегруженные оператqры Резюме Термины Глава

Об ъектно-ориентированное программирование 1 5 .1 . Краткий обзор ООП 1 5.2. Определение базовых и производных классов 1 5.2. 1 . Определение базового класса 1 5.2.2. Определение производного класса 1 5 .2.3. Преобразования и наследование 1 5.3 . Виртуальные функции 1 5.4. Абстрактные базовые классы 1 5 .5. Управление доступом и наследование 1 5.6. Область видимости класса при наследовании 1 5.7. Конструкторы и функции управления копированием 1 5 .7.1 . Виртуальные деструкторы 1 5 .7.2. Синтезируемые функции управления копированием и наследование 1 5.7.3. Функции-члены управления копированием производного класса 1 5.7.4. Унаследованные конструкторы 1 5 .8. Контейнеры и наследование 1 5.8.1 . Разработка класса Ba s ke t 1 5.9. Возвращаясь к запросам текста 1 5.9.1 . Объектно-ориентированное решение 1 5.

13

699 700 705 706 707 71 0 71 1 712 71 3 71 5 71 7 720 722 724 726 728 732 733 737 742 744 745 747 748 750 750 752 759 763 768 772 778 784 785 786 789 793 795 796 801 802

С одер жание

14

1 5.9.2. Классы Que r y_b a s e и Qu e r y 1 5.9.3. Производные классы 1 5.9.4. Виртуальные функции eva l ( )

Резюме Термины Глава

Шаблоны и обобщенное программирование 1 6.1 . Определение шаблона 1 6. 1 . 1 . Шаблоны функций 1 6. 1 .2. Шаблоны класса 1 6. 1 .3. Параметры шаблона 1 6. 1 .4. Шаблоны-члены 1 6. 1 .5. Контроль создания экземпляра 1 6. 1 .6. Эффективность и гибкость 1 6.2. Дедукция аргумента шаблона 1 6.2. 1 . Преобразования и параметры типа шаблона 1 6.2.2. Явные аргументы шаблона функции 1 6.2.3. Замыкающие типы возвращаемого значения и трансформация типа 1 6.2.4. Указатели на функцию и дедукция аргумента 1 6.2.5. Дедукция аргумента шаблона и ссылки 1 6.2.6. Функция s td : : move ( ) 1 6.2.7. Перенаправление 1 6.3. Перегрузка и шаблоны 1 6.4. Шаблоны с переменным количеством аргументов 1 6.4.1 . Шаблоны функции с переменным количеством аргументов 1 6.4.2. Развертывание пакета 1 6.4.3. Перенаправление пакетов параметров 1 6.5. Специализация шаблона Резюме Термины 1 6.

Ч асть IV. Д ополнительн ые темы Глава 1 7. Специализированные средства библиотек 1 7.1 . Тип tup l e 1 7 .1 . 1 . Определение и инициализация кортежей 1 7 . 1 .2. Использование кортежей для возвращения нескольких значений 1 7.2. Тип Ь i t s e t 1 7 .2. 1 . Определение и инициализация наборов битов 1 7 .2.2. Операции с наборами битов

807 809 81 3 81 7 81 7 821 822 822 830 841 846 849 851 853 854 857 859 862 863 867 870 873 878 880 882 884 887 894 895 897 899 899 901 903 906 906 909

С одер жание

1 7 .3. Регулярные выражения 1 7.3. 1 . Использование библиотеки регулярных выражений 1 7.3.2. Типы итераторов классов соответствия и r e ge x 1 7.3.3. Использование подвыражений 1 7.3.4. Использование функции rege x_rep l a c e ( ) 1 7.4. Случайные числа 1 7.4. 1 . Процессоры случайных чисел и распределения 1 7.4.2. Другие виды распределений 1 7.5. Еще о библиотеке ввода и вывода 1 7.5.1 . Форматированный ввод и вывод 1 7.5.2. Не форматированные операции ввода-вывода 1 7.5 .3. Произвольный доступ к потоку

Резюме Термины Глава 1 8. Инструменты для крупномасштабных программ 1 8. 1 . Обработка исключений 1 8. 1 . 1 . Передача исключений 1 8. 1 .2. Обработка исключения 1 8. 1 .3. Блок t r y функции и конструкторы 1 8. 1 .4. Спецификатор исключения n o e xcept 1 8. 1 .5. Иерархии классов исключений 1 8.2. Пространства имен 1 8.2.1 . Определение пространств имен 1 8.2.2. Использование членов пространства имен 1 8.2.3. Классы, пространства имен и области видимости 1 8 .2.4. Перегрузка и пространства имен 1 8.3. Множественное и виртуальное наследование 1 8.3. 1 . Множественное наследование 1 8.3.2. Преобразования и несколько базовых классов 1 8.3.3. Область видимости класса при множественном наследовании 1 8.3.4. Виртуальное наследование 1 8 .3.5 . Конструкторы и виртуальное наследование Резюме Термины Глава

Специализированные инструменты и технологии 1 9 . 1 . Контроль распределения памяти 1 9. 1 . 1 . Перегрузка операторов new и de l e t e 1 9.1 .2. Размещающий оператор new 1 9.2. Идентификация типов времени выполнения 1 9.2. 1 . Оператор d ynami c_c a s t 1 9.2.2. Оператор t ype i d 1 9.

15

91 2 91 3 920 925 929 933 934 939 943 943 952 956 961 962 965 966 966 970 973 974 978 981 981 990 995 999 1 002 1 003 1 006 1 009 1 01 1 1 01 5 1 01 8 1 01 8 1 023 1 023 1 024 1 028 1 029 1 030 1 032

С одер жание

16

1 9.2.3. Использование RTTI 1 9.2.4. Класс t yp e i n f o 1 9.3. Перечисления 1 9.4. Указатель на член класса 1 9.4. 1 . Указатели на переменные-члены 1 9.4.2. Указатели на функции-члены 19 .4.3. Использование функций-членов как вызываемых объектов 1 9.5. Вложенные классы 1 9.6. Класс объединения, экономящий место 1 9.7. Локальные классы 1 9 .8. Возможности, снижающие переносимость 1 9.8.1 . Битовые поля 1 9.8.2. Спецификатор ·v o l a t i l e 1 9.8.3. Директивы компоновки: e xt e rn С Резюме Термины "

"

Приложение А. Библиотека А.1 . Имена и заголовки стандартной библиотеки А.2. Краткий обзор алгоритмов А.2. 1 . Алгоритмы поиска объекта А.2.2. Другие алгоритмы, осуществляющие только чтение А.2.3. Алгоритмы бинарного поиска А.2.4. Алгоритмы записи в элементы контейнера А.2.5. Алгоритмы сортировки и разделения А.2.6. Общие функции изменения порядка А.2.7. Алгоритмы перестановки А.2.8. Алгоритмы набора для отсортированных последовательностей А.2.9. Минимальные и максимальные значения А.2. 1 0. Числовые алгоритмы А.3. Случайные числа А.3.1 . Распределение елучайных чисел А.3.2. Процессоры случайных чисел Предме т ный указате ль

1 034 1 037 1 038 1 042 1 043 1 045 1 049 1 052 1 056 1 063 1 065 1 065 1 067 1 068 1 072 1 073 1 077 1 077 1 082 1 083 1 084 1 085 1 086 1 089 1 091 1 093 1 094 1 095 1 097 1 098 1 098 1 1 00 1 1 03

П освящ ается Бе т, благодаря ко торой с тало возможным написание этой и всех ос тал ы-1 ых кни г. Посвящ ает ся Дэниелю и Анне, для которых возможно п ракт ически все. Стэнли Б. Липпман

Посвящает ся М арку и маме, за их безграничную любовъ и поддержку. Жози Лажойе П освящ ается Энди, научившему меня про граммированию и многому д ругому. Барбара Му

ВВЕДЕНИЕ Благодаря предыдущим изданиям книги язык С++ изучило множество про­ граммистов. За истекшее время язык С++ претерпел существенные усовершен­ ствования, а основное внимание сообщества программистов переместилось главным образом с эффективности использования аппаратных средств к эф­ фективности программирования. В 201 1 году комитет по стандартам С++ выпустил новую основную версию стандарта ISO С++. Этот пересмотренный стандарт является последним эта­ пом развития языка С++, его основное внимание уделено эффективности про­ граммирования. Оснбовные задачи нового стандарта таковы. •

Сделать язык более единообразным, упростить его преподавание и изу­ чение.



Упростить, обезопасить и повысить эффективность использования стан­ дартных библиотек.



Облегчить написание эффективных абстракций и библиотек. 1

Это издание книги полностью пересмотрено так, чтобы использовать последний стандарт языка. Просмотрев раздел "Новые средства С++1 1 " после оглавления, вы можете получить представление о том, насколько сильно но­ вый стандарт повлиял на язык С++. Там перечислены только те разделы, в ко­ торых рассматривается новый материал. Н екоторые из нововведений в новом стандарте, такие как ключевое слово a u t o для выведения типов, весьма распространены. Эти средства существенно облегчают чтение кода в данном издании и делают его понятней. Программи­ сты, конечно, могут игнорировать те средства, которые облегчают концентра­ цию на том, что программа призвана делать. Другие новшества, такие как ин­ теллектуальные указатели и контейнеры с помержкой перемещения, позво­ ляют писать более сложные классы без необходимости справляться со сложностями управления ресурсами. В результате мы можем начать изучение создания собственных классов намного раньше, чем в предыдущем издании. Мы (и вы) больше не должны волноваться о большинстве деталей, которые стояли на нашем пути в предыдущем стандарте.



Этой пиктограммой отмечены места, в которых рассматриваются сред­ ства, определенные новым стандартом. Надеемся, что читатели, которые уже знакомы с ядром языка С++, найдут эти отметки полезными при решении, на чем сосредоточить внимание. Мы также ожидаем, что эти пиктограммы помогут объяснить сообщения об ошибках тех компиля­ торов, которые могут еще не померживать все новые средства. Хотя

В ведение

19

практически все примеры этой книги были откомпилированы на теку­ щем выпуске компилятора GNU, мы понимаем, что у некоторых чита­ телей еще не будет новейшего компилятора. Даже при том, что по по­ следнему стандарту было добавлено множество возможностей, базовый язык остается неизменным и формирует основной объем материала, ко­ торый мы рассматриваем.

Дл я кого написана эта книга Можно считать, что современный язык С++ состоит из трех частей. •

Низкоуровневый язык, большая часть которого унаследована от языка С.



Дополнительные возможности языка, позволтощие определять собствен­ ные типы данных, организовать крупномасппабные программы и системы.



Стандартная библиотека, использующая эти дополнительные возмож­ ности для обеспечения набора структур данных и алгоритмов.

В большинстве книг язык С++ представлен в порядке его развития. Сначала они знакомят с частью С в языке С++, а в конце книги представляются более абстрактные средства С++ как дополнительные возможности. У этого подхода есть две проблемы: читатели могут увязнуть в подробностях, унаследованных от низкоуровневого программирования, и сдаться. Те же, кто будет упорство­ вать в изучении, наживут плохие привычки, от которых впоследствии придет­ ся избавляться. Мы придерживаемся противоположного подхода: с самого начала использу­ ем средства, которые позволяют программистам игнорировать детали, унасле­ дованные от низкоуровневого программирования. Например, мы вводим и ис­ пользуем библиотечные типы s t r i ng и ve c t o r наряду со встроенными цифро­ выми типами и массивами. Программы, которые используют эти библиотечные типы, проще писать, проще понимать, и ошибок в них много меньше. Слишком часто библиотеки преподносят как "дополнительную" тему. Вме­ сто того чтобы использовать библиотеки, во многих книгах используют низко­ уровневые способы программирования с использованием указателей на сим­ вольные массивы и динамического управления памятью. Заставить правильно работать программы, которые используют эти низкоуровневые подходы, куда труднее, чем написать соответствующий код С++, используя библиотеку. Повсюду в этой книге мы демонстрируем хороший стиль программирова­ ния: мы хотим помочь вам выработать хорошие привычки сразу и избежать борьбы с плохими привычками впоследствии, когда вы получите более слож­ ные навыки. Мы подчеркиваем особенно сложные моменты и предупреждаем о наиболее распространенных заблуждениях и проблемах.

В ведение

20

М ы также объясняем прич и ны, по которым были введены те или иные пра­ вила, а не просто пр и н и маем их как данность. М ы пола гаем, что пон и мание причи н поможет ч итателю быстрей овладеть возможностями языка. Хотя для изучения этой кн иги знан и е языка С необязательно, мы подразу­ меваем, что вы знакомы с про грамм и рован и ем достаточно, чтобы напи сать, откомпил и ровать и запустить про грамму хотя бы на одном из современных языков . В част ности , мы подразумеваем, что вы умеете и спользовать перемен­ ные, соз давать и вызыва ть функц ии , а также и спользовать компилятор .

Изменения в пятом издании Н ововведением этого издания являются пикто граммы на полях, призван­ ные помочь чит ателю . Язык С++ обширен, он предоставляет возможности для решения разнообразных специфи ческих проблем про граммирован ия. Неко­ торые и з этих возможностей весьма важны для больших групп разработчиков, но маловажны для малых проектов . В результате не каждому про грамм исту следует знать каждую деталь каждо го средства . М ы добавили эти пиктограм­ мы, чтобы помочь читателю узнать, какие элементы мо гут быть и зучены поз­ же, а какие темы являются насущными . Разделы, рассматривающие основные принци пы языка, отмечены изобра­ жением человека, читающего кни гу . Темы, затронугые в этих разделах, яв­ ляются базовой частью языка . Все эти разделы следует прочитать и понять. М ы также отметили те разделы, которые затра г ивают дополн ительные ил и спец иальные темы . Эти разделы можно пропустить или только про­ смотреть при первом чтен ии. Мы отметили такие разделы стопкой кн иг, указав, что на этом месте вы можете спокойно отложить книгу . Вероят­ но, имеет смысл просмотрет ь т акие разделы и узнать, какие возможно­ сти сущес твуют. Тем не менее нет н и какой пр ичины тратить время на и зучение эти х тем, пока вам факти чески не пр идется использовать в св оих программах описанное средств о.



Особенно сложные концепц ии выделены п и кто граммой с и зображен и ем лупы . Н адеемся, что ч итатели уделят время, чтобы хорошо усвоить ма­ териал, предс тавленный в таких разделах . Еще одна помощь чи тателю этой кни ги - обширное употреблен и е пере­ крестных ссылок . М ы надеемся, что эти ссылки обле гчат читателю переход в середину кн иги и возвращение назад к прежнему материалу, на который ссылаются более поздние примеры. Н о что остается неизменным в кни ге, т ак это четкое и ясное, корректное и полное руководство по языку С++. М ы излагаем язык, предст авляя наборы все более и более сложных примеров, которые объясняют е го средства и де­ монстр ируют способы наилучшего использования С++.

В ведение

21

С труктура кни г и Мы начинаем с рассмотрения основ языка и библиотеки в частях 1 и 11. Эти части содержат достаточно материала, чтобы позволить читателю писать ра­ ботоспособные программы. Большинство программистов С++ должны знать все, что описано в этих частях. Кроме обучения основам языка С++, материал частей 1 и 11 служит и другой важной цели: при использовании абстрактных средств, определенных библиоте­ кой, вы научитесь использовать методики высокоуровневого программирования. Библиотечные средства сами являются абстрактными типами данных, которые обычно пишут на языке С++. Библиотека может бьпъ создана с использованием тех же средств построения класса, которые досrупны для любого программиста С++. Наш опьrг в обучении языку С++ свидетельствует о том, что, если читатели с самого начала используют хорошо разработанные абстрактные типы, то впослед­ ствии им проще понять, как создавать собственные типы. Только после полного освоения основ использования библиотеки (и напи­ сания разных абстрактных программ при помощи библиотеки) мы переходим к тем средствам языка С++, которые позволяют писать собственные абстрак­ ции. В частях 111 и IV главное внимание уделяется написанию абстракции в форме классов. В части 111 рассматриваются общие принципы, а в части IV специализированные средства. В части 111 мы рассматриваем проблемы управления копированием, а также другие способы создания классов, которые так же удобны, как и встроенные типы. Классы - это основа объектно-ориентированного и обобщенного про­ граммирования, которое также будет рассмотрено в части 111. Книга заканчи­ вается частью IV, рассматривающей средства, обычно используемые в боль­ ших и сложных системах. В приложении А приведено краткое описание биб­ лиотечных алгоритмов.

С ог ла Пi ения, п р инятые в книге Каждая глава завершается резюме и словарем терминов. Читатели могут использовать эти разделы как контрольный список: если вы не понимаете термин, следует повторно изучить соответствующую часть главы. Здесь используются соглашения, общепринятые в компьютерной литературе. •

Новые термины в тексте выделяются курсивом. Чтобы обратить внимание читателя на отдельные фрагменты текста, также применяется курсив.



Текст программ, функций, переменных, URL веб-страниц и другой код представлен моноширинным шрифтом.



Все, что придется вводить с клавиатуры, выделено ринным шрифтом.

полужирным моноши­

В ведение

22



Знакоместо в описаниях синтаксиса выделено курсивом. Эго указывает на необходимость заменить знакоместо фактическим именем переменной, параметром или другим элементом, который должен находиться на этом месте. Например: B INDS I ZE= ( максимальная ширина кол онки) * ( номер колонки) .



Пункты меню и названия диалоговых окон представлены следующим образом : Menu Option (Пункт меню) .

Примечание о компиляторах На момент написания этой книги (июль 2012 года) поставщики компилято­ ров интенсивно работали, модифицируя свои компиляторы в соответствии с последним стандартом 150. Чаще всего мы использовали компилятор GNU версии 4.7.0. В этой книге использовано лишь несколько средств, которые в этом компиляторе еще не реализованы: наследование конструкторов, квалификато­ ры ссылок для функций-членов и библиотека регулярных выражений.

Б ла г одарност и Мы очень благодарны за помощь в подготовке этого издания нынешним и прежним членам комитета по стандартизации: Дейв Абрахамс (Dave Abra­ hams), Энди Кёниг (Andy Koenig), Стефан Т. Лававей (Stephan Т. Lavavej), Джейсон Меррилл (Jason Merrill), Джон Спайсер (John Spicer) и Герб Саттер (Herb Sutter) . Они оказали нам неоценимую помощь в понимании некоторых нюансов нового стандарта. Мы также хотели бы поблагодарить многих других людей, которые работали над модификацией компилятора GNU и сделали стандарт реальностью. Как и в предыдущих изданиях этой книги, мы хотели бы выразить отдель­ ную благодарность Бьярне Страуструпу (Bjarne Stroustrup) за его неустанную работу над языком С++ и многолетнюю дружбу с авторами. Хотелось бы так­ же поблагодарить Алекса Степанова (Alex Stepanov) за его объяснения по теме контейнеров и алгоритмов, составляющих ядро стандартной библиотеки. И наконец, сердечная благодарность членам комитета по стандарту С++ за их упорную многолетнюю работу по утверждению и усовершенствованию стан­ дарта языка С++. Авторы также выражают глубокую благодарность рецензентам, чьи ком­ ментарии, замечания и полезные советы помогли улучшить книгу. Спасибо Маршаллу Клоу (Marshall Clow), Джону Калбу (Jon Kalb), Невину Либеру (Nevin Liber), др . К. Л. Тондо (Dr. С. L. Tondo), Дэвиду Вандевурду (Daveed Vandevoorde) и Стиву Виноски (Steve Vinoski) . Эта книга была набрана при помощи системы LaTeX и прилагаемых к ней пакетов. Авторы выражают глубокую благодарность членам сообщества LaTeX, сделавшим доступным такой мощный инструмент.

В ведение

23

И наконец, благодарим сотрудников издательства Addison-Wesley, которые курировали процесс публикации этой книги: Питер Гордон (Peter Gordon)­ нaш редактор, который предложил пересмотреть эту книгу еще раз; Ким Бодихаймер (Kim Boedigheimer) контролировал график выполнения работ; Барбара Вуд (Barbara Wood) нашла множество наших ошибок на этапе редак­ тировании, а Элизабет Райан (Elizabeth Ryan) снова помогала авторам на про­ тяжении всего проекта.

Введение

24

От издательства Вы, читатель этой книги, и есть главный ее критик. Мы ценим ваше мнение и хотим знать, что было сделано нами правильно, что можно было сделать лучше и что еще вы хотели бы увидеть изданным нами. Нам интересно услы­ шать и любые другие замечания, которые вам хотелось бы высказать авторам. Мы ждем ваших комментариев. Вы можете прислать письмо по электрон­ ной почте или просто посетить наш веб-сервер, оставив на нем свои замеча­ ния. Одним словом, любым удобным для вас способом дайте нам знать, нра­ вится ли вам эта книга, а также выскажите свое мнение о том, как сделать на­ ши книги более подходящими для вас. Посылая письмо или сообщение, не забудьте указать название книги и ее авторов, а также ваш e-mail. Мы внимательно ознакомимся с вашим мнением и обязательно учтем его при отборе и подготовке к изданию следующих книг. Наrпи координаты: E-mail:

info@williamspuЬlishing.com

WWW:

http://www.williamspuЬlishing.com

Нап1и почтовые адреса: в России: 1 27055, г. Москва, ул. Лесная, д. 43, стр . 1 в Украине: 031 50, Киев, а/ я 1 52

ГЛАВА

1

ПЕРВЫЕ Ш А Г И

В э то й г лаве ... Создание простой программы на языке С ++ 1 .2. Первый взгляд на ввод-вывод 1 .3. Несколько слов о ком ментариях 1 .4. Средства управления 1 . 5. Введение в классы 1 .6. Програм ма для книжного магазина Резюме Термины

1 .1 .

26 30 35 37 47 52 54 54

Эга глава знакомит с большинством фундаментальных элементов языка С++: типами, переменными, выражениями, операторами и функциями. Кроме того, здесь кратко описано, как откомпилировать программу и запустить ее на вы­ полнение. Изучив эту главу и выполнив соответствующие упражнения, читатель будет способен написать, откомпилировать и запустить на выполнение простую про­ грамму. Последующие главы подразумевают, что вы в состоянии использовать описанные в данной главе средства и рассматривают их более подробно. Лучше всего изучать новый язык программирования в процессе написа­ ния программ. В этой главе мы напишем простую программу для книжного магазина. Книжный магазин хранит файл транзакций, каждая из записей которого соответствует продаже одного или нескольких экземпляров определенной книги. Каждая транзакция содержит три элемента данных: 0 - 2 0 1 - 7 0 3 5 3 -Х 4 2 4 . 9 9

Первый элемент - это ISBN (lnternational Standard Book Number - меж­ дународный стандартный номер книги), второй - количество проданных эк­ земпляров, последний - цена, по которой был продан каждый из этих экзем­ пляров. Владелец книжного магазина время от времени просматривает этот

Глава 1 . П ервые шаги

26

файл и вычисляет для каждой книги количество проданных экземпляров, об­ щий доход от этой книги и ее среднюю цену. Чтобы написать эту программу, необходимо рассмотреть несколько эле­ ментарных средств языка С++. Кроме того, следует знать, как откомпилиро­ вать и запустить программу. Хотя мы еще не разработали свою программу, несложно предположить, что для этого необходимо следующее. •

Определить переменные.



Обеспечить ввод и вывод.



Применить структуру для содержания данных.



Проверить, нет ли двух записей с одинаковым ISBN.



Использовать цикл для обработки каждой записи в файле транзакций.

Сначала рассмотрим, как эти задачи решаются средствами языка С++, а затем напишем нашу программу для книжного магазина.

1 .1 . Создание п р остой п р ог р аммы на яз ыке С++ Каждая программа С++ содержит одну или несколько функций (function), причем одна из них обязательно имеет имя ma i n ( ) . Запуская программу С++, операционная система вызывает именно функцию ma i n ( ) . Вот простая версия функции ma i n ( ) , которая не делает ничего, кроме возвращения значения О операционной системе: int rna i n ( ) re t u r n О ;

Определение функции содержит четыре элемента: тип возвращаемого зна­ чения (return type), имя функции (function name), список параметров (parameter list), который может быть пустым, и тело функции (function body) . Хотя функ­ ция та i n ( ) является в некоторой степени особенной, мы определяем ее таким же способом, как и любую другую функцию. В этом примере список параметров функции ma i n ( ) пуст (он представлен скобками ( ) , в которых ничего нет) . Более подробная информация о пара­ метрах функции ma i n ( ) приведена в разделе 6.2.5. (стр . 288). Функция ma i n ( ) обязана иметь тип возвращаемого значения i n t, который является типом целых чисел. Тип i n t это встроенный тип (built-in type) данных, такие типы определены в самом языке. -

1.1 .

Создание п рос той п ро г рамм ы на языке С++

27

Заключительная часть определения функции, ее тело, представляет собой блок операторов (Ыосk of statements), который начинается открывающей фи­ гурной скобкой (curly brace) и завершается закрывающей фигурной скобкой. { return О;

Единсrвенным оператором в этом блоке является оператор retu rn, который завершает код функции. Оператор r e t u rn может также передать значение назад вызьmающей стороне функции, как в данном случае. Когда оператор re tu rn по­ лучает значение, его тип должен быть совместим с типом возвращаемого значе­ ния функции. В данном случае типом возвращаемого значения функции ma i n ( ) является i n t, и возвращаемое значение О имеет тип i n t . Обратите внимание на точку с запятой в конце оператора r e t u rn. Точкой с запятой отмечают конец большинства операторов языка С++. Ее очень просто пропустить, и это приводит к выдаче компилятором непонятного сообщения об ошибке. В большинстве операционных систем возвращаемое функцией ma i n ( ) зна­ чение используется как индикатор состояния. Возвращение значения О свиде­ тельствует об успехе. Любое другое значение, как правило, означает отказ, а само значение указывает на его причину. КЛ Ю ЧЕВАЯ КОН Ц ЕП ЦИЯ . Типы

Типы - это одна из наиболее фундаментальных концепций в програм­ мировании. К ней мы будем возвращаться в этой книге не раз. Тип опреде­ ляет и содержимое элемента данных, и операции, которые возможны с ним. Данные, которыми манипулируют наши программы, хранятся в перемен­ ных, и у каждой переменной есть тип. Когда типом переменной по имени v яв­ ляется т, мы зачасrую говорим, что "переменная v имеет тип т" или " v есть т".

1.1.1. Компиляция и запуск п р ог р аммы Написанную программу необходимо откомпилировать. Способ компиляции программы зависит от используемой операционной системы и компилятора. Более подробную информацию о работе используемого вами компилятора можно получить в его документации или у хорошо осведомленного коллеги. Большинство РС-ориентированных компиляторов обладают интегрирован­ ной средой разработки (lnte grated Development Environment - IDE), которая объединяет компилятор с соответствующими средствами редактирования и отладки кода. Эти средства весьма удобны при разработке сложных про-

Глава 1 . П е р вые шаги

28

грамм, однако ими следует научиться пользоваться. Описание подобных сис­ тем выходит за рамки этой книги. Большинство компиляторов, включая укомплектованные IDE, обладают ин­ терфейсом командной строки. Если читатель не очень хорошо знаком с IDE ис­ пользуемого компилятора, то, возможно, имеет смысл начать с применения бо­ лее простого интерфейса командной строки. Эго позволит избежать необходи­ мости сначала изучать IDE, а затем сам язык. Кроме того, хорошо понимая язык, вам, вероятно, будет проще изучить интегрированную среду разрабоn c l / E H s c p r o g l . cpp

Глава 1 . П ервые шаги

30

где

\U s e r s \me\P rograms > - это сисгемное приглашение к вводу; \Us e r s \ me \P ro g r ams - имя текущего каталога (или папки) . Команда c l запускает компилятор, а параметр компилятора / EHs c включает стандартную обра­ с:

ботку исключений. Компилятор Microsoft автоматически создает исполняе­ мый файл с именем, которое соответствует первому имени файла исходного кода. У исполняемого файла будет суффикс . е х е и то же имя, что и у фай­ ла исходного кода. В данном ел учае исполняемый файл получит имя p r o g l . ехе .

Как правило, компиляторы способны предупреждать о проблемных кон­ струкциях. Обычно эти возможности имеет смысл задействовать. Поэтому с компилятором GNU желательно использовать параметр -Wa l l, а с компи­ ляторами Microsoft - параметр / W4. Более подробная информация по этой теме содержится в руководстве программиста, прилагаемом к компилятору. Упражнения раздела

1 .1 .1

Упражнение 1 .1 . Просмотрите документацию по используемому компиля­ тору и выясните, какое соглашение об именовании файлов он использует. Откомпилируйте и запустите на выполнение программу, функция ma i n ( ) которой приведена на стр. 26. Упражнение 1 .2. Измените код программы так, чтобы функция ma i n ( ) возвращала значение - 1 . Возвращение значения 1 зачастую свидетельст­ вует о сбое при выполнении программы. Перекомпилируйте и повторно запустите программу, чтобы увидеть, как используемая операционная сис­ тема реагирует на свидетельство об отказе функции ma i n ( ) . -

1 .2. Пе р вь1й в зг ляд на ввод-вь1вод В самом языке С++ никаких операторов для ввода и въюода (lnput/Output 10) нет. Их предосгавляет стандартная библиотека (standard library) наряду с обширным набором подобных средств. Однако для большинства задач, включая примеры этой книги, вполне досгаточно изучить лишь несколько фундамен­ тальных концепций и простьIХ операций. В большинстве примеров этой книги использована библиотека i o s t re am. Ее основу составляют два типа, i s t re am и o s t re am, которые представляют потоки ввода и вывода соответственно. Поток (stream) - это последователь­ ность символов, записываемая или читаемая из устройства ввода-вывода неко­ торым способом. Термин "поток" подразумевает, что символы поступают и передаются последовательно на протяжении определенного времени.

П ервый взгляд на ввод-вывод

1 .2.

31

С тандар тные о б ъ екты ввода и в ывода В библиотеке определены четыре объекта ввода-вывода. Для осуществле­ ния ввода используется объект c i n (произносится "си-ин") типа i s t r e am. Этот объект упоминают также как стандартный ввод (standard input). Для вывода используется объект c o u t (произносится "си-аут") типа o s t r e am. Его зачас­ тую упоминают как стандартный вывод (standard output) . В библиотеке опре­ делены еще два объекта типа o s t re am - это c e r r и c l o g (произносится "си­ ерр" и "си-лог" соответственно) . Объект c e r r, называемый также стандартной ошибкой (standard error), как правило, используется в программах для созда­ ния предупреждений и сообщений об ошибках, а объект c l o g - для создания информационных сообщений. Как правило, операционная система ассоциирует каждый из этих объектов с окном, в котором выполняется программа. Так, при получении данных объ­ ектом c i n они считываются из того окна, в котором выполняется программа. Аналогично при выводе данных объектами c o u t, c e r r или c l og они отобра­ жаются в том же окне.

П рограмма, использующая б и б лиотеку ввода-вывода Приложению· для книжного магазина потребуется объединить несколько записей, чтобы вычислить общую сумму. Сначала рассмотрим более простую, но схожую задачу - сложение двух чисел. Используя библиотеку ввода­ вывода, можно модифицировать прежнюю программу так, чтобы она запра­ шивала у пользователя два числа, а затем вычисляла и выводила их сумму. # i n c l u de < i o s t r e am> int ma i n ( ) s t d : : co u t < < " E n t e r t w o n umbe r s : " < < s t d : : e n d l ; i n t v l = О , v2 = О ; s td : : c i n >> v l >> v2 ; s t d : : c o u t < < " The s um o f " < < v l < < " a n d " < < v2 < < " is " < < vl + v 2 < < s t d : : e n d l ; return О;

Вначале программа отображает на экране приглашение пользователю вве­ сти два числа. E n t e r t w o n umbe r s :

Затем она ожидает ввода. Предположим, пользователь ввел следующие два числа и нажал клавишу : з 7

В результате программа отобразит следующее сообщение: The s um o f 3 a n d 7 i s 1 0

Глава

32

1.

П ервые шаги

Первая сrрока кода (# i n c lude < i o s t ream>) - это директива препроцессора (preprocessor directive), которая указьmает компилятору1 на необходимосrь вклю­ чить в программу библиотеку o s t ream. Имя в угловых скобок - это заголовок (header). Каждая программа, которая использует средства, хранимые в библиоте­ ке, должна подключить соответствующий заголовок. Директива # i nc l ude долж­ на бьrгь написана в одной сrроке. То есть и заголовок, и слово # i nc l ude должны находиться в той же сrроке кода. Директива # i n c l ude должна располагаться вне тела функции. Как правило, все директивы # i n c lude программы располагают в начале файла исходного кода.

З апись в поток Первый оператор в теле функции ma i n ( ) выполняет выр ажение (expression). В языке С++ выражение состоит из одного или нескольких опе­ рандов (operand) и, как правило, оператора (operator) . Чтобы отобразить под­ сказку на стандартном устройстве вывода, в этом выражении используется оператор въюода (output operator), или оператор < < . s t d : : c o u t < < " E n t e r two numbe r s : " < < s t d : : e n d l ;

Оператор < < получает два операнда: левый операнд должен быть объектом класса o s t re am, а правый операнд - это подлежащее отображению значение. Оператор заносит переданное значение в объект c o u t класса o s t r e am. Таким образом, результатом является объект класса o s t re am, в который записано предоставленное значение. Выражение вывода использует оператор < < дважды. Поскольку оператор возвращает свой левый операнд, результат первого оператора становится ле­ вым операндом второго. В результате мы можем сцепить вместе запросы на вывод. Таким образом, наше выражение эквивалентно следующему: ( s td : : c o u t < < " E n t e r t w o numbe r s : " )

> vl

>>

v2;

Глава 1 . П ервые шаги

34

Оператор ввода (input operator) (т.е. оператор > > ) ведет себя аналогично оператору вывода. Его левым операндом является объект типа i s t re am, а пра­ вым операндом - объект, заполняемый данными. Он читает значение из по­ тока, представляемого объектом типа i s t re am, и сохраняет его в объекте, за­ данном правым операндом. Подобно оператору вывода, оператор ввода воз­ вращает в качестве результата свой левый операнд. Другими · словами, эта операция эквивалентна следующей: ( s td : : c in >> v l )

>> v2 ;

Поскольку оператор возвращает свой левый операнд, мы можем объеди­ нить в одном операторе последовательность из нескольких запросов на ввод данных. Наше выражение ввода читает из объекта s t d : : c i n два значения, со­ храняя первое в переменной v l , а второе в переменной v2 . Другими словами, рассматриваемое выражение ввода выполняется как два следующих: s t d : : c i n >> v l ; s t d : : c i n > > v2 ;

З аве рш ение п рогр а ммы Теперь осталось лишь вывести результат сложения на экран. s t d : : co u t v 2 ; / / ошибка : исполь зуе тся " v " вме сто "vl " / / co u t не определен , должно быть s t d : : co u t c o u t < < v l + v 2 < < s td : : e n d l ; return О ;

Сообщение об ошибке содержит обычно номер строки и краткое описа­ ние того, что компилятор считает неправильным. Исправлять ошибки име­ ет смысл в том порядке, в котором поступают сообщения о них. Зачастую одна ошибка приводит к появлению других, поэтому компилятор, как пра­ вило, сообщает о большем количестве ошибок, чем имеется фактически. Целесообразно также перекомпилировать код после устранения каждой ошибки или небольшого количества вполне очевидных ошибок. Этот цикл известен под названием "редактирование, компиляция, отладка " (edit-compile­ debug).

Глава 1 . Пе р вые шаги

44

Упражнения раздела

1 .4.З

Упражнение 1 .1 6. Напишите собственную версию программы, которая вы­ водит сумму набора целых чисел, прочитанных при помощи объекта c i n .

1 .4.4. Опе р ато р

if

Подобно большинству языков, С + + предоставляет оператор i f, который обеспечивает выполнение операторов по условию. Оператор i f можно ис­ пользовать для написания программы подсчета количества последовательных совпадений значений во вводе: # i n c l u de < i o s t r e am> i n t ma i n ( ) / / c u rrVa l подс читыв а емое число ; новые зна чения будем чита ть в va l i n t cu r rVa l = О , v a l = О ; 1 1 про чита ть перв ое число и удо с т ов ерить ся в наличии ца нных / / для о бра ботки i f ( s t d : : c i n > > c u r rVa l ) i n t c n t = 1 ; / / сохра нить счет для т екущего зна чения whi l e ( s t d : : c i n > > va l ) { / / чита ть о с тальные числа i f ( va l == c u r rVa l ) 1 1 е сли зна чение то же ++cnt ; 1 1 доба вить 1 к cn t else { 1 1 в про тивном случа е выв е с ти счет для 1 1 предыдущего зна чения s t d : : c o u t < < c u r rVa l < < " o c c u r s " < < c n t < < " t i me s " < < s t d : : e n d l ; c u r rVa l = va l ; 1 1 з а помнить нов ое зна чение cnt = 1 ; 1 1 сбро сить счетчик -

1 1 цикл wh i l e за ка нчив а е т ся здесь / / не за быть выв е с ти сче т для последнего зна чения s t d : : c o u t < < cu r rVa l < < " o c c u r s " < < c n t < < " t i me s " < < s t d : : e n d l ; 1 1 первый опера тор i f з а ка нчив а е т ся зде сь return О ;

Если задать этой программе следующий ввод: 42 42 42 42 42 5 5 55 62 1 0 0 1 0 0 1 0 0

то результат будет таким: 4 2 o c c u r s 5 t ime s 5 5 o c c u r s 2 t i me s 6 2 o c c u r s 1 t i me s 1 0 0 o c c u r s 3 t i me s

Большая часть кода в этой программе должна быть уже знакома по прежним программам. Сначала определяются переменные va l и c u r rVa l : cu r rVa l бу-

1 .4.

С р едства уп равления

45

дет содержать подсчитываемое число, а переменная va l каждое число, чи­ таемое из ввода. Новыми являются два оператора i f. Первый гарантирует, что ввод не пуст. -

if

( s td : : c i n > > c u r rVa l ) { // . . . 1 1 первый опера тор i f за ка нчив а е тся зде сь

Подобно оператору wh i l e, оператор i f проверяет условие. Условие в пер­ вом операторе i f читает значение в переменную cu r rVa l . Если чтение ус­ пешно, то условие истинно и выполняется блок кода, начинающийся с откры­ той фигурной скобки после условия. Этот блок завершается закрывающей фигурной скобкой непосредственно перед оператором r e t u r n . Как только подсчитываемое стало известно, определяется переменная cn t, содержащая счет совпадений данного числа. Для многократного чтения чисел со стандартного устройства ввода используется цикл wh i l e, подобный приве­ денному в предыдущем разделе. Тел:ом цикла wh i l e является блок, содержащий второй оператор i f : if

( va l = = c u r rVa l ) ++cnt ; else {

/ / е сли зна чение то же 1 1 доба вить 1 к cn t 1 1 в про тивном случа е выв е сти с че т для 1 1 предыдущего зна чения s t d : : c o u t < < c u r rVa l < < " o c c u r s " < < c n t < < " t ime s " < < s t d : : e n d l ; c u r rVa l = v a l ; 1 1 з а помнить нов о е зна чение / / с бро сить с че т чик cnt = 1 ;

Условие в этом операторе i f использует для проверки равенсгва значений пе­ ременных val и cu r rVa l оператор равенства (equality operator) (оператор ==). Ес­ ли условие исrинно, вьшолняется оператор, следующий непосредсгвенно за ус­ ловием. Эгот оператор осуществляет инкремент значения переменной cn t, озна­ чая очередное повторение значения переменной c u r rVa l . Если условие ложно (т.е. значения переменных va l и cu r rVa l не равны), выполняется оператор после ключевого слова e l s e . Этот оператор также яв­ ляется блоком, состоящим из оператора вывода и двух присвоений. Оператор вывода отображает счет для значения, которое мы только что закончили об­ рабатывать. Операторы присвоения возвращают переменной c n t значение 1 , а переменной cu r rVa l - значение переменной va l, которое ныне является новым подсчитываемым числом.

&

ВНИМАНИЕ

В языке С++ для присвоения используется оператор =, а для про­ верки равенства - оператор ==. В условии могут присутствовать оба оператора. Довольно распространена ошибка, когда в условии пишут =, .а подразумевают ==.

Глава 1 . Пе рвые шаги

46

Упражнения раздела

1 .4.4

Упражнение 1 .1 7. Что произойдет, если в рассматриваемой здесь програм­ ме все введенные значения будут равны? Что если никаких совпадающих значений нет? Упражнение 1 .1 8. Огкомпилируйте и запустите на выполнение программу этого раздела, а затем вводите только равные значения. Запустите ее снова и вводите только не повторяющиеся числа. Совпадает ли ваше предполо­ жение с реальностью? Упражнение 1 .1 9. Пересмотрите свою программу, написанную для упраж­ нения раздела 1 .4.1 (стр. 39), которая выводила бы диапазон чисел, обрабаты­ вая ввод, так, чтобы первым отображалось меньше число из двух введенных. К ЛЮЧЕВАЯ КОНЦЕПЦИЯ. ВЫРАВНИВАНИЕ И ФОРМАТИРОВАНИЕ КОДА ПРОГРАММ

С ++

Оформление исходного кода программ на языке С++ не имеет жестких правил, поэтому расположение фигурных скобок, отступ, выравнивание, комментарии и разрыв строк, как правило, никак не влияет на полученную в результате компиляции программу. Например, фигурная скобка, обозна­ чающая начало тела функции ma i n ( ) , может находиться в одной строке со словом ma i n (как в этой книге), в начале следующей строки или где-нибудь дальше. Единственное требование - чтобы открывающая фигурная скобка была первым печатным символом, за исключением комментария, после списка параметров функции ma i n ( ) . Хотя исходный код вполне можно оформлять по своему усмотрению, не­ обходимо все же позаботиться о его удобочитаемости. Можно, например, написать всю функцию ma i n ( ) в одной длинной строке. Такая форма запи­ си вполне допустима, но читать подобный код будет крайне неудобно. До сих пор не стихают бесконечные дебаты по поводу наилучшего спо­ соба оформления кода программ на языках С++ и С. Авторы убеждены, что единственно правильного стиля не существует, но единообразие все же важно. Большинство программистов выравнивают элементы своих про­ грамм так же, как мы в функции ma i n ( ) и телах наших циклов. Однако в коде этой книги принято размещать фигурные скобки, которые разграни­ чивают функции, в собственных строках, а выравнивание составных опера­ · торов ввода и вывода осуществлять так, чтобы совпадал отступ операндов. Другие соглашения будут описаны по мере усложнения программ. Не забывайте, что существуют и другие способы оформления кода. При выборе стиля оформления учитывайте удобочитаемость кода, а выбрав стиль, придерживайтесь его неукоснительно на протяжении всей программы.

------- ·· ·--- -----

1 .5.

Вве дение в классы

1 .5.

47

В ведение в классь1

Единственное средство, которое осталось изучить перед переходом к реше­ нию проблемы книжного магазина, - это определение структуры дт-tных для хранения данных транзакций. Для определения собственных структур данных язык С++ предоставляет классы (class) . Класс определяет тип данных и набор операций, связанных с этим типом . Механизм классов - это одно из важнейших средств языка С++. Фактически основное внимание при проекти­ ровании приложения на языке С++ уделяют именно определению различных типов классов (class type), которые ведут себя так же, как встроенные типы данных. В этом разделе описан простой класс, который можно использовать при решении проблемы книжного магазина. Реализован этот класс будет в сле­ дующих главах, когда читатель больше узнает о типах, выражениях, операто­ рах и функциях. Чтобы использовать класс, необходимо знать следующее. 1.

Каково его имя?

2.

Где он определен?

3.

Что он делает?

Предположим, что класс для решения проблемы книжного магазина имеет имя S a l e s _ i t em, а определен он в заголовке S a l e s _ i t em . h . Как уже было продемонстрировано на примере использования таких биб­ лиотечных средств, как объекты ввода и вывода, в код необходимо включить соответствующий заголовок. Точно так же заголовки используются для досту­ па к классам, определенным для наших собственных приложений. Традици­ онно имена файлов заголовка совпадают с именами определенных в них клас­ сов. У написанных нами файлов заголовка, как правило, будет суффикс h, но некоторые программисты используют расширение н, . hpp или h x x . У заго­ ловков стандартной библиотеки обычно нет никакого суффикса вообще. Компиляторы, как правило, не заботятся о форме имен файлов заголовка, но интегрированные среды разработки иногда это делают. .

.

1 . 5.1 . Класс

.

Sale s i tem

Класс S a l e s_i tem предназначен для хранения ISBN, а также для отслежива­ ния количества проданных экземпляров, полученной суммы и средней цены проданных книг . Не будем пока рассматр:ивать, как эти данные сохраняются и вычисляются. Чтобы применить класс, необходимо знать, что он делает, а не как. Каждый класс является определением типа. Имя типа совпадает с именем класса. Следовательно, класс S a l e s i t em определен как тип S a l e s i t em.

Глава

48

1.

П е р вые шаги

Подобно встроенным типам данных, вполне можно создать переменную типа класса. Рассмотрим пример. S a l e s i t em i t em ;

Этот код создает объект i t em типа s а l е s i t em. Как правило, об этом гово­ рят так: создан "объект типа S a l e s i t em", или "объект класса S a l e s _ i t em", или даже "экземпляр класса S a l e s i t em" . Кроме создания переменных типа S a l e s _ i t em, с его объектами можно вы­ полнять следующие операции. _

_

_



Вызывать функцию i s b n ( ) , чтобы извлечь ISBN из объекта класса S a l e s i t em.



Использовать операторы ввода ( > > ) и вывода ( < < ), чтобы читать и ото­ бражать объекты класса S а l е s _ i t em.



Использовать оператор присвоения ( = ) , чтобы присвоить один объект класса S a l e s _ i t em другому.



Использовать оператор суммы ( + ) , чтобы сложить два объекта класса S a l e s_i t em. ISBN этих двух объектов должен совпадать. Результатом будет новый объект s а l е s _ i t em с тем же ISBN, а количество проданных экземпляров и суммарный доход будуг суммой соответствующих значе­ ний его операндов .



Использовать составной оператор присвоения ( + = ) , чтобы добавить один объект класса S a l e s _ i t em к другому.

КЛЮЧЕВАЯ КОН ЦЕПЦИЯ . ОПРЕДЕЛЕНИЕ ПОВЕДЕНИЯ КЛАССА

Читая эти программы, очень важно иметь в виду, что все действия, кото­ рые могуг быть осуществлены с объектами класса S a l e s _ i t em, определяет его автор . Таким образом, класс s а l е s _ i t em определяет то, что происходит при создании объекта класса S a l e s _ i t em, а также то, что происходит при его присвоении, сложении или выполнении операторов ввода и вывода. Автор класса вообще определяет все операции, применимые к объектам типа класса. На настоящий момент с объектами класса S a l e s i t em можно выполнять только те операции, которые перечислены в этом разделе.

Ч тение и запись о б ъ ектов класса Sales i tems Теперь, когда известны операции, которые можно осуществлять, используя объекты класса S a l e s _ i t em, можно написать несколько простых программ, использующих его. Программа, приведенная ниже, читает данные со стан­ дартного устройства ввода в объект S a l e s_i t em, а затем отображает его на стандартном устройстве вывода.

1 .5.

Введение в классы

49

# i n c l u de < i o s t r e am> # i n c l u de " S a l e s i tem . h " i n t ma i n ( ) S a l e s i t em bo o k ; 1 1 про чита ть ISBN, количе ств о прода нных экз емпляров и цену s td : : c i n >> boo k ; / / выв е с ти ISBN, количе ств о прода нных экз емпляров , 1 1 общую сумму и среднюю цену s td : : cout # i n c l u de " S a l e s i t em . h " i n t ma i n ( ) S a l e s_ i t em i t em l , i t em2 ; s t d : : c i n > > i t em l > > i t em2 ; s t d : : c o u t < < i t e m l + i t em2 < < s t d : : e n d l ; return О ;

Если ввести следующие данные: 0-2 0 1 - 7 8 3 4 5 -Х 3 2 0 . 0 0 0- 2 0 1 - 7 8 34 5 -Х 2 25 . 0 0

то вывод будет таким: 0-2 0 1 - 7 8 3 4 5-Х 5 1 1 0 2 2

/ / про чита ть дв е транза кции / / о тобра зить их сумму

Глава 1 . П ервые шаги

50

Программа начинается с включения заголовков S a l e s _ i tem и i o s t re am. За­ тем создаются два объекта ( i teml и i tem2 ) класса S a l e s _ i tem, предназначенные для хранения транзакций. В эти объекты читаются данные со стандартного уст­ ройства ввода. Выражение вывода суммирует их и отображает результат. Обратите внимание: эта программа очень похожа на программу, приведен­ ную на стр. 31 : она читает два элемента данных и отображает их сумму. Огли­ чаются они лишь тем, что в первом елучае суммируются два целых числа, а во втором - два объекта класса S a l e s _ i t em. Кроме того, сама концепция "сум­ мы" здесь различна. В ел учае с типом i n t получается обычная сумма - резуль­ тат сложения двух числовых значений. В случае с объектами класса S a l e s_i t em используется концептуально новое понятие суммы - результат сложения соот­ ветствующих компонентов двух объектов класса S a l e s _ i tem. И СПОЛЬЗОВАНИЕ ПЕ Р ЕНАП РАВЛЕНИЯ ФАЙЛОВ

Неоднократный ввод этих транзакций при проверке программы может оказаться утомительным . Большинство операционных систем поддерживает перенаправление файлов, позволяющее ассоциировать именованный файл со стандартным устройством ввода и стандартным устройством вывода: $

add i tems < i n f i l e > o u t f i l e

Здесь подразумевается, что $ - это системное приглашение к вводу, а наша программа суммирования была откомпилирована в исполняемый файл a dd i t e m s . е х е (или a d d i t ems на системе UNIX) . Эта команда будет читать транзакции из файла i n f i l e и записывать ее вывод в файл o u t f i l e в текущем каталоге.

Упражнения раздела 1 .5.1 Упражнение 1.20. По адресу h t tp : / / www . i n fo rmi t . com / t i t l e / 0 3 2 1 7 4 1 1 3 в каталоге кода первой главы содержится копия файла S a l e s _ i tem . h. Скопи­ руйте этот файл в свой рабочий каталог и используйте при написании про­ граммы, которая читает набор транзакций проданных книг и отображает их на стандартном устройстве вывода. Упражнение 1 .21 . Напишите программу, которая читает два объекта класса S a l e s _ i t em с одинаковыми ISBN и вычисляет их сумму. Упражнение 1 .22. Напишите программу, читающую несколько транзакций с одинаковым ISBN и отображающую сумму всех прочитанных транзакций.

1 .5. В ведение в классы

51

1 . 5 .2. Пе р вый взгляд на функции-члены Программа суммирования объектов класса S a l e s _i t em должна проверять наличие у этих объектов одинаковых ISBN. Сделаем это так: # i n c l u de < i o s t r e am> # i n c l u de " S a l e s i t em . h " i n t ma i n ( ) S a l e s _i t em i t em l , i t em 2 ; s t d : : c i n > > i te m l > > i t em2 ; 1 1 сна чала пров ерить , предста вляют ли объ е кты i t em l и i t em2 1 1 одну и ту же книгу i f ( i t e m l . i s b n ( ) = = i t em2 . i s b n ( ) ) { s t d : : c o u t < < i t e m l + i t em2 < < s t d : : e n d l ; r e t u r n О ; / / свиде тель ств о успеха else { s t d : : c e r r < < " Da t a mu s t r e f e r t o s ame I S BN " < < s td : : endl ; r e t u r n - 1 ; / / свидетель с тв о о тка з а

Различие между этой программой и предыдущей версией в операторе i f и его ветви e l s e . Даже не понимая смысла условия оператора i f, вполне можно понять, что делает эта программа. Если условие истинно, вывод будет, как прежде, и возвратится значение О , означающее успех. Если условие лож­ но, выполняется блок ветви e l s e, который выводит сообщение об ошибке и возвращает значение - 1 .

Ч то такое ф ункция- член ? Условие оператора i f вызывает функцию-член (member function) i s bn ( ) . i t em l . i s b n ( )

==

i t em2 . i s b n ( )

Функция-член - это функция, определенная в составе класса. Функции­ члены называют также методами (method) класса. Вызов функции-члена обычно происходит от имени объекта класса. На­ пример, первый, левый, операнд оператора равенства использует оператор точка (dot operator) (опер атор . ) для указания на то, что имеется в виду "член i s bn ( ) объекта по имени i t em l ". i t em l . i s b n

Точечный оператор применим только к объектам типа класса. Левый опе­ ранд должен быть объектом типа класса, а правый операнд - именем члена этого класса. Резу льтатом точечного оператора является член класса, задан­ ный правым операндом.

52

Глава

1.

П е р вые шаги

Точечный оператор обычно используется для доступа к функциям-членам при их вызове. Для вызова функции используется оператор вызова (call opera­ tor) (оператор ( ) ) . Оператор обращения - это пара круглых скобок, заклю­ чающих список ар гументов (argument), который может быть пуст. Функция­ член i s b n ( ) не получает аргументов. i t em l . i s b n ( )

Таким образом, это вызов функции i s b n ( ) , являющейся членом объекта i t em l класса S a l e s _ i t em. Эта функция возвращает ISBN, хранящийся в объекте i t e m l . Правый опера1Iд оператора равенства выполняется тем же способом: он возвращает ISBN, хранящийся в объекте i t em2 . Если ISBN совпадают, условие истинно, а в противном случае оно ложно.

Упражнения раздела 1 .5.2 Упражнение 1 .23. Напиши�е программу, которая читает несколько тран­ закций и подсчитывает количество транзакций для каждого ISBN. Упражнение 1 .24. Проверьте предыдущую программу, введя несколько транзакций, представляющих несколько ISBN. Записи для каждого ISBN должны быть сгруппированы.

1 . 6.

П р о г р амма для кни ж но г о ма г азина

Теперь все готово для решения проблемы книжного магазина: следует прочи­ тать файл транзакций и создать отчет, где для каждой книги будет подсчитана общая выручка, средняя цена и количество проданных экземпляров. При этом подразумевается, что все транзакции для каждого ISBN вводятся группами. Программа объединяет данные по каждому ISBN в переменной t o t a l (все­ го) . Каждая прочитанная транзакция будем сохранена во второй переменной, t r an s . В противном случае значение объекта t o t a l выводится на экран, а за­ тем заменяется только что считанной транзакцией. # i n c l u de < i o s t r e am> # i n c l u de " S a l e s i t em . h " i n t ma i n ( ) S a l e s i t em t o t a l ;

/ / переменная для хранения да нных следующей 1 1 транза кции / / про чита ть первую транз а кцию и удо стов ерить ся в наличии да нных 1 1 для обра бо тки i f ( s td : : c in >> tot a l ) { S a l e s_i t em t r a n s ; / / переменна я для хранения те кущей тра нза кции / / чита ть и обра б о та ть о с таль ные тра нза кции wh i l e ( s td : : c i n > > t r a n s ) {

1 .6. П р ог ра мма для кни ж ного магазина

53

/ / е сли в се еще о бра ба тыв а е тся та же книга i f ( total . i sbn ( ) == t ra n s . i sbn ( ) ) t o t a l + = t r a n s ; / / попол�ение те кущей суммы else { 1 1 о то бра зить резуль та ты по предыдущей книге s t d : : c ou t < < t o t a l < < s t d : : e n d l ; t o t a l = t r a n s ; / / теперь t o t a l о тно сит ся к следующей 1 1 книге

s t d : : c o u t < < t o t a l < < s t d : : e n d l ; / / о т о бра зить по следнюю з а пись else { / / не т в в ода ! Предупредить поль з ов а теля s t d : : c e r r < < " N o da t a ? ! " < < s t d : : e n d l ; r e t u r n - 1 ; / / свиде тель с тв о о тка за return О ;

Это наиболее сложная программа из рассмотренных на настоящий момент, однако все ее элементы читателю уже знакомы. Как обычно, код начинается с подключения используемых заголовков: i o s t ream (из библиотеки) и S a l e s _ i t em . h (собственного) . В функции ma i n ( ) определен объект по имени t o t a l (для суммирования данных по текущему ISBN). Начнем с чтения первой транзакции в переменную t o t a l и проверки успешности чтения. Если чтение терпит неудачу, то никаких записей нет и управление переходит к наиболее удаленному оператору e l s e, код которого отображает сообщение, предупреждающее пользователя об отсугствии данных . Если запись введена успешно, управление переходит к блоку после наибо­ лее удаленного оператора i f. Этот блок начинается с определения объекта t ra n s , предназначенного для хранения считываемых транзакций. Оператор wh i l e читает все остальные записи. Как и в прежних прогр аммах, условие цикла wh i l e читает значения со стандартного устройства ввода. В данном случае данные читаются в объект t ra n s класса S a l e s_i t em. Пока чтение ус­ пешно, выполняется тело цикла wh i l e . Тело цикла wh i l e представляет собой один оператор i f, который проверя­ ет равенство ISBN. Если они равны, используется составной оператор при­ своения для суммирования объектов t r an s и t o t a l . Если ISBN не равны, ото­ бражается значение, хранящееся в переменной t o t a l , которой затем при­ сваивается значение переменной t r a n s . После выполнения кода оператора i f управление возвращается к условию цикла wh i l e, читающему следующую транзакцию, и так далее, до тех пор, пока записи не исчерпаются. После выхо­ да из цикла wh i l e переменная t o t a l содержит данные для последнего ISBN в файле. В последнем операторе блока наиболее удаленного оператора i f отображаются данные последнего ISBN.

Глава 1 . П ервые шаги

54

Упражнения раздела 1 .6 Упражнение 1 .25. Используя загруженный с веб-сайта заголовок S a l e s _ i tem . h, оn = . Больше или равно. Проверяет, больше или равен левый операнд правому. Опе ратор > > . Оператор ввода. Считывает в правый операнд данные из потока ввода, определенного левым операндом. Например, выражение c i n > > i считывает следую­ щее значение со стандартного устройства ввода в переменную i . Несколько опера­ ций ввода вполне можно объединить: выражение c i n > > i > > j считьmает данные сначала в переменную i, а затем в переменную j . Оператор f o r . Оператор цикла, обеспечивающий итерационное выполнение. Зачастую используется для повторения вычислений определенное количество раз. Оператор i f . Управляющий оператор, обеспечивающий выполнение на основании зна­ чения определенного условия. Если условие истинно (значение t r ue ) , выполняется тело оператора i f . В противном случае (значение f a l s e ) управление переходит к оператору e l s e . Операто р wh i l e . Оператор цикла, обеспечивающий итерационное выполнение кода тела цикла, пока его условие остается истинным. Переменн а я (variaЬle). Именованный объект. П р исвоение (assignment). Удаляет текущее значение объекта и заменяет его новым. =

=

=

57

Термины

Пространство имен (namespace) . Механизм применения имен, определенных в библио­ теках. Применение пространств имен позволяет избежать случайных конфликтов имени. Имена, определенные в стандартной библиотеке языка С++, находятся в про­ странстве имен std.

Пространство имен

Пространство имен, используемое стандартной библиотекой. Запись std::cout указывает, что используемое имя cout определено в пространстве имен std. std.

Редактирование, компиляция, отладка (edit-compile-debug) . Процесс, обеспечиваю­ щий правильное выполнение программы.

Символьны й ст р оковый литерал (character string literal) . Синоним термина

строковый

литерал .

Список параметров (parameter list). Часть определения функции. Список параметров определяет аргументы, применяемые при вызове функции. Список параметров мо­ жет быть пуст.

С тандартная библиотека (standard library). Коллекция типов и функций, которой должен обладать кажды й компилятор языка С++. Библиотека предоставляет типы для работы с потоками ввода и вывода. Под библиотекой программисты С++ подра­ зумевают либо всю стандартную библиотеку, либо ее часть, библиотеку типов. На­ пример, когда программисты говорят о библиотеке iostr eam, они подразумевают ту часть стандартной библиотеки, в которой определены классы ввода и вывода.

Стандартная ошибка (standard error) . Поток вывода, предназначенный для передачи сообщения об ошибке. Обычно потоки стандартного вывода и стандартной ошибки ассоциируются с окном, в котором выполняется программа.

С тандартны й ввод (standard input). Поток ввода, обычно ассоциируемый с окном, в ко­ тором выполняется программа.

С тандартны й вывод (standard output). Поток вывода, обычно ассоциируемый с окном, в котором выполняется программа.

Строковый литерал (string literal). Последовательность символов, заключенных в двой­ ные кавычки (например, " а string l ite ral " ) . С тр уктура данных (data structure). Логическое объединение типов данных и возможных для них операций.

Тело функции (function body). Блок операторов, определяющий выполняемые функцией действия.

Тип istr eam. Библиотечный тип, обеспечивающий потоковый ввод. Тип ostr eam. Библиотечный тип, обеспечивающий потоковый вывод. Тип возвращаемого значения (retum type). Тип возвращенного функцией значения. Тип класса (class type). Тип, определенный классом. Имя типа совпадает с именем класса. Услови е (condition). Выражение, результатом которого является логическое значение (истина) или fa lse (ложь). Нуль соответствует значению гой - значению true .

true

false ,

а любой дру­

Ф айл исходного кода (source file). Термин, используемый для описания файла, который содержит текст программы на языке С++.

Глава 1 . П ервые шаrи

58

Фигурная скобка (curly brace). Фигурные скобки разграничивают блоки кода. Огкры­ вающая фигурная скобка ( { ) начинает блок, а закрывающая ( } ) завершает его. Функция (function). Именованный блок операторов. Функция rna i n ( ) . Функция, вызываемая операционной системой при запуске програм­ мы С++. У

каждой

программы должна быть одна и только одна функция по имени

rna i n ( ) .

Функция-член (member function). Операция, определенная классом. Как правило, функции-члены применяются для работы с определенным объектом.

ЧАСТЬ

1

0 С Н ОВЬI В э то й части ... Глава 2. Переменные и б азовые типы Глава 3. Типы string, vector и массивы Глава 4. В ыражения Глава 5. Операторы Глава 6. Функции Глава 7. Классы

Все широко распространенные языки программирования предоставляют единый набор средств, отличающийся лишь специфическими подробностями конкретного языка. Понимание подробностей того, как язык предоставляет эти средства, является первым шагом к овладению данным языком. К наибо­ лее фундаментальным из этих общих средств относятся приведенные ниже. •

Встроенные типы данных (например, целые числа, символы и т.д.).



Переменные, позволяющие присваивать имена используемым объектам.



Выражения и операторы, позволяющие манипулировать значениями этих типов.



Управляющие структуры, такие как i f или wh i l e, обеспечивающие ус­ ловное и циклическое выполнение наборов действий.



Функции, позволяющие обратиться к именованным блокам действий.

Большинство языков программирования дополняет эти основные средства двумя способами: они позволяют программистам дополнять язык, определяя собственные типы, а также использовать библиотеки, в которых определены полезные функции и типы, отсутствующие в базовом языке. В языке С++, как и в большинстве языков программирования, допустимые для объекта операции определяет его тип. То есть оператор будет допустимым или недопустимым в зависимости от типа используемого объекта. Некоторые языки, например Smalltalk и Python, проверяют используемые в выражениях

типы во время выполнения программы. В отличие от них, язык С++ осуществ­ ляет контроль типов данных статически, т.е. соответствие типов проверяется во время компиляции. Как следствие, компилятор требует сообщить ему тип каж­ дого используемого в программе имени, прежде чем оно будет применено. Язык С++ предоставляет набор встроенных типов данных, операторы для манипулирования ими и небольшой набор операторов для управления про­ цессом выполнения программы. Эти элементы формируют алфавит, при по­ мощи которого можно написать (и было написано) множество больших и сложных реальных систем . На этом базовом уровне язык С++ довольно прост. Его потрясающая мощь является результатом поддержки механизмов, которые позволяют программисту самостоятельно определять новые структу­ ры данных. Используя эти средства, программисты могут приспособить язык для собственных целей без участия его разработчиков и необходимости ожи­ дать, пока они удовлетворят появившиеся потребности. Возможно, важнейшим компонентом языка С++ является класс, который позволяет программистам определять собственные типы данных. В языке С++ такие типы иногда называют "типами класса", чтобы отличить их от базовых типов, встроенных в сам язык. Некоторые языки программирования позволя­ ют определять типы, способные содержать только данные. Другие, подобно языку С++, позволяют определять типы, в состав которых можно включить операции, выполняемые с этими данными. Одна из главных задач проекта С++ заключалась в предоставлении программистам возможности самостоя­ тельно определять типы данных, которые будут так же удобны, как и встроен­ ные. Стандартная библиотека языка С++ использует эту возможность для реа­ лизации обширного набора классов и связанных с ними функций. Первым шагом по овладению языком С++ является изучение его основ и библиотеки - такова тема части 1, "Основы" . В главе 2 рассматриваются встроенные типы данных, а также обсуждается механизм определения новых, собственных типов. В главе 3 описаны два фундаментальных библиотечных типа: s t r i n g (строка) и ve c t o r (вектор) . В этой же главе рассматриваются массивы, представляющие собой низкоуровневую структуру данных, встро­ енную в язык С++, и множество других языков. Главы 4-6 посвящены выраже­ ниям, операторам и функциям . Завершается часть главой 7 демонстрирую­ щей основы построения собственных типов классов. Как мы увидим, в опреде­ лении собственных типов примиряется все, что мы изучили до сих пор, поскольку написание класса подразумевает использование всех средств, час­ тично раскрытых в части 1 .

ГЛАВА

2

П ЕРЕ М ЕНН ЫЕ И Б АЗ ОВЫЕ Т И П Ы

В э то й r лаве ... 2.1 . Простые встроенные типы 2.2. Переменные 2.3. Составные типы 2.4. Спецификатор const 2.5. Работа с типа.ми 2.6. Определение собственных структур данных Резюме Термины

61 74 84 95 1 05 111 118 118

Типы данных - это основа любой программы: они указывают, что именно означают эти данные и какие операции с ними можно выполнять. У языка С++ обширная поддержка таких типов. В нем определено несколь­ ко базовых типов: символы, целые числа, числа с плавающей запятой и т.д. Язык предоставляет также механизмы, позволяющие программисту опреде­ лять собственные типы данных. В библиотеке эти механизмы использованы для определения более сложных типов, таких как символьные строки пере­ менной длины, векторы и т.д. В этой главе рассматриваются встроенные типы данных и основы применения более сложных типов. Тип определяет назначение данных и операции, которые с ними можно выполнять. Например, назначение простого оператора i = i + j ; полно­ стью зависит от типов переменных i и j . Если это целые числа, данный опе­ ратор представляет собой обычное арифметическое сложение. Но если это объекты класса S a l e s _ i t em, то данный оператор суммирует их компоненты (см раздел 1 .5 . 1 , стр . 47) .

2.1 .

Пр остые вст р оенные типы

В языке С + + определен набор базовых типов, включая арифметические ти­ пы (arithmetic type), и специальный тип vo i d. Ари фметические типы пред-

Глава

62

2.

П еременные и базовые типы

ставляют символы, целые числа, логические значения и числа с плавающей запятой. С типом vo i d не связано значений, и применяется он только при не­ которых обстоятельствах, чаще всего как тип возвращаемого значения функ­ ций, которые не возвращают ничего.



2.1 .1 . Ар ифметические типы

Есть две разновидности арифметических типов: целочисленные типы (включая символьные и логические типы) и типы с плавающей запятой. Размер (т.е. количество битов) арифметических типов зависит от конкрет­ ного компьютера. Стандарт гарантирует минимальные размеры, перечислен­ ные в табл. 2. 1 . Однако компиляторы позволяют использовать для этих типов большие размеры. Поскольку количество битов не постоянно, значение одно­ го типа также может занимать в памяти больше или меньше места.

Таблиц а 2.1 . Ар и ф ме т и ч еские т ип ы языка С++ Тип

Значение

Минимальный р азме р

boo l

Логический тип

Не определен

cha r

Символ

8 битов

wchar t

Широкий символ

1 6 битов

cha r l б t

Символ Unicode

1 6 битов

ch a r 3 2

Символ Unicode

32 бита

s ho r t

Короткое целое число

1 6 битов

int

Целое число

1 6 битов

long

Длинное целое число

32 бита

long long

Длинное целое число

64 бита

f l o at

Число с плавающей запятой одинарной точности

6 значащих цифр

douЫ e

Число с плавающей запятой двойной точности

1 0 значащих цифр

l o ng douЫ e

Число с плавающей запятой повышенной точности

1 0 значащих цифр

t

Тип bo o l представляет только значения t rue (истина) и f a l s e (ложь) . Существует несколько символьных типов, большинство из которых пред­ назначено для поддержки национальных наборов символов. Базовый сим­ вольный тип, cha r, гарантировано велик, чтобы содержать числовые значе­ ния, соответствующие символам базового набора символов машины. Таким образом, тип cha r имеет тот же размер, что и один байт на данной машине .

2.1 . П ростые вст р оенные типы

63

Остальные символьные типы, wcha r_t, cha r l б_t и cha r 3 2_t, используют­ ся для расширенных наборов символов. Тип wcha r_t будет достаточно боль­ шим, чтобы содержать любой символ в наибольшем расширенном наборе символов машины. Типы cha r l б_t и cha r 3 2 _t предназначены для символов Unicode. (Unicode - это стандарт для представления символов, используемых, по существу, в любом языке.) Остальные целочисленные типы представляют целочисленные значения разных размеров. Язык С++ гарантирует, что тип i n t будет по крайней мере не меньше типа s h o r t, а тип l ong l o ng не меньше типа l o ng. Тип l on g l on g введен новым стандартом. -

МАШИННЫЙ УРОВЕНЬ ПРЕДСТАВЛЕНИЯ ВСТРОЕ ННЫХ ТИПОВ

Компьютеры хранят данные как последовательность битов, каждый из которых содержит О или 1 : 00011011011100010110010000111011

Большинство компьютеров оперируют с памятью, разделенной на пор­ ции, размер которых в битах кратен степеням числа 2. Наименьшая порция адресуемой памяти называется байтом (byte) . Основная единица хранения, обычно в несколько байтов, называется словом (word) . В языке С++ байт со­ держит столько битов, сколько необходимо для содержания символа в базо­ вом наборе символов машины . На большинстве компьютеров байт содер­ жит 8 битов, а слово - 32 или 64 бита, т.е. 4 или 8 байтов. У большинства компьютеров каждый байт памяти имеет номер, назы­ ваемый адресом (address). На машине с 8-битовыми байтами и 32-битовыми словами слова в памяти можно было бы представить следующим образом: 736424

о

о

1

1

1

о

1

1

736425

о

о

о

1

1

о

1

1

736426

о

1

1

1

о

о

о

1

736427

о

1

1

о

о

1

о

о

Слева представлен адрес байта, а 8 битов его значения - справа. При помощи адреса можно обратиться к любому из байтов, а также к набору из нескольких байтов, начинающемуся с этого адреса . В этом ел у­ чае говорят о доступе к байту по адресу 736424 или о байте, хранящемуся по адресу 736426 . Чтобы получить представление о значении в области памяти по данному адресу, следует З нать тип хранимого в ней значения . Именно тип определяет количество используемых битов и то, как эти биты интер­ претировать.

64

Глава 2. П еременные и базовые типы

Если известно, что объект в области по адресу 736424 имеет тип f l o a t, и если тип f l o a t на этой машине хранится в 32 битах, то известно и то, что объект по этому адресу охватывает все слово. Значение этого числа зависит от того, как именно машина хранит числа с плавающей запятой. Но если объект в области по адресу 736424 имеет тип un s i gn e d cha r, то на машине, исполь­ зующей набор символов ISO-Latin-1 , этот байт представляет точку с запятой. Типы с плавающей точкой представляют значения с одиночной, двойной и расширенной точностью. Стандарт определяет минимальное количество значащих цифр . Большинство компиляторов обеспечивает большую точность, чем минимально определено стандартом. Как правило, тип f l o a t представля­ ется одним словом (32 бита), тип douЫ e - двумя словами (64 бита), а тип l ong douЫ e - тремя или четырьмя словами (96 или 1 28 битов) . Типы f l o a t и doub l e обычно имеют примерно п о 7 и 1 6 значащих цифр соответственно. Тип l o ng douЫ e зачастую используется для адаптации чисел с плавающей запятой аппаратных средств специального назначения; его точность, вероят­ но, также зависит от конкретной реализации этих средств.

З наковые и б еззнаковые типы За исключением типа bo o l и расширенных символьных типов целочислен­ ные типы могут быть знаковыми (signed) или беззнаковыми (unsigned ). Знако­ _ вый тип способен представлять отрицательные и положительные числа (включая нуль); а беззнаковый тип - только положительные числа и нуль. Типы i n t, s h o r t, l o ng и l o ng l o ng являются знаковыми. Соответствую­ щий беззнаковый тип получают добавлением части u n s i gn e d к названию та­ кого типа, например u n s i g n e d l ong. Тип un s i gn e d i n t может быть сокра­ щен до un s i gn e d. В отличие от других целочисленных типов, существуют три разновидности базового типа cha r : cha r, s i gn e d c h a r и u n s i g n e d cha r . В частности, тип cha r отличается от типа s i gn e d cha r . На три символьных типа есть только два представления: знаковый и беззнаковый. Простой тип cha r использует одно из этих представлений . Какое именно, зависит от компилятора. В беззнаковом типе все биты представляют значение. Например, 8-битовый тип un s i gn e d cha r может содержать значения от О до 255 включительно. Стандарт не определяет представление знаковых типов, но он указывает, что диапазон должен быть поровну разделен между положительными и отри­ цательными значениями. Следовательно, 8-битовый тип s i gn e d cha r гаран­ тированно будет в состоянии содержать значения от -1 27 до 1 27; большинство современных машин использует представления, позволяющие содержать зна­ чения от -1 28 до 1 27.

2 . 1 . Простые встроенные типы

65

СОВЕТ. КА К ОЙ ТИП ИСПОЛЬЗОВАТЬ

Подобно языку С, язык С++ был разработан так, чтобы по необходимо­ сти программа могла обращаться непосредственно к аппаратным средст­ вам. Поэтому арифметические типы определены так, чтобы соответствовать особенностям различных ·а ппаратных средств. В результате количество воз­ можных арифметических типов в языке С++ огромно. Большинство про­ граммистов, желая избежать этих сложностей, ограничивают количество фактически используемых ими типов. Ниже приведено несколько эмпири­ ческих правил, способных помочь при выборе используемого типа. •

Используйте беззнаковый тип, когда точно знаете, что значения не могут быть отрицательными.



Используйте тип i n t для целочисленной арифметики. Тип s h o r t обыч­ но слишком мал, а тип l o n g на практике зачастую имеет тот же размер, что и тип i n t . Если ваши значения больше, чем минимально гарантиру­ ет тип i n t, то используйте тип l o ng l o ng.



Не используйте базовый тип cha r и тип boo l в арифметических выра­ жениях. Используйте их только для хранения символов и логических значений. Вычисления с использованием типа c h a r особенно проблема­ тичны, поскольку на одних машинах он знаковый, а на других беззнако­ вый . Если необходимо маленькое целое число, явно определите тип как s i gn e d cha r или u n s i g n e d c h a r .



Используйте тип doub l e для вычислений с плавающей точкой. У типа f l oa t обычно недостаточно точности, а различие в затратах на вычисле­ ния с двойной и одиночной точностью незначительны. Фактически на некоторых машинах операции с двойной точностью осуществляются бы­ стрее, чем с одинарной. Точность, предоставляемая типом l o ng douЫ e, обычно чрезмерна и не нужна, а зачастую влечет значительное увеличе­ ние продолжительности выполнения.

Упражнени я раздела 2.1 .1 Упражнение 2.1 . Каковы различия между типами i n t, l on g, l o ng l o ng и sho r t ? Между знаковыми и беззнаковыми типами? Между типами f l o at и douЫ e ?

Упражнение 2.2. Какие типы вы использовали б ы для коэффициента, ос­ новной суммы и платежей при вычислении выплат по закладной? Объясни­ те, почему вы выбрали каждый из типов?

Глава 2. П еременные и базовые типы

66



2.1 .2. Преобразование типов

Тип объекта определяет данные, которые он может содержать, и операции, которые с ним можно выполнять. Среди операций, померживаемых множе­ ством типов, есть возможность преобр азовать (convert) объект данного типа в другой, связанный тип. Преобразование типов происходит автоматически, когда объект одного ти­ па используется там, где ожидается объект другого типа. Более подробная информация о преобразованиях приведена в разделе 4.1 1 (стр . 21 8), а пока имеет смысл понять, что происходит при присвоении значения одного типа объекту другого. Когда значение одного арифметического типа присваивается другому bool Ь = 4 2 ; int i = Ь ; i = 3 . 14; douЫ e p i = i ; un s igned char с = - 1 ; s i gn e d c h a r с 2 = 2 5 6 ;

11 11 11 11 11 11

Ь с одержит

t ru e i с одержит зна чение 1 i содержит зна чение 3 pi содержит зна чение 3 . 0 при В - битов ом ch a r содержит зна чение 2 5 5 при В - битов ом ch a r зна чение с 2 неопрелелено

происходящее зависит от диапазона значений, померживаемых типом. •

Когда значение одного из не логических арифметических типов при­ сваивается объекту типа b o o l, результат будет f a l s e, если значением является О, а в противном случае t ru e . -



Когда значение типа b o o l присваивается одному из других арифмети­ ческих типов, будет получено значение 1 , если логическим значением было t rue, и О , если это было f a l s e .



Когда значение с плавающей точкой присваивается объекту целочис­ ленного типа, оно усекается до части перед десятичной точкой.



Когда целочисленное (интегральное) значение присваивается объекту типа с плавающей точкой, дробная часть равна нулю. Если у целого числа больше битов, чем может вместить объект с плавающей точкой, то точность может быть потеряна.



Если объекту беззнакового типа присваивается значение не из его диа­ пазона, результатом будет остаток от деления по модулю значения, ко­ торые способен содержать тип назначения. Например, 8-битовый тип un s i gn e d cha r способен содержать значения от О до 255 включительно. Если присвоить ему значение вне этого диапазона, то компилятор при­ своит ему остаток от деления по модулю 256. Поэтому в результате при­ своения значения - 1 переменной 8-битового типа un s i gn e d cha r будет получено значение 2 5 5 .

2.1 . П ростые встроенные типы •

67

Если объекту знакового типа присваивается значение не из его диапазо­ на, результат оказывается неопределенен. В результате программа может сработать нормально, а может и отказать или задействовать неверное значение.

СОВЕТ. ИЗБЕГАЙТЕ НЕОПРЕДЕЛЕННОГО И МАШИННО -ЗАВИСИМОГО ПОВЕДЕНИЯ

Результатом неопределенного поведения являются такие ошибки, кото­ рые компилятор не обязан (а иногда и не в состоянии) обнаруживать. Даже если код компилируется, то программа с неопределенным выражением все равно ошибочна. К сожалению, программы, характеризующиеся неопределенным пове­ дением на некоторых компиляторах и при некоторых обстоятельствах, мо­ гут работать вполне нормально, не проявляя проблему. Но нет никаких га­ рантий, что та же программа, откомпилированная на другом компилят оре или даже на следующей версии данного компилятора, продолжит работать правильно. Нет даже гарантий того, что, нормально работая с одним набо­ ром данных, она будет нормально работать с другим. Аналогично в программах нельзя полагаться на машинно-зависимое по­ ведение. Не стоит, например, надеяться на то, что переменная типа i n t имеет фиксированный, заранее известный размер . Такие программы назы­ вают непереносимыми (nonportaЬle) . При переносе такой программы на дру­ гую машину любой полагающийся на машинно-зависимое поведение код, вероятней всего, сработает неправильно, поэтому его придется искать и ис­ правлять. Поиск подобных проблем в ранее нормально работавшей про­ грамме, мягко говоря, не самая приятная работа. Компилятор применяет эти преобразования типов при использовании зна­ чений одного арифметического типа там, где ожидается значение другого арифметического типа. Например, при использовании значения, отличного от логического в условии, арифметическое значение преобразуется в тип bo o l таким же образом, как при присвоении арифметического значения пе­ ременной типа boo l : int i = 4 2 ; i f ( i ) / / условие ра с сма трив а е тся ка к ис тинно е i = О;

При значении О условие будет ложным, а при всех остальных (отличных от нуля) - истинным.

Гла ва 2. П еременные и б а зо � ые типы

68

К тому же при использовании значения типа b o o l в арифметическом вы­ ражении оно всегда преобразуется в О или 1 . В результате применение логи­ ческого значения в арифметическом выражении является неправильным .



В ыр ажения, за де й ствующие б еззнаков ы е типы

Хотя мы сами вряд ли преднамеренно присвоим отрицательное значение объ­ екту беззнакового типа, мы можем (причем слишком легко) написать код, кото­ рый сделает это неявно. Например, если использовать значения типа uns i gned и int в арифметическом выражении, значения типа int обычно преобразуются в тип uns i gned. Преобразование значения типа i n t в un s i gned выполняется та­ ким же способом, как и при присвоении: uns igned u = 1 0 ; int i = - 4 2 ; std : : cout > d a t a l . u n i t s s o l d > > p r i c e ; 1 1 вычислить общий доход из pri ce и u n i t s_ s o l d dat a l . reve n u e = d a t a l . u n i t s s o l d * p r i c e ; 11

11

Транзакции содержат цену, по которой была продана каждая книга, но структура данных хранит общий доход. Данные транзакции будем читать

2.6. О пределение собственных структур данных

115

переменную p r i ce (цена) типа douЬ l e, исходя и з которой и вычислим член reve nue (доход) .

в

s t d : : c i n > > da t a l . b o o kN o > > d a t a l . u n i t s_ s o l d > > p r i c e ;

Для чтения значений членов b o o kNo и u n i t s _ s o l d (продано экземпляров) объекта по имени da t a l оператор ввода использует точечный оператор (см. раздел 1 .5.2, стр. 51 ) Последний оператор присваивает произведение dat a l . u n i t s _ s o l d и p r i ce переменной-члену revenue объекта da ta l . Затем программа повторяет тот же код для чтения данных в объект dat a 2 . .

1 1 чита ть в торую тра нз а кцию s t d : : c i n > > da t a 2 . b o o kNo > > d a t a 2 . u n i t s s o l d > > p r i c e ; da t a 2 . r e ve n u e = da t a 2 . u n i t s s o l d * p r i c e ;

В ывод суммы двух о б ъ ектов класса Sales_data Следующая задача - проверить наличие у транзакций одинакового ISBN. Если это так, вывести их сумму, в противном случае отобразить сообщение об ошибке. if

( da t a l . b o o kN o == d a t a 2 . b o o kN o ) { u n s i g n e d t o t a l C n t = d a t a l . un i t s s o l d + d a t a 2 . u n i t s s o l d ; douЬ l e t o t a l Re v e n u e = d a t a l . r e v e n u e + d a t a 2 . r e v e n u e ; 1 1 выв е с ти : ISBN, о бще е количе ств о прода нных экз емпляров , 1 1 о бщий доход , среднюю цену з а книгу s t d : : c o u t < < da t a l . b o o kN o < < " " < < t o t a l C n t < < " " < < t o t a l Re ve n u e < < " " ; i f ( totalCnt ! = 0 ) s t d : : c o u t < < t o t a l Re v e n u e / t o t a l C n t < < s t d : : e n d l ; else s td : : cout v l >> v2 ; c o u t < < " T he s um o f " < < v l < < " a n d " < < v 2 < < " is " < < vl + v2 < < endl ; return О ;

Объявления u s i n g для имен c i n, c o u t и e n d l означают, что их можно те­ перь использовать без префикса s t d : : . Напомню, что программы С++ позво­ ляют поместить каждое объявление u s i n g в отдельную строку или объеди­ нить в одной строке несколько объявлений. Важно не забывать, что для каж­ дого используемого имени необходимо отдельное объявление u s i n g, и каждое из них должно завершаться точкой с запятой.

З аголовки не должны содержать о б ъ явлени й us ing Код в заголовках (см. раздел 2.6.3, стр. 1 1 6) обычно не должен использовать объявления u s i ng. Дело в том, что содержимое заголовка копируется в текст программы, в которую он включен. Если в заголовке есть объявление u s i ng, то каждая включающая его программа получает то же объявление u s i ng. В резуль­ тате программа, которая не намеревалась использовать определенное библио­ течное имя, может елучайно столкнугься с неожиданным конфликтом имен.

П р имечание для читателя Начиная с этого момента подразумевается, что во все примеры включены объявления u s i n g для имен из стандартной библиотеки. Таким образом, в тексте и примерах кода далее упоминается c i n, а не s t d : : c i n . Кроме того, для экономии места в примерах кода не будем показывать да­ лее объявления u s i n g и необходимые директивы # i n c l u de . В табл. А.1 (стр. 1 077) приложения А приведены имена и соответствующие заголовки стандартной библиотеки, которые использованы в этой книге.

Глава 3. Типы string, vector и массивы

126

& ВНИМАНИЕ

Читатели не должны забывать добавить соответствующие объявле­ ния # i n c l u de и u s i n g в свои примеры перед их компиляцией.

Упражнения раздела

3.1

Перепишите упражнения из разделов 1 .4.1 (стр. 39) и 2.6.2 (стр . 1 1 6), используя соответствующие объявления u s i n g.

Упражнение



3. 2.

3.1 .

Библиотечный тип

Строка (string)

s tring

это последовательность символов переменной длины. Что­ бы использовать тип s t r i ng, необходимо включить в код заголовок s t r i ng. Поскольку тип s t r i ng принадлежит библиотеке, он определен в пространстве имен s t d. Наши примеры подразумевают наличие следующего кода: -

# i n c l ude < s t r i n g > u s ing s td : : s t r i ng ;

В этом разделе описаны наиболее распространенные операции со строка­ ми; а дополнительные операции рассматриваются в разделе 9.5 (стр. 462). Кроме определения операций, предоставляемых библиотечными типами, стандарт налагает также требования на эффективность их конструкторов. В результате библиотечные типы оказались весьма эффективны в использовании .



3.2.1 . Оп р еделение и инициализация ст р ок

Каждый класс определяет, как могут быть инициализированы объекты его типа. Класс может определить много разных способов инициализации объек­ тов своего типа. Каждый из способов отличается либо количеством предостав­ ляемых инициализаторов, либо типами этих инициализаторов. Список наибо­ лее распространенных способов инициализации строк приведен в табл. 3.1 , а некоторые из примеров приведены ниже. s t r i ng s t r i ng string string

sl; s2 = sl ; s З = " h i ya " ; s4 (10, ' с ' ) ;

// // // //

инициализ а ция по умолча нию ; s l s2 копия s l sЗ копия с троков ого литерала s4 сссссссссс

-

пус тая с трока

-

-

-

Инициализация строки по умолчанию (см. раздел 2.2. 1 , стр . 77) создает пустую строку; т.е. объект класса s t r i n g без символов. Когда предоставляется строковый литерал (см. раздел 2.1 .3, стр . 70), во вновь созданную строку копи­ руются символы этого литерала, исключая завершающий нулевой символ. При предоставлении количества и символа строка содержит указанное коли­ чество экземпляров данного символа.

3.2. Б иблиотечный тип string

127

Таб лица 3 .1 . С посо б ы иници ал и з ации о б ъ екта кл асс а s tring st r i ng s l

Инициализация по умолчанию; s 1 - пустая строка

string s 2 ( s l )

s 2 - копия s l

s t r i ng s 2

Эквивалент s 2 ( s 1 ) , s 2 - копия s 1

=

sl

s t r i n g s 3 ( " va l ue " )

s 3 - копия строкового литерала, ну левой символ не включен

string s З

Эквивалент s З ( " va l ue " ) , s З - копия строкового литерала

" va l ue "

=

s t r i ng s 4 ( n ,

'с')

Инициализация переменной s 4 символом ' с ' в количестве n штук

Пря м ая ини ц иализа ц ия и ини ц иализа ц ия копие й В разделе 2.2. 1 (стр . 77) упоминалось, что язык С++ поддерживает несколь­ ко разных форм инициализации. Давайте на примере класса s t r i n g начнем изучать, чем эти формы отличаются друг от друга. Когда переменная ини­ циализируется с использованием знака компилятор просят скопировать инициализирующий объект в создаваемый объект, т.е. выполнить и1-шциализа­ ц,uю копией (сору initialization) . В противном случае без знака осуrцествляет­ ся прямая инициализация (direct initialization). Когда имеется одиночный инициализатор, можно использовать и прямую форму, и инициализацию копией. Но при инициализации переменной не­ сколькими значениями, как при инициализации переменной s 4 выше, следу­ ет использовать прямую форму инициализации. =

,

=

s t r i n g s5 " h i ya " ; s t r i n g s б ( " h i ya " ) ; string s 7 ( 1 0 , ' с ' ) ; =

/ / инициализ а ция копией / / пряма я инициализ а ция / / пряма я инициализ а ция ; s 7

-

сссссссссс

Если необходимо использовать несколько значений, можно применить кос­ венную форму инициализации копией при явном создании временного объек­ та для копирования. string

s8

=

s t r i ng ( l O ,

'с' ) ;

/ / инициализа ция копией ; s B

-

с ссссссссс

Инициализатор строки s 8 - s t r i n g ( 1 0 , ' с ' ) - создает строку заданно­ го размера, заполненную указанным символьным значением, а затем копиру­ ет ее в строку s 8 . Это эквивалентно следующему коду: str i n g t emp ( l O , ' с ' ) ; s t r i n g s8 = t emp ;

/ / t emp сссссссссс / / копиров а ть t emp в s B -

Хотя используемый для инициализации строки s 8 код вполне допустим, он менее читабелен и не имеет никаких преимуществ перед способом, которым была инициализирована переменная s 7 .

Глава 3. Типы string, vector и массивы

1 28



3.2.2. Опе р ации со ст р оками

Наряду с определением способов создания и инициализации объектов класс определяет также операции, которые можно выполнять с объектами класса. Класс может определить обладающие именем операции, такие как функция i s bn ( ) класса S a l e s_i t em (см. раздел 1 .5.2 стр. 51 ). Класс также может определить то, что означают различные символы операторов, такие как < < или +, когда они применяются к объектам класса. Наиболее распро­ страненные операции класса s t r i n g приведены в табл. 3.2.

Таб лица 3 .2. Опе р ации кл асса s tring os

>

s

Читает разделенную пробелами строку s из потока i s . Воз­ вращает поток i s

get l i n e ( i s , s )

Читает строку ввода из потока i s в переменную s . Возвращает поток i s

s . emp t y ( )

Возвращает значение t r ue, если строка s пуста. В противном случае возвращает значение fa l s e

s . size ( )

Возвращает количество символов в строке s

s [n]

Возвращает ссылку на символ в позиции n строки s ; позиции отсчитываются от О

sl

+

sl sl sl

1

=

s2

Возвращает строку, состоящую из содержимого строк s l и s 2

s2

Заменяет символы строки s 1 копией содержимого строки s 2

s2 s2

< , < =, >, > =

Строки s l и s 2 равны, если содержат одинаковые символы. Ре­ гистр символов учитывается Сравнение зависит от регистра и полагается на алфавитный порядок символов

Ч тение и запись ст р о к Как уже упоминалось в главе 1 , для чтения и записи значений встроенных типов, таких как i n t, douЫ e и т.д., используется библиотека i o s t r e am. Для чтения и записи строк используются те же операторы ввода и вывода. Обра тите внима ние : перед компиляцией э т о т код следуе т дополнить директив ами #i n c l u de и объявлениями u s i n g 11 i n t rna i n ( ) 11

str ing s ; cin >> s ; cout > i ve c ;

(Ь)

ve c t o r < s t r i n g > s v e c

(с)

ve c t o r < s t r i n g > s ve c ( 1 0 ,

=

i ve c ; " nul l " ) ;

Упражнение 3 . 1 3 . Сколько элементов находится в каждом из следующих векторов? Каковы значения этих элементов? (а)

ve c t o r < i n t > v l ;

(с)

ve c t o r < i n t > v 3 ( 1 0 ,

(е)

\Te c t o r < i n t > v 5 { 1 0 ,

(g)

ve c t o r < s t r i n g > v 7 { 1 0 ,

(Ь)

vecto r < i n t > v2 ( 1 0 ) ;

42 ) ;

(d)

ve c t o r < i n t > v 4 { 1 0 } ;

42 } ;

(f)

ve c t o r < s t r i n g > v 6 { 1 0 } ;

"hi " } ;

3.3.2. До б авление элементов в векто р Прямая инициализация элементов вектора осуществима только при не­ большом количестве исходных значений, при копировании другого вектора и при инициализации всех элементов тем же значением. Но обычно при соз­ дании вектора неизвестно ни количество его элементов, ни их значения. Но даже если все значения известны, то определение большого количества раз­ ных начальных значений может оказаться очень громоздким, чтобы распола­ гать его в месте создания вектора. Если необходим вектор со значениями от О до 9, то можно легко использо­ вать списочную инициализацию. Но что если необходимы элементы от О до 99 или от О до 999? Списочная инициализация была бы слишком громоздкой. В таких ел учаях лучше создать пустой вектор и использовать его функцию­ член pu sh _ba c k ( ) , чтобы добавить элементы во время выполнения. Функция pu sh_ba c k ( ) вставляет переданное ей значение в вектор как новый послед­ ний элемент. Рассмотрим пример.

3.3. Биб л иотечный тип vector

147

ve c t o r < i n t > v 2 ; / / пус той в е ктор for ( i nt i = О ; i ! = 1 0 0 ; + + i ) v 2 . p u s h_b a c k ( i ) ; 1 1 добавить последов а тельно сть целых чисел в v2 1 1 по зав ершении цикла v2 име е т 1 0 0 элементов со зна чениями о т О до 9 9

Хотя заранее известно, что будет 1 00 элементов, вектор v2 определяется как пустой. Каждая итерация добавляет следующее по порядку целое число в век­ тор v2 как новый элемент. Тот же подход используется, если необходимо создать вектор, количество элементов которого до времени выполнения неизвестно. Например, в вектор можно читать введенные пользователем значения. 1 1 чита ть слов а со ста ндартного ус тройс тв а вв ода и сохра нять их 1 1 в в екторе ка к элементы s t r i ng word ; ve c t o r < s t r i n g > t e x t ; / / пустой в е ктор wh i l e ( c i n > > w o r d ) { t e x t . pu s h_b a c k ( wo r d ) ; / / добавить слов о в текст

И снова все начинается с пустого вектора. На сей раз, неизвестное количе­ ство значений читается и сохраняется в векторе строк t e x t . КЛЮЧЕВАЯ КОНЦЕПЦИЯ. Рост ВЕКТОРА ЭФФЕКТИВЕН

Стандарт требует, чтобы реализация шаблона ve c t o r обеспечивала эф­ фективное добавление элементов во время выполнения. Поскольку рост вектора эффективен, определение вектора сразу необходимого размера за­ частую является ненужным и может даже привести к потере производи­ тельности. Исключением является случай, когда все элементы нуждаются в одинаковом значении. При разных значениях элементов обычно эффек­ тивней определить пустой вектор и добавлять элементы во время выполне­ ния, по мере того, как значения становятся известны. Кроме того, как будет продемонстрировано в разделе 9.4 (стр . 457), шаблон ve c t o r предоставляет возможности для дальнейшего увеличения производительности при добав­ лении элементов во время выполнения. Начало с пустого вектора и добавление элементов во время выполнения кардинально отличается от использования встроенных массивов в языке С и других языках. В частности, если вы знакомы с языком С или Java, то, ве­ роятно, полагаете, что лучше определить вектор в его ожидаемом размере, но фактически имеет место обратное.

П осле дствия возможности до б авления э лементов в вектор Тот факт, что добавление элементов в вектор весьма эффективно, сущест­ венно упрощает многие задачи программирования. Но эта простота налагает

148

Глава 3. Типы string, vector и массивы

новые обязательства на наши программы: необходимо гарантировать кор­ ректность всех циклов, даже если цикл изменяет размер вектора. Другое последствие динамического характера векторов станет яснее, когда мы узнаем больше об их использовании. Но есть одно последствие, на которое стоит обратить внимание уже сейчас: по причинам, изложенным в разделе 5.4.3 (стр . 252), нельзя использовать серийный оператор f o r, если тело цикла добавляет элементы в вектор .

&

Тело серийного оператора f o r не должно изменять размер пере­ бираемой последовательности.

ВНИМАНИЕ

Упражнения раздела

3.3.2

Напишите программу, читающую последовательность целых чисел из потока c i n и сохраняющую их в векторе.

Упражнение

3.1 4.

Упражнение 3.1 5. Повторите предыдущую программу, но на сей раз чи­ тайте строки.



3.3.3. Д ругие опе р ации с векто р ами

Кроме функции pu s h _ba c k ( ) , шаблон ve c t o r предоставляет еще не­ сколько операций, большинство из которых подобно соответствующим опе­ рациям класса s t r i ng. Наиболее важные из них приведены в табл. 3.5.

Т аб лица 3 . 5 . Опе р ации с векто р а ми v . emp t y ( )

Возвращает значение t r ue, если вектор v пуст. В противном случае возвращает значение f a l s e

v . s i ze ( )

Возвращает количество элементов вектора v

v . pu s h _ba c k ( t )

Добавляет элемент со значением t в конец вектора v

v[n]

Возвращает ссылку на элемент в позиции n вектора v

vl

v2

vl

{а,Ь, с

vl vl

'=

v2 v2

< , < =, >, >=

Заменяет элементы вектора v l копией элементов вектора v 2

. .

.

}

Заменяет элементы вектора v 1 копией элементов и з разде­ ляемого запятыми списка Векторы v l и v 2 равны, если они содержат одинаковые эле­ менты в тех же позициях Имеют обычное значение и полагаются на алфавитный порядок

3.3. Б иблиотечный тип vector

1 49

Досrуп к элементам вектора осуществляется таким же способом, как и к сим­ волам строки: по их позиции в векторе. Например, для обработки все элементов вектора можно использовать серийный оператор f o r (раздел 3.2.3, стр. 1 34). ve c t o r < i n t > v { l , 2 , 3 , 4 , S , 6 , 7 , 8 , 9 } ; for ( auto & i : v ) / / для каждого элемента в е ктора v ссьиr ка ) 1 1 ( о бра тите внима ние : i / / кв адра т зна чения элемента i *= i; for ( auto i v) / / для каждого элемента в е ктора v c o u t < < i < < " " ; / / выв од элемента c o u t : : s i z e_t yp e / / o k ve c t o r : : s i z e _ t yp e / / ошибка

Операторы равенства и сравнения вектора ведут себя как соответствующие операторы класса s t r i n g (раздел 3.2.2 стр. 1 31 ) . Два вектора равны, если у них одинаковое количество элементов и значения соответствующих элементов совпадают. Операторы сравнения полагаются на алфавитный порядок: если у векторов разные размеры, но соответствующие элементы равны, то вектор с меньшим количеством элементов меньше вектора с болыпим количеством элементов. Если у элементов векторов разные значения, то их отношения оп­ ределяются по первым отличающимся элементам. Сравнить два вектора можно только в том елучае, если возможно сравнить элементы этих векторов. Некоторые классы, такие как s t r i ng, определяют смысл операторов равенства и сравнения. Другие, такие как класс S a l e s _i tem, этого не делают. Операции, поддерживаемые классом S a l e s _i tem, перечисле-

1 50

Глава 3. Типы string, vector и массивы

ны в разделе 1 .5.1 (стр. 47). Они не включают ни операторов равенства, ни срав­ нения. В результате нельзя сравнить два вектора объектов класса S a l e s _ i tern.

В ычисление индекса вектора Используя оператор индексирования (раздел 3.2.3, стр. 1 38), можно вы­ брать указанный элемент. Подобно строкам, индексирование вектора начи­ наются с О; индекс имеет тип s i z e_t ype соответствующего типа; и если век­ тор не константен, то в возвращенный оператором индексирования элемент можно осуществить запись. Кроме того, как было продемонстрировано в раз­ деле 3.2.3 (стр. 1 39), можно вычислить индекс и непосредственно обратиться к элементу в данной позиции. Предположим, имеется набор оценок степеней в диапазоне от О до 1 00. Не­ обходимо рассчитать, сколько оценок попадает в кластер по 1 0. Между нулем и 1 00 возможна 1 01 оценка. Эти оценки могут быть представлены 1 1 кластера­ ми: 1 0 кластеров по 1 0 оценок каждый плюс один кластер для наивысшей оценки 1 00. Первый кластер подсчитывает оценки от О до 9, второй - от 1 0 до 1 9 и т.д. Заключительный кластер подсчитывает количество оценок 1 00. Таким образом, если введены следующие оценки: 4 2 65 9 5 1 0 0 3 9 6 7 9 5 7 6 8 8 7 6 8 3 92 7 6 9 3

результат их кластеризации должен быть таким: о о о 1 1 о 2 3 2 4 1

Он означает, что не было никаких оценок ниже 30, одна оценка в 30-х, одна в 40-х, ни одной в 50-х, две в 60-х, три в 70-х, две в 80-х, четыре в 90-х и одна оценка 1 00. Используем для содержания счетчиков каждого кластера вектор с 1 1 эле­ ментами. Индекс кластера для данной оценки можно определить делением этой оценки на 1 0. При делении двух целых чисел получается целое число, дробная часть которого усекается. Например, 42/ 1 0=4, 65 / 1 0=6, а 1 00 / 1 0=1 0. Как только индекс кластера будет вычислен, его можно использовать для ин­ дексирования вектора и доступа к счетчику, значение которого необходимо увеличить. / / под с че т количе с тв а оценок в кла стере по де сять : 0 - - 9 , 1 1 1 0--1 9, . . . 90--99, 1 00 v e c t o r < u n s i g n e d > s c o r e s ( l l , 0 ) ; / / 1 1 я чеек , в с е со зна чением О u n s i g n e d g r ade ; wh i l e ( c i n > > g r ade ) { 1 1 чита ть оценки i f ( g r ad e < = 1 0 0 ) 1 1 обра ба тыв а ть толь ко допус тимые оценки + + s c o r e s [ g r ade / 1 0 ] ; 1 1 приращение с че т чика текущего кла с тера

3.3. Б иблиотечный тип vector

1 51

Код начинается с определения вектора для хранения счетчиков кластеров. В данном елучае все элементы должны иметь одинаковое значение, поэтому резервируем 1 1 элементов, каждый из которых инициализируем значением О . Условие цикла wh i l e читает оценки. В цикле проверяется допустимость зна­ чения прочитанной оценки (т.е. оно меньше или равно 1 00) . Если оценка до­ пустима, то увеличиваем соответствующий счетчик. Оператор, осуществляющий приращение, является хорошим примером краткости кода С++: + + s c o r e s [ g r a de / 1 0 ] ;

/ / приращение с че т чика текущего кла стера

Это выражение эквивалентно следующему: a u t o i n d = g r a de / 1 0 ; score s [ i nd ] = s cores [ ind ]

+ 1;

/ / получить индекс ячейки / / приращение сче т чика

Индекс ячейки вычисляется делением значения переменной g r a de на 1 0. Полученный результат используется для индексирования вектора s c o r e s, что обеспечивает доступ к соответствующему счетчику для этой оценки. Увеличе­ ние значения этого элемента означает принадлежность текущей оценки дан­ ному диапазону. Как уже упоминалось, при использовании индексирования следует поза­ ботиться о том, чтобы индексы оставались в диапазоне допустимых значений (см . раздел 3 .2.3, стр. 1 39) . В этой программе проверка допустимости подра­ зумевает принадлежность оценки к диапазону 0 - 1 0 0 . Таким образом, можно использовать индексы от О до 1 О . Они расположены в пределах от О до s c o re s . s i z e ( ) - 1 .

И н д ексац ия н е до б авляет э лементов Новички в С++ иногда полагают, что индексирование вектора позволяет добавлять в него элементы, но это не так. Следующий код намеревается доба­ вить десять элементов в вектор i ve c : ve c t o r < i n t > i ve c ; / / пустой в е ктор f o r ( de c l t yp e ( i ve c . s i z e ( ) ) i x = О ; i x ! = 1 0 ; + + i x ) i v e c [ i x ] = i x ; / / ка та с тр о фа : i ve c не име е т элементов

Причина ошибки - вектор i ve c пуст; в нем нет никаких элементов для индексирования! Как уже упоминалось, правильный цикл использовал бы функцию pu s h _b a c k ( ) : for

( de c l t ype ( i ve c . s i z e ( ) ) i x = О ; i x ! = 1 0 ; + + i x ) ive c . p u s h_b a c k ( i x ) ; / / o k : доба вляе т новый элемент со зна чением i x

&

ВНИМАНИЕ

Оператор индексирования вектора (и строки) лишь выбирает су­ ществующий элемент; он не может добавить новый элемент.

1 52

Глава 3 . Типы string, vector и массивы

В НИМАНИЕ ! ИНДЕ К С ИРО В АТЬ МОЖНО ЛИШЬ СУ ЩЕ СТ В УЮЩИЕ ЭЛЕМЕНТЫ !

Очень важно понять, что оператор индексирования ( [ ] ) можно исполь­ зовать для доступа только к фактически существующим элементам. Рас­ смотрим пример. ve c t o r < i n t > i ve c ; c o u t < < i ve c [ O ] ;

1 1 пус той в е ктор / / ошибка : i ve c не имее т элементов !

v e c t o r < i n t > i ve c 2 ( 1 0 ) ; c o u t i v e c ; i ve c [ O ] = 4 2 ;

Упражнение 3.1 9. Укажите три способа определения вектора и заполнения его десятью элементами со значением 42. Укажите, есть ли предпочтитель­ ный способ для этого и почему.

1 53

3. 4 . Знакомство с ите р ато р ами

Упражнение 3.20. Прочитайте набор целых чисел в вектор. Отобразите сумму каждой пары соседних элементов. Измените программу так, чтобы она отображала сумму первого и последнего элементов, затем сумму второ­ го и предпоследнего и т.д.



3 .4.



3.4.1 . Использование ите р ато р ов

З на ко мс т в о с и т е р а т о р ами

Хотя для доступа к символам строки или элементам вектора можно исполь­ зовать индексирование, для этого существует и более общий механизм - ите­ раторы (iterator) . Как будет продемонстрировано в части 11, кроме векторов библиотека предоставляет несколько других видов контейнеров. У всех биб­ лиотечных контейнеров есть итераторы, но только некоторые из них поддер­ живают оператор индексирования. С технической точки зрения тип s t r i ng не является контейнерным, но он поддерживает большинство контейнерных операций. Как уже упоминалось, и строки, и векторы предоставляют опера­ тор индексирования. У них также есть итераторы. Как и указатели (см. раздел 2.3.2, стр. 87), итераторы обеспечивают косвенный доступ к объекту. В случае итератора этим объектом является элемент в контей­ нере или символ в строке. Итератор позволяет выбрать элемент, а также поддер­ живает операции перемещения с одного элемента на другой. Подобно указате­ лям, итератор может бьпъ допустим или недопустим. Допустимый итератор ука­ зывает либо на элемент, либо на позип;ию за последним элементом в контейнере. Все другие значения итератора недопустимы.

В отличие от указателей,

получения итератора не нужно использовать оператор обращения к адресу. Для этого обладающие итераторами типы имеют члены, возвращающие эти итераторы. В частности, они обладают функциями­ членами beg i n ( ) и e n d ( ) . �ункция-член be g i n ( ) возвращает итератор, кото­ рый обозначает первый элемент (или первый символ), если он есть. для

1 1 типы Ь и е определяют компилятор ; см . ра здел 2 . 5 . 2 , стр . 1 0 7 1 1 Ь об озна ча е т первый элемент rj yn t qy t h f v , а е - элемент 11 после по следнего auto Ь = v . b e g i n ( ) , е = v . e n d ( ) ; / / Ь и е имеют одина ковый тип

Итератор, возвращенный функцией e n d ( ) , указывает на следующую по­ зицию за концом контейнера (или строки) . Этот итератор обозначает несуще­ ствующий элемент за концом контейнера. Он используется как индикатор, означающий, что обработаны все элементы. Итератор, возвращенный функ­ цией e n d ( ) , называют итерато ром после конца (off-the-end iterator), или со­ кращенно итератором e n d. Если контейнер пуст, функция b e g i n ( ) возвра­ щает тот же итератор, что и функция e n d ( ) .

Глава 3. Типы string, vector и массивы

1 54

Если контейнер пуст, возвращаемые функциями be g i n ( ) и e n d ( ) итераторы совпадают и, оба являются итератором после конца. Обычно точный тип, который имеет итератор, неизвестен (да и не нужен). В этом примере при определении итераторов Ь и е использовался специфика­ тор a u t o (см. раздел 2.5.2, стр. 1 07) . В результате тип этих переменных будет совпадать с возвращаемыми функциями-членами be g i n ( ) и e n d ( ) соответст­ венно. Не будем пока распространяться об этих типах до стр. 1 56.

О пер а ц ии с ите р ато р а м и Итераторы померживают лишь несколько операций, которые перечисле­ ны в табл. 3.6. Два допустимых итератора можно сравнить при помощи опера­ торов == и ! =. Итераторы равны, если они указывают на тот же элемент или если оба они указывают на позицию после конца того же контейнера. В про­ тивном елучае они не равны.

Т аб лица 3 .6. С тан д ар тн ые опе р ации

с

ите р ато р ами контейне р а

* iter

Возвращает ссылку на элемент, обозначенный итератором i t e r

i t e r - >rnem

Обращение к значению итератора i t e r и выборка члена mem ос­ новного элемента. Эквивалент ( * i t е r ) . mem

+ +i ter

Инкремент итератора i t e r для обращения к следующему эле­ менту контейнера

--iter

Декремент итератора i t e r для обращения к предыдущему эле­ менту контейнера

i ter2 iterl iterl 1 = iter2

Сравнивает два итератора на равенство (неравенство). Два итера­ тора равны, если они указывают на тот же элемент или на сле­ дующий элемент после конца того же контейнера

Подобно указателям, к значению итератора можно обратиться, чтобы по­ лучить элемент, на который он ссылается. Кроме того, подобно указателям, можно обратиться к значению только допустимого итератора, который обо­ значает некий элемент (см. раздел 2.3.2, стр. 89). Результат обращения к зна­ чению недопустимого итератора или итератора после конца непредсказуем . Перепишем программу из раздела 3.2.3 (стр. 1 38), преобразующую строч­ ные символы строки в прописные, с использованием итератора вместо индек­ сирования: s t r i n g s ( " s ome s t r i n g " ) ; i f ( s . be g i n ( ) ! = s . e n d ( ) ) auto i t = s . begin ( ) ; * i t = t o uppe r ( * i t ) ;

1 1 удо с тов ерить ся , что строка s не пус та 1 1 i t ука зыв а е т на первый симв ол строки s 1 1 те кущий симв ол в в ерхний регистр

3.4. Знакомство с ите ратор ами

1 55

Как и в первоначальной программе, сначала удостоверимся, что строка s не пуста. В данном елучае для этого сравниваются итераторы, возвраrценные функциями b e g i n ( ) и e n d ( ) . Эти итераторы равны, если строка пуста. Если они не равны, то в строке s есть по крайней мере один символ. В теле оператора i f функция be g i n ( ) возвращает итератор на первый символ, который присваивается переменной i t . Обращение к значению этого итератора и передача его функции t o uppe r ( ) позволяет перевести данный символ в верхний регистр . Кроме того, обращение к значению итератора i t слева от оператора присвоения позволяет присвоить символ, возвращенный функцией t o uppe r ( ) , первому символу строки s . Как и в первоначальной программе, вывод будет таким: Sorne s t r i n g

Перемеще н и е ит ер ато р а с о дн ого э л еме нта на друг о й Итераторы используют оператор инкремента (оператор + + ) (см. раз­ дел 1 .4. 1 , стр. 38) для перемещения с одного элемента на следующий. Опера­ ция приращения итератора логически подобна приращению целого числа. В случае целых чисел результатом будет целочисленное значение на единицу больше 1 . В случае итераторов результатом будет перемещение итератора на одну позицию. Поскольку итератор, возвращенный функцией e nd ( ) , не указыва­ ет на элемент, он не допускает ни приращения, ни обращения к значению. Перепишем программу, изменяющую регистр первого слова в строке, с использованием итератора. 1 1 обра ба тыв а ть симв олы , пока они не исчерпаются , или не в с тр е тится пробел f o r ( au t o i t = s . b e g i n ( ) ; i t ! = s . e n d ( ) & & ! i s s p a c e ( * i t ) ; + + i t ) * i t = t o upp e r ( * i t ) ; / / пре о бра з ов а ть в в ерхний регис тр

Этот цикл, подобно таковому в разделе 3.2.3 (стр. 1 38), перебирает символы строки s , останавливаясь, когда встречается пробел. Но данный цикл исполь­ зует для этого итератор, а не индексирование. Цикл начинается с инициализации итератора i t результатом вызова функ­ ции s . be g i n ( ) , чтобы он указывал на первый символ строки s (если он есть). Условие проверяет, не достиг ли итератор i t конца строки (s . end ( ) ). Если это не так, то проверяется следующее условие, где обращение к значению итерато­ ра i t, возвращающее текущий символ, передается функции i s space ( ) , чтобы выяснить, не пробел ли это. В конце каждой итерации выполняется оператор + + i t, чтобы переместить итератор на следующий символ строки s . У этого цикла то же тело, что и у последнего оператора i f предыдущей программы. Обращение к значению итератора i t используется и для переда-

Глава 3. Типы string, vector и массивы

1 56

чи текущего символа функции t o uppe r ( ) , и для присвоения полученного ре­ зультата символу, на который указывает итератор i t . КЛЮЧЕВАЯ КОНЦЕПЦИЯ. О БОБ ЩЕННОЕ ПРОГРАММИРОВАНИЕ

Программисты, перешедшие на язык С++ с языка С или Java, могли бы быть удивлены тем, что в данном цикле f о r был использован оператор ! =, а не < . Программисты С++ используют оператор ! = исключительно по при­ вычке. По этой же причине они используют итераторы, а не индексирова­ ние: этот стиль программирования одинаково хорошо применим к контей­ нерам различных видов, предоставляемых библиотекой. Как уже упоминалось, только у некоторых библиотечных типов, ve c t o r и s t r i n g, есть оператор индексирования. Тем не менее у всех библиотечных контейнеров есть итераторы, для которых определены операторы == и ! =. Однако болыпинство их итераторов не имеют оператора < . При обычном использовании итераторов и оператора 1 = можно не заботиться о точном типе обрабатываемого контейнера.

Типы и те р ато р ов Подобно тому, как не всегда известен точный тип s i z e_t ype элемента век­ тора или строки (см. раздел 3.2.2, стр . 1 31 ), мы обычно не знаем (да и не обяза­ ны знать) точный тип итератора. Как и в случае с типом s i z e_t ype, библио­ течные типы, у которых есть итераторы, определяют типы по имени i t e r a t o r и со n s t _ i t е r а t о r, которые представляют фактические типы итераторов. ve c t o r < i n t > : : i t e r a t o r i t ; s t r i ng : : i te rator i t2 ; ve c t o r < i n t > : : c o n s t i t e r a t o r i t З ; s t r i n g : : c o n s t_i t e r a t o r i t 4 ;

// 11 // 11 // 11 // 11

i t позв оляе т чита ть и з а писыв а ть в элементы в е ктора ve c t o r i t 2 позв оляет чита ть и з а писыв а ть симв олы в строку i t З позв оляе т чита ть , но не за писыв а ть элементы i t 4 позв оляе т чита ть , но не за писыв а ть симв олы

Тип c o n s t i te r a t o r ведет себя как константный указатель (см. раз­ дел 2.4.2 стр . 99). Как и константный указатель, тип c o n s t_i t e r a t o r позволя­ ет читать, но не писать в элемент, на который он указывает; объект типа i t e r a t o r позволяет и читать, и записывать. Если вектор или строка являются константой, можно использовать итератор только типа co n s t_i t e r a t o r . Если вектор или строка на являются константой, можно использовать итератор и типа i t e r a t o r, и типа con s t i te ra t o r .

3. 4 . Знакомство с итерато р ами

1 57

ТЕРМИНОЛОГИЯ. ИТЕРАТОРЫ И ТИПЫ ИТЕРАТОРОВ

Термин итератор (iterator) используется для трех разных сущностей. Речь могла бы идти о ко1-щ,епции итератора, или о типе i t e r a t o r, опреде­ ленном классом контейнера, или об объекте итератора. Следует уяснить, что существует целый набор типов, связанных концеп­ туально. Тип относится к итераторам, если он поддерживает общепринятый набор функций. Эти функции позволяют обращаться к элементу в контей­ нере и переходить с одного элемента на другой. Каждый класс контейнера определяет тип по имени i t e r a t o r; который обеспечивает действия концептуального итератора.

Ф ункции begin ( ) и end ( ) Тип, возвращаемый функциями beg i n ( ) и end ( ) , зависит от константности объекта, для которого они были вызваны. Если объект является константой, то функции beg i n ( ) и end ( ) возвращают итератор типа con s t _ i t e ra tor; если объект не константа, они возвращают итератор типа i t e r a t o r . ve c t o r < i n t > v ; co n s t v e c t o r < i n t > c v ; auto i t l = v . beg i n ( ) ; auto i t2 = cv . begi n ( ) ;

rC++l ULJ

/ / i t l име е т тип ve c t o r : : i t e ra t o r 1 1 i t 2 име е т тип ve c t o r : : c o n s t i t era t o r

Зачастую это стандартное поведение желательно изменить. По причи­ нам, рассматриваемым в разделе 6.2.3 (стр. 281 ), обычно лучше исполь­ зовать константный тип (такой как c o n s t_i t e r a t o r), когда необходимо только читать, но не записывать в объект. Чтобы позволить специально задать тип c on s t_i t e r a t o r, новый стандарт вводит две новые функции, cbe g i n ( ) и c e n d ( ) :

auto i t З = v . cbeg i n ( ) ;

/ / i t З имее т тип ve c t o r : : c o n s t_ i t e ra t o r

Подобно функциям-членам be g i n ( ) и e n d ( ) , эти функции-члены возвра­ щают итераторы на первый и следующий после последнего элементы контей­ нера. Но независимо от того, является ли вектор (или строка) константой, они возвращают итератор типа c o n s t_i t e r a t o r .

Об ъ е динение о б р ащения к значению и дост упа к ч лену При обращении к значению итератора получается объект, на который ука­ зывает итератор. Если этот объект имеет тип класса, то может понадобиться доступ к члену полученного объекта. Например, если есть вектор строк, то может понадобиться узнать, не пуст ли некий элемент. С учетом, что i t - это итератор данного вектора, можно следующим образом проверить, не пуста ли строка, на которую он указывает: ( * i t ) . empt у ( )

Глава 3 . Типы string, vector и массивы

1 58

По причинам, рассматриваемым в разделе 4.1 .2 (стр. 1 90), круглые скобки в части ( * i t ) . empt у ( ) необходимы. Круглые скобки требуют применить опе­ ратор обращения к значению к итератору i t, а к результату применить то­ чечный оператор (см. раздел 1 .5.2, стр. 51 ). Без круглых скобок точечный опе­ ратор относился бы к итератору i t, а не к полученному объекту. ( * i t ) . emp t y ( ) * i t . emp t y ( )

// // // 11

обращение к зна чению i t и выз ов функции- члена emp ty ( ) полученного объекта ошибка : попытка выз ов а функции- члена emp ty ( ) итера тора i t но итера тор i t не име е т функции- члена emp ty ( )

Второе выражение интерпретируется как запрос на выполнение функции­ члена ernp t y ( ) объекта i t . Но i t - это итератор, и он не имеет такой функ­ ции. Следовательно, второе выражение ошибочно. Чтобы упростить такие выражения, язык предоставляет опер атор ст релки (arrow operator) (оператор - >) . Оператор стрелки объединяет обращение к значению и доступ к члену. Таким образом, выражение i t - >rnern является синоним выражения ( * i t ) . тет. Предположим, например, что имеется вектор ve c t o r< s t r i n g > по имени t e x t, содержащий данные из текстового файла. Каждый элемент вектора ­ это либо предложение, либо пустая строка, представляющая конец абзаца. Ес­ ли необходимо отобразить содержимое первого параграфа из вектора t e x t , то можно было бы написать цикл, который перебирает вектор t e xt, пока не встретится пустой элемент. 1 1 отобра зить каждую строку в е ктора t ex t до перв ой пустой строки f o r ( a u t o i t = t e x t . cbeg i n ( ) ; i t ! = t e x t . c e n d ( ) & & ! i t - > emp t y ( ) ; + + i t ) cout < < * i t < < endl ;

Код начинается с инициализации итератора i t указанием на первый эле­ мент вектора t e x t . Цикл продолжается до тех пор, пока не будут обработаны все элементы вектора t e x t или пока не встретится пустой элемент. Пока есть элементы и текущий элемент не пуст, он отображается. Следует заметить, что, поскольку цикл только читает элементы, но не записывает их, здесь для управления итерацией используются функции cbe g i n ( ) и cend ( ) .

Н екото р ые опе р ации с векто р ами делают итерато р ы н едопустимыми В разделе 3.3.2 (стр . 1 48) упоминался тот факт, что векторы способны расти динамически. Обращалось также внимание на то, что нельзя добавлять эле­ менты в вектор в цикле серийного оператора f o r . Еще одно замечание: любая операция, такая как вызов функции pu s h _ba c k ( ) , изменяет размер вектора и способна сделать недопустимыми все итераторы данного вектора. Более подробная информация по этой теме приведена в разделе 9.3.6 (стр. 453).

3.4 . Знакомство с ите раторами

&

На настоящий момент достаточно знать, что использующие ите­ раторы цикла не должны добавлять элементы в контейнер, с кото­ рым связаны итераторы.

ВНИМАНИЕ



1 59

3 .4.2. А р ифметические действия с ите р ато р ами

Инкремент итератора перемещает его на один элемент. Инкремент под­ держивают итераторы всех библиотечных контейнеров. Аналогично операто­ ры == и ! = можно использовать для сравнения двух допустимых итераторов (см. раздел 3.4, стр . 1 53) любых библиотечных контейнеров. Итераторы строк и векторов поддерживают дополнительные операции, по­ зволяющие перемещать итераторы на несколько позиций за раз. Они также поддерживают все операторы сравнения. Эти операторы зачастую называют арифметическими действиями с итераторами (iterator arithmetic) . Они приве­ дены в табл. 3.7.

Таб лица 3.7. Опе р ации с ите р ато р а ми векто р ов и с т р ок iter iter

iterl iterl

+ -

n n

+= -=

Добавление (вычитание) целочисленного значения n к (из) итера­ тору возвращае� итератор, указывающий на элемент n позиций вперед (назад) в пределах контейнера. Полученный итератор должен указывать на элемент или на следующую позицию за кон­ цом того же контейнера

n n

Составные операторы присвоения со сложением и вычитанием итератора. Присваивает итератору i t e r l значение на n позиций больше или меньше предыдущего

i te r l - i t e r 2

Вычитание двух итераторов возвращает значение, которое, будучи добавлено к правому итератору, вернет левый . Итераторы долж­ ны указывать на элементы или на следующую позицию за концом того же контейнера

>, > =, < , < =

Операторы сравнения итераторов. Один итератор меньше друго­ го, если он указывает на элемент, расположенный в контейнере ближе к началу. Итераторы должны указывать на элементы или на следующую позицию за концом того же контейнера

Ар иф метические опера ц ии с ите р ато р ами К итератору можно добавить (или вычесть из него) целочисленное значение. Эго вернет итератор, перемещеllliы й на соответствуюrцее количество позиций вперед (или назад). При добавлении или вычитании целочисленного значения из итератора результат должен указывать на элемент в том же векторе (или строке) или на следующую позицию за концом того же вектора (или строки). В качестве примера вычислим итератор на элемент, ближайший к середине вектора: 1 1 вычислить итера тор на элемент , ближайший к середине в е ктора vi auto m i d = v i . b e g i n ( ) + v i . s i z e ( ) / 2 ;

Глава 3. Типы string, vector и массивы

1 60

Если у вектора v i 20 элементов, то результатом v i . s i z e ( ) / 2 будет 1 0. В данном случае переменной m i d будет присвоено значение, равное vi . beg i n ( ) + 1 О . С учетом, что нумерация индексов начинаются с О, это тот же элемент, что и v i [ 1 О J , т.е. элемент на десять позиций от начала. Кроме сравнения двух итераторов на равенство, итераторы векторов и строк можно сравнить при помощи операторов сравнения (, >=) . Итераторы должны быть допустимы, т.е. должны обозначать элементы (или следующую позицию за концом) того же вектора или строки. Предположим, например, что i t является итератором в том же векторе, что и mi d. Следую­ щим образом можно проверить, указывает ли итератор i t на элемент до или после итератора m i d: i f ( i t < mi d ) / / обра б о та ть элементы в перв ой половине в е ктора vi

Можно также вычесть два итератора, если они указывают на элементы ( или следующую позицию за концом) того же вектора или строки. Результат - дис­ танция между итераторами. Под дистанцией подразумевается значение, на ко­ торое следует изменить один итератор, чтобы получить другой. Результат име­ ет целочисленный знаковый тип di f fe r e n c e t ype. Тип di f f e r e n c e t ype оп­ ределен и для вектора, и для строки. Этот тип знаковый, поскольку результатом вычитания может оказаться отрицательное значение.

И спользование ар и ф метических д е йстви й с ите р ато р ами Классическим алгоритмом, использующим арифметические действия с итераторами, является двоичный поиск (Ьinary search) . Двоичный (бинарный) поиск ищет специфическое значение в отсортированной последовательности. Алгоритм работает так: сначала исследуется элемент, ближайший к середине последовательности . Если это искомый элемент, работа закончена. В против­ ном случае, если этот элемент меньше искомого, поиск продолжается только среди элементов после исследованного. Если средний элемент больше искомо­ го, поиск продолжается только в первой половине. Вычисляется новый сред­ ний элемент оставшегося диапазона, и действия продолжаются, пока искомый элемент не будет найден или пока не исчерпаются элементы. Используя итераторы, двоичный поиск можно реализовать следующим об­ разом: те кст должен быть от сортиров ан / / beg и en d огра ничив ают диа па з он , в котором о суще ствляе тся поиск a u t o beg = t e x t . be g i n ( ) , e n d text . end ( ) ; a u t o mi d = t e x t . b eg i n ( ) + ( e nd - beg ) / 2 ; / / исходна я середина / / пока еще е с ть элементы и искомый не найден wh i l e ( m i d ! = e n d & & * m i d ! = s o ug h t ) { i f ( s o ught < * m i d ) / / на ходится ли искомый элемент в перв ой половине ? e n d = mi d ; / / е сли да , то изменить диапа з он , игнорируя в торую 11

=

3 .5. Массивы

1 61

1 1 половину el se / / искомый элемент в о в торой половине beg m i d + 1 ; / / на ча ть поиск с элемента сра зу по сле середины mi d = b e g + ( e nd - be g ) / 2 ; / / нов а я середина

Код начинается с определения трех итераторов: beg будет первым элемен­ том в диапазоне, e n d - элементом после последнего, а mi d - ближайшим к середине. Инициализируем эти итераторы значениями, охватывающими весь диапазон вектора ve c t o r < s t r i n g > по имени t e x t . Сначала цикл проверяет, не пуст ли диапазон. Если значение итератора mid равно текущему значению итератора e n d, то элементы для поиска исчер­ паны. В таком случае условие ложно и цикл wh i l e завершается. В противном случае итератор mi d указывает на элемент, который проверяется на соответ­ ствие искомому. Если это так, то цикл завершается. Если элементы все еще есть, код в цикле wh i l e корректирует диапазон, пе­ ремещая итератор e n d или beg. Если обозначенный итератором mi d элемент больше, чем s ought, то если искомый элемент и есть в векторе, он находится перед элементом, обозначенным итератором m i d. Поэтому можно игнориро­ вать элементы после середины, что мы и делаем, присваивая значение итерато­ ра rni d итератору e n d. Если значение *mid меньше, чем s ought, элемент дол­ жен быть в диапазоне элементов после обозначенного итератором rn i d. В дан­ ном елучае диапазон корректируется присвоением итератору beg позиции сразу после той, на которую указывает итератор mid. Уже известно, что mi d не указывает на искомый элемент, поэтому его можно исключить из диапазона. В конце цикла wh i l e итератор mi d будет равен итератору e n d либо будет указывать на искомый элемент. Если итератор m i d равен e n d, то искомого элемента нет в векторе t e x t . Упражнения раздела

3.4.2

Упражнение 3.24. Переделайте последнее (стр . 1 52) с использованием итераторов.

упражнение

раздела 3.3.3

Упражнение 3.25. Перепишите программу кластеризации оценок из разде­ ла 3.3.3 (стр. 1 50) с использованием итераторов вместо индексации. Упражнение 3.26. Почему в программе двоичного поиска на стр. 1 60 исполь­ зован код mi d = beg + ( end - beg ) / 2 ; , a нe mi d = ( beg + end ) / 2 ; ?

3 .5. М ассивь1 Массив (array) - это структура данных, подобная библиотечному типу ve c t o r (см. раздел 3.3, стр . 1 41 ), но с другим соотношением между производи-

Глава 3. Типы string, vector и массивы

1 62

тельностью и гибкостью. Как и вектор, массив является контейнером безы­ мянных объектов одинакового типа, к которым обращаются по позиции. В от­ личие от вектора, массивы имеют фиксированный размер; добавлять элемен­ ты к массиву нельзя. Поскольку размеры массивов постоянны, они иногда обеспечивают лучшую производительность во время выполнения приложе­ ний. Но это преимущество приобретается за счет потери гибкости.

Q

Сове

Если вы не знаете точно, сколько элементов необходимо, исполь­ зуйте вектор.

3.5.1 . Опр еделение и инициализация вст р оенных массивов Массив является составным типом (см. раздел 2.3 стр. 84). Оператор объяв­ размерность определяе­ ления массива имеет форму а [ d ] , где а - имя; d мого массива. Размерность задает количество элементов массива, она должна быть больше нуля. Количество элементов - это часть типа массива, поэтому она должна быть известна на момент компиляции. Следовательно, размер­ ность должна быть константным выражением (см. раздел 2.4.4, стр. 1 03). -

u n s igned cnt = 4 2 ; c o n s t e xp r u n s i g n e d s z = 4 2 ; int arr [ l O ] ; int *parr [ s z ] ; s t r i ng bad [ c nt ] ; s t r i ng s t r s [ g e t_s i z e ( ) ] ;

// // 11 11 11 11 11 11

не конста нтное выражение конста нтное выражение con s t expr см . р . 2 . 4 . 4 стр . 1 0 3 ма с сив де сяти целых чисел ма с сив 42 ука за телей на i n t ошибка : cn t неконста нтно е выражение o k , е сли ge t_ s i z e - co n s t expr , в против ном случа е - ошибка

По умолчанию элементы массива инициализируются значением по умол­ чанию (раздел 2.2. 1 , стр . 77) .

& ВНИМАНИЕ

Подобно переменным встроенного типа, инициализированный по умолчанию массив встроенного типа, определенный в функции, будет содержать неопределенные значения.

При определении массива необходимо указать тип его элементов. Нельзя использовать спецификатор a u t o для вывода типа из списка инициализато­ ров. Подобно вектору, массив содержит объекты. Таким образом, невозможен массив ссылок.

Я вн ая и н и циализация э леме н тов массива Массив допускает списочную инициализацию (см. раздел 3.3. 1 , стр . 1 43) элементов. В этом елучае размерность можно опустить. Если размерность от­ сутствует, компилятор выводит ее из количества инициализаторов. Если раз­ мерность определена, количество инициализаторов не должно превышать ее.

1 63

3.5. Массивы

Если размерность больше количества инициализаторов, то инициализаторы используются для первых элементов, а остальные инициализируются по умолчанию (см. раздел 3.3. 1 , стр. 1 43) : c o n s t un s i gn e d s z = З ; int i a l [ s z ] = { 0 , 1 , 2 } ; int а2 int а 3 string int а5

[] = { [5] = а4 [ 3 ] [2] =

О, 1, 2}; {0, 1, 2} ; = { " h i " , " Ь уе " } ; {0, 1, 2 } ;

// 11 // // // //

ма ссив из трех целых чисел со зна чениями О , 1 , 2 ма ссив ра змером 3 элемента эквив алент а З [ ] = { 0 , 1 , 2 , О , О } э квив алент а 4 [ ] = { "h i ,, , "Ьуе ,, , ,, ,, } ошибка : слишком много инициализ а торов

Осо б енности символьных массивов У символьных массивов есть дополнительная форма инициализации: стро­ ковым литералом (см. раздел 2.1 .З стр. 70). Используя эту форму инициализа­ ции, следует помнить, что строковые литералы заканчиваются нулевым симво­ лом. Этот нулевой символ копируется в массив наряду с символами литерала. cha r а 1 [ ]

{ 'с' '

' + ' '

' + ' } ;

cha r а 2 [ ]

{ 'с' '

'+' '

' + ' '

cha r а з [ ]

"С++" ;

co n s t c h a r а 4 [ 6 ]

= " Da n i e l " ;

' \о ' } ;

11 11 11 11 11 11 11 11

списо чна я инициализ а ция без нулев ого симв ола списо чна я инициализ а ция с яв ным нулевым симв олом нулев ой симв ол доба вляе тся ав тома тиче ски ошибка : не т ме ста для нулев ого симв ола !

Массив a l имеет размерность 3; массивы а 2 и а з размерности 4. Опреде­ ление массива а 4 ошибочно. Хотя литерал содержит только шесть явных сим­ волов, массив а 4 должен иметь по крайней мере семь элементов, т.е. шесть для самого литерала и один для нулевого символа. -

Н е допускается ни копи р ование, ни пр исвоение Нельзя инициализировать массив как копию другого массива, не допусти­ мо также присвоение одного массива другому. int а [ ] = { 0 , int а2 [ ] = а ; а2 = а ;

& ВНИМАНИЕ

1,

2} ;

11 11 11 11

ма ссив из ·трех целых чисел ошибка : нель зя инициализиров а ть один ма с сив другим ошибка : нель зя присв аив а ть один ма с сив другому

Некоторые компиляторы допускают присвоение массивов при применении расширения компилятор а (compiler extension). Как правило, использования нестандартных средств следует избегать, поскольку они не будут работать на других компиляторах.

П онятие сложных о б ъ явлени й массива Как и векторы, массивы способны содержать объекты большинства типов. Например, может быть массив указателей. Поскольку массив - это объект,

1 64

Глава 3. Типы string, vector и массивы

можно определять и указатели, и ссылки на массивы. Определение массива, содержащего указатели, довольно просто, определение указателя или ссылки на массив немного сложней. int *ptrs [ l O ] ; int & re f s [ l O J /* ? */; &arr ; i n t ( * Pa r r a y ) [ 1 0 ] i n t ( & a r r Re f ) [ 1 0 ] = a r r ; =

=

11 11 11 11

p t rs ма ссив де сяти ука за телей на i n t ошибка : ма ссив ссьиток нев о зможен Pa rray ука зыв а е т на ма ссив из де сяти i n t a rrRe f с сыла е т ся на ма с сив из де сяти i n t s

Обычно модификаторы типа читают спр�ва налево. Читаем определение pt r s справа налево (см. раздел 2.3.3, стр . 95): определить массив размером 1 0 по имени p t r s для хранения указателей на тип i n t . Определение Pa r r a y также стоит читать справа налево. Поскольку раз­ мерность массива следует за объявляемым именем, объявление массива может быть легче читать изнугри наружу, а не справа налево. Так намного проще понять тип P a r r a y. Объявление начинается с круглых скобок вокруг части * Pa r r a y, означающей, что P a r r a y - указатель. Глядя направо, можно заме­ тить, что указатель P a r r a y указывает на массив размером 1 0. Глядя влево, можно заметить, что элементами этого массива являются целые числа. Таким образом, Pa r r ay - это указатель на массив из десяти целых чисел. Точно так же часть ( & a r rRe f ) означает, что a r rRe f - это ссылка, а типом, на который она ссылается, является массив размером 1 0, хранящий элементы типа i n t . Конечно, нет никаких ограничений на количество применяемых модифи­ каторов типа. int * ( &arry) [ 1 0 ]

= ptrs ;

/ / a rry

-

ссьurка на ма ссив из де сяти ука за телей

Читая это объявление изнугри наружу, можно заметить, что a r r y - это ссылка. Глядя направо, можно заметить, что объект, на который ссылается a r r y, является массивом размером 1 0. Глядя влево, можно заметить, что типом элемента является указатель на тип i n t . Таким образом, a r ry - это ссылка на массив десяти указателей.

Q

Сове

Зачастую объявление массива может быть проще понять, начав его чтение с имени массива и продолжив его изнугри наружу.

Упражнения раздела

3.5.1

Упражнение 3.27. Предположим, что функция t x t_s i z e ( ) на получает никаких аргументов и возвращают значение типа i n t . Объясните, какие из следующих определений недопустимы и почему? u n s i g n e d bu f_s i z e = 1 0 2 4 ; ( а ) i n t i a [ b u f _s i z e ] ; (Ь) ( с ) i n t i a [ tx t_s i z e ( ) ] ; (d)

int ia [ 4 * 7 - 1 4 ] ; c h a r s t [ l l ] = " f un dame n t a l " ;

3 .5. М ассивы

1 65

Упражнение 3.28. Какие значения содержатся в следующих массивах? str ing sa [ l O ] ; int ia [ l O ] ; i n t ma i n ( ) { s t r ing sa2 [ 1 0 ] ; int ia2 [ 1 0 ] ;

Упражнение 3.29. Перечислите некоторые из недостатков использования массива вместо вектора.

3. 5 .2. Дост уп к элементам массива Подобно библиотечным типам ve c t o r и s t r i n g, для доступа.· к элементам массива можно использовать серийный оператор f o r или оператор индекси ро­ вания ( [ J ) (subscript). Как обычно, индексы начинаются с О. Для массива из де­ сяти элементов используются индексы от О до 9, а не от 1 до 1 0. При использовании переменной для индексирования массива ее обычно определяют как имеющую тип s i z е _ t . Тип s i z е _ t - это машинозависимый беззнаковый тип, гарантированно достаточно большой для содержания раз­ мера любого объекта в памяти. Тип s i z e_t определен в заголовке c s tdde f, который является версией С++ заголовка s t dde f . h библиотеки С. За исключением фиксированного размера, массивы используются подобно векторам. Например, можно повторно реализовать программу оценок из раз­ дела 3.3.3 (стр. 1 50), используя для хранения счетчиков кластеров массив. 11 подсче т количе ства оценок в кла с тере по де сять : 0 - - 9 , 11 1 0--1 9, . . . 90--99, 1 00 un s i g n e d s c o r e s [ l l ] = { } ; / / 1 1 яче е к , в с е со зна чением О un s i g n e d g r a de ; whi l e ( c i n > > g r ade ) { i f ( g r a de выражение 2

&

Побитовое AND

выр ажение l

&

л

Побитовое XOR

выра жение l

Побитовое OR

выражение l

Оп ератор

л

выр а жение 2

выражение 2 выр ажение 2 выражение 2

Если операнд знаковый и имеет отрицательное значение, то способ обра­ ботки "знакового разряда" большинства битовых операций зависит от кон­ кретной машины. Кроме того, результат сдвига влево, изменяющего знаковый разряд, непредсказуем. '



ВНИМАНИЕ

Поскольку нет никаких гарантий однозначного выполнения побитовых операторов со знаковыми переменными на разных маши­ нах, настоятельно рекомендуется использовать в них только без­ знаковые целочисленные значения.

П о б итовые опе р аторы сдви г а Мы уже использовали перегруженные версии операторов > > и < >), при этом вышедшие за пределы биты от­ брасываются. Оператор сдвига влево () (right-shift operator) зависит от типа левого операнда: если он беззнаковый, то оператор добавляет слева нулевые биты; если он знаковый, то результат зависит от конкретной реализации: сле­ ва вставляются либо копии знакового разряда, либо нули.

Глава 4. В ыра жения

21 2

В этих п р име рах под разумевается, что младш и й бит р асположен сп р ава, т и п ch a r содержит 8 битов, а тип i n t - 32 бита / / 0 2 3 3 - в о с ь ме р и ч ный л и т е р ал ( см . р а здел 2 . 1 . 3 , uns igned cha r b i t s = 0 2 3 3 ; 1 о о 1 1 о 1 1

1

1

стр .

70)

b i t s < < 8 1 1 b i t s пре о бр а з у е т с я в i n t и с д в и г а е т с я вле в о н а 8 бит о в

lo

о о о о о о о о

о о о о о о о

1

о о 1 1 о 1 1

о о о о о о о о

b i t s < < 3 1 1 1 с д в и г в л е в о н а 3 1 б и т о т б р а сы в а е т кр айние л е в ые биты

1

1

о о о о о о о

о о о о о о о о

о о о о о о о о

о о о о о о о о

b i t s > > 3 / / с д в и г в пр а в о н а 3 б и т а о т б р а сы в а е т 3 крайних п р а вых бит а

lo

о о о о о о о

о о о о о о о о

о о о о о о о о

о о о 1 о о 1 1

П о б итовы й о пе р ато р NOT Побитовый оператор N O T ( - ) (Ьitwise NOT operator) создает новое значе­ ние с инвертированными битами своего операнда. Каждый бит, содержа­ щий 1 , превращается в О; каждый бит, содержащий О, - в 1 . uns igned char b i t s = 0 2 2 7 ;

j

1

О О 1 О 1 1 1

--- b i t s

1

1

1

1

1

1

1 1 1

1 1 1 1 1 1 1 1

1 1 1 1 1 1 1 1

о 1 1 о 1 о о о

Здесь операнд типа cha r сначала преобразуется в тип i n t . Это оставляет зна­ чение неизменным, но добавляет нулевые биты в позиции сrарших разрядов. Та­ ким образом, преобразование в тип i n t добавляет 24 бига сrарших разрядов, за­ полненных нулями. Биты преобразованного значения инвертируются.

П о б итовые опе р ато р ы AND , OR и XOR Побитовые операторы AND ( & ), OR ( 1 ) и XOR ( л ) создают новые значения с битовым шаблоном, состоящим из двух их операндов. u n s i g n e d cha r Ы

0145;

uns i gned cha r Ь2

0257 ;

1 1 1

Ы & Ь2 ы

Ь2

Ы л Ь2

1 l

О 1 1 О О 1 О 1 1 О 1 О 1 1 1 1

Все 24 старших бита О

о о 1 о о 1 о 1

Все 24 старших бита О

1 1 1 о 1 1 1 1

Все 24 старших бита О

1 1 О О 1 О 1 О

Каждая битовая позиция результата побитового оператора AND ( & ) содер­ жит 1 , если оба операнда содержат 1 в этой позиции; в противном случае ре­ зультат - О. У побитового оператора OR ( 1 ) бит содержит 1 , если один или оба операнда содержат 1 ; в противном случае результат - О. Для побитового опе­ р атора XOR ( ) бит содержит 1 , если любой, но не оба операнда содержат 1; в противном случае результат - О. л

4 .8. Побитовые операто р ы

&

21 3

Побитовые и логические (см. раздел 4.3, стр. 1 97) операторы нередко пугают. Например, пугают побитовый оператор & с логическим & &, побитовый 1 с логическим 1 1 и побитовый - с логическим ! .

ВНИМАНИЕ

И сп ользование п о б итовых о п е р а то р ов Рассмотрим пример использования побитовых операторов. Предположим, что есть класс с 30 учениками. Каждую неделю класс отвечает на контрольные вопросы с оценкой "сдано / не сдано". Результаты всех контрольных записы­ ваются, по одному биту на ученика, чтобы представить успешную оценку или нет. Каждую контрольную можно представить в виде беззнакового целочис­ ленного значения. un s i gn e d l o n g qu i z l = О ;

/ / э то зна чение исполь зуе тся ка к колле кция битов

Переменная qu i z l определена как u n s i g n e d l o n g . Таким образом, на лю­ бой машине она будет содержать по крайней мере 32 бита. Переменная qu i z l инициализируется явно, чтобы ее значение было определено изначально. Учитель должен быть способен устанавливать и проверять отдельные биты. Например, должна быть возможность установить бит, соответствующий учени­ ку номер 27, означающий, что этот ученик сдал контрольную. Чтобы указать, что ученик 27 прошел контрольную, создадим значение, у которого установлен только бит номер 27. Если затем применить побитовый оператор OR к этому значению и значению переменной qu i z l , то все биты, кроме бита 27, останутся неизменными. В данном примере счет битов переменной qu i z l начинается с О, соответст­ вующего младшему биту, 1 соответствует следующему биту и т.д. Чтобы получить значение, означающее, что ученик 27 сдал контрольную, используется оператор сдвига влево и целочисленный литерал 1 типа u n s i gn e d l o n g (см. раздел 2. 1 .3, стр. 70) . l UL < < 2 7 / / созда е т зна чение толь ко с одним уста новленным битом 1 1 в позиции 2 7

Первоначально переменная 1 UL имеет 1 в самом младшем бите и по крайней мере 31 нулевой бит. Она определена как u n s i gn e d l on g, поскольку тип i n t га­ рантированно имеет только 1 6 битов, а необходимо по крайней мере 27. Эго вы­ ражение сдвигает 1 на 27 битовых позиций, вставляя в биты позади О. К этому значению и значению переменной qu i z l применяется оператор OR. Поскольку необходимо изменить значение самой переменной qu i z l , ис­ пользуем составной оператор присвоения (см. раздел 4.4, стр. 203): qu i z l

1=

l UL ( p ) ;

После сохранения адреса в указателе типа vo i d * можно впоследствии ис­ пользовать оператор s t a t i c_ca s t и привести указатель к его исходному ти­ пу, что позволит сохранить значение указателя. Таким образом, результат приведения будет равен первоначальному значению адреса. Однако следует быть абсолютно уверенным в том, что тип, к которому приводится указатель, является фактическим типом этого указателя; при несоответствии типов ре­ зультат непредсказуем.

Опера тор

cons t

cas t

_

Оператор c o n s t_ca s t изменяет только спецификатор c on s t нижнего уровня своего операнда (см. раздел 2.4.3, стр. 1 01 ): eo n s t eha r * р е ; e h a r * р = e o n s t_e a s t < e h a r * > ( p e ) ;

/ / ok : одна ко за пись при помощи р / / ука з а теля непредска зуема

Принято говорить, что приведение, преобразующее константный объект в неконстантный, "сбрасывает c o n s t " . При сбросе константности объекта компилятор больше не будет препятствовать записи в этот объект. Если объект первоначально не был константным, использование приведения для доступа на запись вполне допустимо. Но применение оператора c on s t_ca s t для за­ писи в первоначально константный объект непредсказуемо. Только оператор con s t_ca s t позволяет изменить константность выраже­ ния. Попытка изменить константность выражения � ри помощи любого друго­ го именованного оператора приведения закончится ошибкой компиляции. Аналогично нельзя использовать оператор c on s t c a s t для изменения типа выражения: e o n s t c h a r * ер ; 1 1 ошибка : s t a t i c ca s t не може т сбро сить con s t e h a r * q = s t a t i e_e a s t < e ha r * > ( ep ) ; s t a t i e_e a s t < s t r i n g > ( ep ) ; / / ok : пре о бра зуе т строковый литерал в с троку e o n s t_ea s t < s t r i n g > ( ep ) ; / / ошибка : con s t ca s t измененяе т толь ко 1 1 конста нтно с ть

Оператор co n s t c a s t особенно полезен в контексте перегруженных функций, рассматриваемых в разделе 6.4 (стр . 305).

4.1 1 . П реобразование типов

Опера т ор

225

re in terpre t cas t _

Оператор re i n t e rp r e t_ea s t осуществляет низкоуровневую интерпрета­ цию битовой схемы своих операндов. Рассмотрим, например, следующее при­ ведение: int * ip ; char * р е = re i n t e rp r e t _ c a s t < ch a r * > ( ip ) ;

Никогда не следует забывать, что фактическим объектом, на который указы­ вает указатель р е, является целое число, а не символ. Любое использование ука­ зателя р е, подразумевающее, что это обычный символьный указатель, вероятно, потерпит неудачу во время выполнения. Например, следующий код, вероятней всего, приведет к непредвиденному поведению во время выполнения: s t r i ng s t r ( p c ) ;

Использование указателя р е для инициализации объекта типа s t r i ng хороший пример небезопасности оператора re i n t e rp r e t_e a s t . Проблема в том, что при изменении типа компилятор не выдаст никаких предупрежде­ ний или сообщений об ошибке. При инициализации указателя ре адресом типа int компилятор не выдаст ни предупреждения, ни сообщения об ошибке, по­ скольку явно указано, что это и нужно. Однако любое последующее примене­ ние указателя р е подразумевает, что он содержит адрес значения типа ehar * . Компилятор не способен выяснить, что фактически это указатель на тип i n t . Таким образом, инициализация строки s t r при помощи указателя р е вполне правомерна, хотя в данном случае абсолютно бессмысленна, если не хуже! Ог­ следить причину такой проблемы иногда чрезвычайно трудно, особенно если приведение указателя i p к р е происходит в одном файле, а использование ука­ зателя р е для инициализации объекта класса s t r i ng - в другом. -

& ВНИМАНИЕ

Оператор re i n t e rp r e t_ea s t жестко зависит от конкретной маши­ ны. Чтобы безопасно использовать оператор re i n te rpret _ e a s t, следует хорошо понимать, как именно реализованы используемые типы, а также то, как компилятор осущесгвляет приведе1П1е.

П риведение типов в с тар о м с тиле В ранних версиях яЗыка С++ явное приведение имело одну из следующих двух форм: тип ( выражение ) ; ( тип ) выражение ;

1 1 форма з а писи прив едения в стиле функции 1 1 форма з а писи прив едения в с тиле языка С

В зависимости от используемых типов, приведение старого стиля срабатыва­ ет аналогично операторам e o n s t_e a s t, s t a t i e_e a s t или re i n te rp re t _ e a s t . В случаях, где используются операторы s t a t i e_e a s t или eo n s t_e a s t, приве-

Глава 4. Вы ра жения

226

дение типов в старом стиле позволяет осуществить аналогичное преобразова­ ние, что и соответствующий именованный оператор приведения. Но если ни один из подходов не допустим, то приведение старого стиля срабатывает ана­ логично оператору r e i n t e rp r e t_c a s t . Например, используя форму записи старого стиля, можно получить тот же результат, что и с использованием r e i n t e rp r e t ca s t . cha r * р е =

( e ha r * )

ip ;

/ / ip ука з а тель на тип i n t

СОВЕ Т . ИЗБЕГАЙТЕ П Р ИВ ЕДЕНИЯ ТИПОВ

Приведение нарушает обычный порядок контроля соответствия типов (см. раздел 2.2, стр . 80), поэтому авторы настоятельно рекомендуют избегать приведения типов. Это особенно справедливо для оператора re i n t e rp r e t_ c a s t . Такие приведения всегда опасны. Операторы c o n s t_ca s t могут быть весьма полезны в контексте перегруженных функций, рассматриваемых в разделе 6.4 (стр . 305). Использование оператора c o n s t_c a s t зачастую свидетельствует о плохом проекте. Другие операторы приведения, s t a t i c_ ca s t и dynami c_c a s t, должны быть необходимы нечасто. При каждом при­ менении приведения имеет смысл хорошо подумать, а нельзя ли получить тот же результат другим способом. Если приведение все же неизбежно, имеет смысл принять меры, позволяющие снизить вероятность возникновения ошибки, т.е. ограничить область видимости, в которой используется приве­ денное значение, а также хорошо документировать все подобные случаи.

& ВНИМАНИЕ

Приведения старого стиля менее очевидны, чем именованные операторы приведения. Поскольку их легко упустить из виду, об­ наружить ошибку становится еще трудней.

Упражнения раздела 4.1 1 .3 Упражнение

4.36.

С учетом того, что i имеет тип i n t, а d - douЫ e, напи­

шите выражение i * = d так, чтобы осуществлялось целочисленное умно­ жение, а не с плавающей запятой.

Упражнение 4.37. Перепишите каждое из следующих приведений старого стиля так, чтобы использовался именованный оператор приведения. i n t i ; douЫ e d ; c o n s t s t r i n g * p s ; e h a r * р е ; v o i d * p v ; ( Ь ) i = i n t ( * pe ) ; ( а ) pv = ( v o i d * ) p s ; ( е ) pv = & d ; ( d ) р е = ( eh a r * ) p v ;

Упражнение 4.38. Объясните следующее выражение: douЫ e s l op e = s t a t i c_e a s t < do u Ы e > ( j / i ) ;

4. 12 . Таблица п р ио р итетов операто р ов

4 .1 2 .

227

Т а б лица п р ио р итетов опе р ато р ов

Таб л и ц а 4.4. П р ио р итет опе р ато р ов П ор я док и опе ратор Действие

П р именение : : имя

Стр. 371

Область видимости класса

кла сс : : имя

1 31

л . .

Обласгь видимости пространства имен

простра нствоимен : : vмя

1 24

л .

Обращение к члену класса

объ ект . член

51

Л ->

Обращение к члену класса

роi п t еr- > член

1 58

л []

Индексирование

выражение [ выражение ]

1 65

л ()

Вызов функции

имя ( список_выражений)

52

л ()

Конструкция t ype

тип ( список_ выражений)

225

П++

Посrфиксный инкремент

1 -зна чение+ +

204

П --

Посrфиксный декремент

1 -зна чение- -

204

П t ype i d

Идентификатор типа

t уре i d ( тип)

1 032

П t ype i d

Идентификатор типа времени выполнения

t уре i d ( выражение )

1 032

П Явное приведение

Преобразование типов

са s t_имя< тип> ( выражение )

223

П ++

Префиксный инкремент

+ + 1 -зна чение

204

П --

Префиксный декремент

- - 1 -зна чение

204

п�

Побитовое NOT

� выражение

21 2

П!

Логическое NOT

! выражение

1 99

П-

Унарный минус

- выражение

195

П+

Унарный плюс

+ выражение

194

П*

Обращение к значению

* выражение

88

п &

Обращение к адресу

& 1 -зна чение

87

п ()

Преобразование типов

( тип) выражение

21 5

П si zeof

Размер объекта

s i z e o f выражение

21 5

П si zeof

Размер типа

s i z e o f ( тип)

21 5

П s i zeof . . .

Размер пакета параметров

s i z e o f . . . ( имя)

21 5

П new

Создание объекта

new тип

584

П new [ ]

Создание массива

new тип [ ра змер ]

584

П de l e te

Освобождение объекта

de l e t e выражение

587

П de l e t e [ ]

Освобождение массива

de l e t e [ ] выражение

587

П noex cept

Способность к передаче

nоех с ерt ( выражение )

976

л . .

л . .

Глобальная область В ИДИМОСТИ

Глава 4. В ыра ж ения

228

Окончан ие табл. 4.4

П рименение

С тр.

Указатель на член класса

ука за тель -

1 044

Указатель на член класса

объ -

Л *

Умножение

выражение *

выражение

1 96

Л /

Деление

выражение /

выражение

196

Л %

Деление по модулю (оста

выражение % выражение

1 96

Порядок и оператор Действие Л ->* л .*

> * ука за тель_ на_ член

1 044

ект . * ука за тель_на_ член

-

ток) Л +

Сумма

выражение + выражение

1 96

Л -

Ра зница

выражение - выражение

1 96

Л >

Поб итовый сдвиг вправо

выражение > > выражение

21 1

Л
выражение

1 97

Л >=

Больше или равно

выражение >= выражение

197

Л --

Равенство

выражение

выражение

197

л !=

Нер авенство

выражение

! = выражение

1 97

Л &

Побитов ый

AND

выражение & выражение

21 2

л л

Поб итовый

XOR

выражение л

выражение

21 2

Л I

Побитовый

OR

выражение

выражение

21 2

л &&

Логический

AND

выражение & & выражение

1 98

л 1 1

Логический

OR

выражение

1 1

1 98

п ?:

Условный опер атор

--

выражение

выражение ? выражение

. .

208

выражение

П=

Присвоение

1 - зна чение = выражение

201

п * =, / =, % =,

Составные

1 - зна чение + = выражение,

203

п + =, -=,

Операторы

и т.д.

203

п > =,

присвое ния

203 203

П & =, 1 =, л =

П throw

Пер едача исключе ния

throw

выражение

258

л ,

Запятая

выражение , выражение

21 7

Резюме

229

Резю м е Язык С++ предоставляет богатый набор операторов и определяет их назна­ чение, когда они относятся к значениям встроенных типов. Кроме того, язык поддерживает перегрузку операторов, позволяющую самостоятельно опреде­ лять назначение операторов для типов класса. Определение операторов для собственных типов рассматривается в главе 14. Чтобы разобраться в составных выражениях (содержащих несколько опе­ раторов), необходимо выяснить приоритет и порядок обработки операндов. Каждый оператор имеет приоритет и порядок. Приоритет определяет груп­ пировку операторов в составном выражении, а порядок определяет группи­ ровку операторов с одинаковым уровнем приоритета. Для большинства операторов порядок выполнения операндов не определен, компилятор выбирает сам, какой операнд обработать сначала - левый или правый. Зачастую порядок вычисления результатов операндов никак не влияет на результат выражения. Но если оба операнда обращаются к одному объекту, причем один из них изменяет объект, то порядок выполнения становится весьма важен, а связанные с ним серьезные ошибки обнаружить крайне сложно. И наконец, компилятор зачастую сам преобразует тип операндов в другой связанный тип. Например, малые целочисленные типы преобразуются в больший целочисленный тип каждого выражения. Преобразования сущест­ вуют и для встроенных типов, и для классов. Преобразования могут быть так­ же осуществлены явно, при помощи приведения.

Т е р минь1 L-значение (1-value) . Выражение, возвращающее объект или функцию. Неконстантное 1-значение обозначает объект, который может быть левым операндом оператора при­ своения. R-зн ачение (r-value). Выражение, возвращающее значение, но не ассоциированную с ним область, если таковое значение вообще имеется. Ар ифметическое прео б ра зование (arithmetic conversion). Преобразование одного арифметического типа в другой. В контексте парных арифметических операторов арифметические преобразования, как правило, сохраняют точность, преобразуя зна­ чения меньшего типа в значения большего (например, меньший целочисленный тип char или s h o rt преобразуется в i n t). В ыражение (expression). Самый низкий уровень вычислений в программе на языке С++. Как правило, выражения состоят из одного или нескольких операторов. Каждое выра­ жение возвращает результат. Выражения могут использоваться в качестве операндов, что позволяет создавать составные выражения, которым для вычисления собствеmюго результата нужны результаты других выражений, являющихся его операндами.

Глава 4. В ыраж ения

230

Вычисление п о сокращенной схеме (short-circuit evaluation). Термин, описывающий способ выполнения операторов логического AND и OR. Если первого операнда этих операторов достаточно для определения общего результата, то остальные операнды не рассматриваются и не вычисляются. Неявное преоб р азование (implicit conversion). Преобразование, которое осуществляется компилятором автоматически. Такое преобразование осуществляется в случае, когда оператор получает значение, тип которого отличается от необходимого. Компилятор автоматически преобразует операнд в необходимый тип, если соответствующее пре­ образование определено. О п еранд (operand). Значение, с которым работает выражение. У каждого оператора есть один или несколько операндов О п е ратор - - . Оператор декремента. Имеет две формы, префиксную и постфиксную. Префиксный оператор декремента возвращает 1-значение. Он вычитает единицу из значения операнда и возвращает полученное значение. Постфиксный оператор дек­ ремента возвращает r-значение. Он вычитает единицу из значения операнда, но воз­ вращает исходное, неизмененное значение. Примечание: итераторы имеют опера­ тор - -, даже если у них нет оператора - . Оп ерато р ! Оператор логического NОТ. Возвращает инверсное значение своего опе­ ранда типа b o o l . Результат t rue , если операнд f a l s e, и наоборот. О п ерато р & . Побитовый оператор AND. Создает новое целочисленное значение, в кото­ ром каждая битовая позиция имеет значение 1 , если оба операнда в этой позиции имеют значение 1 . В противном случае бит получает значение О . О перато р & & . Оператор логического AND. Возвращает значение t rue, если оба операн­ да истинны. Правый операнд обрабатывается, только если левый операнд истинен. О пер ато р . Оператор запятая. Бинарный оператор, обрабатывающийся слева направо. Результатом оператора запятая является значение справа. Результат является 1значением, только если его операнд - 1-значение. О п ерато р ? : . Условный оператор. Сокращенная форма конструкции i f . . . e l s e сле­ дующего вида: условие ? выражение 1 : выражение2 ; Если усло вие исгинно (зна­ чение t rue) выполняется выражение l , в противном случае - выр а жение 2. Тип вы­ ражений должен совпадать или допускать преобразование в общий тип. Выполняется только одно из выражений. О п ерато р . Побитовый оператор XOR. Создает новое целочисленное значение, в кото­ ром каждая битовая позиция имеет значение 1 , если любой (но не оба) из операндов содержит значение 1 в этой битовой позиции. В противном случае бит получает зна­ чение О . Оп ерато р 1 . Побитовый оператор OR. Создает новое целочисленное значение, в кото­ ром каждая битовая позиция имеет значение 1 , если любой из операндов содержит значение 1 в этой битовой позиции. В противном случае бит получает значение О . Оператор 1 1 . Оператор логического OR. Возвращает значение t rue, если любой из опе­ рандов истинен. Правый операнд обрабатывается, только если левый операнд ложен. О п ератор "' . Побитовый оператор NOT. Инвертирует биты своего операнда. О п ератор + + . Оператор инкремента. Оператор инкремента имеет две формы, префикс­ ную и постфиксную. Префиксный оператор инкремента возвращает 1-значение. Он добавляет единицу к значению операнда и возвращает полученное значение. Пост.

,

"'

Те р мины

231

фиксный оператор инкремента возвращает r-значение. Он добавляет единицу к зна­ чению операнда, но возвращает исходное, неизмененное значение. Примечание: ите­ раторы имеют оператор + +, даже если у них нет оператора +. Оп ератор < < . Оператор сдвига влево. Сдвигает биты левого операнда влево. Количество позиций, на которое осущесгвляется сдвиг, задает правый операнд. Правый операнд должен быть нулем или положительным значением, ни в коем случае не превосхо­ дящим количества битов в левом операнде. Левый операнд должен быть беззнаковым; если левый операнд будет иметь знаковый тип, то сдвиг бита знака приведет к не­ предсказуемому результату. Оператор > > . Оператор сдвига вправо. Аналогичен оператору сдвига влево, за исклю­ чением направления перемещения битов. Правый операнд должен быть нулем или положительным значением, ни в коем случае не превосходящим количества битов в левом операнде. Левый операнд должен бьnъ беззнаковым; если левый операнд будет иметь знаковый тип, то сдвиг бита знака приведет к непредсказуемому результату. Операто р c o n s t _c a s t . Применяется при преобразовании объекта со спецификатором con s t нижнего уровня в соответствующий неконстантный тип, и наоборот. Оператор dynami c_c a s t . Используется в комбинации с наследованием и идентифика­ цией типов во время выполнения. См. раздел 1 9.2, стр. 1 029. Опер ато р re i n t e rp r e t _ c a s t . Интерпретирует содержимое операнда как другой тип. Очень опасен и жестко зависит от машины. Оператор s i z e o f . Возвращает размер в байтах объекта, указанного по имени типа, или типа переданного выражения. Операто р s t a t i c _c a s t . Запрос на явное преобразование типов, которое компилятор осуществил бы неявно. Зачастую используется для переопределения неявного преоб­ разования, которое в противном елучае выполнил бы компилятор . Оператор (operator). Символ, который определяет действие, выполняемое выражением. В языке определен целый набор операторов, которые применяются для значений встроенных типов. В языке определен также приоритет и порядок выполнения для каждого оператора, а также задано количество операндов для каждого из них. Опе­ раторы могут быть перегружены и применены к объектам классов. Парный оператор (Ьinary operator) . Операторы, в которых используются два операнда. Перегруженный опе р атор (overloaded operator). Версия оператора, определенного для использования с объектом класса. Определение перегруженных версий операторов рассматривается в главе 1 4. Порядок (associativity) . Определяет последовательность выполнения операторов одина­ кового приоритета. Операторы могут иметь правосторонний (справа налево) или ле­ восторонний (слева направо) порядок выполнения. Порядок вычисления (order of evaluation). Порядок, если он есть, определяет последо­ вательность вычисления операндов оператора. В большинстве ел учаев компилятор С++ самостоятельно определяет порядок вычисления операндов. Однако, прежде чем выполнится сам оператор, всегда вычисляются его операнды. Только операторы & &, 1 1 , ? : и , определяют порядок выполнения своих операндов. Пр еоб разование (conversion) . Процесс, в ходе которого значение одного типа преобра­ зуется в значение другого типа. Преобразования между встроенными типами зало­ жены в самом языке. Для классов также возможны преобразования типов.

232

Глава 4. В ыра жения

П реоб разование (promotion). См. целочисленное преобразование. П риведение (cast) . Явное преобразование типов. П р ио ритет (precedence). Определяет порядок выполнения операторов в выражении. Операторы с более высоким приоритетом выполняются прежде операторов с более низким приоритетом. Результат (result). Значение или объект, полученный при вычислении выражения. Составное выр а жение (compound expression). Выражение, сосrоящее из нескольких операторов. Унарный оператор (unary operator). Оператор, использующий один операнд. Целочисленное преоб р азование (integral promotion) . Подмножество стандартных преобразований, при которых меньший целочисленный тип приводится к ближай­ шему большему типу. Операнды меньших целочисленных типов (например, sho r t, cha r и т.д.) преобразуются всегда, даже если такие преобразования, казалось бы, необязательны.

ГЛАВА

5

О П ЕРА Т ОРЫ

В э то й главе ... Простые операторы 5.2. Операторная область видимости 5.3. Условные операторы 5.4. Итерационные операторы 5.5. Операторы перехода 5.6. Блоки try и обработка исклю чений Рез ю ме Термины 5.1 .

233 236 236 247 254 257 264 264

Подобно большинству языков, язык С++ предоставляет операторы для ус­ ловного выполнения кода, циклы, позволяющие многократно выполнять те же фрагменты кода, и операторы перехода, прерывающие поток выполнения. В данной главе операторы, поддерживаемые языком С++, рассматриваются более подробно. Операторы (statement) выполняются последовательно. За исключением са­ мых простых программ последовательного выполнения недостаточно. Поэто­ му язык С++ определяет также набор операторов управле1-1ия потоком (flow of control), обеспечивающих более сложные пути выполнения кода.



5 .1 . П р остые опе р ато р ы

Большинство операторов в языке С++ заканчиваются точкой с запятой. Выражение типа i v a l + 5 становится оператором въzр аже1-1uя (expression statement), завершающимся точкой с запятой. Операторы выражения ·состав­ ляют вычисляемую часть выражения. iva l + 5 ; cout <
> s & & s ! = s ou g h t ) ; 1 1 пус той опера тор

В условии значение считывается со стандартного устройства ввода, и объект c i n неявно проверяется на успешность чтения. Если чтение прошло успешно, во второй части условия проверяется, не равно ли полученное значение содер­ жимому переменной s o u g h t . Если искомое значение найдено, цикл wh i l e за­ вершается, в противном случае его условие проверяется снова, начиная с чте­ ния следующего значения из объекта c i n. Ре ко ендуем

Случаи применения пустого оператора следует комментировать, чтобы любой, кто читает код, мог сразу понять, что оператор про­ пущен преднамеренно.

О с те р е га й те с ь пр опущенных и лишних точек с запято й Поскольку пустой оператор является вполне допустимым, он может распо­ лагаться везде, где ожидается оператор . Поэтому лишний символ точки с за­ пятой, кото р ый может показаться явно недопустимым, на самом деле является не более, чем пустым оператором. Приведенный ниже фрагмент кода содержит два оператора: оператор выражения и пустой оператор. •

i v a l = v l + v2 ; ;

/ / ok : в т ора я то чка с з а пятой - э то лишний 1 1 пус той опер а т ор

Хотя ненужный пустой оператор зачастую безопасен, дополнительная точ­ ка с запятой после условия цикла w h i l e или оператора i f может решительно изменить поведение кода. Например, следующий цикл будет выполняться бесконечно: / / ка та строфа : лишняя то чка с з а пятой пр евра тила тело цикла 1 1 в пус т ой опера тор wh i l e ( i t e r ! = s v e c . e n d ( ) ) ; 1 1 тело цикла wh i l e пус то ! / / инкремент не являе т ся ча стью цикла ++iter ;

235

5.1 . П р остые опе раторы

Несмотря на отступ, выражение с оператором инкремента не является ча­ стью цикла. Тело цикла - это пустой оператор, обозначенный символом точ­ ки с запятой непосредственно после условия. Лишний пустой оператор не всегда безопасен.

Составн ы е опе р ато ры ( б локи ) Составной оператор (compound statement), обычно называемый блоком (Ьlock), представляет собой последовательность операторов, заключенных в фи­ гурные скобки. Блок операторов обладает собственной областью видимости (см. р�здел 2.2.4, стр. 82) . Объявленные в блоке имена доступны только в данном блоке и блоках, вложенных в него. Как обычно, имя видимо только с того мо­ мента, когда оно определено, и до конца блока включительно. Составные операторы применяются в случае, когда язык требует одного оператора, а логика программы нескольких. Например, тело цикла wh i l e или f o r составляет один оператор. Но в теле цикла зачастую необходимо выпол­ нить несколько операторов. Заключив необходимые операторы в фигурные скобки, можно получить блок, рассматриваемый как единый оператор. Для примера вернемся к циклу wh i l e из кода в разделе 1 .4.1 (стр. 38). wh i l e

( va l < = 1 О ) { s urn + = v a l ; / / присв оить s um сумму va l и s um / / доба вить 1 к va l + + va l ;

Логика программы нуждалась в двух операторах, но цикл wh i l e способен содержать только один оператор. Заключив эти операторы в фигурные скоб­ ки, получаем один (составной) оператор . Блок не завершают точкой с запятой. Как и в елучае с пустым оператором, вполне можно создать пустой блок. Для этого используется пара фигурных скобок без операторов: wh i l e {

( c i n > > s & & s ! = s o u gh t ) } / / пустой блок

Упражнения раздела Упражнение

5.1

5.1 .

Что такое пустой оператор? Когда его можно использо­

5.2.

Что такое блок? Когда его можно использовать?

вать?

Упражнение

Глава 5. О ператор ы

236

Используя оператор запятой (см. раздел 4. 1 0, стр . 21 7), пе­ репишите цикл wh i l e из раздела 1 .4.1 (стр. 38) так, чтобы блок стал больше не нужен. Объясните, улучшило ли это удобочитаемость кода.

Упражнение

5 .2.

5.3.

Опе р ат о р н а я об ласть видимости

Переменные можно определять в управляющих структурах операторов i f, s w i t ch, wh i l e и f o r . Переменные, определенные в управляющей структуре, видимы только в пределах этого оператора и выходят из области видимости по его завершении. whi l e

( int i

=

g e t n um ( ) )

/ / i созда е т ся и инициализируе тся при 1 1 каждой итера ции

c o u t s c o r e s

=

{ "F" ,

"D" ,

"С" ,

"В" ,

" А " , "А + + " } ;

Для решения этой проблемы можно использовать оператор i f e l s e, что­ бы выполнять разные действия проходных и не проходных отметок. если оценка меньше 6 0 - э т о F, в про тивном случа е вычислять индекс s t r i n g l e t t e r g r a de ; i f ( g r ade < 6 0 ) scores [ O ] ; l e t t e r g r a de else s c o r e s [ ( g r a de - 5 0 ) / 1 0 ) ; l e t t e rg r a de 11

В зависимости от значения переменной g ra de оператор выполняется либо после части i f, либо после части е l s е . В части е l s е вычисляется индекс оценки уже без неудовлетворительных. Затем усекающее остаток целочис­ ленное деление (см. раздел 4.2, стр. 1 96) используется для вычисления соответ­ ствующего индекса вектора s c o r e s .

238

Глава 5. О ператоры

В ложенные опе р а т о ры i f Чтобы сделать программу интересней, добавим к удовлетворительным от­ меткам плюс или минус. Плюс присваивается оценкам, заканчивающимся на 8 или 9, а минус - заканчивающимся на О, 1 или 2. if

( g r a de % 1 0 > 7 ) l e t t e r g r a de + = ' + ' ; 1 1 оценки, з а канчив ающие ся на 8 или 9 , получают + e l s e i f ( g r a de % 1 0 < 3 ) l e t t e r g r ade + = ' - ' ; / / оценки, з а кан чив ающие ся на О , 1 и 2 , получают -

Для пол учения остатка и принятия на основании его решения, добавлять ли плюс или минус, используем оператор деления по модулю (см. раздел 4.2, стр. 1 96). Теперь добавим код, присваивающий плюс или минус, к коду, выбираю­ щему символ оценки: 1 1 если оценка неудовле тв оритель на , не т смысла пров ерять ее на + или i f ( g r a de < 6 0 ) scores [ O ] ; l e t t e r g r a de e l se { l e t t e r g r ade s c o r e s [ ( g r a de - 5 0 ) / 1 0 ] ; / / выбра ть симв ол оценки i f ( g r a de ! = 1 0 0 ) / / д о ба влять + или - , толь ко е сли э т о не А + + i f ( g r a de % 1 0 > 7 ) l e t t e r g r a d e + = ' + ' ; 1 1 оценки , з а канчив ающие ся на 8 или 9 , 1 1 получа ют + e l s e i f ( g rade % 1 0 < 3 ) l e t t e r g r ade + = ' - ' ; 1 1 о ценки , з а ка нчив ающие ся на О , 1 и 2 , 1 1 получают -

Обратите внимание, что два оператора, следующих за первым оператором e l s e, заключены в блок. Если переменная g r a de содержит значение 6 0 или больше, возможны два действия: выбор символа оценки из вектора s c o r e s и, при условии, добавление плюса или минуса.

С ледите за ф и rурными ск о б ками Когда несколько операторов следует выполнить как блок, довольно часто забывают фигурные скобки. В следующем примере, вопреки отступу, код до­ бавления плюса или минуса выполняется безусловно: if

( g r ade < 6 0 ) l e t t e r g r ade = s c o r e s [ O ] ; e l s e / / ошибка : о т сут ств уе т фигурна я ско бка l e t t e r g r ade = s c o r e s [ ( g r a de - 5 0 ) / 1 0 ] ; 1 1 не смо тря на внешний вид , без фигурной скобки, э т о т код 1 1 выполняе т ся в сегда 1 1 неудовле тв оритель ным оценкам ошибо чно присв аива е тся - или + i f ( g r ade ! = 1 0 0 ) i f ( g r a de % 1 0 > 7 ) l e t t e r g r a de + = ' + ' ; 1 1 оценки , з а ка нчив ающие ся Ra 8 или 9 ,

239

5.3. Условные опе раторы 1 1 получают +

e l s e i f ( g r a de % 1 0 < 3 ) l e t t e r g r a d e + = ' - ' ; 1 1 оценки , за канчив ающие ся на О , 1 1 получают -

1

и 2,

Найти такую ошибку бывает очень трудно, поскольку программа выглядит правильно. Во избежание подобных проблем некоторые стили программирования ре­ комендуют всегда использовать фигурные скобки после оператора i f или e l s e (а также вокруг тел циклов wh i l e и f o r) . Это позволяет избежать подобных ошибок. Это также означает, что фигур­ ные скобки уже есть, если последующие изменения кода потребуют добавле­ ния операторов. Рекомендуем

У большинства редакторов и сред разработки есть инструменты автоматического выравнивания исходного кода в соответствии с его структурой. Такие инструменты всегда следует использовать, если они доступны.

П оте р янны й опе р ато р else Когда один оператор i f вкладывается в другой, ветвей i f может оказаться больше, чем ветвей e l s e . Действительно, в нашей программе оценивания че­ тыре оператора i f и два оператора e l s e . Возникает вопрос: как установить, которому оператору i f принадлежит данный оператор е l s е ? Эта проблема, обычно называемая потерш-1ным опер атором e l s e (dangling e l s e), присуща многим языкам программирования, предоставляющим опе­ раторы i f и i f e l s e . Разные языки решают эту проблему по-разному. В язы­ ке С++ неоднозначность решается так: оператор e l s e принадлежит ближай­ шему распо�оженному выше оператору i f без е l s е . Неприятности происходят также, когда код содержит больше операторов i f, чем ветвей e l s e. Для иллюстрации проблемы перепишем внутренний оператор i f e l s e, добавляющий плюс или минус, на основании различных наборов условий: 1 1 Ошибка : порядок выполнения НЕ СОО ТВЕ ТСТВУЕ Т о т с тупам ; в е тв ь el se 11 принадлежит внутреннему i f i f ( g r a de % 1 0 >= 3 ) i f ( g r a de % 1 0 > 7 ) l e t t e r g r a de += ' + ' ; 1 1 оценки , за кан чив ающие ся на 8 или 9 , 1 1 получают + else l e t t e r g r a de += ' - ' '. 1 1 оценки , за ка нчив ающие ся на 3 , 4 , 5 , 6 , 1 1 получают - !

Огступ в данном коде подразумевает, что оператор e l s e предназначен для внешнего оператора i f, т.е. он выполняется, 'когда значение g r a de заканчива-

240

Глава 5. Операто ры

ется цифрой меньше 3 . Однако, несмотря на наши намерения и вопреки отсту­ пу, ветвь e l s e является частью внутреннего оператора i f. Эгот код добавляет ' - ' к оценкам, заканчивающимся на 3-7 включительно! Правильно выровнен­ ный, в соответствии с правилами выполнения, этот код выглядел бы так: / / о т с туп соотв е т стуе т порядку выполнения , но не намерению программиста i f ( g r a de % 1 0 > = 3 ) i f ( g r a de % 1 0 > 7 ) l e t t e r g r ade + = ' + ' ; 1 1 оценки , з а ка нчив ающие ся на 8 или 9 , 1 1 получа ют + else l e t t e r g r a de + = ' - ' '. 1 1 оценки , за ка нчив а ющие ся на 3 , 4 , 5 , б , 1 1 получают - !

К онт р оль пути выполнения при помощи ф игурн ы х ско б ок Заключив внутренний оператор i f в блок, можно сделать ветвь e l s e ча­ стью внешнего оператора i f : 11 11

доба влять плюс для оцено к , з а канчив ающихся на 8 или 9 , а минус для з а ка нчив ающихся на О , 1 или 2 i f ( g r a de % 1 0 >= 3 ) { i f ( grade % 1 0 > 7 ) l e t t e r g r a de + = ' + ' ; 1 1 оценки, з а ка нчив ающие ся на 8 или 9 , 1 1 получают + e l s e / / ско бки о б е спе чив ают e l s e для внешнего i f l e t t e r g r a de + = ' - ' ; / / оценки , з а ка нчив ающие ся на О , 1 и 2 , 1 1 получают -

Операторы не распространяются за границы блока, поэтому внутренний цикл i f заканчивается на закрывающей фигурной скобке перед оператором e l s e . Оператор e l s e не может быть частью внутреннего оператора i f . Теперь ближайшим свободным оператором i f оказывается внешний, как и предпола­ галось изначально.

Упражнения раздела

5.3.1

Напишите собственную версию программы преобразова­ ния числовой оценки в символ с использованием оператора i f e l s e .

Упражнение

5.5.

Перепишите программу оценки так, чтобы использовать условный оператор (см. раздел 4.7, стр. 208) вместо оператора i f e l s e .

Упражнение

5.6.

Упражнение

5.7.

Исправьте ошибки в каждом из следующих фрагментов

кода: (а)

(Ь)

if

( i va l l ! = i va l 2 ) iva l l = i va l 2 e l s e i va l l = i va l 2 i f ( i va l < m i n va l ) mi nva l i va l ; occu rs = 1 ;

О;

241

5.3. Условные опер ато р ы (с)

if if

(d)

if

( i n t i v a l = g e t _va l ue ( ) ) " < < iva l < < endl ; c o u t < < " i va l ( ! i va l ) c o u t < < " iv a l 0 \n" ; ( ival = 0 ) i v a l = g e t _v a l u e ( ) ; Ч т о т а к о е " п о т е р я н ный о п е р а т ор e l s e " ? Ка к в я зыке С + +

Упражнение 5 . 8 .

определ я е т с я принадлежн о с т ь

5.3.2. Опе р а то р

ветви e l s e ?

swi tch

Оператор s w i t ch предоставляет более удобный способ выбора одной из множества альтернатив. Предположим, например, что необходимо рассчи­ тать, как часто встречается каждая из пяти гласных в некотором фрагменте текста. Программа будет иметь следующую логику. •

Читать каждый введенный символ.



Сравнить каждый символ с набором искомых гласных.



Если символ соответствует одной из гласных букв, добавить 1 к соответ­ ствующему счетчику.



Отобразить результаты.

Программа должна отобразить результаты в следующем виде: Nurnb e r Nurnbe r Nurnb e r Nurnbe r Nurnb e r

of of of of of

vowe l vowe l vowe l vowe l v owe l

а: е:

i: о: и :

3195 6230 3 1 02 32 8 9 1033

Для непосредственного решения этой задачи можно использовать оператор s w i t ch . 1 1 инициализиров а ть с че т чики для каждой гла сной О; un s i g n e d a C n t = О , e C n t = О , i C n t = О , o C n t = О , u C n t cha r c h ; wh i l e ( c i n > > c h ) { гла сна я , ув еличить с о о тв е т с тв ующий с че т чик 1 1 е сли ch sw i t ch ( c h ) { case ' а ' : + +aCnt ; brea k ; case ' е ' : + + eCnt ; brea k ; case ' i ' : ++ iCnt ; brea k ; case ' о ' : + +oCnt ; brea k ; -

Глава 5. Опе раторы

242 case

: + + uC n t ; brea k ; ' и '

} 11

выв од резуль та та c o u t < < '' Nurnbe r o f vowe l < < " N urnb e r o f vowe l < < " Nurnbe r o f v o we l < < " Nurnbe r o f vowe l < < " Nurnbe r o f vowe l

а: е:

i: о: и :

\t" \t" \t" \t" \t"

и возвращающую сумму элементов списка.

Упражнение

6.27.

типа

Во второй версии функции е r ro r _rns g ( ) , где у нее есть параметр типа E r r C o de, каков тип элемента в цикле f o r ?

Упражнение

6.28.

При использовании типа i n i t i a l i z e r_l i s t в серийном операторе f о r использовали бы вы ссылку как управляющую переменную цикла? Объясните почему.

Упражнение

6 .3.

6.29.

Т ипы возв р а щ аемо г о значения и опе р ато р re turn

Оператор re t u r n завершает выполнение функции и возвращает управле­ ние той функции, которая вызвала текущую. Существуют две формы опера­ тора r e t u rn: return ; r e t u r n выражение ;



6.3.1 . Ф ункции б ез возвр ащаемого значения

Оператор r e t u r n без значения применим только в такой функции, типом возвращаемого значения которой объявлен vo i d. Функции, возвращаемым типом которых объявлен vo i d, необязательно должны содержать оператор re tu rn. В функции типа vo i d оператор r e t u r n неявно размещается после последнего оператора. Как правило, функции типа vo i d используют оператор re tu rn для прежде­ временного завершения выполнения. Это аналогично использованию операто­ ра b r e a k (см. раздел 5.5. 1 , стр. 254) для выход из цикла. Например, можно напи­ сать функцию s wap ( ) , которая не делает ничего, если значения идентичны: v o i d s wap ( i n i & v l , i n t & v 2 ) { 1 1 е сли зна чения ра вны , их замена не нужна ; можно выйти сра зу

6 .3. Типы возвращаемого значения и опе ратор retum if

293

(vl v2 ) return ; 1 1 если мы здесь , приде тся пора б о та ть i n t tmp = v2 ; v2 vl ; v l = tmp ; 1 1 яв но ука зыв а ть опера тор re t u rn не о бяза тель но ==

=

Сначала эта функция проверяет, не равны ли значения, и если это так, то завершает работу. Если значения не равны, функция меняет их местами. Поеле последнего оператора присвоения осуществляется неявныи выход из функции. Функции, для возвращаемого значения которых указан тип vo i d, вторую форму оператора r e t u rn могут использовать только для возвращения резуль­ тата вызова другой функции, которая возвращает тип vo i d. Возвращение лю­ бого другого выражения из функции типа vo i d приведет к ошибке при ком­ пиляции. �





6 . 3 . 2 . Ф ункции, возв р ащающие значение

Вторая форма оператора r e t u rn предназначена для возвращения резуль­ тата из функции. Каждый случай возвращения значения типа, отличного от vo i d, должен возвратить значение. Возвращаемое значение должно иметь тип, либо совпадающий, либо допускающий неявное преобразование (см. раз­ дел 4.1 1 , стр. 21 8) в тип, указанный для возвращаемого значения функции при определении. Хотя язык С++ не может гарантировать правильность результата, он спосо­ бен гарантировать, что каждое возвращаемое функцией значение будет соот­ ветствовать объявленному типу. Это может получиться не во всех случаях, компилятор попытается обеспечить возвращение значения и выход только че­ рез допустимый оператор re tu r n . Например : 1 1 некорре ктное в озвращение зна чения , э т о т код не буде т о ткомпилиров а н boo l s t r s ub r a n ge ( c on s t s t r i n g & s t r l , c o n s t s t r i n g & s t r 2 ) { 1 1 ра змеры одина ковы : в озвра тить о бычный резуль та т сра в нения i f ( s t r l . s i ze ( ) == s t r 2 . s i z e ( ) ) r e t u r n s t r l = = s t r 2 ; / / ok : = = в о звраща е т b o o l 1 1 на йти ра змер меньшей с троки ; услов ный опера тор см . ра здел 4 . 7 , 1 1 стр . 2 0 8 auto s i z e = ( s t r l . s i z e ( ) < s t r 2 . s i z e ( ) ) ? s t r l . s i z e ( ) : s t r2 . s i z e ( ) ; 1 1 про смо тр е ть в се элементы до ра змера меньшей строки f o r ( de c l t ype ( s i z e ) i = О ; i ! = s i z e ; + + i ) { i f ( strl [ i ] 1 - str2 [ i ] ) r e t u r n ; / / ошибка # 1 : не т в о звраща емого зна чения ; компилятор 1 1 должен о бнаружить э ту ошибку

Глава 6. Функции

294 } 1 1 ошибка #2 : выполнение може т дойти д о конца функции , 1 1 не в с тре тив опера тор re t u rn 1 1 компилятор може т и не обнаружить э ту ошибку

та к и

Оператор r e t u rn в цикле f o r является ошибочным потому, что он не в со­ стоянии вернуть значение. Эту ошибку компилятор должен обнаружить. Вторая ошибка заключается в том, что функция не имеет оператора r e t u r n после цикла. Если произойдет вызов этой функции со строкой, яв­ ляющейся подмножеством другой, процесс выполнения минует цикл f о r. Однако оператор re t u rn для этого случая не предусмотрен. Эту ошибку ком­ пилятор может и не обнаружить. В этом елучае поведение программы во вре­ мя выполнения будет непредсказуемо.

& ВНИМАНИЕ

Отсутствие оператора r e t u rn после цикла, который этот оператор содержит, является особенно коварной ошибкой. Однако боль­ шинство компиляторов ее не обнаружит.

К ак возвращаются з н аче н ия Значения функций возвращаются тем же способом, каким инициализиру­ ются переменные и параметры: возвращаемое значение используется для инициализации временного объекта в точке вызова, и этот временный объект является результатом вызова функции. В функциях, возвращающих локальные переменные, важно иметь в виду правила инициализации. Например, можно написать функцию, которой пе­ редают счетчик, слово и окончание. Функция возвращает множественную _ форму слова, если счетчик больше 1 : 1 1 в о звра тить множе ств енную форму слов а , е сли c t r больше 1 s t r i n g ma ke _p l u r a l ( s i z e _ t c t r , c o n s t s t r i ng & wo r d , co n s t s t r in g & endi n g )

return

( ctr > 1 )

? w o r d + e n d i n g : wo r d ;

Тип возвращаемого значения этой функции s t r i n g, это значит, что воз­ вращаемое значение копируется в точке вызова. Функция возвращает копию значения wo rd или безымянную временную строку, полученную конкатена­ цией w o r d и e n d i n g . Когда функция возвращает ссылку, она, подобно любой другой ссылке, яв­ ляется только другим именем для объекта, на который ссылается. Рассмотрим, например, функцию, возвращающую ссылку на более короткую из двух пере­ данный ей строк: -

1 1 во звра тить ссылку на строку , ко торая коро че c o n s t s t r i n g & s h o r t e r S t r i n g ( c on s t s t r i n g & s l , c o n s t s t r i n g & s 2 )

6.3. Типы возв ра щаемого значения и опе ратор retum

return s l . s i z e ( )

( s 2 ) ) ; r e t u r n c o n s t_ca s t < s t r i n g & > ( r ) ;

Эта версия вызывает константную версию функции s h o r t e r S t r i n g ( ) при приведении типов ее аргументов к ссылкам на константу. Функция возвраща­ ет ссылку на тип co n s t s t r i n g, которая, как известно, привязана к одному из исходных, неконстантных аргументов. Следовательно, приведение этой стро­ ки назад к обычной ссылке s t r i n g & при возвраrцении вполне безопасно.

Глава 6. Функции

306

В ы зов пе регруженно й ф ункц ии Когда набор перегруженных функций определен, необходима возмож­ ность вызвать их с соответствующими аргументами. Подбор функции (function matching), известный также как поиск перегруженной функции (overload resolu­ tion), - это процесс, в ходе которого вызов функции ассоциируется с опреде­ ленной версией из набора перегруженных функций. Компилятор определяет, какую именно версию функции использовать при вызове, сравнивая аргумен­ ты вызова с параметрами каждой функции в наборе. Как правило, вовсе несложно выяснить, допустим ли вызов, и если он до­ пустим, то какая из версий функции будет использована компилятором. Функции в наборе перегруженных версий отличаются количеством или ти­ пом аргументов. В таких елучаях определить используемую функцию просто. Подбор функции усложняется в случае, когда количество параметров одина­ ково и они допускают преобразование (см. раздел 4.1 1 , стр . 21 8) переданных аргументов. Распознавание вызовов компилятором при наличии преобразо­ ваний рассматривается в разделе 6.6 (стр. 31 7), а пока следует понять, что при любом вызове перегруженной функции возможен один из трех результатов. •

Компилятор находит одну функцию, которая является наилучш им соот­ ветствием (best match) для фактических аргументов, и создает код ее вызова.



Компилятор не может найти ни одной функции, параметры которой соответствуют аргументам вызова. В этом случае компилятор сообщает об ошибке отсутствия соответствия (no match). Компилятор находит несколько функций, которые в принципе подхо­ дят, но ни одна из них не соответствует полностью. В этом случае ком­ пилятор также сообщает об ошибке, об ошибке неоднозначности вызова (amЬiguous call) .



Упражнения раздела

6.4

Упражнение 6.39. Объясните результат второго объявления в каждом из следующих наборов. Укажите, какое из них (если есть) недопустимо. (а) (Ь) (с)

int calc ( i nt , int ) ; int calc ( const i n t , con s t int ) ; int get ( ) ; douЫ e ge t ( ) ; int * reset ( i nt * ) ; douЫ e * r e s e t ( do u Ы e * ) ;

6.4. Перег р у женные функции



307

6.4.1 . Пе р ег р уз1 i t em . u n i t s s o l d > > p r i c e ; i t em . reve n u e = p r i c e * i t em . u n i t s s o l d ; return i s ;

o s t r e am & p r i n t ( o s t r e am & o s , o s < < i t em . i s bn ( )

c o n s t S a l e s_da t a & i t em )

< < " " < < i t em . u n i t s s o l d < < " "

7.1 . Определение абстрактных типов данных

341

< < i t ern . r e ve n u e < < " " < < i t ern . avg p r i c e ( ) ; return o s ;

Функция r e a d ( ) читает данные из предоставленного потока в заданный объект. Функция p r i n t ( ) выводит содержимое предоставленного объекта в за­ данный поток. В этих функциях, однако, следует обратить внимание на два момента. Во­ первых, обе функции получают ссылки на соответствующие объекты классов ввода и вывода. Классы ввода и вывода - это типы, не допускающие копиро­ вания, поэтому их передача возможна только по ссылке (см. раздел 6.2.2, стр . 277) . Кроме того, чтение и запись в поток изменяют его, поэтому обе функции получают обычные ссылки, а не ссылки на константы. Второй заслуживающий внимания момент: функция p r i n t ( ) не выводит новую строку. Обычно функции вывода осуществляют минимальное форма­ тирование. Таким образом, пользовательский код может сам решить, нужна ли новая строка.

О пределение ф ункц ии add ( ) Функция add ( ) получает два объекта класса S a l e s_da ta и возвращает но­ вый объект класса S a l e s _ da t a, представляющий их сумму: S a l e s d a t a a d d ( c o n s t S a l e s _da ta & l h s , c o n s t S a l e s_da t a & r h s ) { S a l e s da t a s urn = l h s ; 1 1 копиров а ние переменных - членов из l h s в s um surn . c ornb i n e ( r h s ) ; 1 1 д о ба вить переменные - члены rh s в s um r e t u r n s urn ;

В теле функции определяется новый объект класса S a l e s_da t a по имени s um, предназначенный для хранения суммы двух транзакций. Инициализиру­ ем объект s um копией объекта 1 h s . По умолчанию копирование объекта клас­ са подразумевает копирование и членов этого объекта. После копирования у членов b o o kNo, u n i t s _ s o l d и reve n u e объекта s um будут те же значения, что и у таковых объекта l h s . Затем происходит вызов функции c omЬ i n e ( ) , суммирующей значения переменных-членов u n i t s _ s o l d и r evenue объектов rh s и s um в последний. По завершении возвращается копия объекта s um. Упражнения раздела

7 .1 .3

Упражнение 7.6. Определите собственные версии функций add ( ) , r e a d ( ) и pr i n t ( ) . Упражнение 7.7. Перепишите программу обработки транзакций, написан­ ной для упражнений в разделе 7.1 .2 (стр. 339), так, чтобы использовать эти новые функции.

Глава 7. Классы

342

Упражнение 7.8. Почему функция r e a d ( ) определяет свой параметр типа S a l e s_da ta как простую ссылку, а функция p r i n t ( ) - как ссылку на кон­ станту?

Упражнение 7.9. Добавьте в код, написанный для упражнений в разде­ ле 7. 1 .2 (стр. 339), операции чтения и вывода объектов класса Pe r s o n . Упражнение if



7.1 0.

( read ( read ( c i n ,

Что делает условие в следующем операторе i f? da t a l ) ,

da t a 2 ) )

7 .1 .4. Конст р укто р ы

Каждый класс определяет, как могут быть инициализированы объекты его типа. Класс контролирует инициализацию объекта за счет определения одной или нескольких специальных функций-членов, известных как конструкторы (constructor) . Задача конструктора - инициализировать переменные-члены объекта класса. Конструктор выполняется каждый раз, когда создается объект класса . В этом разделе рассматриваются основы определения конструкторов. Конст­ рукторы - удивительно сложная тема. На самом деле мы сможем больше сказать о конструкторах в разделах 7.5 (crp. 372), 15.7 (стр. 784) и 1 8.1 .3 (стр. 973), а также в главе 13. Имя конструкторов совпадает с именем класса. В отличие от других функ­ ций, у конструкторов нет типа возвращаемого значения. Как и другие функ­ ции, конструкторы имеют (возможно пустой) список параметров и (возможно пустое) тело. У класса может быть несколько конструкторов. Подобно любой другой перегруженной функции (см. раздел 6.4, стр . 302), конструкторы должны отличаться друг от друга количеством или типами своих параметров. В отличие от других функций-членов, конструкторы не могут быть объяв­ лены константами (см. раздел 7.1 .2, стр. 337) . При создании константного объ­ екта типа класса его константность не проявится, пока конструктор не закон­ чит инициализацию объекта. Таким образом, конструкторы способны осуще­ ствлять запись в константный объект во время его создания.



С и нтезируемы й стандартны й конст р уктор

Хотя в нашем классе S a l e s _da t a не определено конструкторов, исполь­ зующие его программы компилировались и выполнялись правильно. Напри­ мер, программа на стр. 333 определяла два объекта класса S a l e s_da t a : S a l e s da ta t o t a l ; S a l e s da ta t r a n s ; -

1 1 переменна я для хранения те кущей суммы 1 1 переменная для хранения да нных следующей 1 1 транза кции

Естественно, возникает вопрос: как инициализируются объекты t o t a l и trans?

7.1 . Опреде л ение абстрактных типов данных

343

Настолько известно, инициализатор для этих объектов не предоставлялся, поэтому они инициализируются значением по умолчанию (см. раздел 2.2. 1 , стр. 77). Классы сами контролируют инициализацию по умолчанию, опреде­ ляя специальный конструктор, известный как стандартный конструктор (de­ fault constructor) . Стандартным считается конструктор, не получающий ника­ ких аргументов. Как будет продемонстрировано, стандартный конструктор является осо­ бенным во многом, например, если класс не определяет конструкторы явно, компилятор сам определит стандартный конструктор неявно. Созданный компилятором конструктор известен как синтезируемый стан­ дартный конструктор (synthesized default constructor). У большинства классов этот синтезируемый конструктор инициализирует каждую переменную-член класса следующим образом: •

Если есть внутриклассовый инициализатор (см. раздел 2.6. 1 , стр. 1 1 3), он и используется для инициализации члена класса.



В противном случае член класса инициализируется значением по умол­ чанию (см. раздел 2.2. 1 , стр . 77).

Поскольку класс S a l e s_da t a предоставляет инициализаторы для пере­ менных un i t s _ s o l d и r evenue, синтезируемый стандартный конструктор использует данные значения для инициализации этих членов. Переменная bookNo инициализируется значением по умолчанию, т.е. пустой строкой.

Н екоторые класс ы не могут полагат ься й стандартны й конструктор на с интезируемы Только довольно простые классы, такие как те �ущий класс S a l e s _da t a, могут полагаются на синтезируемый стандартный конструктор. Как правило, собственный стандартный конструктор для класса определяют потому, что компилятор создает его, только если для класса не определено никаких други х конструкторов. Если определен хоть один конструктор, то у класса не будет стандартного конструктора, если не определить его самостоятельно. Основа­ ние для этого правила таково: если класс требует контроля инициализации объекта в одном случае, то он, вероятно, потребует его во всех случаях. Компилятор создает стандартный конструктор автоматически, только если в классе не объЯ8лено никаких конструкторов. Вторая причина для определения стандартного конструктора в том, что у некоторых классов синтезируемый стандартный конструктор работает не­ правильно. Помните, что определенные в блоке объекты встроенного или со­ ставного типа (такого как массивы и указатели) без инициализации имеют не-

344

Глава 7. Классы

определенное значение (см. раздел 2.2. 1 , стр. 77). Это же относится к не ини­ циализированным членам встроенного типа. Поэтому классы, у которых есть члены встроенного или составного типа, должны либо инициализировать их в классе, либо определять собственную версию стандартного конструктора. В противном случае пользователи могли бы создать объекты с членами, значе­ ния которых не определены .

& ВНИМАНИЕ

Классы, члены которых имеют встроенный или составной тип, мо­ гут полагаться на синтезируемый стандартный конструктор, только если у всех таких членов есть внуrриклассовые инициализаторы.

Третья причина определения некоторыми классами собственного стан­ дартного конструктора в том, что иногда компилятор неспособен создать его. Например, если у класса есть член типа класса и у этого класса нет стандартнаго конструктора, то компилятор не сможет инициализировать этот член. Для таких классов следует определить собственную версию стандартного кон­ структора. В противном случае у класса не будет пригодного для использова­ ния стандартного конструктора. Дополнительные обстоятельства, препятст­ вующие компилятору создать соответствующий стандартный конструктор, приведены в разделе 1 3. 1 .6 (стр . 644) .

О п ре д еле ни е конструктор ов класса Sales_da ta Определим для нашего класса S a l e s da t a четыре конструктора со сле­ дуюп:(ими параметрами: •

Типа i s t re am & , для чтения транзакции.



Типа c o n s t s t r i n g & для ISBN; типа u n s i gn e d для количества продан­ ных книг; типа douЫ e для цены проданной книги.



Типа c o n s t s t r i n g & для ISBN. Для других членов этот конструктор бу­ дет использовать значения по умолчанию.



Без параметров (т.е. стандартный конструктор). Этот конструктор при­ дется определить, поскольку определены другие конструкторы.

Добавим эти члены в класс так: s t r u c t S a l e s da t a { 1 1 доба вленные конструкт оры S a l e s da t a ( ) de f a u l t ; S a l e s_da t a ( c o n s t s t d : : s t r i n g & s ) : b o o kN o ( s ) { } S a l e s_da t a ( c o n s t s t d : : s t r i n g & s , u n s i gn e d n , d o u Ы e b o o kNo ( s ) , u n i t s _s o l d ( n ) , r e ve n u e ( p * n ) { S a l e s da t a ( s t d : : i s t r e am & ) ; 1 1 другие члены, ка к прежде s t d : : s t r i n g i s bn ( ) c o n s t { r e t u r n b o o kNo ; S a l e s d a t a & c omb i n e ( c o n s t S a l e s da t a & ) ; do u Ы e avg_p r i c e ( ) c o n s t ; =

-

-

р) : }

7.1 .

Оп р еделение абстрактных типов данных s t d : : s t r i n g b o o kNo ; un s i gn e d un i t s s o l d douЫ e r e v e n u e = О . О ; -

345

О;

};

Ч т о значит = defaul t Начнем с объяснения стандартного конструктора: S a l e s da t a ( )

= de f a u l t ;

В первую очередь обратите внимание на то, что это определение стандарт­ ного конструктора, поскольку он не получает никаких аргументов. Мы опре­ деляем этот конструктор только потому, что хотим предоставить другие кон­ структоры, но и стандартный конструктор тоже нужен. Этот конструктор должен делать то же, что и синтезируемая версия.



По новому стандарту, если необходимо стандартное поведение, можно попросить компилятор создать конструктор автоматически, указав по­ сле списка параметров часть = de f a u l t . Синтаксис = de fau l t может присутствовать как в объявлении в теле класса, так и в определении вне его. Подобно любой другой функции, если часть = de f а u l t присутст­ вует в теле класса, стандартный конструктор будет встраиваемым; если она присутствует в определении вне класса, то по умолчанию этот член не будет встраиваемым.

& ВНИМАНИЕ

Стандартный конструктор работает в классе S a l e s_da ta только по­ тому, что предосгавлены инициализаторы для переменных-членов встроенного типа. Если ваш компилятор не померживает внугри­ классовые инициализаторы, для инициализации каждого члена класса сrандартный конструктор должен использовать список ини­ циализации конструктора (описанный непосредсrвенно ниже).

Список ини ц иализа ц ии конструктора Теперь рассмотрим два других конструктора, которые были определены в классе: S a l e s da t a ( c o n s t s t d : : s t r i n g & s ) : b o o kN o ( s ) { } S a l e s_da t a ( c o n s t s t d : : s t r i n g & s , u n s i g n e d n , douЫ e b o o kN o ( s ) , u n i t s _s o l d ( n ) , r e v e n u e ( p * n ) {

р) : }

Новой частью этих определений являются двоеточие и код между ним и фигурными скобками, обозначающими пустые тела функции. Эта новая часть - список инициализации конструктора (constructor initializer list), опре­ деляющий исходные значения для одной или нескольких переменных-членов создаваемого объекта. Инициализатор конструктора - это список имен пе­ ременных-членов класса, каждое из которых сопровождается исходным зна-

Глава 7. Класс ы

346

чением в круглых (или фигурных) скобках. Если инициализаций несколько, они отделяются запятыми. Конструктор с тремя параметрами использует первые два параметра для инициализации переменных-членов b o o kNo и un i t s s o l d. Инициализатор для переменной reve n u e вычисляется при умножении количества проданных книг на их цену. Конструктор с одним параметром типа s t r i n g использует ее для инициа­ лизации переменной-члена b o o kNo, но переменные u n i t s _ s o l d и reve nue не инициализируются явно. Когда член класса отсутствует в списке инициализа­ ции конструктора, он инициализируется неявно, с использованием того же процесса, что и у синтезируемого стандартного конструктора. В данном елу­ чае эти члены инициализируются внутриклассовыми инициализаторами. Та­ ким образом, получающий строку конструктор эквивалентен следующему. _

1 1 то же пов едение , что и у исходного конс труктора выше S a l e s _ da t a ( c o n s t s t d : : s t r i n g & s ) : b o o kNo ( s ) , u n i t s _s o l d ( O ) , r e v e n u e ( O ) { }

Обычно для конструктора лучше использовать внутриклассовый инициа­ лизатор, если он есть и присваивает члену класса правильное значение. С дру­ гой стороны, если ваш компилятор еще не померживает внутриклассовые инициализаторы, то каждый конструктор должен явно инициализировать каждый член встроенного типа. Ре оменд

Конструкторы не должны переопределять внутр иклассовые ини­ циализаторы, кроме как при использовании иного исходного зна­ чения. Если вы не можете использовать внутриклассовые инициа­ ли з аторы , каждый конструктор должен явно инициализировать каждый член встроенного типа.

Следует заметить, что у обоих этих конструкторов тела пусты. Единствен­ ное, что должны сделать эти конструкторы, - присвоить значения перемен­ ным-членам. Если ничего другого делать не нужно, то тело функции пусто.

О пределение конструктора вне тела класса В отличие от наших других конструкторов, конструктору, получающему поток i s t re am, действительно есть что делать. В своем теле этот конструктор вызывает функцию r e a d чения:

()

, чтобы присвоить переменным-членам новые зна­

S a l e s da t a : : S a l e s da t a ( s t d : : i s t r e am & i s ) read ( i s ,

* thi s ) ;

1 1 rea d чита е т тра нза кцию из i s в те кущий объе кт

7.1 . Оп р еделение абст р актных типов данных

347

У конструкторов нет типа возвращаемого значения, поэтому определе­ ние начинается с имени функции. Подобно любой другой функции-члену, при определении конструктора за пределами тела класса необходимо ука­ зать класс, которому принадлежит конструктор . Таким образом, синтаксис S a l e s _ da t a : : S a l e s _ da t a указывает, что мы определяем член класса S a l e s_da ta по имени S a l e s_da t a . Этот член класса является конструктором, поскольку его имя совпадает с именем класса . В этом конструкторе нет списка инициализации конструктора, хотя с тех­ нической точки зрения было бы правильней сказать, что список инициализа­ ции конструктора пуст. Даже при том, что список инициализации конструк­ тора пуст, члены этого объекта инициализируются прежде, чем выполняется тело конструктора. Члены, отсутствующие в списке инициализации конструктора, инициали­ зируются соответствующим внутриклассовым инициализатором (если он есть) или значением по умолчанию. Для класса S a l e s_da t a это означает, что при запуске тела функции на выполнение переменная b o o kNo будет содержать пустую строку, а переменные un i t s _ s o l d и revenue - значение О . Чтобы стало понятней, напомним, что второй параметр функции read ( ) яв­ ляется ссылкой на объект класса S a l e s_da ta. В разделе 7.1 .2 (стр. 339) мы обра­ щали внимание на то, что указатель th i s используется для доступа к объекту в целом, а не к его отдельному члену. В данном случае для передачи "этого" объекта в качестве арrумента функции read ( ) используется синтаксис * th i s .

Упражнения раздела

7.1 .4

Добавьте в класс S a l e s_da t a конструкторы и напишите программу, использующую каждый из них.

Упражнение

7.1 1 .

Переместите определение конструктора S a l e s _ da ta ( ) , получающего объект i s t r e am, в тело класса S a l e s_da t a .

Упражнение

7.1 2 .

Перепишите программу со стр. 333 так, чтобы использо­ вать конструктор с параметром i s t re am.

Упражнение

7.1 3.

Упражнение 7.1 4. Напишите версию стандартного конструктора, явно инициализирующую переменные-члены значениями, предоставленными внутриклассовыми инициализаторами.

Упражнение



7.1 5.

Добавьте соответствующие конструкторы в класс Pe r s o n .

7 1 5 Копи р ование, п р исвоение и удаление .

.

.

Кроме определения способа инициализации своих объектов, классы кон­ тролируют также то, что происходит при копировании, присвоении и удале­ нии объектов класса. Объекты копируются во многих елучаях: при инициали-

Глава 7. Классы

348

зации переменной, при передаче или возвращении объекта по значению (см. раздел 6.2. 1 , стр. 277 и раздел 6.3.2, стр . 294) . Объекты присваиваются при использовании оператора присвоения (см. раздел 4.4, стр . 201 ). Объекты уда­ ляются, когда они прекращают существование, например, при выходе локаль­ ного объекта из блока, в котором он был создан (см. раздел 6.1 . 1 , стр . 271 ). Объекты, хранимые в векторе (или массиве), удаляются при удалении вектора (или массива) . Если мы н е определим эти операции, компилятор создаст их сам. Обычно создаваемые компилятором версии выполняются, копируя, присваивая или удаляя каждую переменную-член объекта. Например, когда в приложении книжного магазина (см. раздел 7.1 . 1 , стр . 333) компилятор выполняет сле­ дующее присвоение: tota l

=

trans ;

/ / о бра бота ть следующую книгу

оно выполняется, как будто было написано так: / / присв оение по умолча нию для Sa l e s_ da t a эквив алентно следующему : t o t a l . bo o kNo t r a n s . b o o kNo ; total . units s o l d trans . units sold; t o t a l . reve n u e = t r a n s . r e v e n u e ; =

-

=

-

Более подробная информация об определении собственных версий этих операторов приведена в главе 1 3.



Н екоторые классы не могут полагаться на с и нтез ируемые ве р с ии

Хотя компилятор и создает сам операторы копирования, присвоения и удаления, важно понимать, что у некоторых классов их стандартные версии ведут себя неправильно. В частности, синтезируемые версии вряд ли будут правильно работать с классами, которые резервируют ресурсы, располагаю­ щиеся вне самих объектов класса. Пример резервирования и управления ди­ намической памятью приведен в главе 1 2. Как будет продемонстрировано в разделе 1 3.6 (стр . 640), классы, которые управляют динамической памятью, вообще не могут полагаться на синтезируемые версии этих операций. Однако следует заметить, что большинство классов, нуждающихся в дина­ мической памяти, способны (и должны) использовать классы ve c t o r или s t r i n g, если им нужно управляемое хранение. Классы, использующие в е к ­ т оры и с тр о ки, избегают сложностей, связанных с резервированием и освобо­ ждением памяти. Кроме того, синтезируемые версии операторов копирования, присвоения и удаления правильно работают для классов, у которых есть переменные­ члены класса ve c t o r или s t r i n g . При копировании или присвоении объекта, обладаюrцего переменной-членом класса ve c t o r, этот класс сам позаботится

7.2.

Уп равление доступом и инкапсуляция

349

о копировании и присвоении своих элементов. Когда объект удаляется, пере­ менная-член класса ve c t o r тоже удаляется, что в свою очередь удаляет эле­ менты вектора. Класс s t r i n g работает аналогично.

& ВНИМАНИЕ



Пока вы еще не знаете, как определить операторы, описанные в главе 1 3, ресурсы, резервируемые вашими классами, должны храниться непосредственно как переменные-члены класса.

7.2 .

Упр авление доступом и инкапсул яция

На настоящий момент для нашего класса определен интерфейс; однако ничто не вынуждает пользователей использовать его. Наш класс еще не ис­ пользует инкапсуляцию - пользователи вполне могут обратиться к объекту Sale s_da t a и воспользоваться его реализацией. Для обеспечения инкапсуля­ ции в языке С++ используют спецификаторы доступа (access specifier) . •

Члены класса, определенные после спецификатора puЫ i c, доступны для всех частей программы. Открытые члены (puЬlic member) опреде­ ляют интерфейс к классу.



Члены, определенные после спецификатора p r i va t e, являются закры­ тыми (private member), они доступны для функций-членов класса, но не доступны для кода, который использует класс. Разделы p r ivate инкап­ сулируют (т.е. скрывают) реализацию .

Переопределив класс S a l e s_da t a еще раз, получаем следующее: class S a l e s data puЫ i c : / / доба влен специфика тор д о с тупа S a l e s da t a ( ) = de f a u l t ; S a l e s_da t a ( c on s t s t d : : s t r i n g & s , u n s i gn e d n , do u Ы e b o o kN o ( s ) , u n i t s_s o l d ( n ) , r e v e n u e ( p * n ) { S a l e s _d a t a ( c on s t s t d : : s t r i n g & s ) : b o o kNo ( s ) { } S a l e s d a t a ( s t d : : i s t r e am & ) ; s t d : : s t r i n g i s bn ( ) c o n s t { r e t u r n b o o kNo ; } S a l e s_da t a & c omb i ne ( co n s t S a l e s_da t a & ) ; / / до б а влен специфика тор д о с тупа p r iva te : douЫ e avg_p r i c e ( ) c on s t О; } { r e t u r n u n i t s_s o l d ? r e v e n u e / u n i t s s o l d s td : : s t r i n g b o o kN o ; un s i g n e d u n i t s_s o l d = О ; douЫ e r e v e n u e = О . О ;

р) : }

};

Конструкторы и функции-члены, являющиеся частью интерфейса (напри­ мер, i sbn ( ) и c omЬ i n e ( ) ), должны располагаться за спецификатором publ i c; переменные-члены и функции, являющиеся частью реализации, рас­ полагаются за спецификатором p r iva t e .

Глава 7. Классы

350

Класс может содержать любое количество спецификаторов доступа; нет никаких ограничений на то, как часто используется спецификатор . Каждый спецификатор определяет уровень доступа последующих членов. Заданный уровень доступа остается в силе до следующего спецификатора доступа или до конца тела класса.

И спользован и е клю чевых слов clas s и s truct Было также внесено еще одно изменение: в начале определения класса ис­ пользовано ключевое слово c l a s s , а не s t ru c t . Это изменение является чисто стилистическим; тип класса можно определить при помощи любого из этих ключевых слов. Единственное различие между ключевыми словами s t r u c t и с 1 а s s в заданном по умолчанию уровне доступа. Члены класса могут быть определены перед первым спецификатором дос­ тупа. Уровень доступа к таким членам будет зависеть от того, как определяет­ ся класс. Если используется ключевое слово s t ru c t, то члены, определенные до первого спецификатора доступа, будут открытыми; если используется ключевое слово c l a s s, то они будут закрытыми. Общепринятым стилем считается определение классов, все члены которого предположительно будут открытыми, с использованием ключевого слова s t ru c t . Если члены класса должны быть закрытыми, используется ключевое слово c l a s s .

Еди1-1стве1-11-1ое различие между ключевыми словами c l a s s и s t ru c t в задаваемом по умолчанию уровне доступа. КЛЮЧЕВАЯ КОНЦЕ П ЦИЯ . ПРЕИМУЩЕСТВА ИНКАПСУЛЯЦИИ

Инкапсуляция предоставляет два важных преимущества. •

Пользовательский код не может по неосторожности повредить состояние инкапсулированного объекта.



Реализация инкапсулированного класса может со временем изменить­ ся,это не потребует изменений в коде на пользовательском уровне.

Определив переменные-члены закрытыми, автор класса получает воз­ можность вносить изменения в данные. Если реализация изменится, то вы­ званные этим последствия можно исследовать только в коде класса . Пользо­ вательский код придется изменять только при изменении интерфейса. Если данные являются открытыми, то любой использовавший их код может быть нарушен. Пришлось бы найти и переписать любой код, который полагался на прежнюю реализацию, и только затем использовать программу.

7.2. Уп р авление доступом и инкапсуляция

351

Еще одно преимущество объявления переменных-членов закрытыми в том, что данные защищены от ошибок, которые могли бы внести пользо­ ватели. Если есть ошибка, повреждающая состояние объекта, места ее поис­ ка ограничены только тем кодом, который является частью реализации . Это существенно облегчает поиск проблем и обслуживание программы.

Упражнения раздела 7.2 Каковы ограничения (если они есть) на количество спе­ цификаторов доступа в определении класса? Какие виды членов должны быть определены после спецификатора pub l i c? Какие после спецификато­ ра p r i v a t e ?

Упражнение

7.1 6.

Упражнение 7.1 7. Каковы различия (если они есть) между ключевыми сло­ вами c l a s s и s t ru c t ? Упражнение

7.1 8.

Что такое инкапсуляция? Чем она полезна?

Упражнение 7.1 9. Укажите, какие члены класса Pe r s on имеет смысл объявить как publ i c, а какие как p r i vate . Объясните свой выбор.



7 2 1 Д р узья .

.

.

Теперь, когда переменные-члены класса S a l e s_da ta стали закрытыми, функции r e a d ( ) , p r i n t ( ) и a dd ( ) перестали компилироваться. Проблема в том, что хоть эти функции и являются частью интерфейса класса Sale s_da t a, его членами они не являются. Класс может позволить другому классу или функции получить доступ к своим не открытым членам, установив для них дружественные отношения (friend). Класс объявляет функцию дружественной, включив ее объявление с предваряющим ключевым словом f r i e n d: c l a s s S a l e s _da t a { 1 1 доба влены о бъявления друже ств енных функций , не являющих ся 11 членами кла сса Sa l es da t a f r i e n d S a l e s_da t a add ( c o n s t S a l e s _da t a & , c o n s t S a l e s _da t a & ) ; f r i end s t d : : i s t r e am & r e a d ( s t d : : i s t r e am & , S a l e s d a t a & ) ; f r i e n d s t d : : o s t r e am & p r i n t ( s t d : : o s t r e am & , c o n s t S a l e s da t a & ) ; 1 1 другие члены и специфика торы д о с тупа , ка к прежде puЫ i c : S a l e s d a t a ( ) = de f a u l t ; S a l e s _da t a ( c o n s t s t d : : s t r i n g & s , u n s i gn e d n , douЫ e р ) : b o o kN o ( s ) , u n i t s_s o l d ( n ) , r e v e n u e ( p * n ) { } S a l e s_da t a ( c on s t s t d : : s t r i n g & s ) : b o o kN o ( s ) { } S a l e s da t a ( s t d : : i s t r e am & ) ; st d : : s t r i n g i s bn ( ) c o n s t { r e t u r n b o o kNo ; S a l e s_da t a & c omb i n e ( c o n s t S a l e s d a t a & ) ; pr ivate : -

352

Глава 7. Классы s t d : : s t r i ng bo o kNo ; un s i gn e d u n i t s_s o l d douЫ e reve n u e О . О;

О;

=

}; 1 1 о бъявления ча стей, не являющих ся членами интерфейса кла сса Sa l e s da t a S a l e s_da t a add ( c o n s t S a l e s_da t a & , c o n s t S a l e s _da t a & ) ; s t d : : i s t r e am & r e a d ( s t d : : i s t re arn & , S a l e s _d a t a & ) ; s t d : : o s t r e am & p r i n t ( s t d : : o s t r e arn & , c o n s t S a l e s_da t a & ) ;

Объявления друзей могут располагаться только в определении класса; ис­ пользоваться они могут в классе повсюду. Друзья не являются членами класса и не подчиняются спецификаторам доступа раздела, в котором они объявле­ ны. Более подробная информация о дружественных отношениях приведена в разделе 7.3.4 (стр. 363) . Объявления друзей имеет смысл группировать в начале или в ко�ще определения класса. Хотя пользовательский код не должен изменяться при изменении определения класса, файлы исходного кода, использующие этот класс, следует перекомпилировать при каждом изменении класса.

О б ъ явление дружественных отношени й �Объявление дружественных отношений устанавливает только право доступа. Эго не объявление функции. Если необходимо, чтобы пользователи класса были в состоянии вызвать дружесrвенную функцию, ее следует также объявить. Чтобы сделать друзей класса видимыми его пользователям, их обычно объяв­ ляют вне класса в том же заголовке, что и сам класс. Таким образом, в заголовке S a l e s _da ta следует предоставить отдельные объявления (кроме объявлений дружесrвенными в теле класса) для функций read ( ) , p r i n t ( ) и add ( ) . Многие компиляторы не выполняют правило, согласно ко­ торому дружественные функции должны быть объявлены вне класса, прежде чем они будут применены. Некоторые компиляторы позволяют вызвать дружественную функцию, ко­ гда для нее нет обычного объявления. Даже если ваш компилятор позволяет такие вызовы, имеет смысл предоставлять отдельные объявления для дружест­ венных функций. Так не придется переделывать весь код, если вы перейдете на компилятор, который выполняет это правило.

Упражнения раздела 7.2.1 Упражнение 7.20. Когда полезны дружественные отношения? Укажите преимущества и недостатки их использования.

7. 3 . Допо л ните льные средства класса

35 3

Измените свой класс S a l e s _da t a так, чтобы скрыть его реализацию. Написанные вами программы, которые использовали опера­ ции класса S a l e s_da t a, должны продолжить работать. Перекомпилируйте эти программы с новым определением класса, чтобы проверить, остались ли они работоспособными.

Упражнение

7.21 .

Упражнение 7.22. Измените свой класс Pe r s o n так, чтобы скрыть его реа­ лизацию.

7.3.

Д ополнительные с р едства класса

Хотя класс S a l e s_da ta довольно просг, он все же позволил исследовать нема­ _ ло средств помержки классов. В этом разделе рассматриваются некоторые из до­ полнительных средств, связанных с классом, которые класс S a l e s_data не будет использовать. К этим средсгвам относятся типы-члены (type member), внутри­ классовые инициализаторы для типов-членов класса, изменяемые переменные­ члены, всrраиваемые функции-члены, функции-члены, возвращающие * th i s, а также подробносги определения и использования типов класса и дружествен­ ных классов.

7 .3.1 . С нова о членах класса Для исследования некоторых из дополнительных средств определим пару взаимодействующих классов по имени S c r e e n и W i n dow_mg r .

О пределение типов-членов Класс S c r e e n представляет окно на экране. У каждого объекта класса S c re e n есть переменная-член типа s t r i ng, хранящая содержимое окна и три переменные-члена типа s t r i ng : : s i z e_t ype, представляющие позицию кур­ сора, высоту и ширину окна. Кроме переменных и функций-членов, класс может определять собствен­ ные локальные имена таких типов. Определенные классом имена типов под­ чиняются тем же правилам доступа, что и любой другой его член, и могут быть открытыми или закрытыми: class Sc reen { p uЫ i c : t yp e de f s t d : : s t r i ng : : s i z e t yp e p o s ; private : pos cu r s o r = О ; p o s h e i gh t = О , w i d t h О; std : : s t ri ng conten t s ; } ;

Глава 7. Классы

354

Тип po s определен в части puЫ i c класса S c re e n, поскольку пользователи должны использовать это имя. Пользователи класса S c r e e n не обязаны знать, что он использует класс s t r i n g для хранения своих данных. Определив тип p o s как открытый член, эту подробность реализации класса S c re e n можно скрыть. В объявлении типа p o s есть два интересных момента. Во-первых, хоть здесь и был использован оператор t ypede f (см. раздел 2.5.1 , стр. 1 06), с таким же ус­ пехом можно использовать псевдоним типа (см. раздел 2.5. 1 , стр. 1 06): c l a s s S c reen { puЫ i c : / / аль терна тивный спо соб о бъявления типа - члена с исполь з ов а нием 1 1 псевдонима типа u s i n g p o s = s t d : : s t r i n g : : s i z e t yp e ; 1 1 другие члены ка к прежде } ;

Во-вторых, по причинам, которые будут описаны в разделе 7.3.4 (стр. 368), в отличие от обычных членов, типы-члены определяются прежде, чем исполь­ зуются. В результате типы-члены обычно располагают в начале класса.

Ф ункции-члены класса Screen Чтобы сделать наш класс полезней, добавим в него конструктор, позволяю­ щий пользователям задавать размер и содержимое экрана, наряду с членами, позволяющими переместить курсор и получить символ в указанной позиции: c l a s s S c reen { puЫ i c : t yp e de f s t d : : s t r i n g : : s i z e t yp e p o s ; S c r e e n ( ) = de f a u l t ; / / не о бходим , п о сколь ку у кла сса Screen е с ть 1 1 другой конструктор / / внутрикла ссовый инициализа тор инициализирует курсор зна чением О S c r e e n ( p o s h t , p o s wd , c h a r с ) : h e i gh t ( h t ) , w i dt h ( wd ) , c o n t e n t s ( h t * wd , с ) { c h a r ge t ( ) c o n s t / / получить симв ол в курсор е / / неявно в с траив а емая { return contents [ cu r s o r ] ; } i n l i n e c h a r g e t ( po s h t , p o s wd ) c o n s t ; / / явно в страив а емая S c r e e n & rnove ( p o s r , p o s с ) ; / / може т быть сдела на в с траив а емой позже pr ivate : pos cur s o r = О ; p o s he i g h t = О , w i d t h О; s td : : s t r ing conten t s ; };

Поскольку мы предоставляем конструктор, компилятор не будет автомати­ чески создавать стандартный конструктор сам. Если у нашего класса должен быть стандартный конструктор, то придется создать его явно. В данном случае используется синтаксис default, чтобы попросить компилятор самому создать определение стандартного конструктора (см. раздел 7.1 .4, стр. 345). =

7. 3 . Допо л ните льные средства к л асса

355

Стоит также обратить внимание на то, что второй конструктор (получаю­ щий три аргумента) неявно использует внутриклассовый инициализатор для переменной-члена cu r s o r (см. раздел 7.1 .4, стр. 346). Если бы у класса не было внутриклассового инициализатора для переменной-члена cu r s o r, то мы явно инициализировали бы ее наряду с другими переменными-членами.

В ст р а и ваемые члены класса У классов зачастую бывают небольшие функции, которые выгодно сделать встраиваемыми. Как уже упоминалось, определеllliы е в классе функп:ии-члены автоматически являются встраиваемыми ( i n l ine ) (см. раздел 6.5.2, стр. 31 2). Та­ ким образом, конструкторы класса S c reen и версия функции ge t ( ) , возвра­ щающей обозначенный курсором символ, являются встраиваемыми по умолча­ нию. Функцию-член можно объявить встраиваемой явно в ее объявлении в теле класса. В качестве альтернативы функцию можно указать встраиваемой в определении, расположенном вне тела класса: i n l i n e / / функцию можно ука з а ть в с траив а емой в определении S c r e e n & S c r e e n : : rnove ( p o s r , p o s с ) { p o s r o w = r * w i dt h ; / / вычислить положение ряда c u r s o r = row + с ; / / переме стить кур с ор к столбцу эт ого ряда r e t u r n * th i s ; / / в о звра тить э т о т о бъе кт ка к 1 - зна чение

c h a r S c r e e n : : ge t ( p o s r ,

pos с )

p o s r o w = r * w i d th ; r e t u r n c o n t e n t s [ r ow + с ] ;

c o n s t / / объяв ить в с траив а емый в кла ссе / / вычислить положение ряда / / в о звра тить симв ол в данном столбце

Хоть и не обязательно делать это, вполне допустимо указать ключевое сло­ во i n l i ne и в объявлении, и в определении. Однако указание ключевого слова i n l i n e в определении только вне класса может облегчить чтение класса. По тем же причинам, по которым встраиваемые функции опреде­ ляют в заголовках (см. раздел 1 . 1 , стр. 31 3), встраиваемые функ­ ции-члены следует определить в том же заголовке, что и определение соответствующего класса.

П е р ег ру зка функ ц и й-членов Подобно функциям, которые не являются членами класса, функции-члены могут быть перегружены (см. раздел 6.4, стр. 302), если они отличаются коли­ чеством и / или типами параметров. При вызове функции-члена используется тот же процесс подбора функции (см. раздел 6.4, стр. 305), что и у функций, не являющихся членом класса.

Глава 7. Классы

356

Например, в классе S c re e n определены две версии функции ge t ( ) . Одна версия возвращает символ, обозначенный в настоящее время курсором; дру­ гая возвращает символ в указанной позиции, определенной ее рядом и столб­ цом. Чтобы определить применяемую версию, компилятор использует коли­ чество аргументов: S c r e e n mys c r e e n ; c h a r ch = mys c re e n . g e t ( ) ; / / выз ов Screen : : ge t ( ) c h = mys c r e e n . ge t ( 0 , 0 ) ; / / выз ов Screen : : ge t (po s , p o s )

И зменяемые пе р еменные-члены Иногда (но не очень часто) у класса есть переменная-член, которую следует сделать изменяемой даже в константной функции-члене. Для обозначения та­ ких членов в их объявление включают ключевое слово mu t aЫ e . Изменяемая переменная-член (mutaЫe data member) никогда не бывает кон­ стантой, даже когда это член константного объекта. Соответственно кон­ стантная функция-член может изменить изменяемую переменную-член. В ка­ честве примера добавим в класс S c re e n изменяемую переменную-член a c ce s s _ c t r, используемую для отслеживания частоты вызова каждой функ­ ции-члена класса S c re e n : c l a s s Scr een { puЫ i c : v o i d s ome membe r ( ) c o n s t ; pr ivate : mu t a Ы e s i z e t a c c e s s c t r ; -

-

/ / може т изменить ся даже в константном 1 1 о бъекте

1 1 другие члены ка к прежде

}; vo i d S c r e e n : : s ome_memb e r ( ) c o n s t { + + a c c e s s c t r ; / / сохра нить количе с тв о вызов ов любой функции - члена 1 1 без отно ситель но других выполняемых ею действий

Несмотря на то что функция-член s ome _membe r ( ) константная, она может изменить значение переменной-члена a c c e s s _c t r . Этот член класса является изменяемым, поэтому любая функция-член, включая константные, может из­ менить это значение.

И ни ц иализаторы переменных-членов класса Кроме класса S c r e e n, определим также класс диспетчера окон, который представляет коллекцию окон на данном экране. У этого класса будет вектор объектов класса S c r e e n, каждый элемент которого представляет отдельное окно. По умолчанию класс Wi ndow_mg r должен изначально содержать один объект класса S c r e e n, инициализированный значением по умолчанию. По новому стандарту наилучшим способом определения

7 . 3 . Д опо л нительные средства кл асса

357

такого значения по умолчанию является внутриклассовый инициализа­ тор (см. раздел 2.6. 1 , стр. 1 1 3) : c l a s s W i n dow_mg r { p r i va t e : 1 1 по умол ча нию о т слежив ающий о кна объ е кт кла сса Wi n do w_mgr 1 1 содержит одно пуст о е о кно стандартного ра змера s t d : : ve c t o r < S c r e e n > s c r e e n s { S c r e e n ( 2 4 , 8 О , ' ' ) } ; };

При инициализации переменных-членов типа класса их конструктору сле­ дует предоставить аргументы. В этом ел учае применяется список инициали­ зации переменной-члена типа ve c t o r (см. раздел 3.3. 1 , стр. 1 43) с инициализа­ тором для одного элемента. Этот инициализатор содержит значение типа S c r e e n, передаваемое конструктору ve c t o r < S c r e e n > для создания вектора с одним элементом. Это значение создается конструктором класса S c re e n, получающим параметры в виде двух размерностей и заполняющего символа, чтобы создать пустое окно заданного размера. Как уже упоминалось, для внутриклассовой инициализации может исполь­ зоваться форма инициализации = (как при инициализации переменных­ членов класса S c re e n ) или прямая форма инициализации с использованием фигурных скобок (как у вектора s c re e n s ) . При предоставлении внутриклассового инициализатора это сле­ дует сделать после знака = или в фигурных скобках. Упражнения раздела 7.3.1 Упражнение 7.23 . Напишите собственную версию класса S c r e e n . Упражнение 7.24. Добавьте в свой класс S c re e n три конструктора: стан­ дартный; получающий высоту, ширину и заполняющий содержимое соот­ ветствующим количеством пробелов; получающий высоту, ширину и за­ полняющий символ для содержимого экрана. Упражнение 7.25. Может ли класс S c re e n безопасно полагаться на задан­ ные по умолчанию версии операторов копирования и присвоения? Если да, то почему? Если нет, то почему? Упражнение 7.26. Определите функцию S a l e s da t a : : avg _p r i ce как встраиваемую.



7 .3.2. Функции, возв р ащающие указатель

* thi s

Теперь добавим функции, устанавливающие символ в курсоре или в задан­ ной области:

358

Глава 7. Классы

c l a s s S c re e n { pu Ы i c : S c r e e n & s e t ( c ha r ) ; S c r e e n & s e t ( po s , p o s , c h a r ) ; 1 1 другие члены, ка к прежде }; i n l i n e S c r e e n & S c r e e n : : s e t ( ch a r с ) c o n t e n t s [ cu r s o r ] r e t u r n * th i s ;

=

с;

1 1 уста новите нов ое зна чение в текущей позиции 1 / курсора / / в о звра тить э т о т объект ка к 1 -зна чение

i n l i n e S c r e e n & S c r e e n : : s e t ( po s r , p o s c o l , c h a r c h ) { c o n t e n t s [ r * w i dt h + c o l ] = c h ; / / уста новить позицию по данному 1 1 зна чению return * th i s ; 1 1 в озвра тить э т о т о бъ ект ка к 1 -зна чение

Как и функция move ( ) , функция-член s e t ( ) возвращает ссылку на объект, из которого они вызваны (см. раздел 7.1 .2, стр. 339) . Возвращающие ссылку функции являются !-значениями (см. раздел 6.3.2, стр. 296), а это означает, что они возвращают сам объект, а не его копию. Это позволяет связать несколько их вызовов в одно выражение: 1 1 переме стить курсор в ука з а нную позицию и присв оить 1 1 симв олу зна чение myS c r e e n . move ( 4 , 0 ) . s e t ( ' # ' ) ;

Эти операции выполнятся для того же объекта. В этом выражении сначала перемещается курсор (move ( ) ) в окно (myS c re e n), а затем устанавливается ( s e t ( ) ) заданный символ. Таким образом, этот оператор эквивалентен сле­ дующему: myS c r e e n . move ( 4 , 0 ) ; myS c r e e n . s e t ( ' # ' ) ;

Если бы функции move ( ) и s e t ( ) возвращали тип S c r e e n, а не S c re e n & , этот оператор выполнялся бы совсем по-другому. В данном случае он был бы эквивалентен следующему: 1 1 если m o ve в о звраща е т Screen , а не Screen & S c r e e n t emp = myS c r e e n . move ( 4 , 0 ) ; / / в озвраща емо е зна чение бьиrо 1 1 бы скопиров ано t emp . s e t ( ' # ' ) ; 1 1 содержимо е myScreen о с тало сь бы неизменно

Если бы функция move ( ) имела возвращаемое значение не ссылочного ти­ па, то оно было бы копией * th i s (см. раздел 6.3.2, стр. 294) . Вызов функции s e t ( ) изменил бы лишь временную копию, а не сам объект rny S c r e e n .

7.3. Д опо л нительные средства к л асса

359

В озвращение * thi s из константно й функции-члена Теперь добавим функцию di sp l a y ( ) , выводящую содержимое окна. Необ­ ходима возможность включать эту операцию в последовательность операций s e t ( ) и move ( ) . Поэтому, подобно функциям s e t ( ) и move ( ) , функция di sp l a y ( ) возвратит ссылку на объект, для которого она выполняется. Логически отображение объекта класса S c r e e n (окна) не изменяет его, по­ этому функцию di sp l a y ( ) следует сделать константным членом. Но если функция di s p l a y ( ) будет константной, то t h i s будет указателем на кон­ станту, а значение * th i s константным объектом. Следовательно, типом возвращаемого значения функции d i sp l ay ( ) будет co n s t S a l e s_da t a & . Однако, если функция d i s p l a y ( ) возвратит ссылку на константу, мы не сможем вставить вызов функции d i s p l a y ( ) в последовательность действий: -

S c r e e n myS c r e e n ; 1 1 если di sp l ay в о звраща е т константную ссылку , выз ов в п о следов а тельности 1 1 буде т ошибкой myS c r e e n . d i s p l a y ( c o u t ) . s e t ( ' * ' ) ;

Хотя объект my S c r e e n неконстантный, вызов функции s e t ( ) не будет компилироваться. Проблема в том, что константная версия функции di s p l a y ( ) возвращает ссылку на константу, и мы не можем вызвать функцию set ( ) для константного объекта. Тип возвращаемого значения константной функции-члена, воз­ вращающей * th i s как ссылку, должен быть ссылкой на констанrу.

П ере грузка на основании константности Функции-члены вполне можно перегружать исходя из того, являются ли они константными или нет, причем по тем же причинам, по которым функ­ цию можно перегружать исходя из того, является ли ее параметр указателем на константу (см. раздел 6.4, стр. 305) . Неконстантная версия неприменима для константных объектов; она применима только для константных объектов. Для неконстантного объекта можно вызвать любую версию, но неконстантная версия будет лучшим соответствием. В этом примере определим закрытую функцию-член do _ d i sp l a y ( ) для фактического вывода окна. Каждая из функций di sp l a y ( ) вызовет эту функ­ цию, а затем возвратит объект, для которого она выполняется: clas s Screen { puЬ l i c : 1 1 di sp l ay перегружена на о снов ании того , являе т ся ли 1 1 объект конс та нтой или не т S c r e e n & d i s p l a y ( s td : : o s t r e am & o s ) { do_d i s p l a y ( o s ) ; r e t u r n * t h i s ; }

360

Глава 7. Классы

c o n s t S c r e e n & d i s p l a y ( s t d : : o s t r e am & o s ) c o n s t do _ d i s p l a y ( o s ) ; r e t u r n * t h i s ; } p r i va t e : 1 1 функция о то бражения о кна vo i d do_d i s p l a y ( s t d : : o s t r e am & o s ) c o n s t { o s : : s i z e_t ype ; 1 1 сбро сить да нное о кно , з а полнив его пр обелами vo i d c l e a r ( S c r e e n i nde x ) ; pr iva te : s td : : ve c t o r < S c r e e n > s c r e e n s { S c r e e n ( 2 4 , 8 О , ' ' ) } ; }; vo id W i n dow_rng r : : c l e a r ( S c r e e n i n d e x i ) { с сылка на о кно , ко торое предстоит о чис тить 11 s -

3 64

Глава 7. Классы S c reen & s = s c reens [ i ] ; 1 1 сбро сить данно е окно , з а полнив его пробелами s . c o n t e n t s = s t r i n g ( s . he i g h t * s . w i dt h , ' ' ) ;

Сначала определим s как ссылку на класс S c re e n в позиции 1 вектора окон. Затем переменные-члены h e i gh t и w i dth данного объекта класса S c re e n используются для вычисления количества символов новой строки, со­ держащей пробелы. Эта заполненная пробелами строка присваивается пере­ менной-члену c o n t e n t s . Если бы функция c l e a r ( ) не была дружественной классу S c r e e n, то этот код не компилировался бы. Функция c l e a r ( ) не смогла бы использовать пе­ ременные-члены he i ght, w i dth или c o n t e n t s класса S c re e n . Поскольку класс S c r e e n установил дружественные отношения с классом W i ndow_rngr, для его функций доступны все члены класса S c r e e n . Важно понимать, что дружественные отношения н е передаются. Таким об­ разом, если у класса W i ndow_rng r есть собственные друзья, то у них нет приви­ легий доступа к членам класса S c re e n . Каждый класс сам контролирует, какие классы или функции бу­ дут его друзьями.

К ак сделать функц ию -член дружественно й Вместо того чтобы делать весь класс W i n dow _rng r дружественным классу S c reen, можно предоставить доступ только функции-члену c l e a r ( ) . Когда функция-член объявляется дружественной, следует указать класс, которому она принадлежит: c l a s s S c reen { 1 1 кла сс Wi n do w mgr : : c l e a r должен быть о бъявлен перед кла ссом Screen f r i e nd vo i d W i n do w_mg r : : c l e a r ( S c r e e n i n de x ) ; 1 1 . . . о с таль ное ка к ра ньше в кла с се Screen };

Создание дружественных функций-членов требует пцательного сrруктури­ рования программ в соответствии с взаимозависимосгями объявлений и опре­ делений. В данном случае программу следует упорядочить следующим образом. •

Сначала определите класс Wi n dow_rng r, который объявляет, но не может определить функцию c l e a r ( ) . Класс S c re e n должен быть объявлен до того, как функция c l e a r ( ) сможет использовать члены класса S c re e n.



Затем определите класс S c r e e n, включая объявление функции c l e a r ( ) дружественной.



И наконец, определите функцию c l e a r ( ) , способную теперь обращать­ ся к членам класса S c r e e n .

7. 3 . Дополнительные с редства класса

365

П ерегруженные ф ункции и друж ественн ые отношения Хотя у перегруженных функций одинаковое имя, это все же разные функ­ ции. Поэтому класс должен объявить дружественной каждую из перегружен­ ных функций: 11

перегруженные функции s t o reOn exte rn s t d : : o s t r e am & s t o r e On ( s t d : : o s t r e am & , S c r e e n & ) ; exte rn B i tMa p & s t o r eOn ( B i tM a p & , S c r e e n & ) ; clas s Sc reen { / / в ер сия o s t re a m функции s t oreOn може т о браща ть ся к з а крытым членам / / о бъ е ктов кла с са Screen f r i e n d s t d : : o s t r e am & s t o r e O n ( s t d : : o s t r e am & , S c r e e n & ) ; // . . . };

Класс S c re e n объявляет другом версию функции s t o r eOn, получающей поток o s t r e am & . Версия, получающая параметр B i tMap & , особых прав досту­ па к объектам класса s с r е е n не имеет.

Об ъ явление дружественных отношени й и о б ласть ви димост и Классы и функции, не являющиеся членами класса, не следует объявлять прежде, чем они будут использованы в объявлении дружественными. Когда имя впервые появляется в объявлении дружественной, оно неявно подр азуме­ вается принадлежащей окружающей области видимости. Однако сам друг фактически не объявлен в этой области видимости (см. раздел 7.2. 1 , стр. 352) . Даже если мы определим функцию в классе, ее все равно придется объя­ вить З а пределами класса, чтобы сделать видимой. Объявление должно уже существовать, даже если вызывается дружественная функция: struct Х { f r i e n d vo i d f ( ) { / * друже ств е нная функция може т быть определена в теле кла с са * / } Х ( ) { f ( ) ; } / / ошибка : не т о бъявления для f vo i d g ( ) ; vo i d h ( ) ; }; return f ( ) ; vo i d X : : g ( ) 1 1 ошибка : f не была о бъявлена vo i d f ( ) ; 1 1 о бъявляе т функцию , определенную в х return f ( ) ; vo i d X : : h ( ) 1 1 ok : о бъявление f теперь в о бла с ти 1 1 видимо с ти

Важно понимать, что объявление дружественной затрагивает доступ, но не является объявлением в обычном смысле. Помните: некоторые компиляторы не выполняют правил поиска имен друзей (см. раздел 7.2. 1 , стр. 352).

Глава 7. Классы

366

Упражнения раздела

7.3 .4

Упражнение 7.32. Определите собственные версии классов S c reen и Window_ mgr, в которых функция c l e a r ( ) является членом класса Window_mgr и дру­ гом класса S c reen.



7 .4. Область видимости класса

Каждый класс определяет собственную область видимости. Вне области ви­ димости класса (class scope) к обычным данным и функциям его члены могут обращаться только через объект, ссылку или указатель, используя оператор доступа к члену (см. раздел 4.6, стр. 207). Для доступа к членам типа из класса используется оператор области видимости . В любом ел учае следующее за опе­ ратором имя должно быть членом соответствующего класса. S c r e e n : : p o s ht

=

24 ,

wd

S c r e e n s c r ( h t , wd , ' ' ) S creen *р = & s c r ; char с = s c r . get ( ) ; / / // с = p - >ge t ( ) ; 11

=

80;

/ / исполь з о в а ние типа pos , определенного 1 1 в кла ссе Screen

; доступ к члену ge t () объе кта s c r доступ к члену ge t ( ) из о бъ е кта , на который ука зыв а е т р

О б ласть видимости и член ы, определенные вне класса Тот факт, что класс определяет область видимости, объясняет, почему сле­ дует предоставить имя класса наравне с именем функции, при определении функции-члена вне ее класса (см. раздел 7.1 .2, стр. 339). За пределами класса имена ее членов скрыты. Как только имя класса становится видимо, остальная часть определения, включая список параметров и тело функции, находится в области видимости класса. В результате мы можем обращаться к другим членам класса без уточ­ нения. Вернемся, например, к функции-члену c l e a r ( ) класса W i n dow_mg r (см. раздел 7.3.4, стр . 363) . Параметр этой функции имеет тип, определенный в классе Wi ndow_mg r: vo i d W i n dow _rng r : : c l e a r ( S c r e e n i n d e x i ) { S creen & s = s c reens [ i ] ; s . c o n t e n t s = s t r i n g ( s . h e i gh t * s . w i d t h ,

'

' ) ;

Поскольку компилятор видит последующий список параметров и ничего подобного в области видимости класса W i n dowMg r, нет никакой необходимо­ сти определять, что требуется тип S c re e n i n dex, определенный в классе

7 .4 . Об ласть видимости к ла сса

367

W i n dowMg r . По той же причине использование объекта s c r e e n s в теле функ­ ции относится к имени, объявленному в классе W i n dow_mg r . С другой стороны, тип возвращаемого значения функции обычно распола­ гается перед именем функции. Когда функция-член определяется вне тела класса, любое имя, используемое в типе возвращаемого значения, находится вне области видимости класса. В результате тип возвращаемого значения должен определять класс, членом которого он является. Например, мы могли бы доба­ вить в класс W i n dow_mg r функцию a ddS c re e n ( ) , добавляющую еще одно окно на экран. Этот член класса возвратит значение типа S c re e n l ndex, которое пользователь впоследствии сможет использовать для поиска этого окна: c l a s s W i n dow_mg r { puЫ i c : 1 1 добавить окно на экр а н и в озвра тить его индекс S c re e n i n de x addS c r e e n ( c o n s t S c r e e n & ) ; 1 1 другие члены , ка к прежде } ;

1 1 тип в о звраща емого зна чения видим прежде , 1 1 видимо сти кла сса Wi n do w_mgr W i n d ow _mg r : : S c r e e n i n d e x W i n dow_mg r : : a ddS c r e e n ( c o n s t S c re e n & s ) { s c r e e n s . p u s h_b a c k ( s ) ; return screens . s i z e ( ) - 1 ;

чем на чина е т ся о бла сть

Поскольку тип возвращаемого значения встречается прежде имени класса, оно находится вне области видимости класса W i n dow_mg r . Чтобы использовать тип S c re e n i ndex для возвращаемого значения, следует определить класс, в котором определяется этот тип.

Упражнения раздела 7.4 Что будет, если добавить в класс S c re e n переменную­ член s i z e ( ) , определенную следующим образом? Исправьте все обнару­ женные ошибки.

Упражнение

7.33.

pos S c ree n : : s i z e ( ) con s t { r e t u r n h e i gh t * w i dt h ;



7 4 1 Поиск имен в области видимости класса .

.

.

В рассмотренных до сих пор программах поиск имен ( name lookup)

(процесс поиска объявления, соответствующего данному имени) б ыл отно­ сительно прост.

Глава 7. Классы

368 •

Сначала поиск объявления осуществляется в том блоке кода, в котором используется имя. Причем рассматриваются только те имена, объявле­ ния которых расположены перед местом применения.



Если имя не найдено, поиск продолжается в иерархии областей видимо­ сти, начиная с текущей.



Если объявление так и не найдено, прои сходит оши бка.

Когда поиск имен осуществляется в функциях-членах, определенных в классе, может показаться, что он происходит не по правилам поиска . Но в данном елучае внешний вид обманчив. Обработка определений классов осуществляется в два этапа. •

Сначала компилируются объявления членов класса.



Тела функции компилируются только после того, как виден весь класс. Определения функций-членов обрабатываются после того, как компилятор обработает все объявления в классе.

Классы обрабатываются в два этапа, чтобы облегчить организацию кода класса. Поскольку тела функций-членов не обрабатываются, пока весь класс не станет видимым, они смогут использовать любое имя, определенное в клас­ се. Если бы определения функций обрабатывались одновременно с объявле­ ниями переменных-членов, то пришлось бы располагать функции-члены так, чтобы они обращались только к тем именам, которые уже видимы.

П о иск имен для о б ъ явлени й членов класса Этот двухэтапный процесс применяется только к именам, используемым в теле функции-члена. Имена, используемые в объявлениях, включая имя ти­ па возвращаемого значения и типов списка параметров, должны стать видимы прежде, чем они будут использованы. Если объявление переменной-члена бу­ дет использовать имя, объявление которого еще не видимо в классе, то компи­ лятор будет искать то имя в той области (областях) видимости, в которой оп­ ределяется класс. Рассмотрим пример. typede f douЫ e Mo n e y ; str ing bal ; c l a s s Account { pu Ы i c : Mon e y ba l a n c e ( ) { p r i va t e : Mo n e y ba l ; // . . . };

return bal ;

}

Когда компилятор видит объявление функции b a l a n c e ( ) , он ищет объяв­ ление имени Mo n e y в классе A c c o u n t . Компилятор рассматривает только те

369

7.4. Област ь видимости класса

объявления в классе Ac c ou n t , которые расположены перед использованием имени Mo n e y. Поскольку его объявление как члена класса не найдено, компи­ лятор ищет имя в окружающей области видимости. В этом примере компиля­ тор найдет определение типа (t yp e de f) Mo n e y. Этот тип будет использоваться и для типа возвращаемого значения функции ba l a n c e ( ) , и как тип перемен­ ной-члена b a l . С другой стороны, тело функции ba l a n c e ( ) обрабатывается только после того, как видимым становится весь класс. Таким образом, опера­ тор re t u rn в этой функции возвращает переменную-член по имени b a l , а не строку из внешней области видимости.

И мена типов име ю т осо б енности Обычно внутренняя область видимости может переопределить имя из внешней области видимости, даже если это имя уже использовалось во внут­ ренней области видимости. Но если член класса использует имя из внешней области видимости и это имя типа, то класс не сможет впоследствии переоп­ ределить это имя: type de f douЫ e Mon e y ; cl a s s A c c o u n t { puЫ i c : Mo n e y b a l a n c e ( ) {

return bal ;

pr iva te : t yp e de f douЫ e Mo ne y ; Mo n e y b a l ; // . . . };

}

/ / исполь зуе т ся имя Мо п еу из внешней 1 1 обла сть В ИДИМО С Т И

/ / ошиб ка : нель зя переопределить Мо пеу

Следует заметить, что хотя определение типа Mone y в классе A c c o u n t ис­ пользует тот же тип, что и определение во внешней области видимости, этот код все же ошибочен. Хотя переопределение имени типа является ошибкой, не все компиляторы обнаружат эту ошибку. Некоторые спокойно примут такой код, даже если программа ошибочна. Определения имен типов обычно располагаются в начале класса. Так, любой член класса, который использует этот тип, будет рас­ положен после определения его имени.

Пр и поиске имен в о б лае т ях видимости ч лен ы класса следуют о б ычным правилам Поиск имени, используемого в теле функции-члена, осуществляется сле­ дующим образом.

Глава 7. Классы

370 •

Сначала поиск объявления имени осуществляется в функции-члене. Как обычно, рассматриваются объявления в теле функции, только предше­ ствующие месту использования имени.



Если в функции-члене объявление не найдено, поиск продолжается в классе. Просматриваются все члены класса.



Если объявление имени в классе не найдено, поиск продолжится в об­ ласти видимости перед определением функции-члена.

Обычно не стоит использовать имя другого члена класса как имя парамет­ ра в функции-члене. Но для демонстрации поиска имени нарушим это прави­ ло в функции dummy f c n ( ) : _

/ / обра тите внимание : это сугубо демонстра ционный код , о тражающий плохую / / пра ктику пр ограммиров ания . Обычно не стоит исполь з ова ть одина ков ое имя 1 1 для параме тра и функции- члена i n t he i g h t ; / / определяе т имя , в п о следствии исполь зуемое в Screen c l a s s Screen { p uЫ i c : t yp e de f s t d : : s t r i n g : : s i z e t yp e p o s ; v o i d dummy _f c n ( p o s he i gh t ) { cursor w i d th * h e i gh t ; / / ка кое имя h e i gh t имее т ся в виду ? =

p r ivate : p o s cu r s o r p o s he i g h t

};

О; О,

w i d th

О;

Когда компилятор обрабатывает выражение умножения в функции durnrny f cn ( ) , он ищет имена сначала в пределах данной функции. Параметры функции находятся в области видимости функции. Таким образом, имя he i ght, исполь­ зуемое в теле функции durnmy f c n ( ) , принадлежит ее параметру. В данном случае имя he i gh t параметра скрывает имя h e i gh t переменной­ члена класса. Если необходимо переопределить обычные правила поиска, то это можно сделать так:

_

_

/ / плохой подход : имена , локальные для функций - членов , не должны 1 1 скрыв а ть имена переменных - членов кла сса vo i d S c r e e n : : durnmy_ f c n ( p o s h e i gh t ) { cursor w i dt h * t h i s - > he i gh t ; / / переменная - член h e i gh t / / аль терна тив ный спо соб ука з а ния переменной- члена cursor w i d t h * S c r e e n : : h e i g h t ; / / переменная - член h e i gh t =

=

Несмотря на то что член класса скрыт, его все равно можно ис­ пользовать. Достаточно указать его полное имя, включающее имя класса, либо явно применить указатель t h i s . Значительно проще обеспечить доступ к переменной-члену h e i ght, при­ своив параметру другое имя:

7.4. Об ласть видимости класса

371

1 1 хороший подход : не исполь зуйте имена переменных - членов для 1 1 параме тров или других ло кальных пер еменных vo i d S c r e e n : : dummy_ f c n ( p o s h t ) { cu r s o r w i d th * h e i g h t ; / / переменная - член h e i gh t =

Теперь, когда компилятор будет искать имя he i gh t, в функции dummy _ f cn ( ) он его не найдет. Затем компилятор просмотрит класс S c re e n . По­ скольку имя h e i gh t используется в функции-члене dummy f cn ( ) , компиля­ тор просмотрит все объявления членов класса. Несмотря на то что объявление имени he i g h t расположено после места его использования в функции dummy _ f cn ( ) , компилятор решает, что оно относится к переменной-члену he i gh t . _

П осле поиска в о б ласти видимости класса продолжается поиск в ок ружающе й о б ласти видимости Если ком пилятор не находит имя в функции или в области видимости класса, он ищет его в окружающей области видимости. В данном случае имя he i ght объявлено во внешней области видимости, перед определением класса S c reen. Однако объект во внешней области видимости скрывается переменной-членом класса по имени he i ght. Если необходимо имя из внешней области видимости, к нему можно обратиться явно, используя оператор области видим ости: 1 1 плохой подход : не скрыв айте необходимые имена , которые 1 1 определены в о кружающих о бла с тях видимо сти vo i d S c r e e n : : dummy_ f c n ( po s h e i gh t ) { cu r s o r w i dt h * : : h e i gh t ; / / ко торый h e i gh t ? Гло б аль ный =

Несмотря на то что глобальный объект был скрыт, используя опе­ ратор области видимости, доступ к нему вполне можно получ ить.

П оиск имен р ас пространяется по всем у ф а й лу, где они б ыли применены Когда член класса определен вне определения класса, третий этап поиска его имени происходит не только в объявлениях глобальной области видимо­ сти, которые расположены непосредственно перед определением класса S c r e e n, но и распространяется на остальные объявления в глобальной области видимости. Рассмотрим пример . i n t he i gh t ; / / определяе т имя , впоследствии исполь зуемо е в Screen class Screen { p uЫ i c : t yp e de f s t d : : s t r i n g : : s i z e t ype p o s ; vo i d s e t H e i gh t ( p o s ) ; p o s h e i g h t = О ; 1 1 скрыв а е т о бъявление h e i gh t из внешней 1 1 о бла с ти В ИДИМО С ТИ } ;

3 72

Глава 7. Классы

S c r e e n : : p o s ve r i f y ( S c r e e n : : p o s ) ; vo i d S c r e e n : : s e t H e i g ht ( p o s va r ) { 1 1 va r : отно сится к параме тру 1 1 h e i gh t : о тносится к члену кла с са 1 1 ve ri fy : о тно сит ся к глобальной функции he i ght = ve r i f y ( va r ) ;

Обратите внимание, что объявление глобальной функции ve r i f y ( ) не ви­ димо перед определением класса S c r e e n . Но третий этап поиска имени вклю­ чает область видимости, в которой присутствует определение члена класса. В данном примере объявление функции ve r i f y ( ) расположено перед опре­ делением функции s e t H e i gh t ( ) , а потому может использоваться.

Упражнения раздела 7.4.1 Что произойдет, если поместить определение типа p o s в последнюю строку класса S c re e n на стр. 370?

Упражнение

7.34.

Объясните код, приведенный ниже. Укажите, какое из определений, Т уре или i n i tVa l , будет использовано для каждого из имен. · Если здесь есть ошибки, найдите и исправьте их.

Упражнение

7.35.

t ype de f s t r i n g Т уре ; Т уре i n i t Va l ( ) ; c l a s s Ex e r c i s e { puЫ i c : t ype de f d o u Ы e Т уре ; Т уре s e tVa l ( T yp e ) ; Т уре i n i tVa l ( ) ; pr ivate : i n t va l ; }; Т уре E x e r c i s e : : s e tVa l ( T ype pa rm ) va l = p a rm + i n i tVa l ( ) ; r e t u r n va l ;

7 .5. Снова о конст р укто р а х Конструкторы - ключевая часть любого класса С++. Основы конструкто­ ров рассматривались в разделе 7.1 .4 (стр . 342), а в этом разделе описаны неко­ торые из дополнительных возможностей конструкторов и подробности мате­ риала, приведенного ранее.



7.5.1 . Список инициализации конст р укто р а

Когда определяются переменные, они, как правило, инициализируются сразу, а не определяются и присваиваются впоследствии:

7.5. Снова о конст рукто рах s t r i ng foo " H e l l o Wo r l d ! " ; s t r i n g ba r ; bar = " H e l l o Wo r l d ! " ; =

373 / / определить и инициализиро в а ть / / инициализ а ция по умолча нию пустой стро кой / / присв оение нов ого зна чения переменной ba r

Аналогичное различие между инициализацией и присвоением относится к переменным-членам объектов. Если не инициализировать переменную-член явно в списке инициализации конструктора, она инициализируется значени­ ем по умолчанию прежде, чем выполнится тело конструктора. Например: 1 1 допус тимый , но не самый лучший спо соб созда ния конструктора 1 1 кла сса Sa l e s_ da t a : не т инициализ а тора конструктора Sa l e s d a t a : : S a l e s d a t a ( c o n s t s t r i n g & s , u n s i g n e d c n t , douЫ e p r i c e )

b o o kNo s; uni ts sold cnt ; r e ve n u e cnt * price ; =

=

=

Эта версия и исходное определение на стр. 345 дают одинаковый результат: по завершении конструктора переменные-члены содержат те же значения. Различие в том, что исходная версия инициализирует свои переменные-члены, тогда как эта версия присваивает значения им. Насколько существенно это различие, зависит от типа переменной-члена.

И ногда пр именение списка ини ц иализац ии конструктора неиз б ежно Зачастую, но не всегда, можно игнорировать различие между инициализа­ цией и присвоением значения переменной-члену. Переменные-члены, яв­ ляющиеся константой или ссылкой, должны быть инициализированы. Анало­ гично члены класса, для типа которых не определен стандартный конструк­ тор, также следует инициализировать. Например : c l a s s C o n s t Re f { puЫ i c : C o n s t Re f ( i n t i i ) ; p r i va te : int i ; con s t i n t c i ; int & ri ; };

Переменные-члены c i и r i следует инициализировать как любой другой константный объект или ссылку. В результате отсутствие инициализатора конструктора для этих членов будет ошибкой: 1 1 ошибка : ci и ri должны быть инициализиров а ны Con s t Re f : : C o n s tRe f ( i n t i i ) { / / присв оения : i ii ; / / ok =

374

Глава 7. Классы ci ri

ii; i;

/ / ошибка : нель зя присв оить зна чение константе / / ошибка : r i никогда не буде т инициализиров а на

К тому времени, когда начинает выполняться тело конструктора, инициа­ лизация уже завершена. Единственный шанс инициализировать константу или ссылочную переменную-член - в инициализаторе конструктора. Вот правильный способ написания этого конструктора: 1 1 ok : яв ная инициализ а ция констант и ссьиrок Con s t Re f : : C o n s t Re f ( i n t i i ) : i ( i i ) , ci ( i i ) , r i ( i )

{

Для предоставления значений переменным-членам, являющимся константой, ссылкой или классом, у которого нет стандартного конструктора, использование списка инициализации конструкто­ ра неизбежно. СОВЕТ. ИСПОЛЬЗУ ЙТЕ СПИСКИ ИНИ ЦИАЛИЗАЦИИ КОНСТРУКТОРА

Во многих классах различие между инициализацией и присвоением свя­ зано исключительно с вопросом эффективности: зачем инициализировать переменную-член и присваивать ей значение, когда ее достаточно просто инициализировать. Однако важней эффективности тот факт, что некоторые переменные­ члены обязательно должны быть инициализированы . При стандартном ис­ пользовании инициализаторов конструктора можно избежать неожидан­ ных ошибок компиляции, когда класс обладает членом, требующим нали­ чия списка инициализации.

П орядок ини ц иализа ц ии пе р еменных-членов класса Нет ничего удивительного в том, что каждая переменная-член присутству­ ет в списке инициализации конструктора только один раз. В конце концов, зачем переменной-члену два исходных значения? Но что на самом деле неожиданно, так это то, что список инициализации конструктора задает только значения, используемые для инициализации пе­ ременных-членов, но не определяет порядок, в котором осуществляется ини­ циализация . Порядок инициализации переменных-членов задает их расположение при определении. Порядок расположения инициализаторов в списке инициали­ зации конструктора не влияет на порядок инициализации. Порядок инициализации зачастую не имеет значения. Но если одна из пе­ ременных-членов инициализируется с учетом значения другой, порядок их инициализации критически важен. В качестве примера рассмотрим следующий класс:

375

7.5 . Снова о конст рукторах class Х int i ; int j ; puЫ i c : 1 1 ошибка : i инициализируе тся прежде j X ( i n t va l ) : j ( va l ) , i ( j ) { } };

В данном елучае список инициализации конструктора написан так, чтобы инициализировать переменную-член j значением va l, а затем использовать переменную-член j для инициализации переменной-члена i . Но переменная­ член i инициализируется первой. В результате попытка инициализации пе­ ременной-члена i осуществляется в момент, когда переменная-член j еще не имеет значения! Некоторые компиляторы достаточно интеллектуальны, чтобы распознать опасность и выдать предупреждение о том, что переменные-члены в списке инициализации конструктора расположены в порядке, отличном от порядка их объявления. Рекоменд ем

Элементы списка инициализации конструктора имеет смысл рас­ полагать в том же порядке, в котором объявлены переменные­ члены. Кроме того, старайтесь по возможности избегать примене­ ния одних переменных-членов для инициализации других.

Вообще, можно достаточно просто избежать любых проблем, связанных с по­ рядком выполнения инициализации. Достаточно использовать параметры кон­ сrруктора вместо переменных-членов объекта. Конструктор класса х, например, лучше было бы написать следующим образом: Х ( i n t va l ) :

i ( va l ) ,

j ( va l )

{

}

В этой версии порядок инициализации переменных-членов i и j не имеет значения.

Ар гументы по умолчанию и конструкторы Действие стандартного конструктора класса S a l e s_da t a подобно конст­ руктору, получающему один строковый аргумент. Единственное отличие в том, что конструктор, получающий строковый аргумент, использует его для инициализации переменной-члена b o o kN o . Стандартный конструктор (неяв­ но) использует стандартный конструктор типа s t r i ng для инициализации переменной boo kN o . Эти конструкторы можно переписать как единый конст­ руктор с аргументом по умолчанию (см. раздел 6.5 . 1 , стр . 308) : c l a s s S a l e s _ da t a { puЫ i c : 1 1 определить стандартный конструктор ка к получающий строковый 1 1 аргумент

Глава 7 . Классы

376 S a l e s _ da ta ( s t d : : s t r i n g s = " " ) : b o o kN o ( s ) { } 1 1 о с таль ные конс трукторы без изменений S a l e s_d a t a ( s t d : : s t r i n g s , u n s i g n e d c n t , douЫ e r e v ) : b o o kN o ( s ) , un i t s _s o l d ( c n t ) , r e ve n u e ( r ev * c n t ) S a l e s d a t a ( s t d : : i s t r e am & i s ) { r e a d ( i s , * t h i s ) ; } 1 1 о сталь ные члены , ка к прежде

{

}

};

Эта версия класса предоставляет тот же интерфейс, что и исходный на стр. 345. Обе версии создают тот же объект, когда никаких аргументов не пре­ доставлено или когда предоставлен один строковый аргумент. Поскольку этот конструктор можно вызвать без аргументов, он считается стандартным конст­ руктором класса. Конструктор, предоставляющий аргументы по умолчанию для всех своих параметров, также считается стандарПIЬIМ конструктором. Следует заметить, что, вероятно, не нужно использовать аргументы по умолчанию с конструктором S a l e s_da t a ( ) , который получает три аргумен­ та. Если пользователь предоставляет не нулевое количество проданных книг, следует также гарантировать, что пользователь предоставит и цену, по кото­ рой они были проданы.

Упражнения раздела 7.5.1 Упражнение

7.36.

Следующий инициализатор ошибочен. Найдите и ис­

правьте ошибку. struct Х { Х ( i nt i , i n t j ) : ba s e ( i ) , i n t rem , ba s e ;

r em ( b a s e % j )

{

}

} ;

Упражнение 7.37. Используя версию класса S a l e s _da t a из этого раздела, определите, какой конструктор используется для инициализации каждой из следующих переменных, а также перечислите значения переменных­ членов в каждом объекте: S a l e s da ta f i r s t i t em ( c i n ) ; i n t ma i n ( ) S a l e s da t a n e x t ; Sales data l a s t ( " 9 - 9 9 9 - 9 9 9 9 9 - 9 " ) ;

Конструктору, получающему аргумент типа i s t rearn&, можно предоставить объект c i n как аргумент по умолчанию. Напишите объявление конструктора, использующего объект c i n как аргумент по умолчанию.

Упражнение

7.38.

377

7.5. С н ова о ко н структорах

Допустимо ли для конструктора, получающего с т ро ку, и конструктора, получающего тип i s t r e arn & , иметь аргументы по умолча­ нию? Если нет, то почему?

Упражнение

7.39.

Выберите одну из следующих абстракций (или абстрак­ цию по собственному выбору). Определите, какие данные необходимы в классе. Предоставьте соответствующий набор конструкторов. Объясните свои решения. ( с ) Emp l o ye e ( а ) Boo k ( Ь ) Da t e

Упражнение

(d)

Ve h i c l e

7.40.

(е)

Ob j e c t

(f)

T ree

7 . 5.2. Делеги р ующий конст р укто р rc+.il L!!J

Новый стандарт расширяет использование списков инициализации кон­ структора, позволяя определять так называемые делеги ру ющ ие конст­ рукторы (delegating constructor) . Делегирующий конструктор использу­ ет для инициализации другой конструктор своего класса. Он "делегиру­ ет" некоторые (или все) свои задачи другому конструктору.

Подобно любому другому конструктору, делегирующий конструктор имеет список инициализации переменных-членов и тело функции. Список инициали­ зации делегирующего конструктора содержит элемент, являющийся именем самого класса. Подобно другим инициализаторам переменных-членов класса, имя класса сопровождается заключенным в скобки списком аргументов. Список аргументов должен соответствовать другому конструктору в классе. В качестве примера перепишем класс S a l e s_da t a так, чтобы использовать делегирующие конструкторы следующим образом: c l a s s S a l e s_da t a { puЫ i c : 1 1 неделегирующий кон с труктор инициализируе т члены из соотв е тствующих 1 1 аргументов S a l e s_da t a ( s t d : : s t r i ng s , u n s i g n e d c n t , douЫ e p r i c e ) : b o o kN o ( s ) , u n i t s_s o l d ( c n t ) , r e v e n u e ( c n t * p r i c e ) { } 1 1 в се другие конс трукторы делегируют к другому конструктору S a l e s _da ta ( ) : S a l e s _d a t a ( " " , О , 0 ) { } S a l e s_d a t a ( s t d : : s t r i ng s ) : S a l e s_da t a ( s , О , 0 ) { } S a l e s _d a t a ( s t d : : i s t r e am & i s ) : S a l e s_d a t a ( ) { read ( i s , * thi s ) ; 1 1 другие члены ка к прежде

В этой версии класса S a l e s_da t a все конструкторы, кроме одного, делеги­ руют свою работу. Первый конструктор получает три аргумента и использует их для инициализации переменных-членов, но ничего другого не делает. В этой версии класса определен стандартный конструктор, использующий для инициализации конструктор на три аргумента. Он также не делает ничего,

Глава 7. Классы

378

поэтому его тело пусто. Конструктор, получающий строку, также делегирует работу версии на три аргумента. Конструктор, получающий объект i s t ream&, также делегирует свои действия. Он делегирует их стандартному конструктору, который в свою очередь делеги­ рует их конструктору на три аргумента. Как только эти конструкторы заканчи­ вают свою работу, запускается тело конструктора с аргументом i s t ream& . Оно вызьmает функцию read ( ) для чтения данных из потока i s t re am. Когда конструктор делегирует работу другому конструктору, список ини­ циализации и тело делегированного конструктора выполняются оба. В классе S a l e s _da ta тела делегируемых конструкторов пусты. Если бы тела конструк­ торов содержали код, то он выполнялся бы прежде, чем управление возврати­ лось бы к тел у делегирующего конструктора.

Упражнения раздела

7. 5.2

Перепишите собственную версию класса S a l e s _da ta, чтобы использовать делегирующие конструкторы. Добавьте в тело каждого конструктора оператор, выводящий сообщение всякий раз, когда он выпол­ няется. Напишите объявления для создания объекта класса S a l e s da t a лю­ быми возможными способами. Изучите вывод и удостоверьтесь, что пони­ маете порядок выполнения делегирующих конструкторов.

Упражнение

7.41 .

Упражнение 7.42. Вернитесь к классу, написанному для упражнения 7.40 в разделе 7.5.1 (стр. 377), и решите, может ли какой-нибудь из его конструк­ торов использовать делегирование. Если да, то напишите делегирующий конструктор (конструкторы) для своего класса. В противном случае рас­ смотрите список абстракций и выберите ту, которая, по вашему, использо­ вала бы делегирующий конструктор. Напишите определение класса для этой абстракции.



7 . 5 . 3 . Роль станда р тного конст р укто р а

Стандартный конструктор автоматически используется всякий раз, когда объект инициализируется по умолчанию. Инициализация по умолчанию осуществляется в следующем случае. •

При определении нестатических переменных (см. раздел 2.2. 1 , стр . 77) или массивов (см. раздел 3.5. 1 , стр . 1 62) в области видимости блока без инициализаторов.



Когда класс, который сам обладает членами типа класса, использует синтезируемый стандартный конструктор (см. раздел 7.1 .4, стр . 342) .



Когда переменные-члены типа класса не инициализируются явно в спи­ ске инициализации конструктора (см. раздел 7.1 .4, стр. 345) .

7.5. Снова о конструкто р ах

37 9

Инициализация значением по умолчанию осуществляется в следующем случае. •

Во время инициализации массива, когда предоставляется меньше ини­ циализаторов, чем элементов массива (см. раздел 3.5.1 , стр. 1 62).



При определении локального статического объекта без инициализатора (см. раздел 6.1 . 1 , стр. 272) .



Когда явно запрашивается инициализация значением по умолчанию в форме выражения т ( ) , где т - это имя типа . (Конструктор вектора, получающий один аргумент, чтобы определить размер вектора (см. раз­ дел 3.3. 1 , стр. 1 43), использует аргумент этого вида для инициализации значением по умолчанию своего элемента.)

Чт9бы использоваться в этих контекстах, у классов должен быть стандартный конструктор. Большинство этих контекстов должно быть вполне очевидным. Однако значительно менее очевидным может быть влияние на классы, у которых есть переменные-члены без стандартного конструктора: c l a s s N o De f a u l t { puЫ i c : N o De f a u l t ( c on s t s t d : : s t r i ng & ) ; 1 1 далее дополнитель ные члены , но не т других кон с трукторов }; s t r u c t А { / / ту_тет являе тся о ткрытой по умол ча нию ; см . ра здел 7 . 2 , стр . 3 5 0 NoDe f a u l t rny_rnern ; }; / / ошибка : нев озможен синтезируемый конструктор для А А а; struct В { В ( ) { } / / ошибка : не т инициализа тора для Ь member N o De f au l t Ь rnernbe r ; };

Рекомендуем

На практике почти всегда имеет смысл предоставлять стандарт­ ный конструктор, если определены другие конструкторы.

П р и ме н е ни е ста н дарт н ого ко нструктора Следующее объявление объекта ob j компилируется без проблем. Но при попытке его использования компилятор жалуется на невозможность приме­ нения к функции синтаксиса доступа к члену. S a l e s_d a t a ob j ( ) ; / / o k : но определена функция , а не объект if ( ob j . i s b n ( ) == P r irne r_ S t h_e d . i s b n ( ) ) / / ошибка : obj - функция

Проблема в том, что, несмотря на намерение объявить инициализирован­ ный значением по умолчанию объект ob j , фактически была объявлена функ­ ция без параметров, возвращающая объект типа S a l e s_da t a .

Глава 7. Классы

380

Чтобы правильно определить объект, использующий стандартный конст­ руктор для инициализации, следует убрать пустые круглые скобки: 1 1 ok : obj - о бъ е кт , инициализиров а нный зна чением по умолча нию S a l e s _ d a t a ob j ;

& ВНИМАНИЕ

Распространенной ошибкой среди новичков в С++ является объяв­ ление объекта, инициализированного стандартным конструкто­ ром, следующим образом: Sales da ta Sales_data

ob j ( ) ob j 2 ;

Упражнения раздела

; / / упс ! Это объявление функции � а не создание объекта / / ok : obj 2 - это объект , а не функция

7.5.3

Упражнение 7.43. Предположим, имеется класс N o De f a u l t, у которого есть конструктор, получающий параметр типа i n t, но нет стандартного конст­ руктора. Определите класс с, у которого есть переменная-член типа N o De f a u l t. Определите стандартный конструктор для класса с .

Упражнение 7.44. Допустимо л и следующее объявление? Если нет, то по­ чему? ve c t o r ve c ( l O ) ;

Определите вектор, содержащий объекты типа дыдущего упражнения?

Упражнение

7.45.

с

из пре­

Которое из следующих утверждений, если таковое име­ ется, ложно? Почему?

Упражнение

7.46.

(а) Класс должен предоставить по крайней мере один конструктор.

(Ь) Стандартный конструктор - это конструктор с пустым списком пара­ метров. (с) Если для класса не нужно никаких значений по умолчанию, то класс не должен предоставлять стандартный конструктор.

(d) Если класс не определяет стандартный конструктор, компилятор сам создает конструктор, который инициализирует каждую переменную-член значением по умолчанию соответствующего типа.



7 . 5 . 4 . Н еявное п р ео б р азование типов класса

Как упоминалось в разделе 4.1 1 (стр. 21 8), язык С++ автоматически осуще­ ствляет преобразование некоторых встроенных типов. Обращалось также внимание на то, что классы тоже могут определять неявные преобразования. Каждый конструктор, который может быть вызван с одним аргументом, опре­ деляет неявное преобразование в тип класса. Такие конструкторы иногда

7.5. С н ова о ко нст р укто р ах

381

упоминают как конструкторы преобразования (converting constructor) . Опреде­ ление преобразования из типа класса в другой тип рассматривается в разде­ ле 14.9 (стр. 732) . Конструктор , который может быть вызван с одиночным аргумен­ том , вполне позволяет определить неявное преобразование из ти­ па параметра в тип класса. Конструкторы класса S a l e s_da t a, получающие строку и объект класса i s t re am, оба определяют неявные преобразования из этих типов в тип Sa l e s _da t a . Таким образом, можно использовать тип s t r i n g или i s t r e am там, где ожидается объект типа S a l e s da t a : s t r i n g n u l l _b o o k " 9- 9 9 9- 9 9 9 9 9-9" ; 1 1 созда е т временный о бъ е кт типа Sa l e s_ da t a 1 1 с un i t s_ s o l d и re ven u e равными О и bookNo равным n u l l_book it em . c omb i n e ( n u l l_bo o k ) ; =

Здесь происходит вызов функции-члена comЬ i ne ( ) класса S a l e s_da ta со строковым аргументом. Эгот вызов совершенно корректен; компилятор автома­ тически создаст объект класса S a l e s_da ta из данной строки. Эгот вновь создан­ ный (времеm-Iый) объект класса S a l e s_da ta передается функции comЬ i ne ( ) . Поскольку параметр функции comЬ ine ( ) является ссылкой на константу, этому параметру можно передать временный объект.

Допустимо только одно прео б р азован ие типов класса В разделе 4.1 1 .2 (стр. 222) обращалось внимание на то, что компилятор автома­

тически применит только одно преобразование типов класса. Например, сле­ дующий код ошибочен, поскольку он неявно использует два преобразования: 1 1 ошибка : тре буе т двух поль з ов а тель с ких пре о бра з ов а ний : 11 ( 1 ) пре о бра з ов а ние " 9 - 9 9 9 - 9 9 9 9 9 - 9 " в s t ri n g ( 2 ) преобра з ов а ние вр еменной с троки в Sa l e s_ da t a 11 it em . c omЬ i n e ( " 9 - 9 9 9 - 9 9 9 9 9 - 9 " ) ;

Если данный вызов необходим, это можно сделать при явном преобразовании символьной строки в объект класса s t r i ng или в объект класса S a l e s _da ta: 1 1 ok : яв ное пре о бра зов а ние в s t ri n g , неяв ное пре о бра з о в а ние в Sa l e s da t a it em . comЬ i ne ( s t r i n g ( " 9 - 9 9 9 - 9 9 9 9 9 - 9 " ) ) ; 1 1 ok : неявное пре о бра з ов а ние в s t ri n g , яв ное пре о бра з ов а ние в Sa l e s da t a it em . c omЬ i n e ( S a l e s _da t a ( " 9 - 9 9 9 - 9 9 9 9 9 - 9 " ) ) ;

П рео б р азования типов класса не всегда полезны Желательно ли преобразование типа s t r i n g в S a l e s_da t a, зависит от конкретных обстоятельств. В данном елучае это хорошая идея. Строка в пере­ менной nu l l_boo k, вероятнее всего, соответствует несуществующему ISBN . Преобразование из i s t re am в S a l e s_da t a более проблематично:

Глава 7. Классы

382

/ / исполь зуе т конструктор i s t ream при созда нии о бъ екта для переда чи / / функции comb i n e i t em . c omb i n e ( c i n ) ;

Этот код неявно преобразует объект с i n в объект класса s а l е s _ i t em. Это преобразование осуществляет тот конструктор класса s а l е s _ da t а, который получает тип i s t re am. Этот конструктор создает (временный) объект класса S a l e s _da t a при чтении со стандартного устройства ввода. Затем этот объект передается функции s ame _ i s b n ( ) . Этот объект класса S a l e s_i t em временный (см. раздел 2.4. 1 , стр. 99) . По за­ вершении функции comЬ i n e ( ) никакого доступа к нему не будет. Фактиче­ ски создается объект, удаляющийся после того, как его значение добавляется в объект i t em.

П редотвр ащение неявны х прео б р а з овани й, осуществляемых конструкто р ом Чтобы предотвратить использование конструктора в контексте, который требует неявного преобразования, достаточно объявить его явным (explicit constructor) с использованием ключевого слова e xp l i c i t : c l a s s S a l e s data puЫ i c : de f a u l t ; S a l e s da t a ( ) S a l e s _da t a ( c on s t s t d : : s t r i n g & s , u n s i g n e d n , douЫ e р ) : b o o kNo ( s ) , u n i t s_ s o l d ( n ) , r e v e n u e ( p * n ) e x p l i c i t S a l e s _d a t a ( c o n s t s t d : : s t r i n g & s ) : b o o kNo ( s ) { } e xp l i c i t S a l e s da t a ( s t d : : i s t r e am & ) ; 1 1 о с тальные члены , ка к прежде };

{

}

Теперь ни один из конструкторов не применим для неявного создания объек­ тов класса S a l e s_da ta. Ни один из предыдущих способов применения теперь не сработает: i t em . c omb i n e ( n u l l _bo o k ) ; i t em . c omb i n e ( c i n ) ;

/ / ошибка : конс труктор s t ri n g теперь яв ный / / ошибка : конструктор i s t ream теперь яв ный

Ключевое слово e xp l i c i t имеет значение только для тех конструкторов, которые могут быть вызваны с одним аргументом. Конструкторы, требующие большего количества аргументов, не используются для неявного преобразова­ ния, поэтому нет никакой необходимости определять их как expl i c i t . Клю­ чевое слово expl i c i t используется только в объявлениях конструкторов в классе. В определении вне тела класса его не повторяют. / / ошибка : ключев ое слов о exp l i ci t допустимо толь ко для / / объя�лений конс трукторов в за голов ке кла с са e x p l i c i t S a l e s_d a t a : : S a l e s _da t a ( i s t r e am & i s ) { read ( i s , * thi s ) ;

7 . 5. С н ова о ко н структорах

383

Я вн ы е конструкто р ы применяют ся т олько для пр ямо й ини ц и ал и зац ии Одним из контекстов, в котором происходит неявное преобразования, яв­ ляется использование формы инициализации копированием (со знаком = ) (см. раздел 3.2. 1 , стр. 1 26) . С этой формой инициализации нельзя использовать явный конструктор; придется использовать прямую инициализацию: S a l e s _da t a i t em l ( n u l l _bo o k ) ; 1 1 ok : прямая инициализ а ция 1 1 ошибка : с яв ным конструктором нель зя исполь з ов а ть форму 1 1 инициализ а ции копиров анием S a l e s da t a i t em2 nu l l boo k ; =

-

-

Явный консrруктор применим только с прямой формой инициали­ зации (см. раздел 3.2.1 , crp. 1 26). Кроме того, компилятор не бу дет использовать этот консrруктор в автоматическом преобразовании.

П р и менен ие явн ых конструкто р ов для прео б р азован и й Хотя компилятор не будет использовать явный конструктор для неявного преобразования, его можно использовать для преобразования явно: 11 ok : аргумент - яв но созда нный объ ект кла сса Sa l e s_ da t a it em . c omb i n e ( S a l e s_da t a ( n u l l _b o o k ) ) ; 1 1 ok : s t a t i c ca s t може т исполь з ов а ть яв ный конструктор it em . c omb i n e ( s t a t i c_ c a s t < S a l e s _da t a > ( c i n ) ) ;

В первом вызове конструктор S a l e s_da t a ( ) используется непосредствен­ но. Этот вызов создает временный объект класса S a l e s _da t a, используя кон­ структор s а 1 е s da t а ( ) , получающий строку. Во втором вызове используется оператор s t a t i c_ca s t (см. раздел 4. 1 1 .3, стр . 223) для выполнения явного, а не неявного преобразования. В этом вызове оператор s t а t i с_ с а s t исполь­ зует для создания временного объекта класса S a l e s_da t a конструктор с па­ раметром типа i s t r e am. _

Б иб лиотечные классы с явными конструкторами некоторых библиотечных классов, включая уже использованные ранее, есть конструкторы с одним параметром. У



Конструктор класса s t r i n g, получающий один параметр типа c on s t cha r * (см. раздел 3.2. 1 , стр . 1 26), не является явным.



Конструктор класса ve c t o r, получающий размер вектора (см. раздел 3.3.1 , crp. 143), является явным.

Глава 7. Классы

384

Упражнения раздела 7.5.4 Упражнение 7.47. S a l e s_da t a

Объясните,

должен

ли

быть

явным

конструктор

( ) , получающий строку. Каковы преимущества объявления

конструктора явным? Каковы недостатки?

Упражнение 7.48. С

учетом того, что конструктор

S a l e s da t a _

( ) не явля­

ется явным, какие операции происходят во время следующих определений : s t r i n g nu l l _ i s bn ( " 9 - 9 9 9 - 9 9 9 9 9 - 9 " ) ; S a l e s - d a t a i t ern l ( n u l l - i s b n ) ; S a l e s da ta i t ern2 ( " 9 - 9 9 9 - 9 9 9 9 9 - 9 " ) ;

Что будет при явном конструкторе

Упражнение 7.49.

S a l e s da t a _

() ?

Объясните по каждому из следующих трех объявлений

comЬ i ne ( ) , что происходит при вызове i . c omЬ i n e ( s ) , это объект класса S a l e s _ da t a, а s - строка : функции

(а)

(Ь)

(с)

S a l e s _ da t a & c ornb i n e ( S a l e s _ d a t a ) ; S a l e s _da t a & c ornb i ne ( S a l e s _ d a t a & ) ; S a l e s _ da t a & c ornb i n e ( c o n s t S a l e s _da t a & )

Упражнение 7.50. вашего класса

i-

cons c ;

Определите, должен ли какой-либо из конструкторов

Pe r s o n

Упражнение 7.51 .

где

быть явным .

Как, по вашему, почему вектор определяет свой конст­

руктор с одним аргументом как явный, а строка нет?

*' 7 . 5 . 5 . А г р егатные классы Агрегатный класс (a ggre gate class)

предоставляет пользователям прямой

доступ к своим членам и имеет специальный синтаксис инициализации . Класс считается агрегатным в следующем ел учае . •

Все его переменные-члены являются открытыми



Он не определяет конструкторы.



У

него нет никаких внутриклассовых инициализаторов (см. раздел

стр . •

У

(puЫ i c ) . 2.6. 1,

1 1 3).

него нет никаких базовых классов или виртуальных функций, связан­

ных с классом средствами, которые рассматриваются в главе Например, следующий класс является агрегатным: s t r u c t Da ta { i n t i va l ; s t r i ng s ; };

1 5.

7.5. С н ова о ко н стр ук торах

385

Для инициализации переменных-членов агрегатного класса можно пре­ доставить заключенный в фигурные скобки список инициализаторов для пе­ ременных-членов: / / vа 1 1 . i vа 1 Da t a v a l l = {

=

О ; v а 1 1 . s = s t r i п g ( "Ап п а " ) О , " An n a " } ;

Инициализаторы должны располагаться в порядке объявления перемен­ ных-членов. Таким образом, сначала располагается инициализатор для пер­ вой переменной-члена, затем для второй и т.д. Следующей пример ошибочен: // ошибка : нель зя исполь з ов а ть "Ап п а " для инициализ а ции i va l или 1 0 2 4 1 1 для инициализ а ции s Da t a v a l 2 = { " An n a " , 1 0 2 4 } ;

Как и при инициализации элементов массива (см. раздел 3.5 . 1 , стр. 1 62), ес­ ли в списке инициализаторов меньше элементов, чем переменных-членов класса, последние переменные-члены инициализируются значением по умол­ чанию. Список инициализаторов не должен содержать больше элементов, чем переменных-членов у класса. Следует заметить, что у явной инициализации переменных-членов объекта класса есть три существенных недостатка. •

Она требует, чтобы все переменные-члены были открытыми.



Налагает дополнительные обязанности по правильной инициализации каждой переменной-члена каждого объекта на пользователя класса (а не на его автора). Такая инициализация утомительна и часто приводит к ошибкам, поскольку достаточно просто забыть инициализатор или предоставить неподходящее значение.



Если добавляется или удаляется переменная-член, придется изменить все елучаи инициализации.

Упражнения раздела 7.5.5 Упражнение 7.52. На примере первой версии класса S a l e s _da t a из разде­ ла 2.6.1 (стр . 1 1 2) объясните следующую инициализацию. Найдите и ис­ правьте возможные ошибки. S a l e s_da t a i tern =

{ " 978-0590353403" ,

25,

15 . 99 } ;

� 7 . 5 .6. Лите р альные классы В разделе 6.5.2 (стр. 31 2) упоминалось, что параметры и возвращаемое зна­ чение функции c on s t e xp r должны иметь литеральные типы. Кроме арифме­ тических типов, ссылок и указателей, некоторые классы также являются лите­ ральными типами. В отличие от других классов, у классов, являющихся лите-

386

Глава 7. Классы

ральными типами, могут быть функции-члены c o n s t e x p r . Такие функции­ члены должны отвечать всем требованиям функций c o n s t e xp r . Эти функ­ ции-члены неявно константные (см. раздел 7 . 1 .2, стр. 337) . Агрегатный класс (см. раздел 7.5 .5, стр. 384), все переменные-члены которо­ го имеют литеральный тип, является литеральным классом. Неагрегатный класс, соответствующий следующим ограничениям, также является литераль­ ным классом. •

У всех переменных-членов должен быть литеральный тип.



У класса должен быть по крайней мере один конструктор c o n s t e x p r .



Если у переменной-члена есть внутриклассовый инициализатор, ини­ циализатор для переменной-члена встроенного типа должен быть кон­ стантным выражением (см. раздел 2.4.4, стр. 1 03) . Если переменная-член имеет тип класса, инициализатор должен использовать его собственный конструктор c o n s t e xp.



Класс должен использовать заданное по умолчанию определение для своего деструктора - функции-члена класса, удаляющего объекты типа класса (см. раздел 7 . 1 .5, стр . 347) .

К онструкторы соns tехрr Хотя конструкторы не могут быть константными (см. раздел 7 . 1 .4, стр. 342), в литеральном классе они могут быть функциями c o n s t e xp r (см. раздел 6.5 .2, стр. 31 2). Действительно, литеральный класс должен предоставлять по край­ ней мере один конструктор c o n s t e xp r .



Конструктор con s t exp r может быть объявлен как = de f au l t (см. раз­ дел 7.1 .4, стр. 345) или как удаленная функция, которые будут описаны в разделе 1 3.1 .6 (стр. 643) . В противном случае конструктор con s texpr должен отвечать требованиям к конструкторам (у него не может быть оператора retu rn) и к функциям con s t e xp r (его исполняемый оператор может иметь единственный оператор retu rn) (см. раздел 6.5.2, стр. 312). В результате тело конструктора c o n s t exp r обычно пусто. Определению конструктора con s t exp r предшествует ключевое слово c o n s te xp r:

c l a s s Debug { puЫ i c : c o n s t e x p r Debu g ( b o o l Ь c o n s t e x p r Debug ( b o o l h ,

t r u e ) : hw ( b ) , i o ( b ) , o t he r ( b ) bool i , boo l о ) : hw ( h ) , i o ( i ) , o t he r ( o ) c o n s t e x p r b o o l a n y ( ) { r e t u r n hw 1 1 i o 1 1 o t he r ; } vo i d s e t_i o ( b o o l Ь ) { i o = Ь ; } vo i d s e t_hw ( b o o l Ь ) { hw = Ь ; } vo i d s e t_o the r ( b o o l Ь ) { hw = Ь ; pr ivate : boo l hw ; / / а ппара тна я ошибка , о тличная о т ошибки IO =

387

7 . 6. Ст атические ч ле н ы класса boo l i o ; boo l othe r ;

/ / ошибка IO / / другие ошибки

};

Конструктор c o n s t e xp r должен инициализировать каждую переменную­ член. Инициализаторы должны либо использовать конструктор c o n s t e xp r, либо быть константным выражением. Конструктор c o n s t e xp r используется и для создания объектов c o n s t e xp r, и для параметров или типов возвращаемого значения функций c o n s t e xp r : c o n s t e x p r Debug i o_s u b ( f a l s e , t r u e , f a l s e ) ; / / // i f ( i o_ s u b . a n y ( ) ) c e r r < < " p r i n t app r op r i a t e e r r o r me s s a g e s " // cons texpr Debug p rod ( fa l s e ) ; // i f ( p r od . a n y ( ) ) c e r r < < " p r i n t a n e r r o r me s s a g e " E x amp l e : : ve c ;

Резюме Классы - это фундаментальный компонент языка С++. Классы позволяют определять новые типы, наилучшим образом приспособленные к задачам конкретного приложения и позволяющие сделать их короче и проще в моди­ фикации. Основой классов являются абстракция данных (способность определять данные и функции-члены) и инкапсуляция (способность защитить члены класса от общего доступа). Инкапсуляция класса достигается определением членов его реализации закрытыми. Классы могут предоставить доступ к сво­ ему не открытому члену, объявив другой класс или функцию дружественной. Классы могут определять конструкторы - специальные функции-члены, контролирующие инициализацию объектов. Конструкторы могут быть пере­ гружены. Для инициализации всех переменных-членов конструкторы долж­ ны использовать список инициализации конструктора. Классы позволяют объявлять переменные-члены изменяемыми ( mu tab l e ) или статическими ( s t a t i c ) . Изменяемая переменная-член никогда не стано­ вится константой - ее значение может быть изменено даже в константной функции-члене. Статической может быть как функция, так и переменная-член. Статические члены класса существуют независимо от объектов данного класса. Классы могут также определить изменяемые ( mu t aЫ e ) и статические ( s t a t i c ) члены. Изменяемая переменная-член никогда не становится кон­ стантой; ее значение может быть изменено даже в константной функции­ члене. Статический член может быть функцией или переменной; статические члены существуют независимо от объектов типа класса.

Те рмины

39 3

Т е р м инь1 de fau l t . Синтаксис, используемый после списка параметров объявления стандартно­ го конструктора класса, чтобы сообщить компилятору о необходимости создать кон­ структор, даже если у класса есть другие конструкторы . Абст рактны й тип данных (abstract data type). Структура данных, инкапсулирующая (скрывающая) свою реализацию. Абст ракция данных (data abstraction). Технология программирования, сосредоточен­ ная на интерфейсе типа . Абстракция данных позволяет программистам игнориро­ вать детали реализации типа, интересуясь лишь его возможностями. Абстракция данных является основой как объектно-ориентированного, так и обобщенного про­ граммирования. Аг р ег атны й класс (aggregate class). Класс только с открытыми переменными-членами, без внутриклассовых инициализаторов или конструкторов. Члены агрегатного класса могут быть инициализированы заключенным в фигурные скобки списком инициали­ заторов. Делегир ующий конструктор (delegating constructor). Конструктор со списком инициа­ лизации, один элемент которого определяет другой конструктор того же класса мя инициализации. Дружественные отношения (friend). Механизм, при помощи которого класс прелос­ тавляет доступ к своим не оn можно использовать для чтения данных из окна консоли, из файла на диске или из строки. Точно так же этот оператор можно использовать независимо от того, читаются ли символы типа cha r или w ch a r t . Используя наследование (inheritance), библиотека позволяет игнорировать различия между потоками различных видов. Подобно шаблонам (см . раз­ дел 3.3, стр. 1 41 ), свяJанные наследованием классы можно использовать, не вникая в детали того, как они работают. Более подробная информация о наследовании в языке С++ приведена в главе 1 5 и в разделе 1 8.3 (стр. 1 002) . Если говорить коротко, то наследование позволяет сказать, что некий класс происходит от другого класса. Обычно объект производного класса можно использовать так, как будто это объект класса, от которого он происходит. Типы i f s t r e am и i s t r i n g s t r e am происходят от класса i s t r e am. Таким образом, объект типа i f s t r e am или i s t r i ng s t re am можно использовать так, как будто это объект класса i s t re am. Объекты этих типов можно использовать теми же способами, что и объект c i n . Например, можно вызвать функцию get l i n e ( ) объекта i f s t re am или i s t r i ng s t r e am либо использовать их опе­ ратор > > для чтения данных. Точно так же типы o f s t r e am и o s t r i ng s t r e am происходят от класса o s t r e am. Следовательно, объекты этих типов можно ис­ пользовать теми же способами, что и объект c o u t . Все, что рассматривается в остальной части этого раздела, одина­ ково применимо как к простым, файловым и строковым потокам, а также к потокам для символов типа c h a r или wcha r t .



8.1 .1 . О б ъекты ввода-вывода не допускают копи р о вания и п р исвоения

Как упоминалось в разделе 7.1 .3 (стр. 340), объекты ввода-вывода не допус­ кают копирования и присвоения: o f s t r eam o u t l , o u t 2 ; ou t l = ou t 2 ; o f s t r e am p r i n t ( o f s t r e am ) ;

1 1 ошиб ка : нель зя присв аив а ть по токовые объ е кты 1 1 ошиб ка : нельзя инициализирова ть параме тр

Глава 8. Б иблиотека ввода и вывода

402

1 1 типа o fs t ream 1 1 ошибка : нель зя копиров а ть по токовые объ е кты

out2 = p r i n t ( ou t 2 ) ;

Поскольку объекты типа ввода-вывода нельзя копировать, не может быть параметра или типа возвращаемого значения одного из потоковых типов (см. раздел 6.2. 1 , стр. 277) . Функции, осуществляющие ввод-вывод, получают и возвращают поток через ссылки. Чтение или запись в объект ввода-вывода изменяет его состояние, поэтому ссылка не должна быть константой.

8.1 .2. Флаги состояния В связи с наследованием классов ввода-вьmода возможно возникновение оши­ бок. Некоторые из ошибок исправимы, другие происходят глубоко в сисгеме и не могут бьnъ исправлены в области в идимости программы. Классы ввода-вьmода определяют функции и флаги, перечисленные в табл. 8.2, позволяющие обра­ щаться к флагам состояния (condition state) потока и манипулировать ими.

Табл иц а 8.2. Ф лаг и с остояния б и блиотеки вв о да-выво да s t rm : : i o s t a t e

один из типов ввода-вывода, перечисленных в табл. 8.1 (стр. 400). i o s t a t e машинно-зависимый целочисленный тип, предсгавляющий флаг сосгояния потока s t rm

-

-

s t rm : : badЬ i t

Значение флага s t rm : : i o s t a t e указывает, что поток недопусгим

s t rm : : fa i lЬ i t

Значение флага s t rm : : i o s t a t e указывает, что операция ввода­ вывода закончилась неудачей

s t rm : : eo fЬ i t

Значение флага s t rm : : i o s t a t e указывает, что поток достиг конца файла

s t rm : : good.Ь i t

Значение флага s t rm : : i o s t a t e указывает, что поток не находится в недопусгимом сосгоянии. Эго значение гарантированно будет нулевым

s . eo f ( )

Возвращает значение t rue, если для потока s установлен флаг e o fЬ i t

s . fa i l ( )

Возвращает значение t rue, если для потока s установлен флаг f a i lЬ i t

s . bad ( )

Возвращает значение t rue, если для потока s установлен флаг badЬ i t

s . good ( )

Возвращает значение t rue., если поток s находится в допустим ом сосгоянии

s . clear ( )

Возвращает все флаги потока s в допусгимое сосгояние

s . с l е а r ( флаг)

Устанавливает определенный флаг (флаги) потока s в допусгимое состояние. Фла г имеет тип s t rm : : i o s t a t e

s . s e t s tate ( флаг)

Добавляет в поток

s

определенный фла г. Флаг имеет тип

s t rm : : i o s t a t e s . rds tate ( )

Возвращает текущее состояние потока s как значение типа s t rm : : i o s t a t e

8.1 .

Кл ассы ввода-вывода

403

В качестве примера ошибки ввода-вывода рассмотрим следующий код: i n t iv a l ; c i n > > i va l ;

Если со стандартного устройства ввода ввести, например, слово Воо, то операция чтения потерпит неудачу. Оператор ввода ожидал значение типа i n t, но получил вместо этого символ в . В результате объект c i n перешел в со­ стояние ошибки. Точно так же объект c i n окажется в состоянии ошибки, если в вести символ конца файла. Как только произошла ошибка, последующие операции ввода-вывода в этом потоке будут терпеть неудачу. Читать или писать в поток можно только тогда, когда он находится в неошибочном состоянии. Поскольку поток может оказаться в ошибочном состоянии, код должен проверять его, прежде чем ис­ пользовать. Проще всего определить состояние потокового объекта - это ис­ пользовать его в условии: wh i l e ( c i n > > wo r d ) 1 1 ok : опера ция чтения успешна

. . .

Условие оператора wh i l e проверяет состояние потока, возвращаемого вы­ ражением > > . Если данная операция ввода успешна, состояние остается до­ пустимым и условие выполняется.

О п рос состояния потока Использование потока в условии позволяет узнать только то, допустим ли он. Это ничего не говорит о случившемся. Иногда необходимо также узнать причину недопустимости потока. Например, действия после достижения кон­ ца файла, вероятно, будут отличаться от таковых после ошибки на устройстве ввода-вывода. Библиотека ввода-вывода определяет машинно-зависимый целочисленный тип i o s t a t e, используемый для передачи информации о состоянии потока. Этот тип используется как коллекция битов, подобно переменной qu i z 1 в раз­ деле 4.8 (стр. 21 3). Классы ввода-вывода определяют четыре значения con s texp r (разделе 2.4.4, стр. 1 03) типа i o s t a t e, представляющие конкретные битовые схемы. Эги значения используются для указания конкретных видов со­ стояний ввода-вывода. Они используются с побитовыми операторами (см. раз­ дел 4.8, стр. 21 0) для проверки или установки нескольких флагов за раз. Флаг b a db i t означает отказ системного уровня, та� > i ) / * . . * / .

8.1 .3. Уп р авление б уфе р ом вывода Каждый объект ввода-вывода управляет буфером, используемым для хра­ нения данных, которые программа читает или записывает. Например, при выполнении следующего кода литеральная строка могла бы быть выведена немедленно или операционная система могла бы сохранить данные в буфере и вывести их позже: o s < < " p l e a s e e n t e r а va l u e :

";

Использование буфера позволяет операционной системе объединить не­ сколько операций вывода данной программы на системном уровне в одну операцию. Поскольку запись в устройство может занять много времени, воз­ можность операционной системы объединить несколько операций вывода в одну может существенно повысить производительность. Существует несколько условий, приводящих к сбросу буфера, т.е. к факти­ ческой записи на устройство вывода или в файл. •

Программа завершается нормально. Все буфера вывода освобождаются при выходе из функции ma i n ( ) .



В некий елучайный момент времени буфер может оказаться заполнен­ ным. В этом случае перед записью следующего значения происходит сброс буфера.



Сброс буфера можно осуществить явно, использовав такой манипуля­ тор, как e n d l (см. раздел 1 .2, стр. 30) .



Используя манипулятор un i tbu f, можно установить такое внутреннее состояние потока, чтобы буфер освобождался после каждой операции вывода. Для объекта c e r r манипулятор u n i tbu f установлен по умолча­ нию, поэтому запись в него приводит к немедленному выводу.



Поток вывода может быть связан с другим потоком. В таком ел учае бу­ фер привязанного потока сбрасывается при каждом чтении или записи другого потока. По умолчанию объекты c i n и c e r r привязаны к объек­ ту c o u t . Следовательно, чтение из потока c i n или запись в поток c e r r сбрасывает буфер потока c o u t .

Глава 8 . Б иблиотека ввода и вывода

406

Сб р ос б уф е р а в ы вода В приведенных ранее программах уже не раз использовался манипулятор e n d l , который записывает символ новой строки и сбрасывает буфер . Сущест­ вуют еще два подобных манипулятора: f l u s h и e n d s . Манипулятор f l u s h используется для сброса буфер потока без добавления символов в вывод. Ма­ нипулятор e n d s добавляет в буфер нулевой символ, а затем сбрасывает его. cout . Затем используйте объект класса i s t r i ng s t r e am для чтения каждого элемента из вектора по одному слову за раз. Упражнение 8.1 1 . Программа этого раздела определила свой объект класса i s t r i n g s t re am во внешнем цикле wh i l e . Какие изменения необходимо внести, чтобы определить объект r e c o rd вне этого цикла? Перепишите программу, перенеся определение объекта r e c o rd во вне цикла wh i l e, и убедитесь, все ли необходимые изменения внесены. Упражн ение 8.12. Почему в классе P e r s on i n f o не использованы внутри­ _ классовые инициализаторы?

Глава 8. Б иблиотека ввода и вывода

41 6

8. 3 .2. Использование класса o s trings tream Класс o s t ri ngs t ream полезен тогда, когда необходимо организовать вывод небольшими частями за раз, не откладывая его на более позднее время. Напри­ мер, могла бы возникнугь необходимость проверять и переформатировать номе­ ра телефонов, которые были прочитаны в коде предыдущего примера. Если все номера допустимы, необходимо переформатировать номера и вывести их в новый файл. Если у кого-нибудь будут недопустимые номера, то помещать их в новый файл не нужно. Вместо этого следует вывести сообщение об ошиб­ ке, содержащее имя человека и список его недопустимых номеров. Поскольку нельзя включать для человека данные с недопустимыми номе­ рами, мы не можем произвести вывод, пока не просмотрим и не проверим все их номера. Но можно "записать" вывод в оперативную память объекта класса o s t r i ng s t r e am: for

( c on s t a u t o & e n t r y : p e op l e ) { o s t r i n g s t r e am f o rma t t e d , b a dNums ;

/ / для каждой за писи в peopl e / / о бъе кты создаются на каждом / / цикле f o r ( c on s t a u t o & n ums : e n t r y . p h o n e s ) { / / для каждого номера i f ( ! va l i d ( n um s ) ) { b a dNums < < " " < < num s ; / / строка в ba dNums else / / "з апись " в с троку forma t t e d f o rma t t e d < < " " < < f o rma t ( n ums ) ; if

( b a dNums . s t r ( ) . emp t y ( ) ) / / е сли плохих номеров не т o s < < e n t r y . n ame < < " " 1 1 выв е сти имя < < f o rma t t e d . s t r ( ) < < e n d l ; / / и переформа тиров а нные номера e l s e / / в про тив ном случа е выв е с ти имя и плохие номера ce r r < < " i np u t e r r o r : " < < e n t r y . n ame = s z ;

s t r i ng : : s i z e t ype s z )

Но мы не можем использовать эту функцию как аргумент функции fin d_ i f ( ) . Как уже упоминалось, функция f i n d_ i f ( ) получает унарный предикат, поэтому переданное ей вызываемое выражение должно получать один аргумент. Лямбда-выражение, переданное функцией Ь i gg i e s ( ) функ­ ции f i n d_ i f ( ) , использует свой список захвата для хранения значения пере­ менной s z . Чтобы использовать функцию ch e c k_ s i z e ( ) вместо этого лямбда­ выражения, следует выяснить, как передать аргумент s z параметру.

Б иб лиоте ч ная ф ун кция bind ( )



Проблему передачи аргумента размера функции ch e c k_s i z e ( ) можно решить при помощи новой библиотечной функции Ь i n d ( ) , определен­ ной в заголовке f u n c t i o n a l . Функцию Ь i n d ( ) можно считать универ­ сальным адаптером функции (см. раздел 9.6, стр . 473) . Она получает вы­ зываемый объект и создает новый вызываемый объект, который адапти­ рует список параметров исходного объекта.

Общая форма вызова функции b i n d ( ) такова: auto новыйВызыв а емыйОбъ ект = Ь i n d ( вызыв а емыйОбъ ект ,

список_ аргументов ) ;

где новыйВызыва еJМЬIЙОбъ ект - это новый вызьmаемый объект, а список_ аргументов разделяемый запятыми список аргументов, соответствующих па­ раметрам переданного вызываемого объекта вызыв а емыйОбъ ект. Таким образом, когда происходит вызов объекта новыйВызыв а емыйОбъ ект, он вызывает вызы­ ваемыйОбъ ект, передавая аргументы из списка список_ аргументов. Аргументы из списка список_ аргументов могут включать имена в форма­ те _ п, где п целое число. Эти аргументы знакоместа, представляющие параметры объекта новыйВызыв а емыйОбъ е кт. Они располагаются вместо ар­ гументов, которые будут переданы объекту новыйВызыв а емыйОбъ ект. Число п является позицией параметра вновь созданного вызываемого объекта: 1 первый параметр, _ 2 второй и т.д. -

-

-

-

Глава 1 0. Об общенные алгоритмы

510

П р ивязка парам е тра s z к функц ии check_s i ze ( ) В качестве примера использования функции Ь i n d ( ) создадим объект, ко­ торый вызывает функцию che с k_ s i z е ( ) с фиксированным значением ее па­ раметра размера: 1 1 ch e ck б вызыв а емый о бъ ект , получающий один аргумент типа s t ri n g 1 1 и вызыв ающий функцию ch e ck_ s i z e ( ) с э т ой с тро кой и зна чением б a u t o c he c k б = b i n d ( c h e c k_s i z e , 1, 6) ; -

У этого вызова функции Ь i nd ( ) есrь только одно знакоместо, означающее, что вызьmаемый объект che c k б ( ) получает один аргумент. Знакоместо распола­ гается первым в списке аргументов. Эго означает, что параметр вызьmаемого объекта che c k б ( ) соответствует первому параметру функции ch e c k_s i z e ( ) . Этот параметр имеет тип c on s t s t r i n g & , а значит, параметр вызываемого объекта che c k б ( ) также имеет тип con s t s t r i ng & . Таким образом, при вызове . che c k б ( ) следует передать аргумент типа s t r i ng, который вызываемый объект che c k б ( ) передаст в качестве первого аргумента функции che c k_ s i z e ( ) . Второй аргумент в списке аргументов (т.е. третий аргумент функции Ь i nd ( ) ) является значением 6 . Это значение связывается со вторым парамет­ ром функции che c k_s i z e ( ) . Каждый раз, когда происходит вызов вызывае­ мого объекта ch e c k б ( ) , он передает функции c he c k_ s i z e ( ) значение 6 как второй аргумент: s t r i ng s bo o l Ы

= =

" he l l o " ; c he c k б ( s ) ;

1 1 ch e ck б (s ) вызыв а е т ch e ck_ s i z e ( s ,

6)

Используя функцию b i n d ( ) , можно заменить следующий исходный вызов функции f i n d_ i f ( ) на базе лямбда-выражения: auto wc

=

f i nd_i f ( wo r d s . be g i n ( ) , w o r d s . e n d ( ) , [ s z ] ( c on s t s t r i n g & а )

Кодом, использующим функцию che c k_ s i z e ( ) , заменить a u t o wc

=

f i n d _ i f ( wo r d s . be g i n ( ) , w o r d s . e n d ( ) , b i n d ( c he c k_s i z e , 1, sz) ) ;

Этот вызов функции Ь i n d ( ) создает вызываемый объект, который привя­ зывает второй аргумент функции ch e c k_s i z e ( ) к значению параметра s z . Когда функция f i n d_i f ( ) вызовет этот объект для строк вектора wo rds, он, в свою очередь, вызовет функцию c h e c k_s i z e ( ) , передав полученную строку и значение параметра s z . Таким образом, функция f i n d_i f ( ) фактически вызовет функцию с h е с k_ s i z е ( ) для каждой строки в исходном диапазоне и сравнит размер этой строки со значением параметра s z .

51 1

10.3. П еренастро йка функций

И спользование имен из п р ост р анства имен placeholders Имена формата _ п определяются в пространстве имен p l a c e h o l de r s . Са­ мо это пространство имен определяется в пространстве имен s t d (см. раз­ дел 3. 1, стр . 1 24) . Чтобы использовать эти имена, следует предоставить имена обоих пространств имен. Подобно нашим другим примерам, данные вызовы функции b i n d ( ) подразумевали наличие соответствующих объявлений u s i n g. Рассмотрим объявление u s i n g для имени _ 1 : us ing s td : : p l a c e h o l de r s : : _ 1 ;

Это объявление свидетельствует о том, что используется имя _ 1 , опреде­ ленное в пространстве имен p l a c e h o l de r s , которое само определено в про­ странстве имен s t d. Для каждого используемого имени знакоместа следует предоставить отдель­ ное объявление u s i n g. Но поскольку написание таких объявлений может быть утомительно и ведет к ошибкам, вместо этого можно использовать другую форму u s i n g, которая подробно рассматривается в разделе 1 8.2.2 (стр . 991 ): us i n g n arne s p a c e про с транств оимен_ имя ;

Она свидетельствует, что необходимо сделать доступными для нашей про­ граммы все имена из пространства имен про с тра нств оимен_ имя: us i n g n arne s p a c e s t d : : p l a c e h o l de r s ;

Этот код позволяет использовать все имена, определенные в пространстве имен p l a c e h o l de r s . Подобно функции Ь i n d ( ) , пространство имен p l a c e h o l de r s определено в заголовке f u n c t i o n a l .

Аргументы функц ии bind ( ) Как уже упоминалось, функцию Ь i n d ( ) можно использовать для фикси­ рованного значения параметра. В более общем случае функцию Ь i n d ( ) мож­ но использовать для привязки или перестройки параметров в предоставлен­ ном вызываемом объекте. Предположим, например, что f ( ) - вызываемый объект с пятью параметрами: вызыв а емый о бъ е кт , получающий дв а аргумента 11 g 1) ; 2, с, auto g = b i nd ( f , а , Ь , -

Этот вызов функции b i n d ( ) создает новый вызываемый объект, получаю­ щий два аргумента, представленные знакоместами 2 и _ 1 . Новый вызывае­ мый объект передает собственные аргументы как третий и пятый аргументы вызываемому объекту f ( ) . Первый, второй и четвертый аргументы вызывае­ мого объекта f ( ) связаны с переданными значениями а, Ь и с соответственно . Аргументы вызываемого объекта g ( ) связаны со знакоместами по позиции. Таким образом, первый аргумент вызываемого объекта g ( ) связан с парамет_

Глава 1 0. Об о бщ енные алгоритмы

512

ром _ 1 , а второй -. с параметром 2 Следовательно, когда происходит вызов g ( ) , его первый аргумент будет передан как последний аргумент вызываемого объекта f ( ) ; второй аргумент g ( ) будет передан как третий. В действительно­ сти этот вызов функции Ь i n d ( ) преобразует вызов g ( 1 , 2 ) в вызов f ( а , _

.

_

Ь,

2 , с,

_

1) .

Таким образом, вызов вызываемого объекта g ( ) вызывает вызываемый объ­ ект f ( ) с использованием аргументов вызываемого объекта g ( ) для знакомест наряду с аргументами а, Ь и с . Например, вызов g ( Х , У ) приводит к вызову f (a, Ь, У, с, Х) .

И спользование функции bind ( ) для пе р еупоря дочивания п ар а м ет р ов Рассмотрим более конкретный пример применения функции b i n d ( ) для переупорядочивания аргументов. Используем ее для обращения смысла функции i s S ho r t e r ( ) следующим образом: 1 1 сортиров ка по s o r t ( wo r d s . b e g i n 1 1 сортиров ка по s o r t ( wo rd s . be g i n

длине слов о т ( ) , words . end ( ) длине слов о т ( ) , words . end ( )

кор о тких к длинным , is Shorter ) ; длинных к коро тким , bind ( i s Shorter , 2,

1) ) ;

В первом вызове, когда алгоритм s o r t ( ) должен сравнить два элемента, А и в, он вызовет функцию i s S ho r t e r ( А , В ) . Во втором вызове аргументы функции i s S ho r t e r ( ) меняются местами. В данном случае, когда алгоритм s o r t ( ) сравнивает элементы, он вызывает функцию i s S h o r t e r ( В , А ) .

П р ивязка сс ылочных п а р амет р ов По умолчанию аргументы функции b i nd ( ) , не являющиеся знакоместами, копируются в возвращаемый ею вызываемый объект. Однако, подобно лям­ бда-выражениям, иногда необходимо связать аргументы, которые следует пе­ редать по ссылке, или необходимо связать аргумент, тип которого не допуска­ ет копирования. Для примера заменим лямбда-выражение, которое захватывало поток o s t r e am по ссылке: 1 1 o s - локальна я переменна я , ссьиrающа яся на по ток выв ода 1 1 с - локальная переменна я типа ch a r f o r_e a c h ( w o r d s . b e g i n ( ) , w o r d s . e n d ( ) , [ & os , с ] ( co n s t s t r i ng & s ) { o s e n d ;

Итератор после конца для итератора i s t r e am_ i t e r a t o r, читающего значения типа т i n l и i n 2 должны читать одинаковый тип. Они равны, если оба они конечные или оба связаны с тем же ВХОДНЫМ потоком

inl i n2 inl 1 = in2 * in

Возвращает значение, прочитанное из потока

i n - >mem

Синоним для ( * i n ) . т е т

+ + i n, i n + +

Читает следующее значение из входного потока, используя оператор > > для типа элемента . Как обычно, префиксная версия возвращает ссылку на итератор после инкремента. Постфиксная вер­ сия возвращает прежнее значение

10.4. Воз вращаясь к итераторам

51 7

И спользование итерато р а i s tream_i tera tor Когда создается потоковый итератор, необходимо определить тип объектов, которые итератор будет читать или записывать. Итератор i s t ream_i t e rator использует оператор > > для чтения из потока. Поэтому тип, читаемый итерато­ ром i s t ream_i te rato r, должен определять оператор ввода. При создании ите­ ратор i s t ream_i t e ra t o r следует связать с потоком. В качестве альтернативы итератор можно иниЦиализировать значением по умолчанию. В результате будет создан итератор, который можно использовать как значение после конца. i s t r e am i t e r a t o r < i n t > i n t i t ( c i n ) ; i s t r e am i t e r a t o r < i n t > i n t e o f ; i f s t r e am i n ( " a f i l e " ) ; i s t r e am_ i t e r a t o r < s t r i n g > s t r i t ( i n ) ;

/ / чита е т целые числа из ci n / / коне чно е зна чение ит ера тора / / чита е т строки из "a fi l e "

Для примера используем итератор i s t re am_i t e ra t o r для чтения со стан­ дартного устройства ввода в в е к т о р : i s t r e am i te r a t o r < i n t > i n i t e r ( c i n ) ; / / чита е т целые числа из ci n i s t r e am i t e r a t o r < i n t > e o f ; / / , , коне чный , , итера тор i s t ream / / пока е сть что чита ть wh i l e ( i n i t e r ! = e o f ) / / пост фиксный инкремент чита е т поток и в о звраща е т прежнее зна чение / / итера тора . Обращение к зна чению этого итера тора пред о с т а вляе т 1 1 предыдущее зна чение , про чита нное из потока v e c . pu s h_b a c k ( * i n i t e r + + ) ;

Этот цикл читает целые числа из потока c i n, сохраняя прочитанное в век­ тор ve c . На каждой итерации цикл проверяет, не совпадает ли итератор in _ i t e r со значением e o f . Этот итератор был определен как пустой итератор i s t r eam_i t e � a t o r, который используется как конечный итератор . Связан­ ный с потоком итератор равен конечному итератору, только если связанный с ним поток достиг конца файла или произошла ошибка ввода-вывода. Самая трудная часть этой программы - аргумент функции pu s h_bac k ( ) , который использует обращение к значению и постфиксные операторы инкре­ мента. Это выражение работает точно так же, как и другие выражения, совме­ щающие обращение к значению с постфиксным инкрементом (см. раздел 4.5, стр. 204) . Постфиксный инкремент переводит поток на чтение следующего зна­ чения, но возвращает прежнее значение итератора. Это прежнее значение со­ держит прежнее значение, прочитанное из потока. Для того чтобы получить это значение, осуществляется обращение к значению этого итератора. Особенно полезно то, что эту программу можно переписать так: i s t r e am_ i t e r a t o r < i n t > i n i t e r ( c i n ) , ve c t o r < i n t > v e c ( i n i t e r , e o f ) ;

eo f ;

1 1 чита е т целые числа из ci n 1 1 созда е т в ектор ve c из 1 1 диа па з она итера торов

Здесь вектор ve c создается из пары итераторов, которые обозначают диа­ пазон элементов. Это итераторы i s t re am_i t e r a t o r, следовательно, диапазон

Глава 1 0. Обобщенные алгоритмы

51 8

получается при чтении связанного потока. Этот конструкт?р читает поток c i n, пока он не встретит конец файла, или ввод, тип которого отличается от i n t . Прочитанные элементы используются для создания вектора ve c .

И спользование потоковы х ите р ато р ов с алго р итмами Поскольку алгоритмы используют функции итераторов, а потоковые ите­ раторы померживают по крайней мере некоторые функции итератора, пото­ ковые итераторы можно использовать с некоторыми из алгоритмов. Какие именно алгоритмы применимы с потоковыми итераторами, рассматривается в разделе 1 0.5. 1 (стр. 525). В качестве примера рассмотрим вызов функции a c cumu l a t e ( ) с парой итераторов i s t re am_ i t e r a t o r s : i s t r e am_ i t e r a t o r < i n t > i n ( c i n ) , c o u t < < a c c umu l a t e ( i n , e o f , 0 )

eo f ; < < endl ;

Этот вызов создаст сумму значений, прочитанных со стандартного устрой­ ства ввода. Если ввод в этой программе будет таким: 23

109 45

89

6 34

12

90

34 23 56 23

8

8 9 23

то результат будет 6 6 4 .

И тераторы i s tream_i terator позволяют использовать ленивое в ычисление То, что итератор i s t re arn_i te rato r связан с потоком, еще не гарантирует, что он начнет читать поток немедленно. Некоторые реализации разрешают за­ держать чтение потока, пока итератор не будет использован. Гарантированно, что поток будет прочитан перед первым обращением к значению итератора. Для большинсrва программ не имеет никакого значения, будет ли чтение немедлен­ ным или отсроченным. Но если создается итератор i s t re am_i t e ra t o r, который удаляется без использования, или если необходима синхронизация чтения того же потока из двух разных объектов, то тогда придется позаботиться и о моменте чтения.

Использование ите р атора os tream_i tera tor Итератор o s t re am_i t e r a t o r может быть определен для любого типа, у которого есть оператор вывода (оператор < * pv = n e w ve c t o r < i n t > { 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 } ;

Динамически созданный объект можно также инициализировать значени­ ем по умолчанию (см. раздел 3.3. 1 , стр. 1 43), сопроводив имя типа парой пус­ тых круглых скобок: string * ps l = new s t r i ng ; s t r i n g * p s = n e w s t r i ng ( ) ; int * p i l

new i n t ;

int * p i 2

new i n t ( ) ;

// 11 11 // 11 // 11

инициализ а ция по умолча нию пус той стро кой инициализ а ция зна че нием по умолча нию (пус той с трокой) инициализ а ция по умолча нию ; з на чение *pi l не определено инициализ а ция зна чением по умолча нию О ; *pi 2 = о

Для типов классов (таких как s t r i ng), определяющих собственные конст­ рукторы (см. раздел 7.1 .4, стр. 342), запрос инициализации значением по умолчанию не имеет последствий; независимо от формы, объект инициализи­ руется стандартным конструктором. Различие существенно в елучае встроен­ ных типов: инициализация объекта встроенного типа значением по умолча­ нию присваивает ему вполне конкретное значение, а инициализация по умолчанию - нет. Точно так же полагающиеся на синтезируемый стандарт­ ный конструктор члены класса встроенного типа также не будут не инициа­ лизированы, если эти члены не будут инициализированы в теле класса (см. раздел 7 . 1 .4, стр. 343) . Рекомендуем

(3tJ

По тем же причинам, по которым обычно инициализируют пере­ менные, имеет смысл инициализировать и динамически созданные объекты.

Когда предоставляется инициализатор в круглых скобках, для вывода типа объекта, который предстоит зарезервировать для этого инициали­ затора, можно использовать ключевое слово a u t o (см. раздел 2.5.2, стр. 1 07). Но, поскольку компилятор использует тип инициализатора для вывода резервируемого типа, ключевое слово a u t o можно использо­ вать только с одиночным инициализатором в круглых скобках:

auto p l

n e w a u t o ( ob j ) ;

auto р2

new auto { a , b , c } ;

11 11 11 11

р ука зыв а е т на о бъ е кт типа obj э т о т объект инициализируе тся зна чением obj ошибка : для инициализ а тора нужно исполь з о в а ть круглые ско бки

586

Глава 12. Динамичская пам ять

Тип p l - это указатель на автоматически вьmеденный тип obj . Если obj име­ ет тип i n t, то тип p l - i n t * ; если ob j имеет тип s t r i ng, то тип p l - s t ri ng* и т.д. Вновь созданный объект инициализируется значением объекта obj .

Динамически созданные константные о б ъ екты Для резервирования константных объектов вполне допустимо использовать оператор ne w: / / з арез ервиров а ть и инициализирова ть con s t i n t const int *pci = new cons t int ( 1 0 2 4 ) ; / / зарез ервирова ть и инициализирова ть зна чением по умолчанию con s t s t ri n g const s t ri ng *pcs = new con s t s tr i ng ;

Подобно любым другим константным объектам, динамически созданный константный объект следует инициализировать. Динамический константный объект типа класса, определяющего стандартный конструктор (см. раз­ дел 7.1 .4, стр . 343), можно инициализировать неявно. Объекты других типов следует инициализировать явно. Поскольку динамически зарезервированный объект является константой, возвращенный оператором new указатель являет­ ся указателем на константу (см. раздел 2.4.2, стр. 99).

И счерпание п ам яти Хотя современные машины имеют огромный объем памяти, всегда сущест­ вует вероятность исчерпания динамической памяти. Как только программа использует всю доступную ей память, выражения с оператором new будут терпеть неудачу. По умолчанию, если оператор n e w неспособен зарезервиро­ вать требуемый объем памяти, он передает исключение типа b a d_a l l o c (см. раздел 5.6, стр. 257) . Используя иную форму оператора new, можно вос­ препятствовать передаче исключения: 1 1 при неуда че опера тор n e w в о звраща е т нулев ой ука з а тель int *pl n e w i n t ; / / при неуда че опера тор n e w переда е т / / исключение s t d : : ba d a l l o c n e w ( n o t h r o w ) i n t ; / / при неуда че опера тор n e w в о звраща е т int *р2 1 1 нулев ой ука з а тель

По причинам, рассматриваемым в разделе 1 9 .1 .2 (стр . 1 028), эта форма опе­ ратора new упоминается как размещающий оператор new (placement new). Вы­ ражение размещающего оператора new позволяет передать дополнительные аргументы. В данном ел учае передается определенный библиотекой объект noth row. Передача объекта n o t h row оператору n e w указывает, что он не дол­ жен передавать исключения. Если эта форма оператора new окажется неспо­ собна зарезервировать требуемый объем памяти, она возвратит ну левой ука­ затель. Типы bad_a l l o c и n o t h row определены в заголовке new.

1 2. 1 .

Динамическая пам ят ь и интеллектуальные указатели

587

О сво б ождение дин амическо й п ам яти Чтобы предотвратить исчерпание памяти, по завершении использования ее следует возвратить операционной системе. Для этого используется оператор de l e t e, получающий указатель на освобождаемый объект: de l e t e р ;

/ / р должен быть ука з а телем на динамиче ски созда нный о бъ ект 1 1 или нулевым ука з а телем

Подобно оператору new, оператор de l e t e выполняет два действия: удаляет объект, на который указывает переданный ему указатель, и освобождает соот­ ветствующую область памяти.

З начения указателя и оператор delete Передаваемый оператору de l e t e указатель должен либо указывать на ди­ намически созданный объект, либо быть нулевым указателем (см. раздел 2.3.2, стр. 89). Результат удаления указателя на область памяти, зарезервированную не оператором new, или повторного удаления значения того же указателя не­ предсказуем: int i , douЫ e de l e t e de l e t e de l e t e de l e t e

* p i l = & i , * p i 2 = nul lpt r ; * pd = n e w douЬ l e ( 3 3 ) , * pd 2 = p d ; i; / / ошибка : i - не ука з а тель p i l ; / / непредска зуемо : pi l - локальный pd ; / / ok pd2 ; / / непредска зуемо : память , на которую ука зыв а е т pd2 , 1 1 уже о св о бождена de l e t e p i 2 ; / / ok : о св о бождение нулев ого ука з а теля в с егда допус тимо

Компилятор сообщает об ошибке оператора de l e t e i, поскольку знает, что i не указатель. Ошибки, связанные с выполнением оператора de l e te для указателей pi 1 и pd2, коварней: обычно компиляторы неспособны выяс­ нить, указывает ли указатель на объект, созданный статически или динамиче­ ски. Точно так же компилятор не может установить, была ли уже освобождена память, на которую указывает указатель. Большинство компиляторов примет такие выражения de l e t e, несмотря на их ошибочность. Хотя значение константного объекта не может быть изменено, сам объект вполне может быть удален. Подобно любым динамическим объектам, кон­ стантный динамический объект освобождается выполнением оператора de l e t e для указателя, указывающего на этот объект: -

co ns t i n t * p c i = n e w c o n s t i n t ( 1 0 2 4 ) ; de l e t e p c i ; / / ok : удаляе т константный объ ект

Глава 12. Динамичская пам ять

588

Динамиче с ки с оз данные о б ъ екты существую т до тех пор, пока не б удут осво б ождены Как упоминалось в разделе 1 2. 1 .1 (стр. 578), управляемая указателем share d_ pt r память автоматически освобождается при удалении последнего указателя shared_pt r. Динамический объект, управляемый указателем встроенного типа, сущесгвует до тех пор, пока к областям памяти , управляемой при помощи указа­ телей встроенных типов, не будет удален явно. Функции, возвращающие обычные (а не интеллектуальные) указатели на области динамической памяти, возлагают ответственность за их удаление на вызывающую сторону: / / в озвраща е т ука з а тель на динамиче ски созда нный о бъ е кт Fo o * f a c t o r y ( T a r g ) { / / обра б о та ть аргумент с о о тв е тс тв енно r e t u r n n e w Foo ( a r g ) ; / / з а о св о бождение э той памяти о тв е ча е т / / вызыв ающая сторона

Подобно прежней версии функции f a c t o r y ( ) (см. раздел 1 2.1 . 1 , стр . 578), эта версия резервирует объект, но не удаляет его. Ответственность за освобо­ ждение памяти динамического объекта, когда он станет больше не нужен, не­ сет вызывающая сторона функции f a c t o r y ( ) К сожалению, вызывающая сторона слишком часто забывает сделать это: .

vo i d u s e_ f a c t o r y ( T a r g ) { Foo * р factory ( a rg ) ; / / исполь зов а ть р , но не удалить его / / р выходит из о бла сти видимо с ти, но память , на ко торую он ука зыв а е т , 1 1 н е осв о божда е т ся ! =

Здесь функция u s е _ f а с t о r у ( ) вызывает функцию f а с t о r у ( ) резерви­ рующую новый объект типа Foo. Когда функция u s e f a c t o r y ( ) завершает работу, локальная переменная р удаляется. Эта переменная - встроенный указатель, а не интеллектуальный. В отличие от классов, при удалении объектов встроенного типа не происходит ничего. В частносги, когда указатель выходит из области в идимости, с объектом, на который он указывает, ничего не происходит. Если этот указатель указывает на динамическую память, она не освобождается автоматически. _

& ВНИМАНИЕ

Динамическая память, управляемая при помощи встроенных (а не интеллектуальных) указателей, продолжает существование, пока не будет освобождена явно.

12.1. Динамическая памят ь и интеллектуальные указатели

589

В этом примере указатель р был единственным указателем на область па­ мяти, зарезервированную функцией f a c t o r y ( ) . По завершении функции u s е f а с t о r у ( ) у программы больше нет никакого способа освободить эту память. Согласно общей логике программирования, следует исправить эту ошибку и напомнить о необходимости освобождения памяти в функции _

use f a c t o r y ( ) : vo id u s e_ f a c t o r y ( T a r g ) Foo * р = f a c t o r y ( a rg ) ; 1 1 исполь з ов а ние р de l e t e р ; / / не з а быть о св о бодить память сейча с , когда 1 1 она больше не нужна

Если созданный функцией u s е _ f а с t о r у ( ) объект должен использовать другой код, то эту функцию следует изменить так, чтобы она возвращала ука­ затель на зарезервированную ею память: Foo * u s e_ f a c t o r y ( T a r g ) { Foo * р = f a c t o r y ( a rg ) ; 1 1 исполь з ов а ние р r e t u r n р ; / / о св о бодить память должна вызыв ающа я с т орона

В НИМАНИЕ ! УПРАВЛЕНИЕ ДИНАМИЧЕС К ОЙ ПАМЯТЬЮ ПОДВЕРЖЕНО ОШИБКАМ

Есть три общеизвестных проблемы, связанных с использованием опера­ торов new и de l e t e при управлении динамической памятью: 1.

Память забыли освободить. Когда динамическая память не освобождает­ ся, это называется "утечка памяти", поскольку она уже не возвращается в пул динамической памяти. Проверка утечек памяти очень трудна, по­ скольку она обычно не проявляется, пока приложение, проработав дос­ таточно долго, фактически не исчерпает память.

2.

Объект использован после удаления. Иногда эта ошибка обнаруживается при создании ну левого указателя после удаления.

3.

Повторное освобождение той же памяти. Эта ошибка может произойти в случае, когда два указателя указывают на тот же динамически создан­ ный объект. Если оператор delete применен к одному из указателей, то память объекта возвращается в пул динамической памяти. Если впослед­ ствии применить оператор de l e t e ко второму указателю, то динамиче­ ская память может быть нарушена. Допустить эти ошибки значительно проще, чем потом найти и исправить.

Глава 1 2 . Динамичская пам ять

590

Рекомендуем

Избежать всех этих проблем при использовании исключительно интеллектуальных указателей не получится. Интеллектуальный указатель способен позаботиться об удалении памяти т олько то­ гда, когда не останется других интеллектуальных указателей на эту область памяти.

П ереус тановка значения указателя после уд аления ... Когда указатель удаляется, он становится недопустимым. Но, даже став не­ допустимым, на многих машинах он продолжает содержать адрес уже осво­ божденной области динамической памяти. После освобождения области па­ мяти указатель на нее становится пот ерянным указат елем (dangling pointer). Потерянный указатель указывает на ту область памяти, которая когда-то со­ держала объект, но больше не содержит. Потерянным указателям присущи все проблемы неинициализированных указателей (см . раздел 2.3.2, стр. 89) . Проблем с потерянными указателями можно избежать, освободив связанную с ними память непосредственно перед выходом из области видимости самого указателя. Так не появится шанса ис­ пользовать указатель уже после того, как связанная с ним память будет осво­ бождена. Если указатель необходимо сохранить, то после применения опера­ тора de l e t e ему можно присвоить значение nu l l p t r. Это непосредственно свидетельствует о том, что указатель не указывает на объект .

... об еспечивает лишь ча с тичную защиту Фундаментальная проблема с динамической памятью в том, что может быть несколько указателей на ту же область памяти. Переустановка значения указателя при освобождении памяти позволяет проверять допустимость дан­ ного конкретного указателя, но никак не влияет на все остальные указатели, все еще указывающие на уже освобожденную область памяти . Рассмотрим пример: i n t * p ( n ew i n t ( 4 2 ) ) ; 1 1 р ука зыв а е т на динамиче скую память / / р и q ука зыв ают на ту же обла сть памяти auto q р; de l e t e р ; / / дела е т недопус тимыми р и q р = nu l l p t r ; / / ука � ыв а е т , что ука з а тель р больше не связ а н с объектом =

Здесь указатели р и q указывают на тот же динамически созданный объект. Удалим этот объект и присвоим указателю р значение nu l l p t r, засвидетель­ ствовав, что он больше не указывает на объект. Однако переустановка значе­ ния указателя р никак не влияет на указатель q, который стал недопустимым после освобождения памяти, на которую указывал указатель р (и указатель q!). В реальных системах поиск всех указателей на ту же область памяти зачас­ тую на удивление труден.

12.1 .

Динамическая памят ь и интеллектуальные указатели

Упражнения раздела

591

1 2 .1 .2

Напишите функцию, которая возвращает динамически созданный вектор целых чисел. Передайте этот вектор другой функции, ко­ торая читает значения его элементов со стандартного устройства ввода. Пе­ редайте вектор другой функции, выводящей прочитанные ранее значения. Не забудьте удалить вектор в подходящий момент.

Упражнение

1 2.6.

Переделайте предыдущее упражнение, используя на сей раз указатель s h a r e d_p t r .

Упражнение

1 2 .7.

Упражнение

1 2.8.

Объясните, все ли правильно в следующей функции:

boo l Ь ( ) { int* р = new int ; // . . . retu rn р ;

Упражнение

1 2 .9.

Объясните, что происходит в следующем коде:

int * q = new i n t ( 4 2 ) , * r = new i n t ( 1 0 0 ) ; r = q; a u t o q 2 = rna ke_s h a r e d< i n t > ( 4 2 ) , r 2 = rna ke s h a r e d < i n t > ( 1 0 0 ) ; r2 = q2 ;

12.1 .3. Использование указа тел я с опе р а то р ом new

shared_ptr

Как уже упоминалось, если не инициализировать интеллектуальный ука­ затель, он инициализируется как нулевой. Как свидетельствует табл. 1 2.3, ин­ теллектуальный указатель можно также инициализировать указателем, воз­ вращенным оператором n e w: sha r e d_p t r < do u Ы e > p l ; / / sh a red_p t r може т ука зыв а ть на dо иЫ е sha r e d_p t r < i n t > p 2 ( n ew i n t ( 4 2 ) ) ; 1 1 р2 ука зыв а е т на i n t со зна чением 4 2

Конструкторы интеллектуального указателя, получающие указатели, яв­ ляются явными (см. раздел 7.5.4, стр. 383) . Следовательно, нельзя неявно пре­ образовать встроенный указатель в интеллектуальный; для инициализации интеллектуального указателя придется использовать прямую форму инициа­ лизации (см. раздел 3.2. 1 , стр . 1 26) : s h a r e d_p t r < i n t > p l = n e w i n t ( 1 0 2 4 ) ; sha r e d_p t r < i n t > p 2 ( n ew i n t ( 1 0 2 4 ) ) ;

/ / ошибка : нужна прямая инициализ а ция / / ok : исполь зуе т прямую инициализ а цию

Глава 1 2. Динамичская пам ять

592

Таблиц а 1 2.З. Др уг ие с по со б ы опре д еления и и зменен ия указателя shared_ptr s hared_pt r p ( q )

sha red_pt r p ( u )

Указатель р управляет объектом, на который указывает указатель встроеююго типа q; указатель q должен указы­ вать на область памяти, зарезервироваtmуЮ оператором new, а его тип должен быть преобразуем в тип Т * ·

"

Указатель р учитывает собсгвеЮiость указателя u типа u n i que_p t r; указатель u становится нулевым

shared_pt r p ( q , d )

Указатель р учитывает собсгвеIШость объекта, на который указывает встроенный указатель q. Тип указателя q дол­ жен бьпъ преобразуем в тип Т * (см. раздел 4.1 1 .2, стр. 221 ). Для освобождения q указатель р будет использовать вызы­ ваемый объект d (см. раздел 1 0.3.2, стр. 497) вместо опера­ тора de l e te

shared_pt r р ( р2 , d )

Указатель р это копия указателя р 2 типа s ha red_ptr, как описано в табл. 1 2.2, за исключением того, что указа­ тель р использует вызываемый объект d вместо оператора -

de l e t e р . re set ( )

p . re s e t ( q ) p . re s e t ( q , d )

Если р единсгвеIШый указатель shared_pt r на объект, функция re set ( ) освободит сущесгвующий объект р. Ес­ ли передан необязательный всrроенный указатель q, то р будет указывать на q, в противном случае р станет нуле­ вым. Если предоставлен вызьmаемый объект d, то он будет вызван для освобождения указателя q, в противном елучае используется оператор de lete

Инициализация указателя pl неявно требует, чтобы компилятор создал указатель типа s h a re d_pt r из указателя i n t * , возвращенного оператором new. Поскольку нельзя неявно преобразовать обычный указатель в интеллек­ туальный, такая инициализация ошибочна. По той же причине функция, воз­ вращающая указатель s h a r e d_p t r, не может неявно преобразовать простой указатель в своем операторе r e tu r n : s h a r e d_p t r < i n t > c l on e ( i n t р ) { r e t u r n new i n t ( p ) ; / / ошибка : неявное пре о бра з о в а ние 1 1 в s h a red_p t r

Следует явно связать указатель s h a r e d _p t r с указателем, который пр ед­ стоит возвратить: s h a r e d_p t r < i n t > c l o ne ( i n t р ) { 1 1 ok : яв ное созда ние s h a re d_p t r из i n t * r e t u r n s h a r e d_p t r < i n t > ( n ew i n t ( p ) ) ;

12.1 . Динамическая пам ят ь и интеллектуальные указатели

593

По умолчанию указатель, используемый для инициализации интеллекту­ ального указателя, должен указывать на область динамической памяти, по­ скольку по умолчанию интеллектуальные указатели используют оператор de l e t e для освобождения связанного с ним объекта . Интеллектуальные ука­ затели можно связать с указателями на другие виды ресурсов. Но для этого необходимо предоставить собственную функцию, используемую вместо опе­ ратора de l e t e . Предоставление собственного кода удаления рассматривается в разделе 1 2. 1 .4 (стр. 597) .



Н е смешива й те о б ычны е указатели с интеллектуальными

Указатель s h a red_p t r может координировать удаление только с другими указателями s h a r e d_p t r, которые являются его копиями. Действительно, этот факт - одна из причин, по которой рекомендуется использовать функ­ цию ma ke _ s h a r e d ( ) , а не оператор ne w. Это связывает указатель sha red_p t r с объектом одновременно с его резервированием . При этом нет никакого спо­ соба по неосторожности связать ту же область памяти с несколькими незави­ симо созданными указателями s ha re d_p t r . Рассмотрим следующую функцию, работающую с указателем s ha re d_pt r : / / p t r созда е т ся и инициализируе т ся при выз о в е pro ces s ( ) vo i d p r o c e s s ( s h a r e d _p t r < i n t > p t r ) { 1 1 исполь з ов а ние p t r 1 1 p t r выходит из о бла с ти видимо с ти и удаляе тся

Параметр функции p ro c e s s ( ) передается по значению, поэтому аргумент копируется в параметр p t r . Копирование указателя s h a r e d_p t r осуществля­ ет инкремент его счетчика ссылок. Таким образом, в функции p r o c e s s ( ) значение счетчика не меньше 2. По завершении функции p r o c e s s ( ) осуще­ ствляется декремент счетчика ссылок указателя pt r, но он не может достиг­ нуть нуля. Поэтому, когда локальная переменная pt r удаляется, память, на которую она указывает, не освобождается. Правильный способ использования этой функции подразумевает передачу ей указателя s ha re d_p t r : sha red_pt r < i n t > p ( n e w i n t ( 4 2 ) ) ; / / с ч е т чик с сылок proce s s ( p ) ; / / копиров а ние р ув еличив а е т с че т чик ; 1 1 в функции pro c e s s ( ) с че т чик = 2 int i = * р ; / / ok : с че т чик ссылок = 1

1

Хотя функции p r o c e s s ( ) нельзя передать встроенный указатель, ей мож­ но передать временный указатель s h a re d_pt r, явно созданный из встроенно­ го указателя. Но это, вероятно, будет ошибкой:

594

Глава 1 2. Динамичская пам ят ь

i n t * x ( n ew i n t ( 1 0 2 4 ) ) ;

/ / опа сно : х о бычный ука з а тель , а / / не интеллектуаль ный p r o c e s s ( x ) ; / / ошибка : нель зя пре о бра з ов а ть i n t * в s h a re d_p t r p r o c e s s ( s h a r e d_p t r < i n t > ( x ) ) ; / / допус тимо , н о память буде т о св о бождена ! i n t j = * х ; / / непредска з уемо : х п о терянный ука з а тель ! -

-

В этом вызове функции p ro ce s s ( ) передан временный указатель s ha red_ ptr. Эгот временный указатель удаляется, когда завершается выражение, в ко­ тором присутствует вызов. Удаление временного объекта приводит к декремен­ ту счетчика ссылок, доводя его до нуля. Память, на которую указывает времен­ ный указатель, освобождается при удалении временного указателя. Но указатель х продолжает указывать на эту (освобожденную) область па­ мяти; теперь х потерянный указатель. Резу льтат попытки использования значения, на которое указывает указатель х, непредсказуем. При связывании указателя s h a r e d_p t r с простым указателем ответствен­ ность за эту память передается указателю s h a r e d_pt r . Как только ответст­ венность за область памяти встроенного указателя передается указателю s h a r e d_p t r, больше нельзя использовать встроенный указатель для доступа к памяти, на которую теперь указывает указатель s ha re d_pt r . -

& ВНИМАНИЕ

Опасно использовать встроенный указатель для доступа к объекту, принадлежащему интеллектуальному указателю, поскольку нель­ зя быть уверенным в том, что этот объект еще не удален.

Другие опера ц ии с указателем shared_ptr Класс s h a r e d_p t r предоставляет также несколько других операций, пере­ численных в табл. 1 2.2 (стр. 576) и табл. 1 2.3 (стр. 592) . Чтобы присвоить новый указатель указателю s h a r e d_p t r, можно использовать функцию re s e t ( ) : р = new i n t ( 1 0 2 4 ) ;

1 1 нель зя присв оить обычный ука з а тель 1 1 ука з а телю sh a re d_p t r p . r e s e t ( n ew i n t ( l 0 2 4 ) ) ; / / ok : р ука зыв а е т на новый о бъ ект

Подобно оператору присвоения, функция re s e t ( ) модифицирует счетчи­ ки ссылок, а если нужно, удаляет объект, на который указывает указатель р. Функцию-член re s e t ( ) зачастую используют вместе с функцией un i qu e ( ) для контроля совместного использования объекта несколькими указателями s h a r ed_p t r . Прежде чем изменять базовый объект, проверяем, является ли владелец единственным. В противном ел учае перед изменением создается но­ вая копия: if

( ! p . u n i que ( ) ) p . r e s e t ( n ew s t r i n g ( * p ) ) ; / / владелец не один ; рез ервируем новую копию * р += newVa l ; / / теперь , когда изв е с тно , что ука з а тель единств енный , 1 1 можно изменить объе кт

1 2 .1 .

Динамическая па.м ять и интеллектуальные указатели

595

Упражнения раздела 1 2. 1 .3 Упражнение 1 2. 1 0. Укажите, правилен ли следующий вызов функции proce s s ( ) , определенной на стр. 593. В противном случае укажите, как его исправить? sha red_p t r < i n t > p ( n e w i n t ( 4 2 ) ) ; p r o c e s s ( s h a r e d_p t r < i n t > ( p ) ) ;

Упражнение

1 2. 1 1 .

Что будет, если вызвать функцию p r o c e s s ( ) следую­

щим образом? proc e s s ( s h a r e d_p t r < i n t > ( p . ge t ( ) ) ) ;

Используя объявления указателей р и s p, объясните ка­ ждый из следующих вызовов функции p r o c e s s ( ) . Если вызов корректен, объясните, что он делает. Если вызов некорректен, объясните почему:

Упражнение

1 2.1 2.

auto р = n e w i n t ( ) ; auto s p = ma ke _ s h a r e d< i n t > ( ) ; ( а ) proce s s ( sp ) ; ( Ь ) proces s ( new int ( ) ) ; ( с ) proces s (р ) ; ( d ) p r o c e s s ( s h a re d_p t r < i n t > ( p ) ) ;

Упражнение

1 2. 1 3 .

Что будет при выполнении следующего кода?

auto s p = ma k e_s h a r e d< i n t > ( ) ; auto р = s p . ge t ( ) ; de l e t e р ;



1 2.1 .4. Интеллект уальные указатели и исключения

В разделе 5 .6.2 (стр. 261 ) упоминалось, что программы, использующие об­ работку исключений для продолжения работы после того, как произошло ис­ ключение, нуждаются в способе правильного освобождения ресурсов в елучае исключения. Самый простой из них подразумевает использование интеллек­ туальных указателей. При использовании интеллектуального указателя его класс гарантирует освобождение памяти, когда в ней больше нет необходимости, даже при преждевременном выходе из блока: vo id f ( ) { s h a r e d_p t r < i n t > s p ( n e w i n t ( 4 2 ) ) ; / / з арезервирова ть новый объект 1 1 код , передающий исключение , не обра ба тыв а емо е в функции f ( ) , 1 1 s h a red_p t r о св обожда е т память а в тома тиче ски по з а в ершении функции

При выходе из функции, обычном или в связи с исключением, удаляются все ее локальные объекты. В данном случае указатель s p имеет тип shared_p t r, поэтому при удалении проверяется его счетчик ссылок. В дан-

Глава 1 2. Динамичская пам я ть

596

нам случае sp - единственный указатель на контролируемую им область па­ мяти, поэтому она освобождается в ходе удаления указателя sp. Память, контролируемая непосредственно, напротив, не освобождается ав­ томатически, когда происходит исключение. Если для управления памятью используются встроенные указатели и исключение происходит после опера­ тора new, но перед оператором de l e te, то контролируемая память не будет освобождена: vo i d f ( ) { i n t * i p = n e w i n t ( 4 2 ) ; / / динамиче ски зар е з ервиров а ть новый о бъ е кт / / код , передающий исключение , не о бра ба тыв а емое в функции f ( ) , de l e t e i p ; / / о св обожда е т память перед выходом

Если исключение происходит между операторами n e w и de l e t e и не обра­ батывается в функции f ( ) , то освободить эту память никак не получится. Вне функции f ( ) нет указателя на эту память, поэтому нет никакого способа ос­ вободить ее.



И нте ллек туа ль ные указатели и класс ы б ез деструкторов

Большинство классов языка С++, включая все библиотечные классы, оп­ ределяют деструкторы (см . раздел 1 2. 1 . 1 , стр . 578), заботящиеся об удале­ нии используемых объектом ресурсов. Но не все классы таковы . В частно­ сти, классы, разработанные для использования и в языке С, и в языке С++, обычно требуют от пользователя явного освобождения всех используемых ресурсов . Классы, которые резервируют ресурсы, но не определяют деструкторы для их освобождения, подвержены тем же ошибкам, которые возникают при самостоятельном использовании динамической памяти . Довольно про­ сто забыть освободить ресурс . Аналогично, если произойдет исключение после резервирования ресурса, но до его освобождения, программа поте­ ряет его . Для управления классами без деструкторов зачастую можно использовать те же подходы, что и для управления динамической памятью. Предположим, например, что используется сетевая библиотека, применимая как в языке С, так и в С++ . Используюrцая эту библиотеку программа могла бы содержать такой код: s t r u c t de s t i n a t i o n ; / / пр едста вляе т то , с чем уста новлено со единение / / инф орма ция для исполь з ов а ния соединения s t ruct connection ; c o n ne c t i o n c o n n e c t ( de s t i n a t i o n * ) ; 1 1 о ткрыв а е т со единение vo i d di s c o n n e c t ( c o n n e c t i o n ) ; 1 1 з а крыв а е т да нное со единение vo i d f ( d e s t i n a t i o n & d / * другие параме тры * / )

12.1 . Динамическая па.м ять и интеллектуальные указатели

597

1 1 получить соединение ; не за быть з а крыв а ть по з а в ершении conne c t i on с = connect ( & d ) ; 1 1 исполь з о в а ть со единение 1 1 если за быть вызыв а ть функцию di s c o n n e c t ( ) перед выходом из 1 1 функции f ( ) , то уже не будет ника кого спос оба з а крыть соединение с

Если бы у структуры c o n n e c t i on был деструктор, то по завершении функции f ( ) он закрыл бы соединение автоматически. Однако у нее нет дест­ руктора. Эта проблема почти идентична проблеме предыдущей программы, использовавшей указатель s h a r e d_p t r, чтобы избежать утечек памяти. Здесь также можно использовать указатель s h a r ed_p t r для гарантии правильности закрытия соединения.

IJs

И спользование со б ственного кода уд аления

По умолчанию указатели s ha red_p t r подразумевали, что они указывают на динамическую память. Следовательно, когда указатель s h a red_pt r удаляется, он по умолчанию выполняет оператор de l e t e для содержащегося в нем указа­ теля. Чтобы использовать указатель s ha red_pt r для управления соединением conne ct i on, следует сначала определить функцию, используемую вместо опе­ ратора de l e t e . Должна быть возможность вызова этой функции удаления (dele­ ter) с указателем, хранимым в указателе s h a r e d_pt r . В данном случае функция удаления должна получать один аргумент типа conn e c t i on * : vo id e n d _ c o n n e c t i o n ( c o n n e c t i o n * р )

{ dis connect ( * p ) ;

}

При создании указателя s h a r e d_p t r можно передать необязательный ар­ гумент, указывающий на функцию удаления (см. раздел 6.7, стр. 322) : vo id f ( de s t i n a t i o n & d / * другие параме тры * / ) { conn e c t i o n с = connect ( & d ) ; s h a r e d_p t r < c o n n e c t i o n > р ( & с , e n d c o n n e c t i o n ) ; 1 1 исполь з ов а ть со единение 1 1 при выходе из функции f ( ) , даже в случа е исключения , со единение 1 1 буде т за крыто пра виль но

При удалении указателя р для хранимого в нем указателя вместо операто­ ра de l e te будет вызвана функция end_ conne ct i on ( ) . Функция end conne ct i on ( ) , в свою очередь, вызовет функцию di s connect ( ) , гарантируя та­ ким образом закрытие соединения. При нормальном выходе из функции f ( ) указатель р будет удален в ходе процедуры выхода. Кроме того, указатель р будет также удален, а соединение закрыто, если произойдет исключение.

Глава 12. Динамичская памят ь

598

В НИМАНИЕ ! ПРОБЛ Е МЫ ИНТЕЛЛЕКТУАЛЬНОГО УКАЗАТЕЛЯ

Интеллектуальные указатели могут обеспечить безопасность и удобство работы с динамически созданной памятью только при правильном исполь­ зовании . Для этого следует придерживаться ряда соглашений. •

Не используйте значение того же встроенного указателя для инициали­ зации (переустановки) нескольких интеллектуальных указателей.



Не используйте оператор de l e t e для указателя, возвращенного функ­ цией ge t ( ) .



Не используйте функцию ge t ( ) для инициализации или переустановки другого интеллектуального указателя.



Используя указатель, возвращенный функцией ge t ( ) , помните, что ука­ затель станет недопустимым после удаления последнего соответствую­ щего интеллектуального указателя.



Если интеллектуальный указатель используется для управления ресур­ сом, отличным от области динамической памяти, зарезервированной оператором new, не забывайте использовать функцию удаления (раз­ дел 1 2.1 .4, стр. 597 и раздел 1 2. 1 .5, стр. 601 ).

Упражнения раздела

1 2 .1 .4

Напишите собственную версию функции, использую­ щую указатель s h a red_p t r для управления соединением.

Упражнение

1 2. 1 4.

Перепишите первое упражнение так, чтобы использо­ вать лямбда-выражение (см. раздел 1 0.3.2, стр . 497) вместо функции end

Упражнение

1 2.1 5.

con n e c t i o n ( ) .

12.1 .5. Класс uni que_p tr



Указатель u n i que_p t r "владеет" объектом, на который он указывает. В отличие от указателя s h a re d_pt r, только один указатель un i que_ptr может одновременно указывать на данный объект. Объект, на который указывает указатель u n i que _pt r, удаляется при удалении указателя. Список функций, специфических для указателя un i que _pt r, приведен в табл. 1 2.4. Функции, общие для обоих указателей, приведены в табл. 1 2. 1 (стр. 575).

В отличие от указателя s h a r e d_p t r, нет никакой библиотечной функции, подобной функции ma ke _ s h a re d ( ) , которая возвращала бы указатель un i que_p t r . Вместо этого определяемый указатель un i que_p t r связывается

12.1 .

Динамическая память и интеллектуальные указатели

599

с указателем,

возвращенным оператором new. Подобно указателю shared_p t r, можно использовать прямую форму инициализации:

u n i que _p t r < douЫ e > p l ; / / ука з а тель u n i qu e_p t r на тип dо иЫ е u n i que_p t r < i n t > p 2 ( n e w i n t ( 4 2 ) ) ; / / р2 ука зыв а е т на i n t со з на чением 4 2

Таблица 12.4 . Функции указателя unique_ptr (см. такж е таб л . 12.1 на ст р . 575) un i que_p t r < T >

ul

un i que_pt r < T ,

D>

u2

Обнуляет указатель u n i que _p t r, способный указывать на объект типа т. Указатель u l использует для освобож­ дения своего указателя оператор de l e t e; а указатель u2 вызываемый объект типа D -

unique_pt r < T ,

и =

и.

D>

nu l lp t r

re l e a s e

()

re s e t ( ) u . re s e t ( q ) u . re s e t ( n u l lpt r )

и.

u (d)

Обнуляет указатель u n i que _p t r, указывающий на объ­ екты типа т. Использует вызываемый объект d типа D вместо оператора de l e t e Удаляет объект, на который указывает указатель u; об­ нуляет указатель u Прекращает контроль содержимого указателя u ; воз­ вращает содержимое указателя u и обнуляет его Удаляет объект, на который указывает указатель u. Если предоставляется встроенный указатель q, то u будет ука­ зывать на его объект. В противном случае указатель u обнуляется

Поскольку указатель un i qu e _p t r владеет объектом, на который указывает, он не померживает обычного копирования и присвоения: u n i que_p t r < s t r i n g > u n i qu e_p t r < s t r i n g > u n i que _p t r < s t r i n g > рЗ = р 2 ; / / ошибка

p l ( n ew s t r i n g ( " S t e g o s a u r u s " ) ) ; p 2 ( p l ) ; / / ошибка : нев озможно копиров а ние u n i que_ p t r рЗ ; : нев озможно присв оение u n i que_p t r

Хотя указатель u n i que_p t r нельзя ни присвоить, ни скопировать, можно -передать собственность от одного (неконстантного) указателя u n i qu e_p t r другому, вызвав функцию re l e a s e ( ) или r e s e t ( ) : 1 1 переда е т с о б с тв енно с ть о т p l (ука зыв ающего на / / строку "S t ego s a u r u s ") к р 2 u n i que_p t r < s t r i n g > p 2 ( p l . r e l e a s e ( ) ) ; / / re l ea s e ( ) обнуляе т p l u n i que_p t r < s t r i n g > p З ( n ew s t r i n g ( " T r e x " ) ) ; 1 1 переда е т собств енно сть о т рЗ к р2 p2 . re s e t ( p 3 . re l e a s e ( ) ) ; / / res e t ( ) о св о божда е т память , на которую 1 1 ука зыв ал ука з а тель р2

Функция-член re l e a s e ( ) возвращает указатель, хранимый в настоящее время в указателе un i qu e_p t r, и обнуляет указатель un i que_p t r. Таким об-

600

Глава 1 2. Динамичская пам ять

разом, указатель р2 инициализируется указателем, хранимым в указателе pl, а сам указатель pl становится нулевым . Функция-член re s e t ( ) получает необязательный указатель и переуста­ навливает указатель un i que_p t r на заданный указатель. Если указатель u n i que_p t r не нулевой, то объект, на который он указывает, удаляется. По­ этому вызов функции re s e t ( ) указателя р2 освобождает память, используе­ мую строкой со значением " S t e go s au ru s " , передает содержимое указателя рЗ указателю р2 и обнуляет указатель р З . Вызов функции re l e a s e ( ) нарушает связь между указателем u n i que_ptr и объектом, который он контролирует. Зачастую указатель, возвращенный функцией re l e a s e ( ) , используется для инициализации или присвоения дру­ гому интеллектуальному указателю. В этом случае ответственность за управ­ ление памятью просто передается от одного интеллектуального указателя другому. Но если другой интеллектуальный указатель не используется для хранения указателя, возвращенного функцией re l e a s e ( ) , то ответственность за освобождения этого ресурса берет на себя программа: p 2 . re l e a s e ( ) ; auto р

=

/ / ОШИБКА : р2 не о с в о б одит память , и ука за тель 1 1 буде т потерян p 2 . re l e a s e ( ) ; 1 1 ok , но следуе т не за быть de l e t e (p )

П ередача и возвращение указателя unique_ptr Из правила, запрещающего копирование указателя u n i que_p t r, есть одно исключение: можно копировать и присваивать те указатели u n i que_p t r, ко­ торые предстоит удалить. Наиболее распространенный пример - возвраще­ ние указателя u n i que _p t r из функции: u n i qu e_p t r < i n t > c l o ne ( i n t р ) { / / ok : яв ное созда ние un i que_p t r для i n t * r e t u r n u n i q u e _p t r < i n t > ( n ew i n t ( p ) ) ;

В качестве альтернативы можно также возвратить копию локального объекта: u n i que_p t r< i n t > c l o n e ( i n t р ) { u n i qu e_p t r < i n t > r e t ( n ew i n t // . . . return ret ;

(р) ) ;

В обоих случаях компилятор знает, что возвращаемый объект будет сейчас удален. В таких случаях компилятор осуществляет специальный вид "копиро­ вания", обсуждаемый в разделе 1 3.6.2 (стр . 676) .

12.1 . Динамическая пам ят ь и интеллектуальные указатели

601

СОВМЕС ТИМОСТЬ С ПРЕЖНЕЙ ВЕРСИЕЙ : КЛАСС AUTO PTR

Прежние версии библиотеки включали класс a u t o_p t r, обладавший не­ которыми, но не всеми, свойствами указателя un i que_p t r . В частности, не­ возможно было хранить указатели a u t o_p t r в контейнере и возвращать их из функции. Хотя указатель a u t o_p t r все еще присутствует в стандартной библиоте­ ке, вместо него следует использовать указатель un i qu e_p t r .

П ередача ф ун кции удаления указа те лю unique_ptr Подобно указателю s h a re d_p t r, для освобождения объекта, на который указывает указатель u n i qu e_p t r, по умолчанию используется оператор de l e t e . Подобно указателю s ha r e d_pt r, функцию удаления указателя un ique_p t r (см. раздел 1 2. 1 .4, стр. 597) можно переопределить. Но по причи­ нам, описанным в разделе 1 6. 1 .6 (стр. 851 ), способ применения функции уда­ ления указателем un i qu e _pt r отличается от такового у s h a r e d_pt r. Переопределение функции удаления указателя un i qu e_pt r влияет н а тип и способ создания (или переустановки) объектов этого типа. Подобно переоп­ ределению оператора сравнения ассоциативного контейнера (см. раз­ дел 1 1 .2.2, стр. 543), тип функции удаления можно предоставить в угловых скобках наряду с типом, на который может указывать указатель un i que_pt r. При создании или переустановке объекта этого типа предоставляется вызы­ ваемый объект определенного типа: 11 р ука зыв а е т на о бъ ект типа obj T и исполь зуе т объ е кт типа de l T 1 1 для его о св обождения 11 он выз о в е т объ е кт по имени fcn типа de l T unique_pt r < o b j T , de l T > р ( n ew ob j T , f c n ) ;

В качестве несколько более конкретного примера перепишем программу соединения так, чтобы использовать указатель un i qu e_p t r вместо указателя share d_p t r следующим образом: vo id f ( de s t i n a t i o n & d / * другие не обходимые параме тры * / ) { c o n n e c t i o n с = c o n n e c t ( & d ) ; / / о ткрть со единение / / когда р буд е т удален , со единение буд е т з а крыто un i qu e _p t r < c o n n e c t i o n , de c l t ype ( e n d_c o n n e c t i o n ) * > р ( & с , e n d_c o n n e c t i o n ) ; / / исполь з ов а ть с о единение 1 1 по зав ершении f ( ) , даже при исклю чении , со единение будет 1 1 з а крыто пра вильно

Для определения типа указателя на функцию используется ключевое слово de c l type (см. раздел 2.5.З, стр. 1 1 0) . Поскольку выражение de c l t ype ( end

f лава

602

1 2. Динамичская пам ять

conne c t i on ) возвращает тип функции, следует доб авить символ * , указываю­ щий, что используется указатель на этот тип (см. раздел 6.7, стр. 326). Упражнения раздела 1 2.1 .5 Упражнение 12.1 6. Компиляторы не всегда предоставляют понятные сооб­ щения об ошиб ках, если осуществляется попытка скопировать или присво­ ить указатель un i que_p t r . Напишите программу, которая содержит эти ошибки, и посмотрите, как компилятор диагностирует их. Упражнение 1 2.1 7. Какие из следующих об ъявлений указателей un i que_p t r недопустимы или вероятнее всего приведут к ошиб ке впослед­ ствии? Об ъясните проблему каждого из них. int ix = 1 0 2 4 , * p i = & i x , *pi2 = t yp e de f u n i que_p t r < i n t > I n t P ; (Ь) ( а ) IntP рО ( ix) ; (d) ( с ) I n t P р2 ( p i 2 ) ; ( f) ( е ) I n t P p 4 ( n ew i n t ( 2 0 4 8 ) ) ;

new int ( 2 0 4 8 ) ; IntP pl (pi ) ; Int P р З ( & i x ) ; I n t P p 5 ( p2 . ge t ( ) ) ;

Упражнение 1 2.1 8. Почему класс указателя s h a r e d_p t r не имеет функции­ члена re l e a s e ( ) ?

1Р 12.1 .6. Класс weak_ptr � Класс wea k_p t r (табл. 1 2.5) представляет интеллектуальный указатель,

который не контролирует продолжительность существования об ъекта, на который он указывает. Он только указывает на объект, который кон­ тролирует указатель s h a r e d_p t r. Привязка указателя we a k_p t r к ука­ зателю s h a r e d_p t r не изменяет счетчик ссылок этого указателя s h a r e d_p t r . Как только последний указатель s h a r e d_p t r на этот объ­ ект б удет удален, удаляется и сам об ъект. Этот об ъект будет удален, да­ же если останется указатель we a k_p t r на него. Имя we a k_p t r отражает концепцию "слабого" совместного использования об ъекта.

Создаваемый указатель we a k_p t r инициализируется из указателя s h a red_

pt r: a u t o р = ma ke _ s h a r e d< i n t > ( 4 2 ) ; we a k_p t r < i n t > wp ( p ) ; 1 1 wp сла бо связ а н с р ; с че т чик ссыло к р неизменен

Здесь указатели wp и р указывают на тот же об ъект. Поскольку совместное использование слаб о, создание указателя wp не изменяет счетчик ссылок ука­ зателя р; это делает возможным удаление объекта, на который указывает ука­ затель wp.

1 2.1 .

Динамическая пам ять и интеллектуальные указатели

603

Таблица 12.5. Фу нкции указателя weak_p tr we a k_pt r < T > w

Обнуляет указатель we a k_p t r, способный указывать на объект типа т

we ak_pt r < T > w ( sp )

Указатель we a k_p t r на тот же объект, что и указатель sp типа s h a r e d_p t r . Тип т должен быть приводим к типу, на который указывает sp

w = р

Указатель р может иметь тип s h a re d_p t r или we a k p t r . После присвоения w разделяет собственность с указателем р

w . re s e t ( )

Обнуляет указатель w

w . u se count ( )

Возвращает количество указателей s h a re d_p t r, разде­ ляющих собственность с указателем w

w . e xp i re d ( )

Возвращает значение t r u e, когда функция w . u s e_c o u n t ( ) должна возвратить нуль, и значение f a l s e в противном случае

w . lock ( )

Возвращает нулевой указатель s h a re d_p t r, если функция e xp i re d ( ) должна возвратить значение t r ue; в противном случае возвращает указатель s h a r e d _p t r на объект, на ко­ торый указывает указатель w

Поскольку объект может больше не существовать, нельзя использовать указа­ тель wea k_pt r для непосредственного доступа к его объекту. Для этого следует вызвать функцию l o c k ( ) Она проверяет существование объекта, на который указывает указатель we ak_pt r. Если это так, то функция l o c k ( ) возвращает ука­ затель sha red_p t r на совместно используемый объект. Такой указатель гаранти­ рует существование объекта, на который он указывает, по крайней мере, пока существует этот указатель shared_ptr. Рассмотрим пример : .

if

( s ha r e d_p t r < i n t > np = wp . l o c k ( ) ) { / / t r u e , е сли пр не нулев ой 1 1 в i f , пр совме с тно исполь з уе т св ой объект с р

Внутренняя часть оператора i f доступна только в ел учае истинности вызо­ ва функции l o c k ( ) . В операторе i f использование указателя np для доступа к объекту вполне безопасно.

П рове р я емы й класс указателя Для того чтобы проиллюстрировать, насколько полезен указатель we a k_p t r, определим вспомогательный класс указателя для нашего класса S t rB l ob. Класс указателя, назовем его S t rB l ob P t r, будет хранить указатель we ak_p t r на пе­ ременную-член da ta класса S t rB l ob, которым он был инициализирован. Ис­ пользование указателя wea k_p t r не влияет на продолжительность существова-

604

Глава 12. Динамичская память

ния вектора, на который указывает данный объект класса S t rB l ob. Но можно воспрепятствовать попыn > c h e c k ( s t d : : s i z e_t , c o n s t s t d : : s t r i n g & ) c o n s t ; / / хранит we a k_p t r , озна ча я в о зможно с ть удаления о снов ного в е ктора s t d : : we a k _p t r < s t d : : ve c t o r < s t d : : s t r i n g > > wp t r ; s t d : : s i z e _ t c u r r ; / / те куща я позиция в предела х ма ссив а };

Стандартный конструктор создает нулевой указатель S t r B l ob P t r . Спи­ сок инициализации его конструктора (см . раздел 7 . 1 .4, стр . 345) явно ини­ циализирует переменную-член c u r r нулем и неявно инициализирует ука­ затель-член wp t r как нулевой указатель we a k_p t r . Второй конструктор получает ссылку на S t r B l o b и (необ язательно) значение индекса. Этот конструктор инициализирует wp t r как указатель на вектор данного об ъек­ та класса S t r B l o b и инициализирует переменную c u r r значением s z . Ис­ пользуем аргумент по умолчанию (см . раздел 6.5 . 1 , стр . 308) для инициали­ зации переменной c u r r, что б ы о б означить первый элемент. Как б удет про­ демонстрировано, ниже параметр s z б удет использован функцией-членом e n d ( ) класса S t r B l o b . Следует заметить, что нельзя связать указатель S t rB l ob P t r с константным об ъектом класса S t rB l ob. Это ограничение следует из того факта, что конст­ руктор получает ссылку на неконстантный объект типа S t rB l ob . Функция-член ch e c k ( ) класса S t rB l ob P t r отличается от таковой у класса S t rB l ob, поскольку она должна проверять, существует ли еще вектор, на ко­ торый он указывает: s t d : : s h a r e d_p t r < s t d : : ve c t o r < s t d : : s t r i ng > > S t r B l ob P t r : : c h e c k ( s t d : : s i z e t i , c o n s t s t d : : s t r i n g & ms g )

c on s t

12.1 . Динамическая пам ят ь и интеллектуальные указатели

605

a u t o r e t = wp t r . l o c k ( ) ; / / суще ствуе т ли еще в е ктор ? i f ( ! re t ) t h r ow s t d : : r u n t ime e r r o r ( " u nbo u n d S t r B l o b P t r " ) ; i f ( i >= ret - > s i z e ( ) ) t h r ow s t d : : o u t _o f _ r a nge ( m s g ) ; r e t u r n r e t ; / / в противном случа е , в о звра тить s h a r e d_p t r на в е ктор

Так как указатель we a k _p t r не влияет на счетчик ссылок соответствующе­ го указателя s h a r e d_p t r, вектор, на который указывает S t r B l ob P t r, может быть удален. Если вектора нет, функция l o c k ( ) возвратит нулевой указатель. В таком случае любое обращение к вектору потерпит неудачу и приведет к передаче исключения. В противном случае функция ch e c k ( ) проверит пере­ данный индекс. Если значение допустимо, функция ch e c k ( ) возвратит указа­ тель s ha r e d_p t r, полученный из функции l o c k ( ) .

Опера ц ии с указателями Определение собственных операторов рассматривается в главе 1 4, а пока определим функции de re f ( ) и i n c r ( ) для обращения к значению и инкре­ мента указателя класса S t r B l ob P t r соответственно. Функция-член de re f ( ) вызьmает функцию che c k ( ) для проверки безопасно­ сrи использования вектора и принадлежности индекса cu r r его диапазону: st d : : s t r i n g & S t r B l ob P t r : : de r e f ( ) c o n s t { auto р c he c k ( c u r r , " de r e f e r e n c e p a s t e n d " ) ; r e t u r n ( * р ) [ c u r r ] ; / / ( *р ) - в е ктор , на ко торый ука зыв а е т э то т объе кт

Если проверка прошла успешно, то р будет указателем типа s h a r e d_pt r на вектор, на который указывает данный указатель S t r B l ob P t r . Выражение ( * р ) [ cu r r ] обращается к значению данного указателя s h a r e d_pt r, чтобы получить вектор, и использует оператор индексирования для доступа и воз­ вращения элемента по индексу c u r r . Функция-член i n c r ( ) также вызывает функцию ch e c k ( ) : 1 1 пре фикс : в озвра тить с сылку на о бъ е кт после инкремента S t rB l ob P t r & S t r B l ob P t r : : i n c r ( ) { 1 1 е сли c u rr уже ука зыв а е т на элемент после конца контейнера , 1 1 его инкремент не нужен c h e c k ( c u r r , " i n c r eme n t p a s t e n d o f S t r B l ob P t r " ) ; + + c u r r ; / / инкремент текущего с о с тояния r e t u r n * th i s ;

Безусловно, чтобы получить доступ к переменной-члену da t a, наш класс указателя должен быть дружественным классу S t r B l ob (см. раздел 7.3.4,

Глава 12. Динамичская память

606

стр. 363) . Снабдим также класс S t rB l ob функциями b e g i n ( ) и e n d ( ) , воз­ вращающими указатель S t rB l ob P t r на себя: 1 1 предв аритель ное о бъявление не о бходимо для о бъявления друже с тв енным 1 1 кла с су S t rB l ob c l a s s S t rB l ob P t r ; c l a s s S t rB l ob { f r i e n d c l a s s S t rB l o b P t r ; 1 1 другие члены , ка к в ра зделе 1 2 . 1 . 1 ( с тр . 5 82 ) 1 1 в о звра тить ука з а тель S t rB l ob P t r на первый и следующий 1 1 п о сле по следнего элементы S t rB l ob P t r b e g i n ( ) { r e t u r n S t rB l ob P t r ( * t h i s ) ; S t rB l ob P t r e n d ( ) { a u t o r e t = S t rB l ob P t r ( * th i s , d a t a - > s i z e ( ) ) ; return ret ; } };

Упражнения раздела

1 2 .1 .6

Определите собственную версию класса S t rB l ob P t r и модифицируйте класс S t rB l ob соответствующим объявлением дружест­ венным, а также функциями-членами b e g i n ( ) и e n d ( ) .

Упражнение

1 2. 1 9.

Напишите программу, которая построчно читает ис­ ходный файл в операционной системе класса S t rB l ob и использует указа­ тель S t rB l ob P t r для вывода каждого его элемента.

Упражнение

1 2 .20.

Функцию-член de re f ( ) класса S t rB l ob P t r можно на­ писать следующим образом:

Упражнение

1 2.21 .

s t d : : s t r i n g & de r e f ( ) c on s t { r e t u r n ( * c he c k ( cu r r , " de re f e r e n c e p a s t e n d " ) ) [ c u r r ] ;

Какая версия по-вашему лучше и почему?

Упражнение 12.22. Какие изменения следует внести в класс S t rB l obPt r, что­ бы получигь класс, применимый с типом c on s t S t rB l ob? Определите класс по имени Con s t S t rB l ob P t r, способный указьmать на co n s t S t rB l ob.

8' 12.2. Д инамические массивы Операторы n e w и de l e t e резервируют объекты по одному. Некоторым приложениям нужен способ резервировать хранилище для многих объектов сразу. Например, векторы и строки хранят свои элементы в непрерывной па­ мяти и должны резервировать несколько элементов сразу всякий раз, когда контейнеру нужно повторное резервирование (см. раздел 9.4, стр. 457) . Для этого язык и библиотека предоставляют два способа резервирования всего массива объектов. Язык определяет второй вид оператора new, резерви­ рующего и инициализирующего массив объектов. Библиотека предоставляет

12.2.

Динамические массивы

607

шаблон класса a l l o c a t o r, позволяющий отделять резервирование от ини­ циализации. По причинам, описанным в разделе 1 2.2.2 (стр . 61 3), применение класса a l l o c a t o r обычно обеспечивает луч1пую производительность и более гибкое управление памятью. У многих (возможно, у большинства) приложений нет никакой непосред­ ственной необходимости в динамических массивах. Когда приложение нужда­ ется в переменном количестве объектов, практически всегда проще, быстрей и безопасней использовать вектор (или другой библиотечный контейнер), как было сделано в классе S t r B l ob. По причинам, описанным в разделе 1 3.6 (стр. 672), преимущества использования библиотечного контейнера даже бо­ лее явны по новому стандарту. Библиотеки, поддерживающие новый стан­ дарт, работают существенно быстрее, чем предыдущие версии. екомендуем

Большинство приложений должно использовать библиотечные контейнеры, а не динамически созданные массивы. Использовать контейнер проще, так как меньше вероятность допустить ошибку управления памятью, и, вероятно, он обеспечивает лучшую произ­ водительность.

Как уже упоминалось, использующие контейнеры классы могут использо­ вать заданные по умолчанию версии операторов копирования, присвоения и удаления (см. раздел 7 . 1 .5, стр. 34 7) . Классы, резервирующие динамические массивы, должны определить собственные версии этих операторов для управ­ ления памятью при копировании, присвоении и удалении объектов.

& ВНИМАНИЕ

Не резервируйте динамические массивы в классах, пока не прочи­ таете главу 1 3.

� 12.2.1 . Опе р ато р new и .массивы Чтобы запросить оператор n e w зарезервировать массив объектов, после имени типа следует указать в квадратных скобках количество резервируемых объектов. В данном случае оператор n e w резервирует требуемое количество объектов и (при успешном резервировании) возвращает указатель на первый из них: 1 1 выз ов int *pia

ge t_ s i ze ( ) определит количе с тв о резервируемых целых чисел n e w i n t [ g e t_s i z e ( ) ] ; / / pi a ука зыв а е т на перв о е из них

=

Значение в скобках должно иметь целочисленный тип, но не обязано быть константой. Для представления типа массива при резервировании можно также ис­ пользовать псевдоним типа (см. раздел 2.5. 1 , стр. 1 06) . В данном случае скобки не нужны :

Глава 1 2. Динамичская памят ь

608 t yp e de f i n t a r r T [ 4 2 ] ; int *р = new a r rT ;

/ / a rr T имя типа ма ссив а из 4 2 целых чисел / / резервируе т ма ссив из 4 2 целых чисел ; / / р ука зыв а е т на первый его элемент -

Здесь оператор n e w резервирует массив целых чисел и возвращает указа­ тель на его первый элемент. Даже при том, что никаких скоб ок в коде нет, компилятор выполняет это выражение, используя оператор n e w [ ] . Таким об­ разом, компилятор выполняет это выражение, как будто код б ыл написан так: int * р = . new i nt [ 4 2 ] ;

Резе р ви р ование массива возвр ащает указатель на тип э лемента Хотя об ычно память, зарезервированную оператором n e w т [ ] , называют "динамическим массивом", это несколько вводит в заб луждение. Когда мы ис­ пользуем оператор n e w для резервирования массива, об ъект типа массива по­ лучен не б удет. Вместо этого б удет получен указатель на тип элемента масси­ ва. Даже если для определения типа массива использовать псевдоним типа, оператор n e w не резервирует об ъект типа массива. И в данном случае резер­ вируется массив, хотя часть [ число ] не видима. Даже в этом ел учае оператор new возвращает указатель на тип элемента.



Поскольку зарезервированная память не имеет типа массива, для дина­ мического массива нельзя вызвать функцию be g i n ( ) или end ( ) (см. раз­ дел 3.5.3, стр. 1 68). Для возвращения указателей на первый и следующий после последнего элементы эти функции используют размерность масси­ ва (являющуюся частью типа массива) . По тем же причинам для обработ­ ки элементов так называемого динамического массива нельзя также ис­ пользовать серийный оператор f о r .

&

ВНИМАНИЕ

Важно помнить, что у так называемого динамического массива нет типа массива.

И ни циализац ия массива динамичес ки с оз данных о б ъ ектов Зарезервированные оператором n e w об ъекты (б удь то одиночные или их массивы) инициализируются по умолчанию. Для инициализации элементов массива по умолчанию (см. раздел 3.3. 1 , стр. 1 43) за размером следует распо­ ложить пару круглых ско б ок: int * p i a = new in t [ l O ] ;

// // int *pia2 = new int [ l O ] ( ) ; / / // s t r ing * p s a = new s t r i ng [ l O ] ; s t r i n g * p s a 2 = new s t r i n g [ l O ]

блок из де сяти неинициализиров анных целых чисел блок из де сяти целых чисел , инициализиров а нных по умолча нию зна чением О / / бло к из де сяти пустых строк ( ) ; / / бло к из де сяти пус тых с трок

1 2 .2 .



Динамические массивы

609

По новому стандарту можно также предоставить в скобках список ини­ циализаторов элементов:

/ / бло к из де сяти целых чисел , инициализиров а нных с о о тв е т ствующим 1 1 инициализа тором int * р i а З = n e w i n t [ 1 0 ] { 0 , 1 , 2 , 3 , 4 , S , 6 , 7 , 8 , 9 } ; 1 1 бло к из де сяти с тр о к ; первые че тыре инициализируются зада нными 1 1 инициализа торами , о сталь ные элементы инициализируются зна чением ! / по умолча нию str ing * р s а З = new s t r i ng [ l O ] { " а " , " an " , " the " , s t r i ng ( З , ' х ' ) } ;

При списочной инициализации объекта типа встроенного массива (см. раз­ дел 3.5.1, crp. 1 62) инициализаторы используются для инициализации первых элементов массива. Если инициализаторов меньше, чем элементов, остальные инициализируются значением по умолчанию . Если инициализаторов больше, чем элементов, оператор new потерпит неудачу, не зарезервировав ничего. В данном случае оператор new передает исключение типа bad_a r ray_new_ length. Подобно исключению bad_a l l o c, этот тип определен в заголовке new.



Хотя для инициализации элементов массива по умолчанию можно использовать пусть1е круглые скобки, в них нельзя предоставить инициали­ заторы для элементов. Благодаря этому факту при резервировании мас­ сива нельзя использовать ключевое слово a u t o (см. раздел 1 2.1 .2, стр. 585).

Динамичес кое р езе р ви р ование пустого масс ива вполне д опустимо Для определения количества резервируемых объектов можно использовать произвольное выражение: s i z e t n = ge t s i z e ( ) ;

/ / ge t_ s i z e ( ) в озвраща е т количе ств о не обходимых 1 1 элементов int * р new in t [ n ] ; / / резервируе т ма ссив для содержа ния элементов for ( i nt * q = р ; q ! = р + n ; + + q ) / * о бра б о тка ма ссив а * / ;

Возникает интересный вопрос: что будет, если функция ge t_s i z e ( ) воз­ вратит значение О? Этот код сработает прекрасно. Вызов функции new [ n J при n равном О вполне допустим, даже при том, что нельзя создать переменную типа массива размером О: ch a r a r r [ O ] ; / / ошибка : нель зя определить ма с сив нулев ой длины ch a r * ер = n e w c h a r [ O ] ; / / o k : но о бращение к зна чению ер нев озможно

При использовании оператора n e w для резервирования массива нулевого размера он возвращает допустимый, а не ну левой указатель. Этот указатель гарантированно будет отличен от любого другого указателя, возвращенного оператором n e w. Он будет подобен указателю на элемент после конца (см. раздел 3.5.3, стр. 1 69) для нулевого элемента массива. Этот указатель мож-

Глава 1 2. Динамичская пам ят ь

61 0

но использовать теми способами, которыми используется итератор после кон­ ца. Его можно сравнивать в цикле, как выше. К нему можно добавить нуль (или вычесть нуль), такой указатель можно вычесть из себя, получив в резуль­ тате ну ль. К значению такого указателя нельзя обратиться, в конце концов, он не указывает на элемент. В гипотетическом цикле, если функция ge t_s i z e ( ) возвращает О, то n также равно О. Вызов оператора n e w зарезервирует нуль объектов. Условие оператора f o r будет ложно (р равно q + n, поскольку n равно О) . Таким обра­ зом, тело цикла не выполняется.

О сво б ождение динамических массивов Для освобождения динамического массива используется специальная фор­ ма оператора de l e te, имею1цая пустую пару квадратных скобок: de l e te р ; de l e t e

[]

ра ;

// 11 // 11

р должен ука зыв а ть на динамиче ски созда нный о бъ ект или быть нулевЫlv1 ра должен ука зыв а ть на динамиче с ки созда нный о бъ ект или быть нулев Ыlv1

Второй оператор удаляет элементы массива, на который указывает ра, и освобождает соответствующую память. Элементы массива удаляются в обрат­ ном порядке. Таким образом, последний элемент удаляется первым, затем предпоследний и т.д. При применении оператора de l e t e к указателю на массив пустая пара квадратных скобок необходима : она указывает компилятору, что указатель со­ держит адрес первого элемента массива объектов. Если пропустить скобки опе­ ратора de l e t e для указателя на массив (или предоставить их, передав операто­ ру de l e t e указатель на объект), то его поведение будет непредсказуемо. Напомним, что при использовании псевдонима типа, определяющего тип массива, можно зарезервировать массив без использования [ ] в операторе new. Но даже в этом случае нужно использовать скобки при удалении указа­ теля на этот массив: typ e de f i n t a r r T [ 4 2 ] ; i n t * р = n e w a r rT ; de l e t e

[]

р;

// // // // //

a r r T имя типа ма ссив из 4 2 целых чисел резервируе т ма с сив из 4 2 целых чисел ; р ука зыв а е т на первый элемент ско бки необходиlvfЫ , посколь ку был зарезервиров ан ма с сив

Несмотря на внешний вид, указатель р указывает на первый элемент мас­ сива объектов, а не на отдельный объект типа a r r T . Таким об разом, при уда­ лении указателя р следует использовать [ ] .

& ВНИМАНИЕ

Компилятор вряд ли предупредит нас, если забыть скобки при удалении указателя на массив или использовать их при удалении указателя на объект. Программа будет выполняться с ошибкой, не предупреждая о ее причине.

61 1

12.2. Динамические массивы

И н теллектуальные указатели и ди н амиче ские масс ив ы Библиотека предоставляет версию указателя u n i qu e_p t r, способную кон­ тролировать массивы, зарезервированные оператором new. Чтобы использо­ вать указатель u n i qu e _p t r для управления динамическим массивом, после типа объекта следует расположить пару пустых скобок: 1 1 ир ука зыв а е т на ма ссив из де сяти неинициализирова нных целых чисел u n i que_p t r < i n t [ ] > up ( n e w i n t [ l O ] ) ; up . re l e a s e ( ) ; / / а в тома тиче ски исполь зуе т опера тор de l e t e [ ] для / / удаления ука з а теля

Скобки в спецификаторе типа ( < i n t [ ] >) указывают, что указатель up ука­ зывает не на тип i n t, а на массив целых чисел. Поскольку указатель up указы­ вает на массив, при удалении его указателя автоматически используется опе­ ратор de l e t e [ ] . Указатели u n q i u e _p t r на массивы предоставляют несколько иные функ­ ции, чем те, которые использовались в разделе 1 2.1 .5 (стр. 598). Эти функции описаны в табл. 1 2.6. Когда указатель u n i qu e _p t r указывает на массив, нельзя использовать точечный и стрелочный операторы доступа к элементам. В кон­ це концов, указатель u n q i u e _p t r указывает на массив, а не на объект, поэто­ му эти операторы были бы бессмысленны . С другой стороны, когда указатель unqi u e _p t r указывает на массив, для доступа к его элементам можно исполь­ зовать оператор индексирования: for

( s i z e_t i = О ; i ! = 1 0 ; + + i ) up [ i ] = i ; / / присв оить нов о е зна чение каждому из элементов

Таблица 1 2.6. Фун кции указателя unique_J>tr на масси в Операторы доступа к э лементам (точка и стрелка ) не поддер ж иваются указателями unique_ptr на массивы. Другие его функции неизменны u n i que_p t r < T [ ] > u

u может указывать на динамически созданный массив

типа т u n i qu e _ p t r < T [ ] > u ( p )

u указывает на динамически созданный массив, на ко­

торый указывает встроенный указатель р . Тип указателя р должен допускать приведение к типу Т * (см. раздел 4. 1 1 .2, стр. 221 ) . Выражение u [ i ] возвратит объект в позиции i массива, которым владеет указатель u . u должен бытъ у казателем на массив

В отличие от указателя u n i qu e _p t r, указатель s h a r e d_p t r не оказывает прямой помержки управлению динамическим массивом. Если необходимо использовать указатель s h a r e d_p t r для управления динамическим массивом, следует предоставить собственную функцию удаления:

Глава 1 2. Динамичская память

61 2

/ / чтобы исполь зов а ть ука за тель s h a red_p t r , нужно предо ста вить 1 1 функцию удаления s h а r е d _р t r < i n t > s р ( n е w i n t [ 1 О ] , [ ] ( i n t * р ) { d е 1 е t е [ ] р ; } ) ; sp . r e s e t ( ) ; / / исполь зуе т пред о с та вленное лямбда -выражение , ко торое в / / св ою о чередь исполь зуе т опера тор de l e t e [ ] для о св о бождения ма ссив а

Здесь лямбда-выражение (см. раздел 1 0.3.2, стр . 497), использующее опера­ тор de l e t e [ ] , передается как функция удаления. Если не предоставить функции удаления, результат выполнения этого кода непредсказуем. По умолчанию указатель s h a r e d_p t r использует оператор de l e t e для удаления объекта, на который он указывает. Если объект является динамическим массивом, то при использовании оператора de l e t e возникнут те же проблемы, что и при пропуске [ ] , когда удаляется указатель на дина­ мический массив (см. раздел 1 2.2. 1 , стр. 61 1 ) . Поскольку указатель s h a r e d_p t r не поддерживает прямой доступ к мас­ сиву, для обращения к его элементам применяется следующий код: / / s h a red_p t rs не име е т опера тора индексиров а ния и не поддержив а е т / / арифме тиче ских действий с ука з а телями for ( s i z e t i = О ; i ! = 1 0 ; ++i ) * ( s p . ge t ( ) + i ) = i ; / / для дос тупа к в стр о енному ука з а телю / / исполь зуе т ся функция ge t ( )

Указатель s h a r e d_p t r не имеет оператора индексирования, а типы интел­ лектуальных указателей не поддерживают арифметических действий с указа­ телями. В результате для доступа к элементам массива следует использовать функцию ge t ( ) , возвращающую встроенный указатель, который можно за­ тем использовать обычным способом.

Упражнения раздела

1 2.2.1

Напишите программу, конкатенирующую два строко­ вых литерала и помещающую результат в динамически созданный массив символов. Напишите программу, конкатенирующую две строки библио­ течного типа s t r i n g, имеющих те же значения, что и строковые литералы, используемые в первой программе.

Упражнение

12.23.

Напишите программу, которая читает строку со стан­ дартного устройства ввода в динамически созданный символьный массив. Объясните, как программа обеспечивает ввод данных переменного размера. Проверьте свою программу, введя строку, размер которой превышает дли­ ну зарезервированного массива.

Упражнение

1 2.24.

С учетом следующего оператора n e w, как будет уда­ ляться указатель ра?

Упражнение

1 2.25.

int *ра = new i n t [ l O ] ;

1 2.2.

Динамические массивы

12.2.2. Класс

61 3

al l oca tor

Важный аспект, ограничивающий гибкость оператора new, заключается в том, что он объединяет резервирование памяти с созданием объекта (объек­ тов) в этой памяти. Точно так же оператор de l e t e объединяет удаление объекта с освобождением занимаемой им памяти. Обычно объединение инициализации с резервированием - это именно то, что и нужно при резервировании одиноч­ ного объекта. В этом случае почти наверняка известно значение, которое дол­ жен иметь об Ъект. Когда резервируется блок памяти, обычно в нем планируется создавать объекты по мере необходимости. В таком случае желательно было бы отделить резервирование памяти от создания объектов. Это позволит резервировать память в больших объемах, а дополнительные затраты на создание объектов нести только тогда, когда это фактически необходимо. Зачастую объединение резервирования и создания оказывается расточи­ тельным. Например: s t r i ng * c o n s t р = n e w s t r i ng [ n ] ; / / с о зда е т п пус тых с трок s t r i ng s ; st r i ng * q = р ; / / q ука зыв а е т на первую строку whi l e ( c i n > > s & & q ! = р + n ) * q+ + = s ; 1 1 присв аив а е т нов о е зна чение * q con s t s i z e_t s i z e = q - р ; / / з а помнить количе ств о про чита нных с трок 11 исполь з ов а ть ма ссив de l e t e [ ] р ; / / р ука зыв а е т на ма ссив ; не за быть исполь зов а ть de l e t e [ ]

Этот оператор new резервирует и инициализирует n строк. Но n строк мо­ жет не понадобиться, - вполне может хватить меньшего количества строк. В результате, возможно, были созданы объекты, которые никогда не будут ис­ пользованы. Кроме того, тем из объектов, которые действительно используют­ ся, новые значения присваиваются немедленно, поверх только что инициали­ зированных строк. Используемые элементы записываются дважды: сначала, когда им присваивается значение по умолчанию, а затем, когда им присваива­ ется значение. Еще важней то, что классы без стандартных конструкторов не могут быть динамически созданы как массив.

К ласс allocator и спе ц иальн ы е алгоритмы Библиотечный класс a l l o c a t o r, определенный в заголовке rnerno r y , позво­ ляет отделить резервирование от создания. Он обеспечивает не типизирован­ ное резервирование свободной области память. Операции, поддерживаемые классом a l l o c a t o r, приведены в табл. 1 2.7. Операции с классом a l l o c a t o r описаны в этом разделе, а типичный пример его использования - в разде­ ле 1 3.5 (стр. 664) .

61 4

Глава 1 2. Динамичская пам ять

Подобно типу ve c t o r, тип a l l o c a t o r является шаблоном (см. раздел 3.3, стр. 1 4) ). Чтобы определить экземпляр класса a l l o c a t o r, следует указать тип объектов, которые он сможет резервировать. Когда объект a l l o c a t o r резер­ вирует память, он обеспечивает непрерывное хранилище соответствующего размера для содержания объектов заданного типа: al locato r < s t r ing> al loc ; / / объект , спо с о бный резервиров а ть строки a u t o c o n s t р = a l l o c . a l l o c a t e ( n ) ; / / рез ервируе т п нез аполненных строк

Этот вызов функции a l l o c a t e ( ) резервирует память для n строк.

Т аблица 12.7. С та ндар тный класс al l ocator и специал ьные алгоритмы al locator а

Определяет объект а класса a l l o c a t o r, способный ре­ зервировать память для объектов типа т

a . al locate ( n )

Резервирует пустую область памяти для содержания n объектов типа т

a . de a l l o c a t e ( p , n )

Освобождает область памяти, содержавшую n объектов типа т, начиная с адреса в указателе р типа т * . Указа­ тель р должен быть ранее возвращен функцией a l l o c a t e ( ) , а размер n соответствовать запрошен­ ному при создании указателя р. Функцию de s t r o y ( ) следует выполнить для всех объектов, созданных в этой памяти, прежде, чем вызвать функцию de a l l o c a t e ( ) -

a . con s t r u c t ( p , a rgs )

Указатель р на тип т должен указывать на незаполнен­ ную область памяти; аргументы a rgs передаются кон­ структору типа т, используемому для создания объекта в памяти, на которую указывает указатель р

a . de s t ro y ( p )

Выполняет деструктор (см. раздел 1 2.1 . 1 , стр. 578) для объекта, на который указывает указатель р типа т *

К ласс al locator резервирует незаполненную пам я т ь



Память, которую резервирует объект класса a l l o ca t o r, не заполнена. Эта область памяти используется при создании объектов. В новой библиотеке функция-член co n s t ru c t ( ) получает указатель и любое количество до­ полнительных аргументов; она создает объекты в заданной области памя­ ти. Для инициализации создаваемого объекта используются дополни­ тельные аргументы. Подобно аргументам функции rna ke s h a red ( ) (см. раздел 1 2.1 . 1 , стр. 576), эти дополнительные аргументы должны быть допустимыми инициализаторами объекта создаваемого типа. В частности, если типом объекта является класс, эти аргументы должны соответство­ вать конструктору этого класса: _

12.2.

Динамические массивы

61 5

auto q = р ;

/ / q ука зыв а е т на следующий элемент после по следнего 1 1 созданного a l l o c . c o n s t r u c t ( q+ + ) ; 1 1 *q - пус тая строка a l l o c . c o n s t r u c t ( q+ + , 1 0 , ' с ' ) ; 1 1 *q - сссссссссс a l l o c . c on s t ru c t ( q+ + , " h i " ) ; 1 1 *q - hi !

В прежних версиях библиотеки функция co n s t r u c t ( ) получала только два аргумента: указатель для создаваемого объекта и значение его типа. В ре­ зультате можно было только скопировать объект в незаполненную область, но никакой другой конструктор этого типа использовать было нельзя. Использование незаполненной области памяти, в которой еще не был соз­ дан объект, является ошиб кой: cout f o l de r s ; / / па пки, содержащие э то с о о бщение 1 1 в спомога тель ные функции , исполь зуемые конструктором копий , 1 1 опера тором присв оения и де с труктором 1 1 добавить э то с о о бщение в па пки, на ко торые ука зыв а е т параме тр v o i d a dd_t o_Fo l de r s ( c o n s t Me s s age & ) ; / / удалить это с о о бщение из каждой па пки в fo l ders v o i d r ernove f r orn Fo l d e r s ( ) ; ; }

Класс определяет две переменные-члена: con t e n t s для хранения текста сообщения и f o l de r s для хранения указателей на объекты класса Fo l der, в которых присутствует данное сообщение. Получающий строку конструктор копирует ее в переменную co n t e n t s и (неявно) инициализирует переменную fo l de r s пустым набором. Поскольку у этого конструктора есть аргумент по умолчанию, он также является стандартным конструктором класса Me s s age (см. раздел 7.5.1 , стр. 375). -

-

13.4.

Пр имер уп р авления копированием

661

Фун кц ии -член ы save ( ) и remove ( ) Кроме функций управления копированием, у класса Me s s age есть только две открытых функции-члена: s a ve ( ) , поме1цающая сообщение в данную папку, и remove ( ) , извлекающая его: v o i d Me s s a ge : : s ave ( Fo l de r & f ) { fo l de r s . i n s e r t ( & f ) ; / / доба вить да нную па пку в спис ок па пок f . addM s g ( t h i s ) ; 1 1 доба вить да нное с о о бщение в на бор сообщений void Me s s a g e : : r emove ( Fo l de r & f ) { f o l de r s . e r a s e ( & f ) ; / / удалить да нную па пку из списка папок f . re mМ s g ( t h i s ) ; / / удалить данное с о общение из на бора сообщений

Чтобы сохранить (или удалить) сообщение, требуется модифицировать член f o l de r s класса Me s s age . При сохранении сообщения сохраняется ука­ затель на данный объект класса Fo l de r; при удалении сообщения этот указа­ тель удаляется. Эги функции должны также модифицировать заданный объект класса Folde r. Модификация этого объекта является задачей, коmролируемой классом Folde r при помощи функций-членов addМsg ( ) и remМsg ( ) , которые добавляют или удаляют указатель на данный объект класса Me s s age соответственно.

Управление копированием класса Mes sage При копировании сообщения копия должна появляться в тех же папках, что и оригинальное сообщение. В результате необходимо перебрать набор указателей класса Fo l de r, добавляя указатель на новое сообщение в каждую папку, на которую указывал оригинал сообщения. Для этого и конструктор копий, и оператор присвоения копии должны будут выполнять те же дейст­ вия, поэтому определим функцию для этой общей работы: 1 1 добавить это с о о бщение в па пки , на которые ука зыв а е т т v o i d Me s s a ge : : a dd_t o Fo l de r s ( c o n s t Me s s a g e & m ) { f o r ( a u t o f : m . f o l de r s ) / / для каждой па пки , содержащей т , f - > addMs g ( t h i s ) ; / / добавить ука з а тель на это соо бщение 1 1 в да нную па пку

Здесь происходит вызов функции addM s g ( ) для каждого объекта класса Fo lde r в m . fo l de r s . Функция a ddM s g ( ) добавит указатель на этот объект класса Me s s age в данный объект класса Fo l de r . Конструктор копий класса Me s s age копирует переменные-члены данного объекта:

Глава 1 3. Управление копи р ованием

662

Me s s age : : Me s s a ge ( c on s t Me s s a ge & m ) : c o n t e n t s ( m . c o n t e n t s ) , f o l de r s ( m . f o l de r s ) add_t o_Fo l de r s ( rn ) ;

/ / доба вить э т о с о о бщение в па пки , на которые 1 1 ука зыв а е т т

А также вызывает функцию add_ t o _ Fo l de r s ( ) , чтобы добавить указатель

на недавно созданный объект класса Me s s age каждому объекту класса Fo l de r, который содержит оригинал сообщения.

Деструктор класса Me s sage При удалении объекта класса Me s s age следует удалить это сообщение из папок, которые указывают на него. Это общее действие с оператором при­ своения копии, поэтому определим для этого общую функцию: / / удалить это сообщение из с о о тв е тств ующих папок vo i d Me s s a ge : : r emove _ f r om_ F o l de r s ( ) { f o r ( a u t o f : fo l d e r s ) / / для каждого ука за теля в fo l ders f - > r emМs g ( t h i s ) ; / / удалить э то с о о бщение из данной папки

Реализация функции remove_ f r om_Fo l de r s ( ) подобна таковой у функ­ ции add_ to _ Fo l de r s ( ) , за исключением того, что она использует функцию remМs g ( ) для удаления текущего сообщения. При наличии функции r emove_f r om_Fo l de r s ( ) написать деструктор не­ сложно: Me s s age : : - Me s s a ge ( ) { rernove _ f r om_ Fo l de r s ( ) ;

Вызов функции remove_ f r om_Fo l de r s ( ) гарантирует отсутствие у объек­ тов класса Fo l de r указателей на удаленный объект класса Me s s age . Компиля­ тор автоматически вызывает деструктор класса s t r i ng для освобождения объекта co n t e n t s, а деструктор класса s e t освобождает память, используе­ мую элементами набора.

О ператор присвоения копии класса Mes sage Как обычно, оператор присвоения и оператор присвоения копии класса Fo l de r должны выполнять действия конструктора копий и деструктора. Как всегда, крайне важно структурировать свой код так, чтобы он выполнялся правильно, даже если операнды слева и справа - тот же объект. В данном елучае защита против присвоения самому себе осуществляется за счет удаления указателей на это сообщение из папок левого операнда прежде, чем вставить указатели в папки правого операнда:

13 .4. П р име р управления копированием

663

Me s s age & Me s s a ge : : op e r a t o r= ( c o n s t Me s s a ge & rh s ) { 1 1 отра б о та ть присв о ение с е бе самому , удаляя ука з а тели прежде в ста в ки rernove f r orn_F o l de r s ( ) ; 1 1 о бновить суще ствующие папки con t e n t s = r h s . c o n t e n t s ; 1 1 копиров а ть содержимо е с о о бщения из rh s fo l de r s = r h s . f o l de r s ; 1 1 копиров а ть ука з а тели Fo l der из rh s add_t o Fo l de r s ( rh s ) ; 1 1 добав ить это соо бщение к да нным па пкам return * thi s ;

Если левый и правый операнды - тот же объект, то у них тот же адрес. Если вызвать функцию remove _ f rom_ Fo l de r s ( ) после вызова функции add_ t o _ Fo lde r s ( ) , это сооб щение будет удалено изо всех соответствующих ему папок.

Функц ия swap ( ) класс а Mes sage Библиотека определяет версии функции swap ( ) для классов s t r i n g и s e t (см. раздел 9.2.5, стр. 436). В результате класс Me s s a ge извлечет пользу из оп­ ределения соб ственной версии функции s wap ( ) . При определении специфи­ ческой для класса Me s s a ge версии функции s wap ( ) можно изб ежать лишних копирований членов c on t e n t s и f o l de r s . Но наша функция s wap ( ) должна также управлять указателями Fo l de r, которые указывают на об мениваемые сооб щения. После такого вызова, как s wap ( m l , m2 ) , указатели Fo l de r, указывающие на об ъект m l , должны теперь указать на об ъект m2, и наоб орот. Для управления указателями Fo l de r осуществляются два прохода по всем элементам f o l de r s . Первый проход удалит сооб щения из соответствующих папок. Затем вызов функции s wap ( ) совершит об мен переменных-членов. Второй проход по элементам f о 1 de r s . доб авляет указатели на об мениваемые сообщения: vo id s wap ( Me s s a ge & l h s , Me s s ag e & rh s ) { u s i n g s t d : : s wap ; / / в да нном случа е не о бяз а тель но , но привычка 1 1 хорошая 1 1 удалить ука з а тели на каждое с о о бщение из их (оригинальных ) папок f o r ( a u t o f : l h s . f o l de r s ) f - > r ernМs g ( & l h s ) ; f o r ( a u t o f : r h s . f o l de r s ) f - > r ernМs g ( & r h s ) ; 1 1 о бмен на боров ука з а телей con t en t s и fol ders swap ( l h s . f o l de r s , r h s . f o l de r s ) ; / / исполь зуе т s wap ( s e t & , s e t & ) s wap ( l h s . c o n t e n t s , r h s . c o n t e n t s ) ; / / s wap ( s t ri n g& , s t ri n g& ) / / доба вляе т ука з а тели на каждое с о о бщение в их (новые ) папки f o r ( au t o f : l h s . f o l de r s ) f - > a ddMs g ( & l h s ) ; f o r ( au t o f : r h s . f o l de r s ) f - > a ddМs g ( & r h s ) ;

Глава 1 3. Уп равление копированием

664

Упражнения раздела

1 3.4

Упражнение 1 3.33. Почему параметр функций-членов s ave ( ) и remove ( ) класса Me s s age имеет тип Fo l de r & ? Почему этот параметр не определен как Fo l de r или co n s t Fo l de r & ? Упражнение

1 3.34.

Напишите класс Me s s age, как описано в этом разделе.

Упражнение 1 3.35. Что случилось бы, используй класс Me s s a ge синтези­ руемые версии функций-членов управления копированием? Упражнение 1 3 .36. Разработайте и реализуйте соответствующий класс Fo l de r . Этот класс должен содержать набор указателей на сообщения в той папке. Упражнение 1 3.37. Добавьте в класс Me s s age функции-члены удаления и вставки заданного F o l de r * в f o l de r s . Эти члены аналогичны функциям­ членам a ddM s g ( ) и remM s g ( ) класса Fo l de r . Упражнение 1 3 .38. Для определения оператора присвоения класса Me s s age не использовалась технология копирования и обмена. Почему, по вашему?

Si?

1 3. 5.

К л асс ы, уп р авляющ ие динамичес1 для резервирования памяти, используемой классом S t rVe c . У класса также будет четыре вспомогательных функции . •

Функция a l l o c_n_c opy ( ) будет резервировать пространство и копиро­ вать заданный диапазон элементов.



Функция f r e e ( ) будет удалять созданные элементы и освобождать про­ странство.



Функция ch k_n_ a l loc ( ) будет гарантировать наличие достаточного места для добавления по крайней мере еще одного элемента в вектор S t rVe c. Ес­ ли месга для следующего элемента нет, то функция ch k_n_al l o c ( ) вызо­ вет функцию re a l l ocate ( ) для резервирования большего пространства.



Функция r e a l l o c a t e ( ) будет пересоздавать вектор S t rVe c, когда прежнее пространство окажется исчерпано.

Хотя основное внимание уделено реализации, определим также несколько членов из интерфейса класса ve c t o r.

Глава 13 . Управление копированием

666

Опр еделе ни е класса

S trVe c

Теперь, сделав наб росок реализации, можно определить класс S t rVe c: / / упрощенная ре ализ а ция с тра тегии р е з ервиров а ния памяти для подо бного в ектору кла с с а c l a s s S t rVe c { puЫ i c : S t rVe c ( ) : / / член a l l o ca t o r инициализируе т ся п о умолчанию e l eme n t s ( n u l l p t r ) , f i r s t f re e ( nu l l p t r ) , c a p ( n u l l p t r ) { S t rVe c ( c o n s t S t rVe c & ) ; / / кон с труктор копий S t rVe c & ope r a t o r = ( c o n s t S t rVe c & ) ; / / присв о ение копии / / де с труктор - S t rVe c ( ) ; vo i d pu s h_ba c k ( c o n s t s t d : : s t r i n g & ) ; / / копируе т элемент s i z e_t s i z e ( ) c o n s t { r e t u r n f i r s t_ f r e e - e l eme n t s ; s i z e t c a p a c i t y ( ) c o n s t { r e t u r n с ар - e l eme n t s ; } s t d : : s t r i n g * be g i n ( ) c o n s t { r e_t u r n e l eme n t s ; } s t d : : s t r i n g * e n d ( ) c o n s t { r e t u rn f i r s t f r e e ; } // . . . p r ivate : s t d : : a l l o c a t o r < s t d : : s t r i ng > a l l o c ; / / рез ервируе т элементы / / исполь зуе тся функциями , которые доба вляют элементы в S t r Ve c vo i d c h k_n_a l l o c ( ) { i f ( s i z e ( ) = = c a pa c i t y ( ) ) r e a l l o c a t e ( ) ; } / / в спомога тельные члены , исполь зуемые конс труктором копий, / / опера тором присв о ения и д е с трукт ором s t d : : pa i r < s t d : : s t r i n g * , s t d : : s t r i ng * > a l l o c_n _ c o p y ( con s t s td : : s t ring* , con s t s t d : : s t ri n g* ) ; v o i d f re e ( ) ; / / удаляе т элементы и о св о божда е т про странств о v o i d r e a l l o c a t e ( ) ; / / р е з ервируе т больше ме с та и копируе т / / суще с тв ующие элементы s t d : : s t r i n g * e l eme n t s ; / / ука з а тель на первый элемент ма с сив а s t d : : s t r i n g * f i r s t f r e e ; / / ука з а тель на п ервый св о б одный 1 1 элемент ма с сив а s t d : : s t r i n g * с ар ; / / ука з а тель на следующий элемент по сле 1 1 конца ма с сив а };

11

Тело класса определяет некоторые из своих членов. •

Стандартный конструктор (неявно) инициализирует по умолчанию пе­ ременную-член a l l o c и (явно) инициализирует указатели как nu l lpt r, означая, что никаких элементов нет.



Функция-член s i z e ( ) возвращает количество фактически используе­ мых элементов, соответствует значению f i r s t_ f r e e - e l eme n t s .



Функция-член c apa c i t y ( ) возвращает количество элементов, которые может содержать объект класса S t rVe c, соответствует значению сар -

e l eme n t s . •

Функция-член ch k_n_a l l o c ( ) приводит к пересозданию объекта класса S t rVe c, когда б ольше нет места для доб авления следующего элемента. Это происходит при с ар f i rs t free. ==

13.5. Классы, уп р авляющие динамической пам ятью •

667

Функции-члены b e g i n ( ) и e n d ( ) возвращают указатели на первый (т.е. e l eme n t s ) и следующий после последнего существующего элемент (т.е. f i r s t f r ee) соответственно.

Использ ование ф ункции-члена cons truct ( ) Функция pu s h_ba c k ( ) вызывает функцию c h k _n _ a l l o c ( ) , чтобы удосто­ вериться в наличии места для элемента. В случае необходимости функ­ ция ch k_n_a l l o c ( ) вызовет функцию re a l l o c a t e ( ) . После вызова функции ch k_n_a l l o c ( ) функция pu s h_ba c k ( ) знает, что место для нового элемента есть. Она запрашивает свой член класса a l l o c a t o r создать новый последний элемент: vo i d S t rVe c : : p u s h_b a c k ( c o n s t s t r i n g & s ) { c h k n a l l o c ( ) ; / / удо стов ерить ся в наличии ме ста для другого элемента 1 1 созда ть копию s в элементе , на который ука зыв а е т fi rs t free a l l o c . c o n s t r u c t ( f i r s t_ f r e e + + , s ) ;

При использовании класса a l l o c a t o r для резервирования памяти следует помнить, что память резервируется пус той (см. раздел 1 2.2.2, стр . 61 4). Чтобы использовать эту память, следует вызвать функцию con s t ruct ( ) , которая создаст объект в этой памяти. Первый аргумент функции co n s t ru c t ( ) - это указатель на пустое пространство, зарезервированное вызовом функции a l l o c a t e ( ) . Остальные аргументы определяют, какой конструктор исполь­ зовать при создании объекта в этом пространстве. В данном елучае есть только один дополнительный аргумент типа s t r i ng, поэтому этот вызов использует строковый конструктор копий. Следует заметить, что вызов функции co n s t ru c t ( ) осуществляет прира­ щение указателя f i r s t_ f r e e, чтобы он снова указывал на элемент, который предстоит создать. Поскольку используется постфиксный инкремент (см. раз­ дел 4.5, стр. 204), этот вызов создает объект в текущей позиции указателя fi r s t_ f r e e, а инкр емент переводит его на следующий пустой элемент.

Ф ункция- член al loc_n_copy ( ) Функция-член a l l o c_n_c opy ( ) вызывается при копировании или при­ своении объекта класса S t rVe c . У класса S t rVe c будет подобное значению поведение (см. раздел 1 3.2. 1 , стр. 648), как у вектора; при копировании или присвоении объекта класса S t rVe c необходимо зарезервировать независимую память и скопировать элементы из оригинала в новый объект класса S t rVe c. Функция-член a l l o c_n_copy ( ) будет резервировать достаточно места для содержания заданного диапазона элементов, а затем копировать эти элементы во вновь созданное пространство. Эта функция возвращает значение типа pa i r (см. раздел 1 1 .2.3, стр. 545), переменные-члены которого являются указа-

668

Глава 1 3. Уп р авление копированием

телем на начало нового пространства и следующую позицию после последне­ го скопированного элемента: pa i r < s t r i n g * , s t r i n g * > S t rVe c : : a l l o c n c o p y ( c o n s t s t r i n g * Ь , c o n s t s t r i n g * е ) { / / резервирова ть про с транств о для содержа ния элементов диа па зона a u t o da t a = a l l o c . a l l o c a t e ( e - Ь ) ; / / инициализиров а ть и в о звра тить пару , созда нную из да нных , / / в о звращенных функцией u n i n i t i a l i z e d_ copy ( ) r e t u r n { d a t a , un i n i t i a l i z e d_c op y ( b , е , da t a ) } ;

Функция a l l o c _n _сору ( ) вычисляет объем резервируемого пространсrва, вычитая указатель на первый элемент из указателя на следующий после послед­ него. Зарезервировав память, функция создает в ней копии заданных элементов. Копирование осущесгвляется в операторе re turn при списочной инициали­ зации возвращаемого значения (см. раздел 6.3.2, стр. 296) . Указатель-член f i r st возвращенной пары указывает на начало зарезервированной памяти; значение для указателя-члена s e c ond возвращается функцией un i n i t i a l i zed_copy ( ) (см. раздел 12.2.2, стр. 61 6). Эго значение будет указателем на следующий элемеm после последнего созданного элемента.

Ф ункция-член f ree ( ) У функции-члена f re e ( ) две обязанности: она должна удалить элементы, а затем освободить пространство, зарезервированное объектом класса S t rVe c. Цикл fo r вызывает функцию de s t ro y ( ) класса a l l o c a t o r, перебирая эле­ менты в обратном порядке, начиная с последнего существующего элемента и заканчивая первым: v o i d S t rVe c : : f r e e ( ) { 1 1 нель зя осв ободить О ука за телей ; / / е сли элемент нулев ой - не дела ть ничего i f ( e l eme n t s ) { 1 1 удалить прежние элементы в о бра тном порядке f o r ( a u t o р = f i r s t _f r e e ; р ! = e l erne n t s ; / * пус то * / ) a l l o c . de s t r o y ( - - p ) ; a l l o c . de a l l o c a t e ( e l erne n t s , с а р - e l eme n t s ) ;

Функция de s t r o y ( ) запускает деструктор класса s t r i ng. Деструктор класса s t r i ng освобождает память, занятую самой строкой. Как только элементы будут удалены, освобождается пространство, зарезер­ вированное классом S t rVe c при вызове функции de a l l o c a t e ( ) . Указатель, передаваемый функции de a l l o c a t e ( ) , должен быть именно тем, который ранее создал вызов функции a l l o c a t e ( ) . Поэтому перед вызовом функции de a l l o c a t e ( ) сначала проверяется, тот ли это e l eme n t s, а не нулевой.

13.5. Классы, уп р авляющие динамической пам ятью

669

Ф ун кц ии-член ы управления копированием При наличии функций-членов a l l o c_n_copy ( ) и f r e e ( ) функции-члены управления копированием нашего класса очень просты. S t rVe c : : S t rVe c ( c o n s t S t rVe c & s ) {

1 1 выз ов функции a l l o c_n_ copy ( ) для резервиров а ния количе ств а 1 1 элементов ка к в s a u t o n e wda ta = a l l o c n c o p y ( s . b e g i n ( ) , s . e n d ( ) ) ; e l emen t s = n e wda t a . f i r s t ; f i r s t_ f r e e = с а р = n e wda t a . s e c o n d ;

Конструктор копий вызывает функцию a l l o c _ n _ сор у ( ) , а затем присваи­ вает результат вызова переменным-членам. Возвращаемое значение функции a l l o c _n_c op y ( ) является парой указателей. Первый указатель указывает на первый созданный элемент, а второй - на следующий после последнего соз­ данного. Поскольку функция a l l o c _ n _сору ( ) резервирует пространство для точно такого количества элементов, которое было задано, указатель сар также указывает только на следующий после последнего созданного. Деструктор вызывает функцию f re e ( ) : S t rVe c : : --.. s t rVe c ( )

{

f ree ( ) ;

}

Оператор присвоения копии вызывает функцию a l l o c _n _сору ( ) прежде, чем освободить существующие элементы. Это защищает от копирования в се­ бя самого: S t rVe c & S t rVe c : : op e r a t o r = ( c o n s t S t rVe c & r h s ) 1 1 выз ов a l l o c_n_ copy ( ) для резервиров а ния то чно та кого количе ств а 1 1 элементов , ка к в rh s a u t o d a t a = a l l o c n c o p y ( r h s . be g i n ( ) , r h s . e n d ( ) ) ; f ree ( ) ; e l eme n t s = d a t a . f i r s t ; f i r s t_ f r e e = с а р = da t a . s e c o n d ; return * thi s ;

Подобно конструктору копий, оператор присвоения копии использует значения, возвращенные функцией a l l o c n с ору ( ) , для инициализации своих указателей.



П еремещение, а не копирование э лементов при резервировании

Прежде чем пристуri:ить к функции r e a l l o c a t e ( ) , следует обдумать то, что она должна делать:

Глава 1 3 . Уп р авление копированием

670 •

зарезервировать память для нового, большего массива строк;



заполнить первую часrь этого пространства сущесгвующими элементами;



удалить элементы в существующей памяти и освободить ее.

Глядя на этот список, можно заметить, что пересоздание объекта класса S t rVe c влечет за собой копирование каждой строки из прежнего объекта S t rVe c в новый. Даже без подробностей реализации класса s t r i ng известно, что строки ведут себя подобно значению. После копирования новая строка и оригинальная независимы друг от друга. Изменения, внесенные в оригинал, не распространяются на копию, и наоборот. Поскольку строки действуют, как значения, можно сделать вывод, что у каждой строки должна быть собственная копия составляющих ее символов. Копирование строки должно резервировать память для этих символов, а уда­ ление строки должно освободить используемую ею память. Копирование строки подразумевает копирование данных, поскольку обычно после копирования строки у нее будет два пользователя. Но когда функция re a l l o c a t e ( ) копирует строки объекта класса S t rVe c, у этих строк будет только один пользователь. Как только копирование элементов из прежнего пространства в новое завершается, исходные строки немедленно удаляются. Копирование данных этих строк не нужно. Производительность класса S t rVe c будет значи телыю выше, если удастся избежать дополнительных за­ трат на резервирование и освобождение строк при каждом его пересоздании.

[ с1�+ )

К онст р уктор пе р емещения и ф ункц ия s td : : move ( )

Коrrnрования строки можно избежать при помощи двух средств, введенных новой библиотекой. Во-первых, некоторые из библиотечных классов, включая класс s t r i ng, определяют так называемые конструкторы перемrujения (move constructor). Подробности работы конструктора перемещения класса s t r ing (равно как и все остальные подробности его реализации) не раскрываются. Одна­ ко общеизвестно, что конструкторы перемещения обычно "перемещают" ресур­ сы из заданного объекта в создаваемый. Библиотека гарантирует также то, что "перемещенная" строка останется в допустимом состоянии. В случае класса s t r i ng можно предположить, что у каждого его объекта есть указатель на массив типа cha r. По-видим ому, конструктор перемещения класса s t r ing копирует указатель вместо резервирования нового пространства и копирования символов. Второе доступное для использования средство - это библиотечная функ­ ция rnove ( ) , определенная в заголовке u t i l i t y. Есть два важных момента, ко­ торые следует знать о функции rnove ( ) . Во-первых, по причинам, рассматри­ ваемым в разделе 1 3.6.1 (стр. 673), когда функция r e a l l o c a t e ( ) создает стро­ ки в новой области памяти, она должна вызвать функцию rnove ( ) , чтобы сообщить о необходимости использования конструктора перемещения класса

671

13.5. Классы, уп р авляющие динамической пам я тью

s t r i n g. Если пропустить вызов функции move ( ) , то будет использован кон­ структор копий класса s t r i n g. Во-вторых, по причинам, рассматриваемым в разделе 1 8.2.3 (стр. 997), объявление u s i n g (см. раздел 3. 1 , стр. 1 24) для функции move ( ) обычно не предоставляется. Когда используется функция move ( ) , вызывается функция s t d : : move ( ) , а не move ( ) .

Ф ункция- член realloca te ( ) Используя эту информацию, можно написать собственную функцию re a l l o c a t e ( ) . Сначала вызовем функцию a l l o c a t e ( ) , чтобы зарезервиро­ вать новое пространство. При каждом пересоздании объекта класса S t rVe c будем удваивать его емкость. Если вектор S t rVe c пуст, резервируем место для одного элемента: vo i d S t rVe c : : r e a l l o c a t e ( ) {

/ / будем резервиров а ть вдв ое больше элементов , чем те кущий ра змер auto newcapa c i t y = s i z e ( ) ? 2 * s i z e ( ) : 1 ; 1 1 резервиров а ть новую память a u t o n e wda t a = a l l o c . a l l o c a t e ( n e w c a p a c i t y ) ; 1 1 переме стить да нные из прежней памяти в новую a u t o de s t n e wda t a ; / / ука зыв а е т на следующую св ободную позицию в 1 1 нов ом ма ссив е e l erne n t s ; / / ука зыв а е т на следующий элемент в старом a u t o e l ern 1 1 ма с сив е f o r ( s i z e_t i = О ; i ! = s i z e ( ) ; + + i ) a l l o c . c o n s t r u c t ( de s t + + , s t d : : rnove ( * e l ern+ + ) ) ; f r e e ( ) ; / / о св о божда е т старое про с тра нств о п о сле перемещения 1 1 элементов 1 1 обновить структуру да нных , чтобы ука з а ть на новые элементы e l erne n t s = n ewda t a ; f i r s t f r e e = de s t ; с а р = e l erne n t s + n e w c ap a c i t y ;

Цикл f о r перебирает существующие элементы и создает соответствующие элементы в новом пространстве. Указатель de s t используется для указания на область памяти, в которой создается новая строка, а указатель e l em для указания на элемент в оригинальном массиве. Для перемещения указателей de s t и e l em на следующий элемент этих двух массивов используем постфикс­ ный инкремент. Второй аргумент в вызове функции c o n s t ru c t ( ) (т.е. аргумент, опреде­ ляющий используемый конструктор (см. раздел 1 2.2.2, стр . 61 4)) является зна­ чением, возвращенным функцией rnove ( ) . Вызов функции rnove ( ) возвраща­ ет результат, заставляющий функцию c o n s t ru c t ( ) использовать конструк­ тор перемещения класса s t r i n g. Поскольку используется конструктор перемещения, управляемая память строки не будет скопирована. Вместо этого каждая создаваемая строка пол учит в собственность область памяти из строки, на которую указывает указатель e l ern. -

Глава 1 3. Управление копированием

672

После перемещения элементов происходит вызов функции f re e ( ) для удаления прежних элементов и освобождения памяти, которую данный век­ тор S t rVe c использовал перед вызовом функции r e a l l o c a t e ( ) . Сами строки больше не управляют памятью, в которой они располагались; ответственность за их данные была передана элементам нового вектора S t rVe c. Нам неизвест­ но содержимое строк в памяти прежнего вектора S t rVe c, но нам гарантиро­ вана безопасность запуска деструктора класса s t r i ng для этих объектов. Остается только обновить указатели адресами вновь созданного и инициа­ лизированного массива. Указатели f i r s t_ f r e e и с ар обозначат элемент сле­ дующий после последнего созданного и следующий после последнего заре­ зервированного соответственно. Упражнения раздела

1 3.5

Упражнение 1 3.39. Напишите собственную версию класса S t rVec, включая функции re s e rve ( ) , c apa c i t y ( ) (см. раздел 9.4, стр. 458) и re s i z e ( ) (см. раздел 9.3.5, стр. 452) . Упражнение 1 3.40. Добавьте в класс S t rVe c конструктор, получающий ар­ гумент типа i n i t i a l i z e r _ l i s t < s t r i n g > . Упражнение 1 3.41 . Почему в вызове функции c on s t ru c t ( ) в функции pu s h_ba c k ( ) был использован постфиксный инкремент? Что случилось бы при использовании префиксного инкремента? Упражнение 13.42. Проверьте свой класс S t rVe c, использовав его в классах Tex tQue ry и Que ryRe s u l t (см. раздел 1 2.3, стр. 61 7) вместо вектора ve ctor < s t r i ng>. Упражнение 1 3.43. Перепишите функцию-член f re e ( ) так, чтобы для уда­ ления элементов вместо цикла f о r использовалась функция f о r _ е а с h ( ) и лямбда-выражение (см. раздел 1 0.3.2, стр . 497). Какую реализацию вы предпочитаете и почему? Упражнение 1 3.44. Напишите класс по имени S t r i ng, являющийся упро­ щенной версией библиотечного класса s t r i ng. У вашего класса должен быть по крайней мере стандартный конструктор и конструктор, получаю­ щий указатель на строку в стиле С. Примените для резервирования исполь­ зуемой классом S t r i ng памяти класс a l l o c a t o r .

[ с1'1+ ] 1 3. 6 . П е р еме щ ение о б ъ ектов Одной и з главных особенностей нового стандарта является способность пе­ ремещать объект, а не копировать. Как упоминалось в разделе 1 3. 1 . 1 (стр. 631 ), копирование осуществляется при многих обстоятельствах. При некоторых из

13.6. Перемещение об ъ ек тов

673

них объект разрушается немедленно после копирования. В этих ел учаях пе­ ремещение объекта вместо копирования способно обеспечить существенное увеличение производительности. Как было продемонстрировано только что, наш класс S t rVe c - хороший пример лишнего копирования. Во время пересоздания нет никакой необхо­ димости в копировании элементов из старой памяти в новую, лучше переме­ щение. Вторая причина предпочесть перемещение копированию - это такие классы как un i que _p t r и классы ввода-вывода. У этих классов есть ресурс (та­ кой как указатель или буфер ввода-вывода), который не допускает совместно­ го использования. Следовательно, объекты этих типов не могут быть скопиро­ ваны, но могут быть перемещены. В прежних версиях языка не было непосредственного способа перемеще­ ния объекта. Копию приходилось делать, даже если в этом не было никакой потребности. Когда объекты велики или когда они требуют резервирования памяти (например, строки), бесполезное копирование может обойтись очень дорого. Точно так же в предыдущих версиях библиотеки классы хранимых в контейнере объектов должны были допускать копирование. По новому стандарту в контейнерах можно хранить объекты типов, которые не допуска­ ют копирования, но могут быть перемещены. Контейнеры библиотечных типов, классы s t r i ng и s h a r e d_p t r померживают как перемещение, так и копирование. Классы вво­ да-вывода и класс un i que -� t r допускают перемещение, но не копирование.

• 13.6.1 . С сылки на r-значение � Для обеспечения операции пересылки, новый стандарт вводит новый вид

ссылок - ссылки на r-значение. Ссылка на r-значение (r-value reference) это ссылка, которая должна быть связана с r-значением. Ссылку на r-значение получают с использованием символа & &, а не & . Как будет про­ демонстрировано далее, у ссылок на r-значение есть важное свойство они могут быть связаны только с тем объектом, который будет удален. В результате можно "перемещать" ресурсы от ссылки на r-значение в другой объект.

Напомним, что 1- и· r-значение - свойства выражения (см. раздел 4.1 . 1 , стр. 1 89) . Некоторые выражения возвращают или требуют 1-значений; другие возвращают или требуют r-значений. Как правило, выражение 1-значения от­ носится к идентификатору объекта, тогда как выражение r-значения - к зна­ чению объекта. Как и любая ссылка, ссылка на r-значение - это только другое имя для объ­ екта. Как известно, нельзя связать обычные ссылки (которые далее будем назы-

Глава 1 3. Управление копированием

674

вать ссылками на [-значение (l-value reference), чтобы отличить их от ссылок на r-значения) с выражениями, требующими преобразования, с литералами и с выражениями, которые возвращают r-значение (см. раздел 2.3. 1 , стр. 86). У ссылок на r-значение противоположные свойства привязки: можно связать ссылку на r-значение с выражениями, приведенными выше, но нельзя непо­ средственно связать ссылку на r-значение с 1-значением: int i = 4 2 ; int & r = i ; int & & rr i;

1 1 ok : r ссыла е т ся на i / / ошибка : нель зя связа ть ссылку на r - зна чение 1 1 с 1 - з н а чением i n t & r 2 = i * 4 2 ; / / ошибка : i * 42 - это r - зна чение i * 4 2 ; / / ok : с сылку на конста нту можно con s t int & r З 1 1 связа ть с r - зна чением i n t & & r r 2 = i * 4 2 ; / / ok : связа ть rr2 с резуль та том умножения =

Функции, возвращающие ссылки на 1-значение, наряду с присвоением, ин­ дексированием, обращением к значению, а также префиксные операторы ин­ кремента и декремента являются примерами выражений, возвращающих 1-зна­ чения. Ссылку на 1-значение можно также связать с результатом любого из этих выражений. Все функции, возвращающие не ссылочный тип, наряду с арифметически­ ми, реляционными, побитовыми и постфиксными операторами инкремента и декремента возвращают r-значения. С этими выражениями нельзя связать ссылку на !-значение, но можно связать либо константную ссылку на 1-зна­ чение, либо ссылку на r-значение.

1 -зн аче н и я

-

усто й ч и в ы; r-значен и я

-

э ф емер н ы

Глядя на список выражений 1- и r-значений, становится понятно, что 1- и r-зна­ чения существенно отличаются друг от друга: у 1-значений есть постоянное со­ стояние, тогда как r-значения, литералы и временные объекты создаются лишь в ходе вычисления выражений. Поскольку ссылки на r-значение могут быть связаны только с временным объектом, известно, что: •

упомянутый объект будет удален,



у этого объекта не может быть других пользователей.

Совместно эти факты означают, что использующий ссылку на r-значение со6

§ �;;;;�;1�;;�����;;;��;:;�:�:;�;;�;;��;;н;.:;::� v i ( l 0 0 ) ; int? rl f() ; vi [ O ] ; int? r2 rl ; int ? rЗ vi [ O ] * f ( ) ; int ? r4

Упражнение 1 3.47. Снабдите конструктором копий и оператором присвое­ ния копии класса S t r i n g из упражнения 1 3.44 раздела 1 3.5 (стр. 672), функ­ ции которого выводят сообщения при каждом вызове. Упражнение 1 3 .48. Определите вектор ve c t o r < S t r i n g> и вызовите для не­ го функцию pu s h b a c k ( ) несколько раз. Запустите программу и посмот­ рите, как часто копируются строки. _

� 13.6.2. Конст р укто р пе р емещения и п р исваивание п р и пе р емещении Подобно классу s t r i n g (и другим библиотечным классам), наши собствен­ ные классы могут извлечь пользу из способности перемещения ресурсов вме­ сто копирования. Чтобы позволить собственным типам операции перемеще­ ния, следует определить конструктор перемещения и оператор присваивания при перемещении. Эти члены подобны соответствующим функциям копиро­ вания, но они захватывают ресурсы заданного объекта, а не копируют их.



Как и у конструктора копий, у конструктора перемещения есть началь­ ный параметр, являющийся ссылкой на тип класса. В отличие от конст­ руктора копий, ссылочный параметр конструктора перемещения явля­ ется ссылкой на r-значение. Подобно конструктору копий, у всех допол­ нительных параметров должны быть аргументы по умолчанию.

Кроме перемещения ресурсов, конструктор перемещения должен гаран­ тировать такое состояние перемещенного объекта, при котором его удаление будет безопасно. В частности, сразу после перемещения ресурса оригиналь­ ный объект больше не должен указывать на перемещенный ресурс, ответст­ венность за него принимает вновь созданный объект.

1 3.6.

Пе р емещение об ъектов

677

В качестве примера определим конструктор перемещения для класса St rVe c, чтобы перемещать, а не копировать элементы из одного объекта клас­ са S t rVe c в другой: S t rVe c : : S t rVe c ( S t rV e c & & s )

n o e x c e p t / / перемещение не будет пер еда в а ть / / исклю чений 1 1 инициализ а торы членов получают ре сурсы из s : e l eme n t s ( s . e l eme n t s ) , f i r s t_ f r e e ( s . f i r s t_ f r e e ) , c ap ( s . c a p )

1 1 о с та вить s в сост оянии , при котором в а пуск деструктора без опа сен s . e l eme n t s = s . f i r s t _ f r e e = s . c ap = nu l l p t r ;

Оператор n o e x cept (уведомляющий о том, что конструктор не передает исключений) описан ниже, а пока рассмотрим, что делает этот конструктор . В отличие от конструктора копий, конструктор перемещения не резерви­ рует новую память; он получает ее от заданного объекта класса S t rVe c. По­ лучив область памяти от своего аргумента, тело конструктора присваивает указателям заданного объекта значение n u l lp t r . После перемещения ориги­ нальный объект продолжает существовать. В конечном счете оригинальный объект будет удален, а значит, будет выполнен его деструктор . Деструктор класса S t rVe c вызывает функцию de a l l o c a t e ( ) для указателя f i r s t_f r e e . Если забыть изменить указатель s . f i r s t_ f r e e, то удаление оригинального объекта освободит область памяти, которая была только что передана.

О перац ии перемещения, б и б лиотечн ы е конте й неры и исклю чения Поскольку операция перемещения выполняется при "захвате" ресурсов, она обычно не резервирует ресурсы. В результате операции перемещения обычно не передают исключений. Когда создается функция перемещения, не­ способная передавать исключения, об этом факте следует сообщить библиоте­ ке. Как будет описано вскоре, если библиотека не знает, что конструктор пе­ ремещения не будет передавать исключений, она предпримет дополнитель­ ные меры по отработке возможности передачи исключения при перемещении объекта этого класса. Один из способов сообщить об этом библиотеке - определить оператор n o e x cept в конструкторе. Введенный новым стандартом оператор noexcept подробно рассматривается в разделе 1 8.1 .4 (стр. 974), а пока дос­ таточно знать, что он позволяет уведомить, что функция не будет переда­ вать исключений. Оператор noex cept указывают после списка параметров функции. В конструкторе его располагают между списком параметров и символом : , начинающим список инициализации конструктора: cl a s s S t rVe c puЫ i c :

Глава 1 3. Уп равление копированием

678

S t rVe c ( S t rVe c & & ) n o e x c e p t ; / / конструктор перемещения / / другие члены , ка к прежде }; S t rVe c : : S t rVe c ( S t rVe c & & s ) n o e x c ep t { / * тело конструктора * / }

:

/ * инициализ а торы членов * /

Оператор noex cept следует объявить и в заголовке класса, и в определе­ нии, если оно расположено вне класса. Конструкторы перемещения и операторы присваивания при пе­ ремещении, которые не могут передавать исключения, должны быть отмечены как n o e x c ep t . Понимание того, почему необходим оператор n o e x c ept, может помочь уг­ лубить понимание того, как библиотека взаимодействует с объектами напи­ санных вами типов. В основе требования указывать, что функция перемеще­ ния не будет передавать исключения, лежат два взаимосвязанных факта: во­ первых, хотя функции п еремещения обычно не передают исключений, им это разрешено. Во-вторых, библиотечные контейнеры предоставляют гарантии относительно того, что они будут делать в случае исключения. Например, класс ve c t o r гарантирует, что, если исключение произойдет при вызове функции pu sh_ba c k ( ) , сам вектор останется неизменным. Теперь рассмотрим происходящее в функции pu s h_ba c k ( ) . Подобно соот­ ветствующей функции класса S t rVe c (см. раздел 1 3.5, стр. 667), функция pu sh_ba c k ( ) класса ve c t o r могла бы потребовать пересоздания вектора. При пересоздании вектор перемещает элементы из прежней своей области памяти в новую, как в функции rea l l o c a t e ( ) (см. раздел 1 3.5, стр . 671 ). Как только что упоминалось, перемещение объекта обычно изменяет со­ стояние оригинального объекта. Если пересоздание использует конструктор перемещения и этот конструктор передает исключение после перемещения некоторых, но не всех элементов, возникает проблема. Перемещенные эле­ ментов в прежнем пространстве были бы изменены, а незаполненные элемен­ ты в новом пространстве еще не будут созданы. В данном случае класс ve c t o r н е удовлетворял б ы требованию оставаться неизменным при исключении . С другой стороны, если класс ve c t o r использует конструктор копий, то при исключении он может легко удовлетворить это требование. В данном случае, пока элеменгы создаются в новой памяти, прежние элементы остаются неиз­ менными. Если происходит исключение, вектор может освободить зарезерви­ рованное пространство (оно могло бы и не быть успешно зарезервировано) и прекратить операцию. Элеменгы оригинального вектора все еще существуют. Во избежание этой проблемы класс ve c t o r должен использовать во время пересоздания конструктор копий вместо конструктора перемещения, если толъко не известно, что конструктор перемещения типа элемента не может передать исключение. Если необходимо, чтобы объекты типа были перемеще-

13.6. П еремещение об ъектов

679

ны, а не скопированы при таких обстоятельствах, как пересоздание вектора, то следует явно указать библиотеке, что использовать конструктор перемещения безопасно. Для этого конструктор перемещения (и оператора присваивания при перемещении) следует отметить как n o e x c ep t .

О ператор пр исваивания пр и перемещении Оператор присваивания при перемещении делает то же, что и деструктор с конструктором перемещения. Подобно конструктору перемещения, если оператор присваивания при перемещении не будет передавать исключений, то его следует объявить как n o e x c ep t . Подобно оператору присвоения копии, оператор присваивания при перемещении должен принять меры против при­ своения себя себе: S t rVe c & S t rVec : : ope r a t o r= ( S t rVe c & & rh s ) noex cept {

1 1 прямая пров ерка на присв оение себя себе i f ( th i s ! = & rhs ) { free ( ) ; / / осв ободить существующие элементы e l eme n t s = rhs . e l eme n t s ; / / получить ре сурсы о т rh s f i r s t - f r e e = rhs . f i r s t - f r e e ; с а р = rhs . cap ; 1 1 оставить rh s в удаляемом состоянии nu l l p t r ; rhs . e l eme nt s rhs . f i r s t f ree = rhs . cap retur n * th i s ;

В данном елучае осуществляется прямая проверка совпадения адресов в указателях rh s и th i s . Если это так, то правый и левый операнды относятся к тому же объекту, и делать ничего не надо. В противном случае следует осво­ бодить память, которую использовал левый операнд, а затем принять память от заданного объекта. Как и в конструкторе перемещения, указателю rhs присваивается значение n u l l p t r . Может показаться удивительным, что мы потрудились проверить присвоение себя самому. В ко�ще концов, присваивание при перемещении требует для пра­ вого операнда r-значения. Проверка осущесгвляется потому, что то r-значение могло быть результатом вызова функции move ( ) . Подобно любому другому опе­ ратору присвоения, крайне важно не освобождать ресурсы левого операнда пре­ жде, чем использовать (возможно, те же) ресурсы правого операнда.



И сходны й о б ъ ект пе р емещения должен б ы ть в удаляемом состоянии

Перемещение объекта не удаляет его оригинал: иногда после завершения операции перемещения оригинальный объект следует удалить. Поэтому, соз­ давая функцию перемещения, следует гарантировать, что после перемещения

680

Глава 1 3. Уп равление копированием.

оригинальный объект будет находиться в состоянии, допускающем запуск де­ структора. Функция перемещения класса S t rVe c выполняет это требование и присваивает указателям-членам оригинального объекта значение n u l lptr. Кроме гарантии безопасного удаления оригинального объекта, функции перемещения должны оставлять объект в допустимом состоянии. Обычно до­ пустимым считается тот объект, которому может быть безопасно присвоено новое значение или который может быть использован другими способами, не зависящими от его текущего значения. С другой стороны, у функций пере­ мещения нет никаких требований относительно значения, которое остается в оригинальном объекте. Таким образом, программы никогда не должны за­ висеть от значения оригинального объекта после перемещения. Например, при перемещении объекта библиотечного класса s t r i ng или контейнера известно, что оригинальный объект перемещения остается допус­ тимым. В результате для оригинальных объектов перемещения можно выпол­ нять такие функции, как emp t y ( ) или s i z e ( ) . Однако предсказать результат их выполнения затруднительно. Логично было бы ожидать, что оригинальный объект перемещения будет пуст, но это не гарантируется. Функции перемещения класса S t rVe c оставляют оригинальный объект пе­ ремещения в том же состоянии, в котором он находился бы после инициали­ зации по умолчанию. Поэтому все функции класса S t rVe c продолжат выпол­ няться с его объектом точно так же, как с любым другим инициализирован­ ным по умолчанию объектом класса S t rVe c . Другие классы, с более сложной внутренней структурой, могут вести себя по-другому.

& ВНИМАНИЕ

После операции перемещения "оригинальный объект" должен ос­ таться корректным, допускающим удаление объектом, но для пользователей его значение непредсказуемо.

С и нтезируе м ые функц ии пере м ещения Подобно конструктору копий и оператору присвоения копии, компилятор способен сам синтезировать конструктор перемещения и оператор присваивания при перемещении. Однако условия, при которых он синтезирует функ­ ции перемещения, весьма отличаются от тех, при которых он синтезирует функ­ ции копирования. Помните, что если не объявить собственный конструктор копий или опера­ тор присвоения копии, компилятор всегда синтезирует их сам (см. раздел 13.1 .1, стр . 631 и раздел 13.1 .2, стр . 634) . Функции копирования определяются или как функции по членного копирования либо присвоения объекта, или как удален­ ные функции.

13.6. Пе р емещение об ъектов

681

В отличие от функций копирования, для некоторых классов компилятор не синтезирует функции перемещения вообще. В частности, если класс определя­ ет собственный конструктор копий, оператор присвоения копии или деструк­ тор, конструктор перемещения и оператор присваивания при перемещении не синтезируются. В результате у некоторых классов нет конструктора пере­ мещения или оператора присваивания при перемещении. Как будет проде­ монстрировано вскоре, когда у класса нет функции перемещения, вместо него в результате обычного подбора функции будет использована соответствую­ щая функция копирования. Компилятор синтезирует конструктор перемещения или оператор при­ сваивания при перемещении, только если класс не определяет ни одной из собственных функций-членов управления копированием и если каждая неста­ тическая переменная-член класса может быть перемещена. Компилятор мо­ жет перемещать члены встроенного типа, а также члены типа класса, если у него есть соответствующая функция-член перемещения: 1 1 компилятор синтезируе т функции перемещения для Х и h a sX struct Х { i n t i ; / / в строенные типы могут быть перемещены s t d : : s t r i n g s ; / / s t ri n g определяе т с о б ств енные функции перемещения ; } st ruct hasX { Х тет ; 1 1 для Х синтезиров а ны функции перемещения }; Х х , х 2 = s t d : : тove ( x ) ; / / исполь зуе т синтезируемый конструктор 1 1 перемещения h a s X h x , h x 2 = s t d : : тove ( h x ) ; 1 1 исполь зуе т синтезируемый конструктор 1 1 перемещения

& ВНИМАНИЕ



Компилятор синтезирует конструктор перемещения и оператор присваивания при перемещении, только если класс не определяет ни одной из собственных функций-членов управления копирова­ нием и только если все переменные-члены могут быть созданы пе­ ремещением и присвоены при перемещении соответственно.

В отличие от функций копирования, функции перемещения никогда не определяются неявно как удаленные. Но если явно запросить компилятор создать функцию перемещения, применив = de fau l t (см. раздел 7.1 .4, стр. 345), но компилятор окажется неспособен переместить все члены, то функция перемещения будет определена как удаленная. Важное исклю­ чение из правила, согласно которому синтезируемая функция перемеще­ ния определяется как удаленная, подобно таковому для функций копи­ рования (см. раздел 1 3.1 .6, стр. 644) .

Глава 1 3. Управление копированием

682 •



В отличие от конструктора копий, конструктор перемещения определя­ ется как удаленный, если у класса есть член, определяющий собствен­ ный конструктор копий, но не определяющий конструктор перемеще­ ния, или если у класса есть член, который не определяет собственные функции копирования и для которого компилятор неспособен синтези­ ровать конструктор перемещения. То же относится к присваиванию при перемещении. Конструктор перемещения и оператор присваивания при перемещении определяются как удаленные, если у класса есть член, собственный кон­ структор перемещения которого или оператор присваивания при пере­ мещении которого удален или недоступен.



Как и конструктор копий, конструктор перемещения определяется как удаленный, если деструктор удален или недоступен.



Как и оператор присвоения копии, оператор присваивания при пере­ мещении определяется как удаленный, если у класса есть константный или ссылочный член.

Предположим, например, что в классе У определен собственный конструк­ тор копий, но не определен собственный конструктор перемещения: / / кла сс У определяе т соб ств енный кон с труктор копий , но не конс труктор 1 1 перемещения struct hasY { hasY ( ) de f a u l t ; h a s Y ( h a s Y & & ) = de f a u l t ; У mem ; / / h a s Y буде т иметь удаленный конс труктор перемещения }; h a s Y hy , h y2 s t d : : move ( h y ) ; / / ошибка : конструктор перемещения удален =

=

Компилятор может скопировать объекты типа У, но не может переместить их. Класс h a s Y явно запросил конструктор перемещения, который компилятор не способен создать. Следовательно, класс h a s Y получит удаленный конструк­ тор перемещения. Если бы у класса h a s Y отсутствовало объявление конструк­ тора перемещения, то компилятор не синтезировал бы конструктор перемеще­ ния вообще. Функции перемещения не синтезируются, если в противном слу­ чае они были определены как удаленные. И последнее взаимоотношение между функциями перемещения и синте­ зируемыми функциями-членами управления копированием: тот факт, опре­ деляет ли � асс собственные функции перемещения, влияет на то, как синте­ зируются функции копирования. Если класс определит любой конструктор перемещения и (или) оператор присваивания при перемещении, то синтези­ руемый конструктор копий и оператор присвоения копии для этого класса будуг определены как удаленные.

13.6. П еремещение об ъектов

683

Классы, определяющие конструктор перемещения или оператор присваивания при перемещении, должны также определять собст­ венные функции копирования. В противном ел учае эти функциичлены по умолчанию удаляются.

R -значения пе р емеща ю тся, а 1 -значения копи рую тся ... Когда у класса есть и конструктор перемещения и конструктор копий, компилятор использует обычный подбор функции, чтобы выяснить, какой из конструкторов использовать (см. раздел 6.4, стр. 305) . С присвоением точно так же. Например, в классе S t rVe c версия копирования получает ссылку на con s t S t rVe c. В результате она применима к любому типу, допускающему приведение к классу S t rVe c. Версия перемещения получает S t rVe c & & и при­ менима только к аргументам r-значениям (неконстантным): S t rVe c v l , v 2 ; v l = v2 ; S t rVe c g e t Ve c ( i s t r e am & ) ; v2 = ge tVe c ( c i n ) ;

// 11 // 11

v2 - 1 - зна чение ; присв оение копии ge t Ve c в озвраща е т r -зна чение ge t Ve c ( ci n ) - r - зна чение ; присв о ение перемещения

В первом случае оператору присвоения передается объект v2 . Его типом является S t rVe c, а выражение v2 является 1-значением. Версия присвоения при перемещении не является подходящей (см. раздел 6.6, стр. 31 7), поскольку нельзя неявно связать ссылку на r-значение с 1-значением. Следовательно, в этом елучае используется оператор присвоения копии. Во втором случае присваивается результат вызова функции ge tVe c ( ) , - это r-значение. Теперь подходящими являются оба оператора присвоения - ре­ зультат вызова функции ge tVe c ( ) можно связать с любым параметром опера­ тора. Вызов оператора присвоения копии требует преобразования в константу, в то время как S t rVe c & & обеспечивает точное соответствие. Следовательно, второе присвоение использует оператор присваивания при перемещении .

... но r-значения копируются, если нет конструкто р а пе р емещения Что если класс имеет конструктор копий, но не определяет конструктор перемещения? В данном случае компилятор не будет синтезировать конст­ руктор перемещения. Это значит, что у класса есть конструктор копий, но нет конструктора перемещения. Если у класса нет конструктора перемещения, подбор функции гарантирует, что объекты этого типа будут копироваться, даже при попытке перемещения их вызовом функции move ( ) : clas s Foo puЬ l i c : Foo ( )

de f a u l t ;

Глава 1 3 . Управление копированием

684

Foo ( c on s t Foo & ) ; /. / кон с труктор копий / / другие члены , но Fo o не определяе т конструктор перемещения }; Foo х ; Foo у ( х ) ; Foo z ( s t d : : mov e ( x ) ) ;

/ / конструктор копий ; х - это 1 - зна чение / / конструктор копий, по сколь ку конструктора 1 1 перемещения не т

Вызов функции move ( х ) при инициализации объекта z возвращает указа­ тель Foo & & , привязанный к объекту х. Конструктор копий для класса Foo явля­ ется подходящим, поскольку вполне допустимо преобразовать Fo o & & в con s t Foo & . Таким образом, инициализация объекта z использует конструктор копий класса Foo. Следует заметить, что использование конструктора копий вместо конст­ руктора перемещения почти безусловно безопасно (то же справедливо и для оператора присвоения) . Обычно конструктор копий отвечает требованиям со­ ответствующего конструктора перемещения: он копирует заданный объект и оставляет оригинальный объект в допустимом состоянии. Конструктор ко­ пий, напротив, не будет изменять значение оригинального объекта. Если у класса будет пригодный конструктор копий и не будет кон­ структора перемещения, то объекты будут перемещены конструк­ тором копий. То же справедливо для оператора присвоения копии и присвоения при перемещении.

О ператоры присвоения копии и о б мена и перемещение Версия класса Ha s P t r, определявшая оператор пр исвоения копии и обмена (copy-and-swap assignment operator) (см. раздел 1 3.3, стр . 657), - хорошая ил­ люстрация взаимодействия механизма подбора функции и функций переме­ щения. Если в этот класс добавить конструктор перемещения, то фактически будет получен также оператор присваивания при перемещении: c l a s s Ha s P t r { p uЫ i c : 1 1 до б а влен кон с труктор перемеще �ия Ha s P t r ( H a s P t r & & р ) n o e x c e p t : p s ( p . p s ) , i ( p . i ) { p . p s = О ; } / / опера тор присв оения - и опера тор перемещения , и присв оения копии Ha s P t r & ope r a t o r = ( H a s P t r r h s ) { s w a p ( * t h i s , rh s ) ; r e t u r n * t h i s ; 1 1 другие члены ка к в р . 1 3 . 2 . 1 ( с тр . 64 8 ) ; }

В этой версии класса добавлен конструктор перемещения, получающий зна­ чения из своего аргумента. Тело конструктора обнуляет указатель-член данного объекта класса Ha s Pt r, чтобы гарантировать безопасное удаление оригинального объекта перемещения. Эта функция не делает ничего, она не может передать ис­ ключение, поэтому отметим ее как noex cept (см. раздел 13.6.2, стр. 677).

13.6. Пе р емещение об ъектов

685

Теперь рассмотрим оператор присвоения. У него есть не ссылочный параметр, а значит, этот параметр инициализируется копией (см. раздел 13.1 . 1 , стр. 631 ). В зависимости от типа аргумента инициализация копией использует либо конст­ руктор копий, либо конструктор перемещения; 1-значения копируются, а r-значения перемещаются. В результате этот оператор однократного присвое­ ния дейсгвует и как присвоение коШ1И, и как присваивание при перемещении. Предположим, например, что объекты hp и hp 2 являются объектами класса Ha s P t r: hp hp

hp2 ;

/ / hp2 1 - зна чение ; для копиров а ния hp2 исполь зуе тся 1 1 конс труктор копий s t d : : move ( hp 2 ) ; / / hp2 перемеща е т конструктор перемещения -

В первом случае присвоения правый операнд - 1-значение, поэтому конст­ руктор перемещения не подходит. Для инициализации r h s будет использо­

ваться конструктор копий. Он будет резервировать новую строку и копиро­ вать ту строку, на которую указывает hp 2 . Во втором елучае присвоения вызывается функция s td : : rnove ( ) для связыва­ ния ссылки на r-значение с объектом hp2 . В данном случае подходят и конструк­ тор копий, и конструктор перемещения. Но поскольку аргумент - это ссылка на r-значение, точное соответствие обеспечит конструктор перемещения. Конструк­ тор перемещения копирует указатель из объекта hp2 и не резервирует память . Независимо от того, использовался ли конструктор копии или перемещения, тело оператора присвоения обменивает содержимое двух своих операндов. Об­ мен объектов класса Has Pt r приводит к обмену указателями-членами и пере­ менными-членами (типа i n t ) этих двух объектов. После вызова функции swap ( ) правый операнд будет содержать указатель на строку, который ранее принадле­ жал левому. При выходе rhs из области видимости эта строка будет удалена. СОВЕТ. О БНОВЛЕННОЕ ПРАВИЛО ТРЕХ

Все пять функций-членов управления копированием можно считать единым блоком: если класс определяет любую из этих функций, он должен обычно определять их все. Как уже упоминалось, для правильной работы некоторые классы должны определять конструктор копий, оператор при­ своения копии и деструктор (см. раздел 1 3.6, стр. 640) . Как правило, у таких классов есть ресурс, который должны копировать функции-члены копиро­ вания. Обычно копирование ресурса влечет за собой некоторые дополни­ тельные затраты. Классы, определяющие конструктор перемещения и опе­ ратор присваивания при перемещении, могут избежать этих затрат в тех обстоятельствах, где копия не обязательна.

Глава 1 3. Уп р авление копир ованием

686

Ф ункц ии перемещения для класса Mes sage Классы, определяющие собственный конструктор копий и оператор при­ своения копии, обычно определяют и функции перемещения. Например, на­ ши классы Me s s age и Fo l de r (см. раздел 1 3.4, стр. 658), должны определять функции перемещения. При определении функций перемещения класс Me s s age может использовать функции перемещения классов s t r i ng и s e t, чтобы избежать дополнительных затрат при копировании членов c o n t e nts и f o l de r s . Н о в дополнение к перемещению члена f o l de r s следует также обновить каждый объект класса Fo l de r, указывавший на оригинал объекта класса Me s s age. Следует также удалить указатели на прежний объект класса Me s s age и добавить указатели на новый. И конструктор перемещения, и оператор присваивания при перемещении должны обновлять указатели Fo l de r, поэтому начнем с определения функ­ ций для выполнения этих действий: / / переме стить ука за тели Fo l de r из т в данное Me s s a ge vo i d Me s s a ge : : move _ Fo l de r s ( Me s s a ge * m ) { f o l de r s = s t d : : move ( m- > f o l de r s ) ; / / исполь зуе т присв о ение перемещения 1 1 кла сса s e t f o r ( a u t o f : f o l de r s ) { / / для каждого Fo l de r f - > remМ s g ( m ) ; / / удалить старый Me s s a ge из Fo l de r f - > a d dMs g ( t h i s ) ; / / доба вить э т о т Me s s a ge в э т о т Fo l de r m- > f o l de r s . c l e a r ( ) ;

/ / гара нтиров а ть без опа сное удаление т

Функция начинает работу с перемещения набора f o l de r s . При вызове функции move ( ) используется оператор присвоения при перемещении класса s e t, а не его оператор присвоения копии. Если пропустить вызов функции move ( ) , код все равно будет работать, но осуществляя ненужное копирование. Затем функция перебирает папки, удаляя указатель на оригинал сообщения и добавляя указатель на новое сообщение. Следует заметить, что вставка элемента в набор может привести к передаче исключения, поскольку добавление элемента на контейнер требует резерви­ рования памяти, вполне может быть передано исключение bad_a l l oc (см. раздел 1 2.1 .2, стр . 586). Таким образом, в отличие от функций перемеще­ ния классов Ha s P t r и S t rVe c, конструктор перемещения и операторы при­ сваивания при перемещении класса Me s s age могли бы передать исключения, поэтому не будем отмечать их как noex cept (см. раздел 1 3.6.2, стр. 677) .

13.6. Перемещение об ъектов

687

Функция заканчивается вызовом функции c l e a r ( ) объекта m . f o l de r s . Известно, что после перемещения объект m . f o l de r s вполне допустим, но его содержимое непредсказуемо. Поскольку деструктор класса Me s s age переби­ рает набор f o l de r s , необходимо убедиться, что набор пуст. Конструктор перемещения класса Me s s age вызывает функцию move ( ) , чтобы переместить содержимое и инициализировать по умолчанию свой член fo l de r s : Me s s ag e : : Me s s a ge ( Me s s a ge & &m ) : {

move_Fo l de r s ( & m ) ;

c o n t e n t s ( s t d : : move ( m . c o n t e n t s ) )

1 1 переме с тить fo l de r s и обновить ука з а тели Fo l de r

В теле конструктора происходит вызов функции move _ Fo 1 de r s ( ) , чтобы удалить указатели на m и вставить указатели на данное сообщение. Оператор присваивания при перемещении непосредственно проверяет елучай присвоения себя себе: Me s s a ge & Me s s a ge : : ope r a t o r= ( Me s s a ge & & r h s ) { i f ( thi s ! = & rhs ) { / / пряма я пр ов ерка присв о ения себя себе r emove_ f r om_ Fo l de r s ( ) ; c o n t e n t s = s t d : : move ( r h s . c o n t e n t s ) ; / / присв оение при пер емещении move_Fo l de r s ( & r h s ) ; / / сбро сить па пки , чтобы ука зыв а ть на это 1 1 с о о бщение return * th i s ;

Подобно любым операторам присвоения, оператор присваивания при пере­ мещении должен удалить прежние данные левого операнда. В данном случае удаление левого операнда требует удаления указателей на это сообщение из су­ щесгвующих папок, что и делает вызов функции remove _f rom_Fo l de r s ( ) . По­ сле удаления из папок происходит вызов функции move ( ) , чтобы переместить content s из объекта rh s в thi s . Остается только вызвать функцию move _ Fo l de r s ( ) , чтобы модифицировать указатели Fo l de r .

И тераторы перемещения Функция rea l l o c a t e ( ) класса S t rVe c (см. раздел 1 3.5, стр. 671 ) использо­ вала вызов функции c o n s t ru c t ( ) в цикле f o r для копирования элементов из прежней памяти в новую. Альтернативой циклу был бы просто вызов функ­ ции un i n i t i a l i z e d_copy ( ) для создания нового пространства в памяти. Од­ нако функция un i n i t i a l i z e d_cop y ( ) делает именно то, о чем говорит ее имя: она копирует элементы. Нет никакой аналогичной библиотечной функ­ ции для перемещения объектов в пустую память.

688

Глава 1 3. Уп р авление копированием

Вместо нее новая библиотека определяет адаптер и тератора перемеще­ ния (move iterator) (см. раздел 1 0.4, стр. 5 1 4). Итератор перемещения адаптирует переданный ему итератор, изменяя поведение его оператора обращения к значению. Обычно оператор обращения к значению ите­ ратора возвращает ссылку на !-значение элемента. В отличие от других итераторов, оператор обращения к значению итератора перемещения возвращает ссылку на r-значение. Обычный итератор преобразуется в итератор перемещения при вызове библиотечной функции ma ke_move_i t e r a t o r ( ) , которая получает итератор и возвращает итератор перемещения. Все остальные функции первоначального итератора работают, как обычно. Поскольку эти итераторы поддерживают обычные функции итераторов, пару итераторов перемещения вполне можно передать алгоритму. В частности, итераторы перемещения можно передать алгоритму un i n i t i a l i z e d_ copy ( ) : vo i d S t rVe c : : r e a l l o c a t e ( ) { / / зарезервиров а ть вдв ое больше про с транств а , чем для те кущего 1 1 количе ств а элементов auto newcapa c i t y = s i z e ( ) ? 2 * s i z e ( ) : 1 ; a u t o f i r s t = a l l o c . a l l o c a t e ( n e w c a pa c i t y ) ; / / переме с тить элементы a u t o l a s t = u n i n i t i a l i z e d_c o p y ( rna ke_rnove_i t e r a t o r ( be g i n ( ) ) , rna ke_rnove_i t e r a t o r ( e n d ( ) ) , f i rs t ) ; / / о св о б одить прежне е про с транств о f ree ( ) ; e l erne n t s = f i r s t ; / / обновить ука за тели f i r s t free = las t ; с а р = e l erne n t s + n e w c apa c i t y ;

Алгоритм un i n i t i a l i z e d_ copy ( ) вызывает функцию c on s t ru c t ( ) для каждого элемента исходной последовательности, чтобы скопировать элемент по назначению. Для выбора элемента из исходной последовательности данный алгоритм использует оператор обращения к значению итератора. Поскольку был передан итератор перемещения, оператор обращения к значению воз­ вращает ссылку на r-значение. Это означает, что функция co n s t ru c t ( ) будет использовать для создания элементов конструктор перемещения. Следует заметить, что стандартная библиотека не дает гарантий примени­ мости всех алгоритмов с итераторами перемещения. Так как перемещение объекта способно удалить оригинал, итераторы перемещения следует пере­ дать алгоритмам, только тогда, когда вы уверены, что алгоритм не будет обра­ щаться к элементам после того, как он присвоил этот элемент или передал его пользовательской функции.

13.6. Пе р емещение объектов

689

СОВЕТ . НЕ СЛИШКОМ СПЕШИТЕ С ПЕРЕМЕ Щ ЕНИЕМ

Поскольку состояние оригинального объекта перемещения неопреде­ ленно, вызов для него функции s t d : : move ( ) - опасная операция. Когда происходит вызов функции move ( ) , следует быть абсолютно уверенным в том, что у оригинального объекта перемещения не может быть никаких других пользователей. Взвешенно использованная в коде класса, функция move ( ) способна обеспечить существенный выигрыш в производительности . Небрежное ее использование в обычном пользовательском коде (в отличие от кода реали­ зации класса), вероятней всего, приведет к загадочным и трудно обнаружи­ ваемым ошибкам, а не к повышению производительности приложения. Рекомендуем

За пределами кода реализации класса, такого как конструкторы перемещения или операторы присваивания при перемещении, используйте функцию s t d : : move ( ) только при абсолютной уве­ ренност и в необходимости перемещения и в том, что перемещение гарантированно будет безопасным.

Упражнения раздела

1 3 .6.2

Упражнение 1 3.49. Добавьте конструктор перемещения и оператор при­ сваивания при перемещении в классы S t rVe c, S t r i ng и Me s s age . Упражнение 1 3 .50. Снабдите функции перемещения класса S t r i ng опера­ торами вывода и снова запустите программу из упражнения 1 3.48 разде­ ла 1 3.6.1 (стр. 676), в котором использовался вектор ve c t o r < S t r i n g>, и по­ смотрите, когда теперь удается избежать копирования. Упражнение 1 3 .51 . Хотя указатель un i qu e_pt r не может быть скопирован, в разделе 1 2.1 .5 (стр . 601 ) была написана функция c l one ( ) , которая возвра­ тила указатель u n i qu e_p t r по значению. Объясните, почему эта функция допустима и как она работает. Упражнение 1 3 .52. Объясните подробно, что происходит при присвоении объектов класса Ha s P t r на стр . 685. В частности, опишите шаг за шагом, что происходит со значениями hp, hp2 и параметром r h s в операторе присвое­ ния класса Ha s P t r. Упражнение 1 3.53. С точки зрения низкоуровневой эффективности опера­ тор присвоения класса Ha s P t r не идеален. Объясните почему. Реализуйте для класса Ha s P t r оператор присвоения копии и присваивания при пере­ мещении и сравните действия, выполняемые в новом операторе присваива­ ния при перемещении, с версией копии и обмена.

Глава 1 3 . Управление копированием

690

Упражнение 1 3 .54. Что бы случилось, если бы мы определи оператор при­ сваивания при перемещении для класса Ha s P t r, но не изменили оператор копии и обмена? Напишите код для проверки вашего ответа.

� 13.6.3. С сылки на r-значение и функции-члены Все функции-члены, кроме конструкторов и операторов присвоения, могут извлечь пользу из предоставления версии копирования и перемещения. Такие функции-члены с поддержкой перемещения обычно используют ту же схему параметров, что и конструктор копий / перемещения и операторы присвое­ ния, - одна версия получает ссылку на константное 1-значение, а вторая ссылку на не константное r-значение. Например, библиотечные контейнеры, определяющие функцию push b a c k ( ) , предоставляют две версии: параметр одной является ссылкой на r-значение, а другой - ссылкой на константное 1-значение. С учетом того, что х является типом элемента, эти функции контейнера определяются так: _

vo id pu s h_b a c k ( c o n s t Х & ) ; vo i d p u s h _ba c k ( X & & ) ;

1 1 копиров ание : привяз ка к любому Х / / перемещение : привязка толь ко к изменяемым 1 1 r - зна чениям типа Х

Первой версии функции pu s h b a c k ( ) можно передать любой объект, кото­ рый может быть приведен к типу х . Эта версия копирует данные своего пара­ метра. Второй версии можно передать только r-значение, которое не является константой. Эта версия точнее и лучшее соответствует неконстантным r-значениям и будет выполнена при передаче поддающегося изменению r-значения (см. раздел 1 3.6.2, стр . 683) . Эга версия способна захватить ресурсы своего параметра. Обычно нет никакой необходимости определять версии функций полу­ чающих co n s t Х & & или просто Х & . Обычно ссылку на r-значение передают при необходимости "захватить" аргуме.нт. Для этого аргумент не должен быть константой. Точно так же копирование объекта не должно изменять скопиро­ ванный объект. В результате обычно нет никакой необходимости определять версию, получающую простой параметр Х & . _

·

У перегруженных функций, различающих перемещение и копи­ рование параметра, обычно есть одна версия, получающая пара­ метр типа c on s t Т &, и вторая, получающая параметр типа Т & & . В качестве более конкретного примера придадим классу S t rVe c вторую версию функции pu s h ba c k ( ) : _

c l a s s S t rVe c puЬ l i c :

13.6. Пе р емещение объектов

691

vo i d p u s h_b a c k ( c o n s t s t d : : s t r i n g & ) ; / / копируе т элемент vo i d pu s h_ba c k ( s t d : : s t r i n g & & ) ; 1 1 перемеща ет элемент / / другие члены ка к прежде }; 1 1 неизменно с оригинальной в ерсии в ра зделе 1 3 . 5 (стр . 6 6 7 ) vo i d S t rVe c : : pu s h_b a c k ( c o n s t s t r i n g & s ) {

c h k _n_a l l o c ( ) ; / / удо стов ерить ся в наличии ме ста для другого элемента 1 1 созда ть копию s в элементе , на который ука зыв а е т fi rs t fre e a l l o c . c o n s t ru c t ( f i r s t f r e e + + , s ) ;

vo i d S t rVe c : : p u s h_ba c k ( s t r i ng & & s ) {

c h k _n_a l l o c ( ) ; / / пер е с о зда е т S t r Ve c при не обходимо сти a l l o c . c o n s t ru c t ( f i r s t f r e e + + , s t d : : rnove ( s ) ) ;

Эти функции-члены почти идентичны. Различие в том, что версия ссылки на r-значение функции pu s h_ba c k ( ) вызывает функцию move ( ) , чтобы пере­ дать этот параметр функции co n s t ru c t ( ) . Как уже упоминалось, функция con s t ru c t ( ) использует тип своего второго и последующих аргументов для определения используемого конструктора. Поскольку функция move ( ) воз­ вращает ссылку на r-значение, аргумент функции co n s t ru c t ( ) будет иметь тип s t r i n g & & . Поэтому для создания нового последнего элемента будет ис­ пользован конструктор перемещения класса s t r i ng. Когда вызывается функция pu s h _ba c k ( ) , тип аргумента определяет, ко­ пируется ли новый элемент в контейнер или перемещается: S t rVe c v e c ; / / пустой S t rVe c s t r i ng s = " s orne s t r i n g o r a n o t he r " ; ve c . pu s h_b a c k ( s ) ; / / выз о в p u sh_ba ck (con s t s t ri n g & ) ve c . pu s h_ba c k ( " do n e " ) ; / / выз ов p u sh_ba ck ( s t ri n g & & )

Эти вызовы различаются тем, является ли аргумент 1-значением ( s ) или r-значением (временная строка, созданная из слова " done " ) . Вызовы распо­ знаются соответственно.

Ссылки на 1 - значения, r- значения и функц ии-член ы Обычно функцию-член объекта можно вызвать независимо от того, являет­ ся ли этот объект 1- или r-значением. Например: s t r i ng s l = " а v a l u e " , s 2 = " an o t h e r " ; auto n = ( s l + s 2 ) . f i n d ( ' a ' ) ;

Здесь происходит вызов функции-члена f i n d ( ) (см. раздел 9.5.3, стр . 467) для r-значения класса s t r i n g, полученного при конкатенации двух строк. Иногда такой способ применения может удивить: sl + s2

=

" wow ! " ;

692

Глава 1 3. Управление копир ованием

Здесь r-значению присваивается результат конкатенации двух строк. До нового стандарта не было никакого способа предотвратить подобное применение. Для обеспечения совместимости с прежней версией библиотечные классы продолжают поддерживать присвоение r-значению; в собственных клас­ сах такое может понадобиться предотвратить. В таком елучае левый операнд (т.е. объект, на который указывает указатель th i s ) обязан быть 1-значением. rc++l

Свойство 1- или r-значения указателя t h i s задают таким же образом, как и константность функции-члена (см. раздел 7.1 .2, стр. 337) : помещая ква­ лификатор ссъzлкu (reference qualifier) после списка параметров :

L1LJ

{

clas s Foo puЫ i c : Foo

& op e r a t o r = ( c on s t

Foo & )

&;

1 1 в озможно присв оение толь ко · / / изменяемым 1 -зна чениям

1 1 другие члены кла с са Fo o }; Foo

& Foo : : ope r a t o r = ( c o n s t

Foo

& rh s )

&

1 1 сдела ть в се не о бходимо е для присв оения rh s э тому объекту return

* th i s ;

Квалификаторы ссылки & или & & означают, что указатель th i s может ука­ зывать на r- или 1-значение соответственно. Подобно спецификатору c o n s t, квалификатор ссылки может быть применен только к (нестатической) функ­ ции-члену и должен присутствовать как в объявлении, так и в определении функции. Функцию, квалифицированную символом & , можно применить только к 1-значению, а функцию, квали фицированную символом & & , только к r-значению: -

Foo

// // 11 11 11 11 11 11

& r e t Fo o ( ) ;

Foo r e tVa l ( ) ; Foo

i,

j ;

i = j ; re t Foo ( )

=

j ;

r e tVa l ( )

=

j ;

i =

r e tVa l ( ) ;

в о звраща е т с сьитку ; выз ов re t Fo o ( ) являе т ся 1 -зна чением в о звраща е т зна чение ; выз ов re t Va l ( ) - r - зна чение i и j - э то 1 - зна чения ok : i - э то 1 - зна чение ok : re t Foo ( ) в озвраща е т 1 -зна чение ошибка : re t Va l ( ) в о звраща е т r -зна чение ok : вполне можно переда ть r - зна чение ка к пра вый операнд присв оения

Функция может быть квалифицирована и ссылкой, и константой. В таких случаях квалификатор ссылки должен следовать за спецификатором co n s t : class

Foo

{

puЫ i c : F o o s orneMern ( )

&

F o o a n o t h e rMern ( )

};

con s t ; con s t

/ / ошибка : первым должен быть специфика тор co n s t & ; 1 1 ok : специфика тор con s t ра сположен первым

13.6. Пе реме щение объектов

693

П ерегрузка и сс ылочные функц ии Подобно тому, как можно перегрузить функцию-член на основании кон­ стантности параметра (см. раздел 7.3.2, стр . 359), ее можно перегрузить на ос­ новании квалификатора ссылки. Кроме того, функцию можно перегрузить на основании квалификатора ссылки и константности. В качестве примера при­ дадим классу Foo член типа ve c t o r и функцию s o rted ( ) , возвращающую копию объекта класса Foo, в котором сортируется вектор: cl a s s Foo { puЫ i c : / / применимо к изменяемым r - зна чениям Foo s o r t e d ( ) & & ; F o o s o r t e d ( ) c o n s t & ; 1 1 применимо к любому о бъ е кту кла сса Fo o / / другие члены кла с са Fo o private : ve c t o r < i n t > da t a ; }; 1 1 это т о бъ е кт - r - зна чение , поэтому его можно сортиров а ть на ме с те Foo F o o : : s o r t e d ( ) & & s o r t ( da t a . b e g i n ( ) , return * thi s ;

data . end ( ) ) ;

} 1 1 э т о т о бъ е кт либо конс та нта , либо 1 - зна чение ; та к или ина че , его нель зя 1 1 сортиров а ть на ме с те Foo F o o : : s o r t e d ( ) c o n s t & { / / с о зда е т копию F o o r e t ( * th i s ) ; s o r t ( r e t . da t a . be g i n ( ) , r e t . d a t a . e n d ( ) ) ; 1 1 сортируе т ко пию / / в о звраща е т копию return ret ;

При выполнении функции s o r t e d ( ) для r-значения вполне безопасно сор­ тировать ве�тор-член da ta непосредственно. Объект является r-значением, а это означает, что у него нет никаких других пользователей, поэтому данный объект можно изменить непосредственно. При выполнении функции s o r ted ( ) для константного r- или !-значения изменить этот объект нельзя, поэтому перед сортировкой вектор-член da t a необходимо скопировать. Поиск перегруженной функции использует свойство 1-значение /r-значение объекта, вызвавшего функцию s o rted ( ) для определения используемой версии: retVa l ( ) . s o r t e d ( ) ; retFoo ( ) . s o r ted ( ) ;

/ / re t Va l ( ) / / re t Fo o ( )

- э то r - va l u e , выз ов Fo o : : s o r t e d ( ) - э то 1 - va l u e , выз ов Fo o : : s o r t e d ( )

&& con s t &

При определении константных функций-членов можно определить две версии, отличающиеся только тем, что одна имеет квалификатор con s t, а другая нет. Для ссылочной квалификации функций ничего подобного по умолчанию нет. При определении двух или более функций-членов с тем же именем и тем же списком параметров следует предоставить квалификатор ссылки для всех или ни для одной из этих функций:

Глава 1 3. Уп равление копированием

694

c l a s s Foo { puЫ i c : Foo s o r t e d ( ) & & ; F o o s o r t e d ( ) c o n s t ; / / ошибка : должен быть кв алифика тор ссылки / / Сотр - пс евдоним для типа функции ( см . р . 6 . 7 , стр . 32 4 ) / / он применим для сра внения цело численных зна чений u s i n g С отр = bo o l ( c o n s t i n t & , c o n s t i n t & ) ; / / o k : другой список параме тров F o o s o r t e d ( C ornp * } ; Foo s o r t e d ( C ornp * ) c o n s t ; / / o k : ни одна из в ер сий не кв алифициров ана 1 1 ка к ссылка } ;

Здесь объявление константной версии функции s o r t e d ( ) без параметров является ошибкой. Есть вторая версия функции s o r t e d ( ) без параметров, и у нее есть квалификатор ссылки, поэтому у константной версии этой функ­ ции также должен быть квалификатор ссылки. С другой стороны, те версии функции s o r t e d ( ) , которые получают указатель на функцию сравнения, прекрасно работают, поскольку ни у одной из функций нет спецификатора. Если у функции-члена есть квалификатор ссылки, то у всех версий этой функции-члена с тем же списком параметров должны быть квалификаторы ссылки. Упражнения раздела

1 3 .6.3

Упражнение 13.55. Добавьте в класс StrBlob функцию push_back() в версии ссылки на r-значение. Упражнение 1 3 .56. s o r ted ( ) :

Что

бы

было

при

таком

определении

функции

Foo Foo : : s o r t e d ( ) c o n s t & Foo r e t ( * t h i s ) ; return ret . s o rted ( ) ;

Упражнение

1 3 .57.

Что если бы функция s o r t e d ( ) была определена так:

Foo Foo: :sorted() const & { return Foo(*this) .sorted(); } Упражнение 1 3 .58. Напишите версию класса Foo с операторами вывода в функциях s o r t e d ( ) , чтобы проверить свои ответы на два предыдущих упражнения.

Резю ме Каждый класс контролирует происходящее при копировании, перемещении, присвоении и удалении объектов его типа. Эги действия определяют специаль­ ные функции-члены: конструктор копий, конструктор перемещения, оператор присвоения копии, оператор присваивания при перемещении и деструктор.

695

Термины

Консrруктор перемещения и оператор присваивания при перемещении (обычно неконстангный) получают ссылку на r-значение; версии оператора копирования (обычно констангные) получают обычную ссылку на 1-значение. Если класс не объявит ни одну из этих функций, то компилятор определит их автоматически. Если они не определены как удаленные, эти функции­ члены инициализирует, перемещают, присваивают и удаляют объект, обраба­ тывая каждую нестатическую переменную-член по очереди. Синтезируемая функция делает то, что соответствует типу элемента для перемещения, копи­ рования, присвоения и удаления этого элемента. Классы, резервирующие память или другие ресурсы, почти всегда требуют, чтобы класс определил функции-члены управления копированием для управ­ ления зарезервированным ресурсом. Если класс нуждается в деструкторе, то он почти наверняка должен определить конструкторы перемещения и копи­ рования, а также операторы перемещения и присвоения копии.

Т е р мин ы Деструктор (destructor). Специальная функция-член, освобождающая занmую объектом память, когда он выходит из обласrи видим осги или удаляется. Компилятор автомати­ чески удаляет каждый член класса. При удалении переменных-членов типа класса ис­ пользуются их собсгвенные десrрукторы, а при удалении переменных-членов встроен­ ного или сосгавного типа конструктор ничего не делает. В частносги, объект, на кото­ рый указывает указатель-член класса, автоматически не удаляется деструктором. И нициализация копией (сору initialization). Форма инициализаuии с использованием оператора = и предоставления инициализатора для создаваемого объекта. Использу­ ется также при передаче и возвращении объекта по значению, при инициализации массива или агрегатного класса. Инициализация копией использует конструктор ко­ пий или конструктор перемещения, в зависимости от того, является ли инициализа­ тор 1- или r-значением. И тератор перемещения (move iterator) . Адаптер, позволяющий создать итератор, об­ ращение к значению которого возвращает ссылку на r-значение. Квалификатор ссылки (reference qualifier). Символ, обычно указывающий, что нестати­ ческая функция-член может быть вызвана для 1- или r-значения. Спецификатор & или & & следует за списком параметров или спецификатором co n s t, если он есть. Функ­ ция с квалификатором & может быть вызвана только для 1-значений, а функция с ква­ лификатором & & только для r-значений. Конструкто р копий (сору constructor). Конструктор, который инициализирует новый объект как копию другого объекта того же типа. При передаче объекта в функцию или из функции конструктор копий применяется неявно. Если конструктор копий не определен явно, комп илятор синтезирует его самостоятельно. Конст руктор перемещения (move constructor) . Конструктор, получающий ссылку на r-значение своего типа. Как правило, конструктор перемещения перемещает данные -

696

Глава 1 3. Управление копированием

своего параметра во вновь созданный объект. После перемещения запуск деструктора для правого операнда должен быть безопасен. Копи рован ие и обмен (сору and swap). Техника написания операторов присвоения за счет копирования правого операнда, сопровождаемого вызовом функции swap ( ) , обменивающей копию с левым операндом. Оператор пр исваивания пр и перемещении (move-assignment operator). Версия опера­ тора присвоения, получающая ссылку r-значения на ее тип. Как правило, оператор присваивания при перемещении переме1цает данные из правого операнда в левый. После присвоения запуск деструктора для правого операнда должен быть безопасен. Опе ратор пр исвоения копии (copy-assignment operator) . Версия оператора присвоения, получающая объект того же типа, что и у нее. Обычно оператор присвоения копии имеет параметр, являющийся ссылкой на константу, и возвращает ссылку на свой объект. Компилятор сам с и:нтезирует оператор присвоения копии, если класс не пре­ доставляет его явно. П ерегруженный опе ратор (overloaded operator). Функция, переопределяющая один из операторов для работы с операндами данного класса. В этой главе описано определе­ ние лишь оператора присвоения, а более подробно перегрузка операторов рассмат­ ривается в главе 1 4 . Почленное копи р ование и п р исвоение (memberwise сору / assign) . Так работают син­ тезируемые конструкторы копирования и перемещения, а также операторы при­ сваивания при переме1цении и копи. Перебирая все нестатические переменные­ члены по очереди, синтезируемый конструктор копий или перемещения инициа­ лизирует каждую из них, копируя или при перемещая соответствующее значение из заданного объекта; оператор присваивания при перемещении и копии присваи­ вают при переме1цении или копируют каждую переменную-член правого объекта в левый. Инициализация и присвоение переменных-членов встроенного или со­ ставного типа осу1цествляются непосредственно, а членов типа класса - с исполь­ зованием соответствуюrцего конструктора перемещения или копирования либо оператора присвоения копии или присваивания при перемещении. С интезируемые конструкторы копирования и перемещения (synthesized сору /move constructor). Версии конструкторов копирования и перемещения, синтезируемые компилятором лля классов, которые не определяют соответствующие конструкторы явно. Если они не определены как удаленные функции, синтезируемые конструкто­ ры копирования и перемещения почленно инициализируют новый объект, копируя или перемещая члены из заданного объекта. Синтези руемы й дест р уктор (synthesized destructor). Версия деструктора, создаваемая (синтезируемая) компилятором для классов, в которых он не определен явно. Тело синтезируемого деструктора пусто. Синтезируемы й оператор пр исвоения (synthesized assignment operator). Версия опера­ тора присвоения, создаваемого (синтезируемого) компилятором для классов, у которых он не определен явно. Если он не определен как удаленная функция, синтезируемый оператор присвоения почленно присваивает (перемещает) правый операнд левому. Ссылка на 1-значение (1-value reference). Ссылка, которая может быть связана с 1-зна­ чением. Ссылка на r-з начение (r-value reference). Ссылка на объект, который будет удален.

Те р мины

697

Счетчик ссылок (reference count). Программное средство, обычно используемое в чле­ нах управления копированием. Счетчик ссылок отслеживает количество объектов, совместно использующих некую сущность. Конструкторы (кроме конструкторов ко­ пирования и перемещения) устанавливают счетчик ссылок в 1 . Каждый раз, когда создается новая копия, значение счетчика увеличивается . Когда объект удаляется, значение счетчика уменьшается. Оператор присвоения и деструктор проверяют, не достиг ли декремент счетчика ссылок нуля, и если это так, то они удаляют объект. Удаленн а я функция (deleted function) . Функция, которая не может быть использована. Для удаления функции в ее объявление включают часть = de l e t e. Обычно удален­ ные функции используют для запрета компилятору синтезировать операторы копи­ рования и (или) перемещения для класса. Уп равлен ие копирован ием (сору control). Специальные функции-члены, которые оп­ ределяют действия, осуществляемые при копировании, присвоении и удалении объ­ ектов класса. Если эти функции не определены в классе явно, компилятор синтезиру­ ет их самостоятельно. Функция move ( ) . Библиотечная функция, обычно используемая для связи ссылки r-значения с 1-значением. Вызов функции move ( ) неявно обещает, что объект не будет использован для перемещения, кроме его удаления или присвоения нового значения.

ГЛАВА

14

П Е Р Е Г РУЗ К А О П Е Р АТ О Р О В И П Р Е О Б РАЗ О В А Н И Й

В э т о й главе ... Фундаментальные концепции 14.2. Операторы ввода и вывода 14.3. Арифметические операторы и операторы отношения 14.4. Операторы присвоения 14.5. Оператор индексировани я 1 4.6. Операторы инкремента и декремента 1 4.7. Операторы доступа к членам 14.8. Оператор вызова функции 14.9. Перегрузка, преобразование и операторы Рез ю ме Термины

14.1 .

700 705 71 0

71 3 71 5 71 7 720

722 732 744 745

Как упоминалось в главе 4, язык С++ предоставляет для встроенных типов множество операторов и автоматических преобразований . Они позволяют создавать разнообразные выражения, где используются разные типы данных . Язык С++ позволяет переопределять смысл операторов, применяемых для объектов типа класса, а также определять для класса функции преобразова­ ния типов. Функции преобразования типа класса используются подобно встроенным преобразованиям для неявного преобразования (при необходи­ мости) объекта одного типа в другой. Перегрузка оператора (overloaded operator) позволяет определить смысл оператора, когда он применяется к операнду (операндам) типа класса. Разум­ ное применение перегрузки операторов способно упростить программы, об­ легчить их написание и чтение. Например, поскольку наrп первоначальный класс S a l e s_i t em (см . раздел 1 .5.1 , стр . 47) определял операторы ввода, выво­ да и суммы, сумму двух объектов класса s а 1 е s _ i t em можно вывести так: c o u t . Как обычно, при индексировании карты возвращается ссылка на ассоции­ рованное значение. При индексировании карты Ь i n op s возвращается ссылка на объект типа fu n c t i on . Тип fun c t i o n перегружает оператор вызова. Этот оператор вызова получает собственные аргументы и передает их хранимому вызываемому объекту: b i n op s Ьinops Ьi nops Ьi nops Ь i n op s

[ "+" ] [ "-" ] ["/" ] ["*"] ["%" ]

(10, (10, (10, (10, (10,

5) 5) 5) 5) 5)

; ; ; ; ;

11 11 11 11 11

выз ов a dd ( l O , 5 ) исполь з уе т опера тор ; выз ов объе кта m i n u s исполь зуе т опера тор ; выз ов о бъ екта di v выз ов о бъ екта лямбда - функции выз ов о бъ екта лямбда - функции

Здесь происходит вызов каждой из операций, хранимых в карте Ь i nop s . В первом вызове возвращаемый элемент является указателем на функцию, указывающим на функцию a dd ( ) . Вызов b i n op s [ " + " ] ( 1 О , 5 ) использует этот указатель для вызова функции a dd с передачей ей значений 1 О и 5 . Сле-

Глава 1 4. Перегрузка операторов и преобразований

732

дующий вызов, Ь i n op s [ " - " ] , возвращает объект класса f u n c t i o n, хранящий объект типа s t d : : mi n u s < i n t > . Затем можно вызвать оператор этого объекта и других.

П ерегруженные ф ункции и тип function Нельзя непосредственно хранить имя перегруженной функции в объекте типа fu n c t i o n : i n t a dd ( i n t i , i n t j ) { r e t u r n i + j ; } S a l e s _da t a add ( c o n s t S a l e s _da t a & , c o n s t S a l e s da t a & ) ; map < s t r i n g , fu n c t i o n < i n t ( i n t , i n t ) > > b i n o p s ; Ь i nop s . i n s e r t ( { " + " , a dd } ) ; / / ошибка : ка кой именно a dd ?

Один из способов разрешения двусмысленности подразумевает хранение указателя на функцию (см. раздел 6.7, стр . 322) вместо имени функции: i n t ( * fp ) ( i n t , i n t ) = ad d ; / / ука за тель на в ерсию a dd , получающую дв а i n t Ь i n o p s . i n s e r t ( { " + " , fp } ) ; / / ok : fp ука зыв а е т на пра в ую в ерсию a dd

В качестве альтернативы для устранения неоднозначности можно исполь­ зовать лямбда-выражение: / / ok : исполь з ов а ние лямбда -выражения для устранения неодно зна чно сти при 1 1 выборе исполь зуемой в ерсии a dd Ь i nop s . i n s e r t ( { " + " , [ ] ( i n t а , i n t Ь ) { r e t u r n a d d ( a , Ь ) ; } } ) ;

Вызов в теле лямбда-выражения передает два целых числа. Этому вызову может соответствовать только та версия функции add ( ) , которая получает два целых числа, а следовательно, эта функция и применяется при выполне­ нии лямбда-выражения. Класс fun c t i o n в новой библиотеке никак не связан с классами u n a r y fu n c t i o n и Ь i n a r y _ fu n c t i o n, которые были частью прежних версий библиотеки. Эти классы были заменены более общей функцией b i n d ( ) (см. раздел 1 0.3.4, стр. 5 1 3) . _

Упражнения раздела

1 4.8.3

Упражнение 1 4.44. Напишите собственную версию простого калькулятора, способного выполнять бинарные операции.

if' 1 4. 9. П е р е r р узк а, п р ео б р азование и опе р а то р ь1 В разделе 7.5.4 (стр . 380) упоминалось, что неявный конструктор, который может быть вызван с одним аргументом, определяет неявное преобразование. Такие конструкторы преобразовывают объект типа аргумента в тип класса.

1 4. 9 .

Перегрузка, преобразование и операторы

733

Можно также определить преобразование из типа класса. Для этого нужно определить оператор преобразования . Конструкторы преобразования и опе­ раторы преобразования определяют п реобразованиsz типа класса (class-type conversion) . Такие преобразования называются также пользовательскими п ре­ образованиszми (user-defined conversion).

14. 9 .1 . Опе р ато р ы п р ео б р азования Оператор преобразованиsz (conversion operator) - это специальный вид функции-члена класса. Общий синтаксис функции преобразования имеет следующий вид: ope r a t o r тип ( ) c o n s t ; где тип - это имя типа. Операторы преобразования могут быть определены для любого типа (кроме vo i d ) , который может быть типом возвращаемого значения функции (см. раздел 6.1 , стр . 270) . Преобразование в тип массива или функции недопустимо. Однако преобразование в тип указателя на дан­ ные или функцию, а также ссылочные типы вполне возможны. У операторов преобразования нет явно заданного типа возвращаемого зна­ чения и нет параметров, их следует определять как функции-члены. Операции преобразования обычно не должны изменять преобразуемый объект. В резуль­ тате операторы преобразования обычно определяют как константные члены. Функция преобразования должна быть функцией-членом, у нее не определен тип возвращаемого значения и пустой список парамет­ ров. Функция обычно должна быть константой.

О пределение кла сс а с оператором прео б р азования Для примера определим небольшой класс, представляющий целое число в диапазоне от О до 255: c l a s s Srna l l i n t { puЫ i c : 0 ) : va l ( i ) Sma l l i n t ( i n t i { if (i < о 1 1 i > 255) t h r o w s t d : : o u t o f r a n g e ( " B a d Srna l l i n t va l u e " ) ; ope r a t o r i n t ( ) c o n s t pr ivate : s t d : : s i z e t va l ; };

{ r e t u r n va l ;

}

Класс Sma l l i n t определяет преобразования в и из своего типа. Конструк­ тор преобразует значения арифметического типа в тип Sma l l i n t. Оператор преобразования преобразует объекты класса Sma l l i n t в тип i n t:

734

Глава 1 4. П ере г рузка операторов и преоб р азований

Sma l l i n t s i ; si 4 ; / / неяв но пре о бра зуе т 4 в Sma l l in t , а з а тем / / вызыв а е т Sma l l in t : : op e ra t or= s i + 3 ; / / неяв но пр е о бра зуе т s i в i n t с по следующим цело численным 1 1 суммиров а нием

Хотя компилятор применяет только одно пользовательское преобразова­ ние за раз (см. раздел 4.1 1 .2, стр . 222), неявное пользовательское преобразова­ ние можно предварить или сопроводить стандартным (встроенным) преобра­ зованием (см. раздел 4. 1 1 . 1 , стр. 21 9) . В результате конструктору Sma l l i n t можно передать любой арифметический тип. Точно так же можно использо­ вать оператор преобразования для преобразования объекта класса Sma l l i nt в i n t, а затем преобразовать полученное значение типа i n t в другой арифме­ тический тип: / / аргумент типа dо иЫ е пр еобра зуе тся в i n t с исполь з ов а нием в строенного 1 1 пре о бра з о в а ния Sma l l i n t s i = 3 . 1 4 ; / / выз ов конструктора Sma l l in t ( i n t ) / / опера тор пре о бра з о в а ния кла с с а Sma l l in t пре о бра зуе т s i в i n t s i + 3 . 1 4 ; / / i n t пре о бра зуе т ся в dо иЫ е с исполь з ов а нием в строенного 1 1 преобра зов а ния

Поскольку операторы преобразования применяются неявно, нет никакого способа передать аргументы этим функциям. Следовательно, операторы пре­ образования не могут быть определены как получающие параметры. Хотя функция преобразования не определяет тип возвращаемого значения, каждая из них должна возвратить значение соответствующего типа: c l a s s Sma l l i n t ; ope r a t o r i n t ( Sma l l i n t & ) ; 1 1 ошибка : не член кла сса c l a s s Sma l l i n t { pu Ы i c : i n t ope r a t o r i n t ( ) c o n s t ; 1 1 ошибка : тип в озвраща емого з на чения ope r a t o r i n t ( i n t = 0 ) c o n s t ; 1 1 ошибка : список параме тров ope r a t o r i n t * ( ) c o n s t { r e t u r n 4 2 ; } / / ошибка : 4 2 не ука за тель ; }

В НИМАНИЕ ! НЕ ЗЛОУПОТРЕБЛЯ ЙТЕ ФУНКЦИЯМИ ПРЕОБРАЗОВАНИЯ

Как и в случае с перегруженными операторами, разумное использование функций преобразования помогает существенно упростить работу разра­ ботчика класса и сделать полученный класс удобным в применении. Однако здесь есть две потенциальные ловушки: определение слишком большого количества функций преобразования может привести к неоднозначности кода, а некоторые преобразования могут оказаться скорее вредными, чем полезными. Для примера рассмотрим класс Da te, представляющий данные о дате. Вполне очевидно, что имеет смысл предоставить способ преобразования

14.9. Пе р ег р узка, преобразование и операторы

735

объекта класса D a t e в объект типа i n t . Но какое значение должна возвра­ щать функция преобразования? Она могла бы возвратить десятичное пред­ ставление года, месяца и дня. Например, 30 июля 1 989 года могло бы быть представлено как значение 1 9800730 типа i n t . В качестве альтернативы оператор преобразования мог бы возвращать целое число, соответствующее количеству дней, начиная с некоторой эпохальной даты. Счетчик мог бы считать дни с 1 января 1 970 года или некой другой отправной точки. У обо­ их преобразований есть желаемое свойство, что более поздние даты соответ­ ствуют большим целым числам, что может быть очень полезно. Проблема в том, что нет единого и полного соответствия между объек­ том типа Da t e и значением типа i n t . В таких случаях лучше не определять оператор преобразования. Вместо него класс должен определить один или несколько обычных членов, чтобы извлекать эту информацию в различных форматах.

О ператоры преобразования могут приве с ти к удивительны м результатам На практике классы редко предоставляют операторы преобразования. Пользователи, вероятней всего, будут просто удивлены, случись преобразова­ ние автоматически, без помощи явного преобразования. Но из этого эмпири­ ческого правила есть одно важное исключение: преобразование в тип bo o l является вполне общепринятым для классов. По прежним версиям стандарта перед классами с преобразованием в тип bool стояла проблема: поскольку тип bo o l арифметический, объект этого типа, допускающего преобразование в тип bool, применим в любом контексте, где ожидается арифметический тип. Такие преобразования могут происходить весьма удивительными способами. В частности, если бы у класса i s t r eam было преобразование в тип bool, то следующий код вполне компилировался бы: int i = 4 2 ; c i n < < i ; / / эт от код бьur бы допус тим , е сли бы пр еобра з ов а ние в тип b o o l 1 1 не бьurо яв ным !

Эта программа пытается использовать оператор вывода для входного пото­ ка. Для класса i s t r e am оператор < < не определен, поэтому такой код безус­ ловно ошибочен. Но этот код мог бы использовать оператор преобразования в тип b o o l , чтобы преобразовать объект c i n в boo l . Полученное значение ти­ па boo l было бы затем преобразовано в тип i n t, который вполне применим как левый операнд встроенной версии оператора сдвига влево. В результате преобразованное значение типа bo o l (1 или О) было бы сдвинуто влево на 42 позиции.

Глава 14. П ере г рузка операторов и п рео б р азова н ий

736



Я вный опе р а т ор прео б разования

Чтобы предотвратить подобные проблемы, новый стандарт вводит явный оператор преобразования (explicit conversion operator) : c l a s s Sma l l i n t { puЫ i c : / / компилятор не буде т а в тома тиче с ки применять э то пре о бра з ов а ние e x p l i c i t ope r a t o r i n t ( ) c o n s t { r e t u r n va l ; } / / другие члены ка к прежде };

Подобно явным конструкторам (см. раздел 7.5.4, стр. 383), компилятор не будет (обычно) использовать явный оператор преобразования для неявных преобразований: Sma l l i n t s i 3 ; / / ok : конструктор кла сса Sma l l in t не являе тся яв ным s i + 3 ; / / ошибка : нужно неяв но е пре о бра з о в а ние , но опера тор i n t 1 1 являе тся яв ным s t a t i c c a s t < i n t > ( s i ) + 3 ; / / ok : яв ный з а пр о с пре о бра з о в а ния =

Если оператор преобразования является явным, такое преобразование вполне можно осуществить. Но за одним исключением такое приведение следует осуще­ сгвить явно. Исключение состоит в том, что компилятор применит явное преобразование в выражении, используемом как условие. Таким образом, явное преобразование будет использовано неявно для преобразования выражения, используемого как: •

условие оператора i f, wh i l e или do;



выражение условия в заголовке оператора f o r;



операнд логического оператора NOT ( ! ), OR ( 1 1 ) или AN D ( & & ); выражение условия в условном операторе ( ? : ).



П рео бразование в тип bool В прежних версиях библиотеки типы ввода-вывода определяли преобразо­ вание в тип vo i d * . Это было сделано во избежание проблем, описанных выше. По новому стандарту библиотека ввода-вывода определяет вместо этого явное преобразование в тип boo l . Всякий раз, когда потоковый объект используется в условии, применяется оператор ope r a t o r boo l ( ) , определенный для типов ввода-вывода. Например: wh i l e

( s t d : : c i n > > va l u e )

Условие в операторе wh i l e выполняет оператор ввода, который читает в переменную va l ue и возвращает объект c i n . Для обработки условия объект c i n неявно преобразуется функцией преобразования i s t ream ope rator boo l ( ) Эта функция возвращает значение t rue, если флагом состояния по­ тока c i n является good (см . раздел 8.1 .2, стр. 402), и fa l s e в противном случае. .

1 4 .9 . Перегрузка, преоб р азование и операторы

е омеНду

737

Преобразование в тип boo l обычно используется в условиях. В ре­ зультате оператор ope r a t o r boo l обычно должен определяться как явный.

Упражнения раздела

1 4.9.1

Упражнение 1 4.45. Напишите операторы преобразования для преобразо­ вания объекта класса S a l e s _ da t a в значения типа s t r i n g и douЬ l e . Какие значения, по-вашему, должны возвращать эти операторы? Упражнение 1 4.46. Объясните, является ли определение этих операторов преобразования класса S a l e s_da t a хорошей идеей и должны ли они быть явными. Упражнение 1 4.47. Объясните различие между этими двумя операторами преобразования: s t r u c t I n te g r a l { ope r a t o r con s t in t ( ) ; ope r a t o r i n t ( ) c on s t ; };

Упражнение 1 4.48. Должен ли класс из упражнения 7.40 раздела 7.5.1 (стр. 377) использовать преобразование в тип b o o l . Если да, то объясните почему и укажите, должен ли оператор быть явным. В противном елучае объясните, почему нет. Упражнение 1 4.49. Независимо от того, хороша ли эта идея, определите преобразование в тип bo o l для класса из предыдущего упражнения.

lifr 1 4 . 9 .2. Из б егайте неоднозначных п р ео б р азований Если у класса есть один или несколько операторов преобразования, важно гарантировать наличие только одного способа преобразования из типа класса в необходимый тип. Если будет больше одного способа осуществления преоб­ разования, то будет весьма затруднительно написать однозначный код. Есть два случая, когда возникает несколько путей осуществления преобра­ зования. Первый - когда два класса обеспечивают взаимное преобразование. Например, взаимное преобразование осуществляется тогда, когда класс А оп­ ределяет конструктор преобразования, получающий объект класса в, а класс в определяет оператор преобразования в тип А . Второй случай возникновения нескольких путей преобразования - опре­ деление нескольких преобразований в и из типов, которые сами связаны пре­ образованиями. Самый очевидный пример - встроенные арифметические типы. Каждый класс обычно должен определять не больше одного преобразо­ вания в или из арифметического типа.

Глава 1 4. Перегрузка операторов и преобразований

738

&

ВНИМАНИЕ

Обычно не .следует определять классы со взаимными преобразова­ ниями или определять преобразования в или из арифметических типов.

Р ас познавание а ргум ента и взаимные прео б разования В следующем примере определены два способа пол учения объекта класса А из в : либо при помощи оператора преобразования класса в, либо при помощи конструктора класса А, получающего объект класса в : / / обычно в заимное пре о бр а з о в а ние между дв умя типами - плохая идея s t ruct В ; s t ruct А { А ( ) = de f a u l t ; A ( c o n s t В & ) ; / / пре о бра зуе т В в А / / другие члены }; s t ruct В { ope r a t o r А ( ) c o n s t ; / / тоже пре о бра зуе т В в А / / другие члены }; А f ( con s t А & ) ; В Ь; А а = f ( b ) ; 1 1 ошибка не однозна чно сти : f (B : : opera t o r А ( ) ) 1 1 или f (A : : A (con s t В & ) )

Поскольку существуют два способа получения объекта класса А из в, ком­ пилятор не знает, какой из них использовать; поэтому вызов функции f ( ) не­ однозначен. Для пол учения объекта класса в этот вызов · может использовать конструктор класса А или оператор преобразования класса в, преобразующий объект класса в в А. Поскольку обе эти функции одинаково хороши, вызов не­ однозначен и ошибочен. Если этот вызов необходим, оператор преобразования или конструктор следует вызвать явно: А a l = f ( b . ope r a t o r А ( ) ) ; А а2 = f ( A ( b ) ) ;

/ / o k : исполь зов а ть опера тор пре о бра з о в а ния / / o k : исполь з ов а ть конс труктор кла сса А

В

Обратите внимание: нельзя решить неоднозначность при помощи приве­ дения - у самого приведения будет та же двусмысленность.

Двусм ыс ленно с ть и множес твенно с ть путей прео б разования во вс троенные типы Двусмысленность возникает также в случае, когда класс определяет не­ сколько преобразований в (или из) типы, которые сами связываются преобра­ зованиями. Самый простой и наглядный пример (а также особенно проблема­ тичный) - это когда класс определяет конструкторы преобразования в или из более, чем один арифметический тип.

14.9. Перегрузка, преобразование и операторы

739

Например, у следующего класса есть конструкторы преобразования из двух разных арифметических типов и операторы преобразования в два раз­ ных арифметических типа: struct А { A ( int = 0 ) ; 1 1 обычно плохая идея име ть дв а A ( douЬ l e ) ; 1 1 пре о бра з ов а ния из арифме тиче ских типов ope r a t o r i n t ( ) c o n s t ; 1 1 о бычно плоха я идея име ть дв а ope r a t o r douЬ l e ( ) c o n s t ; / / пре о бра з о в а ния в арифме тиче ские типы / / другие члены }; vo i d f 2 ( l o n g douЬ l e ) ; А а; f2 ( a ) ; 1 1 ошибка не однозна чно с ти : f (A : : opera t o r i n t ( ) ) 1 1 или f (A : : opera t o r do uЬ l e ( ) ) long l g ; А a 2 ( l g ) ; / / ошибка не однозна чно с ти : A : : A ( i n t ) или A : : A ( do uЬl e )

В вызове функции f 2 ( ) ни одно из преобразований не соответствует точно типу l o ng douЫ e. Но для его получения применимо любое преобразование, сопровождаемое стандартным преобразованием. Следовательно, никакое из преобразований не лучше другого, значит, вызов неоднозначен. Возникает та же проблема, что и при попытке инициализации объекта а 2 значением типа l ong. Ни один из конструкторов не соответствует точно типу long. Каждый требовал преобразования аргумента прежде, чем использовать конструктор. •

Стандартное преобразование l on g в do uЫ e, затем А ( douЬ l e ) .



Стандартное преобразование l on g в i nt, затем А ( i n t ) .

Эти последовательности преобразований равнозначны, поэтому вызов не­ однозначен. Вызов функции f 2 ( ) и инициализация объекта а 2 неоднозначны, посколь­ ку у необходимых стандартных преобразований одинаковый ранг (см. раз­ дел 6.6. 1 , стр. 320) . Когда используется пользовательское преобразование, ранг стандартного преобразования, если таковые вообще имеются, позволяет вы­ брать наилучшее соответствие: s ho r t s = 4 2 ; / / пре о бр а з о в а ние s h o r t в i n t лучше , А а З ( s ) ; 1 1 исполь зуе т ся A : : A (i n t )

чем s h o r t в do uЫ e

В данном случае преобразование s h o r t в i n t предпочтительней, чем s h o r t в douЫ e. Следовательно, объект а з создается с использованием конст­ руктора А : : А ( i n t ) , который запускается для преобразования значения s .

& ВНИМАНИЕ

Когда используются два пользовательских преобразования, ранг стандартного преобразования, если таковое вообще имеется, ис­ пользуется для выбора наилучшего соответствия.

740

Глава 1 4. Перегрузка операторов и преобразований

П ерегруженные ф ункц ии и конс трукторы прео б разования Выбор из нескольких возможных преобразований еще более усложняется, когда происходит вызов перегруженной функции. Если два или более преоб­ разования обеспечивают подходящее соответствие, то преобразования счита­ ются одинаково хорошими. Например, могут возникнуть проблемы неоднозначности, когда перегру­ женные функции получают параметры, отличающиеся типами классов, кото­ рые определяют те же конструкторы преобразования: В НИМАНИЕ ! ПРЕОБРАЗОВАНИЯ И ОПЕРАТОРЫ

Корректная разработка перегруженных операторов, конструкторов преобразования и функций преобразования для класса требует большой осторожности. В частности, если в классе определены и операторы преобра­ зования, и перегруженные операторы, вполне возможны неоднозначные ситуации. Здесь могут пригодиться следующие эмпирические правила. • Никогда не создавайте взаимных преобразований типов. Другими сло­ вами, если класс Foo имеет конструктор, получающий объект класса Bar, не создавайте в классе B a r оператор преобразования для типа Foo. • Избегайте преобразований во встроенные арифметические типы. Но ес­ ли преобразование в арифметический тип необходимо, то придется учесть следующее. - Не создавайте перегруженных версий тех операторов, которые полу­ чают аргументы арифметических типов. Если пользователи исполь­ зуют эти операторы, функция преобразования преобразует объект данного типа, а затем применит встроенный оператор. Не создавайте функций преобразования больше, чем в один арифме­ тический тип. Позвольте осуществлять преобразования в другие арифметические типы стандартным функциям преобразования. Самое простое правило: за исключением явного преобразования в тип boo l , избегайте создания функций преобразования и ограничьте неявные конструкторы теми, которые безусловно необходимы. struct С { С ( i nt ) ; / / другие члены }; s t ru c t D { D ( i nt ) ; / / другие члены }; v o i d ma n i p ( c o n s t С & ) ; vo i d ma n i p ( c o n s t D & ) ; ma n i p ( l O ) ; 1 1 ошибка неоднозна чнос ти : ma n ip (C ( l O ) ) или ma n i p (D ( l O ) )

1 4 .9 .

Перегрузка, преобразование и операторы

741

Здесь у структур с и о есть конструкторы, получающие значение типа i n t . Для версий функции man ip ( ) подходит любой конструктор. Следовательно, вы­ зов неоднозначен: он может означать преобразование i n t в с и вызов первой вер­ сии man i p ( ) или может означать преобразование int в о и вызов второй версии. Вызывающая сторона может устранить неоднозначность при явном созда­ нии правильного типа: rna n i p ( C ( l O ) ) ;

& ВНИМАНИЕ

/ / ok : выз ов ma n ip ( co n s t С& )

Необходимость в использовании конструктора или приведения для преобразования аргумента при обращении к перегруженной функции - это признак плохого проекта.

П ерегруженные ф ункции и пользователь с кие прео б разования Если при вызове перегруженной функции два (или больше) пользовательских преобразования обеспечивают подходящее соответсгвие, они считаются одина­ ково хорошими. Ранг любых сrандартных преобразований, которые могли бы (или не могли) бьггь обязательными, не рассматриваются. Необходимость всrро­ енного преобразования также рассматривается, только если набор перегружен­ ных версий может бьггь подобран и использован той же функцией преобразования. Например, вызов функции ma n i p ( ) был бы неоднозначен, даже если бы один из классов определил конструктор, который требовал бы для аргумента стандартного преобразования: s t ru c t Е { Е ( douЬ l e ) ; / / другие члены } ;

vo i d rna n i p 2 ( c o n s t С & ) ; vo i d rna n i p 2 ( c o n s t Е & ) ; 1 1 ошибка не однозна чно с ти : применимы дв а ра зных поль з ов а тель ских 1 1 пре о бра з ов а ния rnan i p 2 ( 1 0 ) ; / / man ip2 (C ( 1 0 ) или m a n ip2 (E ( do uЬ l e ( 1 0 ) ) )

В данном ел учае у класса с есть преобразование из типа i n t и у класса Е есть преобразование из типа douЫ e . Для вызова man ip2 ( 1 О ) подходят обе версии функции man ip2 ( ) : •

Версия man ip2 ( c on s t С & ) подходит потому, что у класса с есть конст­ руктор преобразования, получающий тип i n t . Этот конструктор точно соответствует аргументу.



Версия man i p 2 ( c on s t Е & ) подходит потому, что у класса Е есть конст­ руктор преобразования, получающий тип doub l e и возможность ис­ пользовать стандартное преобразование для преобразования аргумента типа i n t, чтобы использовать этот конструктор преобразования.

Глава 14. Перегрузка операторов и преобразований

742

Поскольку вызовы перегруженных функций требуют разных пользователь­ ских преобразований друг от друга, этот вызов неоднозначен. В частности, даже при том, что один из вызовов требует стандартного преобразования, а другой является точным соответствием, компилятор все равно отметит этот вызов как ошибку. Ранг дополнительного стандартного преобразования (если оно есть) при вызове перегруженной функции имеет значение, только если подходящие функции требуют того же пользовательского преобразования. Если необходимы разные пользовательские пре­ образования, то вызов неоднозначен. Упражнения раздела

14.9.2

Упражнение 1 4.50. Представьте возможные последовательности преобразо­ ваний типов для инициализации объектов e x l и е х 2 . Объясните, допустима ли их инициализация или нет. s t ru c t LongDouЫe { L o n g Do uЬ l e ( douЫ e ope r a t c r douЬ l e ( ) ; ope r a t o r f l o a t ( ) ; }; L o n g D ouЫ e l dOb j ; int exl l dOb j ; float ех2 l dOb j ;

=

О . О) ;

=

=

Упражнение 1 4.51 . Представьте последовательности преобразования (если они есть), необходимые для вызова каждой версии функции ca l c ( ) , и о бъ­ ясните, как подбирается наилучшая подходящая функция. vo i d c a l c ( i n t ) ; vo i d c a l c ( L o n g D ouЬ l e ) ; douЫ e dva l ; c a l c ( dva l ) ; / / которая ca l c ( ) ?

� 1 4.9.3. Подбо р функций и пе р ег р уженные опе р ато р ы Перегруженные операторы Э!О перегруженные функции. При выявлении, который из встроенных или перегруженных операторов применяется для да нно­ го выражения, используется обычный подбор функции (см. раздел 6.4, стр. 305). Однако, когда в выражении используется функция оператора, набор функций­ кандидатов шире, чем при вызове функций, использующих оператор вызова . Ес­ ли объект а имеет тип класса, то выражение а sym Ь может быть следующим: -

a . ope r a t o r s yrn ( b ) ; ope r a t o r s yrn ( a , Ь ) ;

/ / кла с с а содержит опера тор sym ка к функцию - член 1 1 опера тор sym о бычна я функция -

14.9. П ере г р узка, п рео б р азование и операто ры

743

В отличие от обычных вызовов функции, нельзя использовать форму вызо­ ва для различения функции-члена или не члена класса. Когда используется перегруженный оператор с операндом типа класса, функции-кандидаты включают обычные версии, не являющиеся членами класса этого оператора, а также его встроенные версии. Кроме того, если ле­ вый операнд имеет тип класса, определенные в нем перегруженные версии оператора (если они есть) также включаются в набор кандидатов. Когда вызывается именованная функция, функции-члены и не члены класса с тем же именем не перегружают друг друга. Перегрузки нет потому, что син­ таксис, используемый для вызова именованной функции, различает функции­ члены и не члены класса. При вызове через объект класса (или ссылку, или ука­ затель на такой объект) рассматриваются только функции-члены этого класса. При использовании в выражении перегруженного оператора нет никакого спо­ соба указать на использование функции-члена или не члена класса. Поэтому придется рассматривать версии и функции-члены, и не члены класса. Набор функций-кандидатов для используемого в выражении опе­ ратора может содержать функции-члены и не члены класса. Определим, например, оператор суммы для класса Sma l l i n t: c l a s s Srna l l i n t { f riend Srna l l i n t ope r a t o r + ( c o n s t Srna l l i n t & , c o n s t Srna l l i n t & ) ; puЫ i c : Srna l l i n t ( i n t = 0 ) ; / / пре о бра зов а ние из i n t ope r a t o r i n t ( ) c o n s t { r e t u r n va l ; } / / пре о бра зов а ние в i n t p r i va t e : s t d : : s i z e t va l ; };

Этот класс можно использовать для суммирования двух объектов класса Sma l l i n t, но при попытке выполнения смешанных арифметических опера­ ций возникнет проблема неоднозначности: Srna l l i n t s l , s 2 ; Srna l l i n t s З = s l + s 2 ; int i = s З + О ;

/ / исполь зов а ние перегруженного о пера тора / / ошибка : неоднозна чно с ть

+

Первый елучай суммирования использует перегруженную версию опера­ тора + для суммирования двух значений типа Sma l l i n t. Второй случай неод­ нозначен, поскольку О можно преобразовать в тип Sma l l i n t и использовать версию оператора + класса Sma l l i n t либо преобразовать объект s З в тип i n t и использовать встроенный оператор суммы для типа i n t .

&

ВНИМАНИЕ

Предоставление функции преобразования в арифметический тип и перегруженных операторов для того же типа может привести к неоднозначности между перегруженными и встроенными опе­ раторами.

Глава 1 4. Перегрузка операторов и п р еобразований

744

Упражнения раздела

1 4.9.3

Упражнение 1 4.52. Какой из операторов ope r a t o r+, если таковые вообще имеются, будет выбран для каждого из следующих выражений суммы? Пе­ речислите функции-кандидаты, подходящие функции и преобразования типов для аргументов каждой подходящей функции: s t r u c t L o n g D ouЫ e { 1 1 опера тор - член op e ra t o r + толь ко для демонстра ции ; 1 1 о бычно он не являе тся членом кла сса L o n g Do u Ь l e o p e r a t o r + ( c o n s t Sma l l i n t & ) ; 1 1 другие члены ка к в р . 1 4 . 9 . 2 ( с тр . 7 4 1 ) }; L o n g DouЫ e ope r a t o r + ( L o n g Dou Ь l e & , douЬ l e ) ; Sma l l i n t s i ; LongDouЫe ld ; ld = s i + l d ; ld = ld + s i ;

Упражнение 1 4.53. С учетом определения класса Sma l l i n t на стр. 743 оп­ ределите, допустимо ли следующее выражение суммы. Если да, то какой оператор суммы используется? В противном елучае, как можно изменить код, чтобы сделать его допустимым? Sma l l i n t s l ; douЫ e d = s l + 3 . 1 4 ;

Р езюме Перегруженный оператор должен либо быть членом класса, либо иметь по крайней мере один операнд типа класса. У перегруженных операторов долж­ но быть то же количество операндов, порядок и приоритет, как у соответст­ вующего оператора встроенного типа. Когда оператор определяется как член класса, его неявный указатель t h i s связан с первым операндом. Операторы присвоения, индексирования, вызова функции и стрелки должны быть члена­ ми класса. Объекты классов, которые перегружают оператор вызова функции, ope r a t o r ( ) называются "объектами функций" . Такие объекты зачастую используются в комбинации со стандартными алгоритмами. Лямбда-выра­ жения - это от личный способ определения простых классов объектов функции. Класс может определить преобразования в или из своего типа, которые бу­ дут использованы автоматически. Неявные конструкторы, которые могут быть вызваны с одним аргументом, определяют преобразования из типа параметра в тип класса; операторы неявного преобразования определяют преобразова­ ния из типа класса в другие типы.

Термины

745

Т е р минь1 Об ъект функции (function object) . Объект класса, в котором определен перегружен­ ный оператор вызова. Объекты функций применяются там, где обычно ожидаются функции. Оператор преобразования (conversion operator). Оператор преобразования - это функция-член, которая осуществляет преобразование из типа класса в другой тип. Операторы преобразования должны быть константными членами их класса. Такие функции не получают параметров и не имеют типа возвращаемого значения . Они возвращают значение типа оператора преобразования. То есть оператор ope r a t o r i n t возвращает тип i n t, оператор ope r a t o r s t r i n g - тип s t r i n g и т.л. Перегруженный оператор (overloaded operator). Функция, переопределяюrцая зна­ чение одного из встроенных операторов. Функция перегруженного оператора име­ ет имя ope r a t o r с последующим определяемым символом. У перегруженных опе­ раторов должен быть по крайней мере один операнд типа класса . У перегруженных операторов тот же приоритет, порядок и количество операндов, что и у их встро­ енных аналогов. Пользовательское преобразование (user-defined conversion) . Синоним термина пре­ образование типа класса. Преобразование типа класса (class-type conversion) . Преобразования в или из типа класса определяются конструкторами и операторами преобразования соответст­ венно. Неявные конструкторы, пол учаюrцие один аргумент, определяют преобра­ зование из типа аргумента в тип класса . Операторы преобразования определяют преобразования из типа класса в задаНJ:iЫЙ тип. Сигнатура вызова (call signature) . Представляет интерфейс вызываемого объекта . Сигнатура вызова включает тип возвращаемого значения и заключенный в круглые скобки разделяемый запятыми список типов аргументов. Таблица функций (function tаЫе). Контейнер, как правило, карта или вектор, содержа­ щий значения, позволяющие выбрать и выполнить функцию во время выполнения. Шаблон функции (function template) . Библиотечный шаблон, способный представить любой вызываемый тип. Явный оператор преобразования (explicit conversion operator) . Оператор преобразо­ вания с предшествующим ключевым словом exp l i c i t . Такие операторы исполь­ зуются для неявных преобразований только в определенных условиях.

ГЛАВА

15

ОБЪЕКТНО-ОРИЕНТИРОВАНН ОЕ ПРО ГРАММИРОВАНИЕ

В э то й главе ... Краткий обзор ООП 1 5.2. Определение базовых и производных классов 1 5.3. Виртуальные функции 1 5.4. Абстрактные базовые классы 1 5.5. Управление доступом и наследование 1 5.6. Область видимости класса при наследовании 1 5.7. Конструкторы и функции управления копированием 1 5.8. Контейнеры и наследование 1 5.9. Возвращаясь к запросам текста Резюме Термины

1 5.1 .

748 750 763 768 772 778 784 795 801 81 7 81 7

Объектно-ориентированное программирование основано на трех фунда­ ментальных концепциях: абстракция данных, наследование и динамическое связывание. Наследование и динамическое связывание рационализируют программы двумя способами: они упрощают создание новых классов, которые подобны, но не идентичны другим классам, а также облегчают написание программы, позволяя игнорировать незначительные различия в подобных классах. При создании большинства приложений используются одинаковые принци­ пы, которые различаются лишь способами их реализации. Например, рассмат­ риваемый для примера книжный магазин мог бы применять различные систе­ мы тарификации для разных книг. Некоторые книги можно было бы продавать лишь по фиксированной цене, а для других применить гибкую систему скидок. Можно было бы предоставлять скидку тем покупателям, которые покупают не­ сколько экземпляров книги. Скидку можно было бы также предоставить на не­ сколько первых экземпляров, а для остальных оставить полную цену. Объектно-

Глава 1 5. Об ъектно-ориентированное программирование

748

ориентированное программирование (Object-Oriented Programming, или ООП) это наилучший способ создания приложений такого типа.



1 5. 1 .

-

К р аткий обзо р ООП

Ключевыми концепциями объектно-ориентированного программирования являются абстракция данных, наследование и динамическое связывание. Ис­ пользуя абстракцию данных, можно определить классы, отделяющие интер­ фейс от реализации (см. главу 7) . Наследование позволяет определять классы, моделирующие отношения между подобными типами. Динамическое связы­ вание позволяет использовать объекты этих типов, игнорируя незначительные различия между ними .

На следо вание Связанные наследованием (inheritance) классы формируют иерархию. В корне иерархии обычно находится базовый класс (base class), от которого прямо или косвенно происходят другие классы. Эти унаследованные классы известны как производные классы (derived class) . В базовом классе определяют те члены, которые будут общими у всех типов в иерархии. В производных классах определяются те члены, которые будут специфическими для данного производного класса. Для моделирования разных стратегий расценок определим класс Quo te, который будет базовым классом нашей иерархии. Объект класса Qu o t e пред­ ставит книгу без скидок. От него унаследуем второй класс, Bu l k_qu o t e, пред­ ставляющий книги, которые могут быть проданы со скидкой за опт. У этих классов будут две функции-члена. •

Функция i s bn ( ) будет возвращать ISBN. Она никак не зависит от спе­ цифических особенностей производных классов; поэтому будет опреде­ лена только в классе Qu o t e .



Функция n e t_p r i c e ( s i z e_t ) будет возвращать цену при покупке оп­ ределенного количества экземпляров книги. Эта операция специфична для типа; классы Qu o t e и Bu l k qu o t e определят собственные версии этой функции.

В языке С++ базовый класс отличает функции, специфические для типа, от тех, которые предполагается наследовать в производных классах без измене­ ний. Те функции, которые производные классы должны определять самостоя­ тельно, базовый класс определяет как vi r t u a l . Исходя из этого, класс Quote можно первоначально написать так: c l a s s Qu o t e puЫ i c :

15.1 . К ратк ий обзор ООП

749

s t d : : s t r i n g i s bn ( ) c o n s t ; vi r t u a l douЫ e n e t_p r i c e ( s t d : : s i z e t n )

con s t ;

};

Производный класс должен указать класс (классы), который он намерева­ ется унаследовать. Для этого используется находящийся после двоеточия сп и ­ сок наследования класса (class derivation list), представляющий собой разделяе­ мый запятыми список базовых классов, у каждого из которых может быть не­ обязательный спецификатор доступа: c l a s s Bu l k_qu o t e : puЫ i c Qu o t e { puЫ i c : douЫ e n e t _p r i c e ( s t d : : s i z e _ t ) };

/ / B u l k_ qu o t e на следуе тся от Q u o t e c o n s t ove r r i de ;

Поскольку класс Bu l k_qu o t e использует в списке наследования специфи­ катор puЫ i c, его объекты можно использовать так, как будто они являются объектами класса Qu o t e . Тело производного класса должно включать объявления всех виртуалъных функций (virtual function), которые он намеревается определить для себя. Про­ изводный класс может включить в эти функции ключевое слово vi r t ual, но не обязательно. По причинам, рассматриваемым в разделе 1 5 .З (стр. 765), но­ вый стандарт позволяет производному классу явно указать, что функция-член предназначена для переопределения (override) унаследованной виртуальной функции. Для этого после списка ее параметров располагают ключевое слово ove r r i de.

Дина мическое связывание Динамическое связъюание (dynamic Ьinding) позволяет взаимозаменяемо ис­ пользовать тот же код для обрабоn